新着記事

Viewing posts by 工藤淳

nginx+FPMの環境のタイムアウトの解決法の備忘録

前回、記事を書いたPHPをnginx+FPMの環境で、実際にwebアプリを作成。
ページが重くなる場合にタイムアウトが発生、その原因が複数の理由のため解決に時間がかかったのでその備忘録。

さきに原因を書くと
  1. phpの実行時間によるタイムアウト
  2. ブラウザとnginxのタイムアウト
  3. nginxとphp-fpmのタイムアウト
と3つの箇所でタイムアウトが発生していた。

まずは最初に表示された
504 Gateway Timeout
を解消する。
phpのタイムアウト時間max_execution_timeを設定して確認。
まだ504エラーが表示されるので、つぎにnginxのsend_timeoutkeepalive_timeoutを設定。
この設定の追加で504エラーは表示されなくなったが、別のエラーがnginxから表示されるようになった。
An error occurrerd. Sorry, the page you are looking for is currently unavailable. Please try again later.

このエラーメッセージがnginxとphp-fpmのタイムアウトだと気づくのに時間がかかった。
これの対応はnginxの設定にfastcgi_connect_timeoutfastcgi_read_timeoutfastcgi_read_timeoutを追加することで解消できた。

最終的にnginx.confphp.iniに下記の設定を追加しています。
nginx.conf
http {
send_timeout 300; # クライアントへの応答のタイムアウト時間
keepalive_timeout 300; # クライアントとの接続をキープする時間

fastcgi_connect_timeout 300; # nginxとphp-fpmの接続を確立するためのタイムアウト時間
fastcgi_send_timeout 300; # nginxからphp-fpmへのリクエスト送信のタイムアウト時間
fastcgi_read_timeout 300; # php-fpmからの応答のタイムアウト時間
}
php.ini
max_execution_time = 600 # phpのスクリプトの実行時間

webサーバーとアプリケーション・サーバーを分けた場合、その間のタイムアウトも気にしなければいけなかった。

PHPをnginx+FPMの環境で動作させる

古いPHPのWebアプリの改修を行うことになりました。
まずはメンテナンス性の確保のためにオレオレフレームワークからLaravelに載せ替える。
また、1台サーバーを使用して動かしていたが、あまり大きなアプリではないので、kubernetesで動作させることにします。
そして合わせて将来的な技術投資としてWebサーバーをApacheからnginx + FastCGI Process Manager(FPM)を試してみることにしました。
その際のdokcerとkubernetesの設定の備忘録になります。

nginxのconfファイルの作成

まずはLaravelとphp-fpm用にしたnginxのconfファイルを作成。
default.conf
server {
    listen       80;
    listen  [::]:80;
    server_name  example.com;

    access_log  /var/log/nginx/host.access.log;
    error_log  /var/log/nginx/host.error.log;

    location / {
        root   /usr/share/nginx/html;
        index  index.php index.html index.htm;
        # リクエストされたファイルが存在しなければ、Laravelのフロントコントローラーに内部リダイレクトさせる
        try_files $uri /index.php?$query_string;
    }

    # 400番台のエラーページの設定
    error_page  404              /404.html;

    # 500番台のエラーページの設定
    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # Apacheの設定なので今回はコメントアウトのまま
    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # FastCGIの設定
    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    location ~ \.php$ {
        # Laravelのルートディレクトリ
        root           /var/www/html/public;
        # nginxからphp-fpmに受け渡すIPアドレスとポート番号の設定
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        # 全てのリクエストをLaravelのフロントコントローラーで実行させる
        fastcgi_param  SCRIPT_FILENAME  $document_root/index.php;
        include        fastcgi_params;
    }

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}
ここでの注意点はnginxからphp-fpmに受け渡すIPアドレスとポート番号の設定
ローカル環境で動作させる場合はdokcerのphp-fpmのコンテナを指定してください。
fastcgi_pass   php-fpm:9000;
kubernetesで動作させる場合は1pod内でnginxのコンテナとphp-fpmのコンテナを動作させるため127.0.0.1:9000で受け取ることができます

nginxのDockerfileの作成

Docker Hubから最新のものを使用。
Dockerfile
FROM nginx:1.23.3

COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf

EXPOSE 80 443

STOPSIGNAL SIGQUIT

CMD ["nginx", "-g", "daemon off;"]

php-fpmのDockerfileの作成

こちらもDocker Hubから最新のものを使用。
注意点はnginxがアクセスできるようにLaravelのプロジェクトを配置しているディレクトリの所有ユーザーを変更すること。
アクセスしてくるのはnginxなので所有ユーザーをwww-dataに変更しておかないと404エラーになります。
Dockerfile
FROM php:8.1.16-fpm

#PHPの拡張機能のインストール
RUN apt update \
    && apt install -y libonig-dev libxml2-dev libcurl4-openssl-dev libssl-dev libzip-dev \
    && docker-php-ext-install pdo pdo_mysql mysqli simplexml curl phar zip ftp \
    && pecl install xdebug redis \
    && docker-php-ext-enable xdebug redis

# nginxがアクセスできるように所有ユーザーを変更
COPY project /var/www/html
RUN chown -R www-data:www-data /var/www/html

EXPOSE 9000

ENTRYPOINT ["docker-php-entrypoint"]

CMD ["php-fpm"]

kubernetesの設定の作成

kubernetesに反映させるdeployment、service、ingressを作成します。
deployment.yaml
1pod内で動作させるためcontainersにnginxとphp-fpmのコンテナを記載します。
apiVersion: apps/v1
kind: Deployment
metadata:
  name: example-deployment
  labels:
    app: example
spec:
  replicas: 1
  selector:
    matchLabels:
      app: example
  template:
    metadata:
      labels:
        app: example
    spec:
      containers:
        - image: php-fpm:latest
          name: example-php-fpm
          ports:
            - containerPort: 9000
              protocol: TCP
        - image: nginx:latest
          name: example-nginx
          ports:
            - containerPort: 80
              protocol: TCP
service.yaml
外からのアクセスはnginxが受けるのでポートは80番。
kind: Service
apiVersion: v1
metadata:
  name: example-service
spec:
  type: NodePort
  selector:
    app: example
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example-ingress
spec:
  rules:
    - host: example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: example-service
                port:
                  number: 80

kubernetesに反映を実行。 これで動作できました。

mysqlのセキュリティ更新で新たなユーザー権限が必要になっていた

久々にmysqldumpを使用した際の話

とある検証のために本番環境の最新のデータを検証環境のデータベースにいれる必要があり、久々にmysqldumpを実行。

まずはテーブル名を指定してdumpファイルを作成する。

mysqldump -uユーザー名 -pパスワード -hホスト スキーマ名 テーブル名 > /tmp/dump.sql 

実行。

 Access denied; you need (at least one of) the PROCESS privilege(s) for this operation 

アクセス拒否、PROCESS権限が必要です。

以前なら問題なく実行できていたユーザーなのに、必要な権限が増えている?

ということで調べてみると、リリースノートのセキュリティ注釈に記載があり

https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-31.html
Security Notes Incompatible Change: Access to the INFORMATION_SCHEMA.FILES table now requires the PROCESS privilege. This change affects users of the mysqldump command, which accesses tablespace information in the FILES table, and thus now requires the PROCESS privilege as well. Users who do not need to dump tablespace information can work around this requirement by invoking mysqldump with the --no-tablespaces option. (Bug #30350829) The linked OpenSSL library for MySQL Server has been updated to version 1.1.1g. Issues fixed in the new OpenSSL version are described at https://www.openssl.org/news/cl111.txt and https://www.openssl.org/news/vulnerabilities.html. (Bug #31296697)

PROCESS権限が必要になっていました。

ユーザーに権原を付与する、または

--no-tablespaces

オプションをつけることで回避できるそうです。

今回はオプションで対応しました。

改めて実行。

mysqldump -uユーザー名 -pパスワード -hホスト --no-tablespaces スキーマ名 テーブル名 > /tmp/dump.sql

今度は問題なくダンプ終了

mysql -uユーザー名 -pパスワード -hホスト スキーマ名 < /tmp/dump.sql

ロードする。

Access denied; you need (at least one of) the SUPER privilege(s) for this operation

さらにアクセス拒否が表示される。SUPER権限が必要です?

これについては

SET @@GLOBAL.GTID_PURGED

が原因でした

https://dev.mysql.com/doc/refman/5.7/en/mysqldump.html#option_mysqldump_set-gtid-purged
This option enables control over global transaction ID (GTID) information written to the dump file, by indicating whether to add a SET @@GLOBAL.gtid_purged statement to the output. This option may also cause a statement to be written to the output that disables binary logging while the dump file is being reloaded.

グローバルトランザクションIDについては変更したくないので、これもオプションで対応。

--set-gtid-purged=OFF

を追記して、再度ダンプし直します。

 mysqldump -uユーザー名 -pパスワード -hホスト --no-tablespaces --set-gtid-purged=OFF スキーマ名 テーブル名 > /tmp/dump.sql 

そしてロードする。

mysql -uユーザー名 -pパスワード -hホスト スキーマ名 < /tmp/dump.sql

問題なくできました。これで最新のデータで検証を行うことができそうです。

照合順序が違うカラムの外部結合

増築を繰り返しているMysqlデータベースを使用しているとたまにある問題の解決方法

作成されたタイミングの違うテーブルの照合順序があっていないことがあります。
古いテーブルのカラムの照合順序がutf8_unicode_ciで、
最近作られたテーブルのカラムの照合順序がutf8_general_ci
必要データがこのふたつのテーブルにあるため外部結合しなければならない自体になった。

普通にLEFT JOINしようとすると

SELECT t1.*
FROM t1
LEFT JOIN t2
ON t1.code = t2.code

照合順序が違うカラム同士では外部結合できないとエラーが発生します。

Illegal mix of collations (utf8_unicode_ci,IMPLICIT) and (utf8_general_ci,IMPLICIT) for operation '='

これの解決のためにカラムの設定を変えてしまうのは影響範囲が大きいのでできれば避けたい。
そんな場合にはCOLLATE句を使用することで解決できます。

SELECT t1.*
FROM t1
LEFT JOIN t2
ON t1.code COLLATE utf8_general_ci = t2.code

これで問題なく外部結合して結果を取得することができました。

mysqlのドキュメントにはWHERE句、ORDER句、GROUP句などでの使い方は書いてありましたが、外部結合での使い方は書いてなかったので備忘録として残しておきます。

MySQL 5.6 リファレンスマニュアル 10.1.7.2 SQL ステートメントでの COLLATE の使用

10年に一度巡る雑誌コードの話

ISBNコード(書籍JANコード)とともに本に付番されている雑誌コードについて解説します。
雑誌コードからJANコードを作成しなければならない時に役に立つ知識です。

雑誌コードとは

雑誌扱いの本に付番されている5桁+4桁、または5桁+2桁のコードです。
この5桁に意味がずっしりと詰まっています。
また、バーコードはISBNコード(書籍JANコード)と一般商品JANコードのものがあります。
雑誌コードの始まる1桁目で雑誌コードの種類がきまります。

1. 1桁目が01は月刊誌(隔月刊や季刊の雑誌も含む)

基本、末尾は奇数。別冊や増刊号は末尾の数に+1して偶数になる。
本に印刷されるバーコードは一般商品JANコード

例)雑誌コード5桁+「-」+月2桁+年の下2桁

月刊TORICO 8月号
01234-0821、または省略して01234-08
月刊TORICO 8月増刊号
01235-0821、または省略して01235-08

2. 1桁目が23は週刊誌(隔週刊や月2回発売の雑誌も含む)

末尾は発売した週で1〜5。
別冊や増刊号は末尾に6以上の数が振られる。
本に印刷されるバーコードは一般商品JANコード

例)雑誌コード5桁+「-」+月2桁+年の下2桁

週刊TORICO 8/6号(8月1週目)
21231-0821、または21231-8/6
週刊TORICO 8/13号(8月2週目)
21232-0821、または21232-8/13
週刊TORICO 8/20号(8月3週目)
21233-0821、または21233-8/20
週刊TORICO 8/20増刊号
21236-0821、または21236-8/20

3. 1桁目が45は漫画のコミックス

雑誌コード5桁は固定。
だいたいレーベルごとになっている(〇〇コミック)
月年はなく雑誌コードに対して発売された巻数の通巻数2桁。
本に印刷されるバーコードは書籍JANコード

例)雑誌コード5桁+「-」+通巻数2桁

TORICOコミックス 1巻
41234-01
TORICOコミックス 2巻
41234-02

4. 1桁目が6はムック

雑誌コード5桁は固定。
だいたいレーベルごとになっている(〇〇ムック)
月年はなく雑誌コードに対して発売された巻数の通巻数2桁。
本に印刷されるバーコードは書籍JANコード

例)雑誌コード5桁+「-」+通巻数2桁

TORICOムック よくわかるTORICO
61234-01
TORICOムック TORICO大全
61234-02

5. 珍しいコード

1桁目が7で始まる、オーディオ・ビジュアル商品
1桁目が8で始まる、直販誌
1桁目が9で始まる、PB商品

雑誌コードから一般商品JANコードの作成方法

月刊誌、週刊誌にはISBNコード(書籍JANコード)がありません。
雑誌コードのみ付番されています。
ではバーコードは何でできているか?というと雑誌コードから一般商品JANコードを作成することができます。

例)01234-0821の場合
一般商品JANコードは4910012340819となります。
内訳としては
491=>定期刊行物コードのプレフィックス。固定
0=>予備。雑誌コードが満数になった場合使うかもしれない
01234=>雑誌コード5桁
08=>月号2桁
1=>年の下1桁
9=>JANのチェックデジット。形式はモジュラス10、ウエイト1、3
これをphpで書くと下記の様になります

/**
 * 雑誌コードをJANコードに変換する
 * @param string $code 雑誌コード、5桁-4桁
 * @return string JANコード
 */
function magazineCode2Jan($code)
{
    // 雑誌コードと月号で分ける
    $exp_code = explode('-', $code);
    $magazine_code = $exp_code[0];
    // 年の下1桁
    $year = substr($exp_code[1], 3, 1);
    $month = substr($exp_code[1], 0, 2);
    $prefix = 491;
    $spare = 0;
    $jan12 = $prefix . $spare . $magazine_code . $month . $year;
    // チェックデジット計算
    $chkdgt = getCheckDigit13($jan12);
    $jan13 = $jan12 . $chkdgt;
    return $jan13;
}

※チェックデジットの計算はエンジニアの知らないISBNの話のコード例にある「ISBN13桁のチェックディジット」を使用しています。
ここでタイトル回収になるのですが、年の下1桁を使用するため雑誌コードは10年に一度同じコードになります。
データベースにカラムを作る際はユニーク制約をしない、あるいは絶版になった雑誌を定期的に削除するような仕組みが必要になります。

エンジニアの知らないISBNの話

本には商品判別のためのバーコードがついています。
いわゆるISBNコード(JANコード)です。
ECショップでは本を取り扱うために本の書誌を記録するテーブルを作成しますが、その際にint型でいいだろう? ユニーク制約でいいか?とおもってはいけないません。

先に結論を書いてしまうと

  • 文字が含まれる可能性がある
  • 先頭に0が来る可能性がある
  • ユニークではない

char型にして、ユニーク制約をつけないほうがよい理由

1. 文字「X」が含まれる可能性がある

ISBNコードには10桁と13桁があります。
10桁ISBNコードは9桁+チェックデジット1桁で構成されます。
古い体系ですが、一部で使うことがあり、チェックデジットに文字「X」がはいる可能性があります。
これはチェックデジットがモジュラス11、ウェイト10-2という方法で算出されるためです。
計算方法をphpで書くと

/**
 * ISBN10桁のチェックディジットを返す
 */
function getCheckDigit10($jan9)
{
    $sum = 0;
    $cnt = 0;
    for ($i = 10; ; $i--) {
        if ($cnt > 10) {
            break;
        }
        $sum += substr($jan9, $cnt, 1) * $i;
        $cnt++;
    }
    $remainder = $sum % 11;
    if ($remainder == 0) {
        return 0;
    }
    $check_digit = 11 - $remainder;
    if ($check_digit == 10) {
        $check_digit = 'X';
    }
    return $check_digit;
}

2. 先頭に0がつく可能性がある

ISBNコードと書きましたが、実際にバーコードになっているのはJANコードです。
そしてJANコードは先頭が「0」になる可能性があります。
本を売るショップとして取り扱いそうなJANコードは3種類。

  1. 一般商品JANコード
    45,または49で始まる12桁+チェックデジット1桁の13桁JANコード。
    45と49は日本に割り振られた国コードなので固定。
    雑誌やグッズなどに使われています。
  2. 書籍JANコード
    9784で始まる12桁+チェックデジット1桁の13桁JANコード。
    978は世界共通の書籍コード
    4は日本に割り振られた国コードなので固定。
    いわゆる13桁ISBNコード。
    コミックや書籍などに使われています。
    バーコードは2段になっており、上段は書籍JANコード、分類・価格のコード。
  3. インストアコード
    02、04、20〜29で始まる12桁+チェックデジット1桁の13桁JANコード。
    ポイントカード、会員証やレジ袋などの管理・販売用に使われる、小売店が独自につくって使用してよいJANコード。
    20〜29始まりのJANコードを使用すればいい話だが、02、04も準備しておくに越したことはない。

JANコードのチェックデジットはモジュラス10、ウエイト3-1という方法で算出されるため0〜9の数字になります。

/**
 * ISBN13桁のチェックディジットを返す
 */
function getCheckDigit13($jan12)
{
    $sum_odd = 0;
    $sum_even = 0;
    for ($i = 11; $i >= 0; $i--) {
        if ($i % 2) {
            $sum_odd += substr($jan12, $i, 1);
        } else {
            $sum_even += substr($jan12, $i, 1);
        }
    }
    $sum = $sum_odd * 3 + $sum_even;
    $division = $sum % 10;
    return (10 - $division) % 10;
}

3. 同じJANコードが使われる

JANコードは同じコードが存在するのでユニークではありません。

  1. 書籍JANコードの場合
    「今現在販売中の本」と限定すれば書籍JANコードはユニークになります。
    ただし、本が絶版になったあと、そのJANコードは別の本に付番されます。
    そのため、今までに発売したすべての本のJANコードだと同じJANコードをもつ本が複数存在します。
  2. 雑誌の一般商品JANコードの場合
    雑誌に付番されている雑誌コードから一般商品JANコードが作成されます。
    その際に西暦の下1桁を使用しているため、10年ごとに同じコードになります。

以上がISBNコード(JANコード)をint型、ユニーク制約にしては行けない理由でした。
10桁と13桁のどちらも記録できるようにしてあると便利かもしれません。

Search