はじめに

五回目となったこのシリーズですが、今回はWordPressとMariaDBの導入および高速化するための設定について紹介します。

現在、当サイトでは静的サイトジェネレータのZolaでウェブサイトを構築しているため、既にどちらも使用していません。

しかし、Zolaを使う以前の旧サイトではWPおよびMariaDBを長い間使っていたこともあるため、自身のメモ代わりとしても残しておこうと思います。

これまでのシリーズ一覧は以下のとおりです。

なお、このシリーズではホスティングにVultrを使っています。

Vultr

サーバのスペックおよび構成は以下の通りです。

  • $ cat /etc/lsb-release

    • DISTRIB_ID=Ubuntu
    • DISTRIB_RELEASE=20.04
    • DISTRIB_CODENAME=focal
    • DISTRIB_DESCRIPTION="Ubuntu 20.04.3 LTS"
  • Vultr : High Frequency Server

    • 32GB MVMe
    • 1CPU
    • 1GB Memory
    • Additional Features
      • Virtual Private Clouds: ON

ウェブサーバにはNginxを用い、HTTPS化に必要な証明書の取得にはLet'sEncryptを使いますが、この記事ではこれらは既に導入済みとして扱います。

PHP8のインストール

まずはWordPressの実行に必要となるPHP(執筆時点ではPHP8.1)をインストールします。

ただし、Ubuntu20.04のリポジトリにあるPHPはPHP7であることから、バージョンが古いです(22.04からはPHP8をサポート)。

そのため、最新版であるPHP8を手軽にインストールするには、Ondrej Sury氏による非公式(しかし有名です)の個人用リポジトリ(Personal Package Archive PPA)を追加する必要があります(Ubuntu22.04からは不要)。

Ondrej Sury氏はチェコ出身のエンジニアで、BINDとDHCPの保守開発などを行うISC(The Internet Systems Consortium)にて、DNSエンジニアリングのディレクターを務めています。

  1. $ sudo add-apt-repository ppa:ondrej/phpでリポジトリを登録しようとすると、以下の文章が出てきます。
Co-installable PHP versions: PHP 5.6, PHP 7.x and most requested extensions are included. Only Supported Versions of PHP (http://php.net/supported-versions.php) for Supported Ubuntu Releases (https://wiki.ubuntu.com/Releases) are provided. Don't ask for end-of-life PHP versions or Ubuntu release, they won't be provided.

Debian oldstable and stable packages are provided as well: https://deb.sury.org/#debian-dpa

You can get more information about the packages at https://deb.sury.org

IMPORTANT: The <foo>-backports is now required on older Ubuntu releases.

BUGS&FEATURES: This PPA now has a issue tracker:
https://deb.sury.org/#bug-reporting

CAVEATS:
1. If you are using php-gearman, you need to add ppa:ondrej/pkg-gearman
2. If you are using apache2, you are advised to add ppa:ondrej/apache2
3. If you are using nginx, you are advised to add ppa:ondrej/nginx-mainline
???or ppa:ondrej/nginx

PLEASE READ: If you like my work and want to give me a little motivation, please consider donating regularly: https://donate.sury.org/

WARNING: add-apt-repository is broken with non-UTF-8 locales, see
https://github.com/oerdnj/deb.sury.org/issues/56 for workaround:

# LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php
 More info: https://launchpad.net/~ondrej/+archive/ubuntu/php
Press [ENTER] to continue or Ctrl-c to cancel adding it.
  1. これはppa:ondrej/phpの処理に関する注意書きですが、基本的には特に問題無いため、そのままEnterで処理を続行します。

  2. するとリポジトリが追加されるので、$ sudo apt updateでリポジトリのアップデートをチェックします。

    1. $ apt search php8.1でphp8.1関連のパッケージ(モジュール)一覧と説明を見ることができます。
  3. WordPress使用時には複数のモジュールが必要となってくるため、$ sudo apt install php8.1 php8.1-cgi php8.1-common php8.1-curl php8.1-dev php8.1-fpm php8.1-gd php8.1-intl php8.1-mbstring php8.1-mysql php8.1-xml php8.1-zipで各種インストールします。

    1. 順にphp8(PHP本体)、cgi(Common Gateway Interface)、common(Common module)、curl(cURL)、dev(Development)、fpm(FastCGI Process Manager)、gd(Graphics library)、intl(The Internationalization)、mbstring(Multibyte string)、mysql(MySQL)、xml(Extensible Markup Language)、zip(ZipArchive)となっています。
    2. インストール後、$ php -vでバージョンをチェックできます(執筆時点ではPHP8.1.2)。

MariaDBの導入

次にWordPressのデータベースとして使うMariaDBをインストールします。

MariaDBについて

その前にMariaDBについてですが、MySQLのオリジナルコードの作者であるMichael Widenius氏によって2009年に制作された、オープンソースソフトウェアであるMariaDBは、数々の最適化によってパフォーマンスが高いと言われています。

例えばデータベースのViewの場合、MySQLは接続されているテーブルすべてを照会しますが、MariaDBではクエリによって必要とされるテーブルのみが照会されるため、大きくパフォーマンスを向上することができます。

基本的にWordPressではMySQLを用いることが多いですが、MariaDBはその高い性能によって人気を獲得し、多くの人々に愛用されています。

MariaDBのインストール

ではインストール方法ですが、執筆時点でのMariaDBのバージョン10.6.5なのに対し、Ubuntu20.04に搭載されているMariaDBでは10.3とバージョンが古くなっています。

そのため、MariaDBの公式からリポジトリを登録してインストールします。

よく紹介される方法としてapt-keyがありますが、このコマンドは廃止予定となっているため、当記事ではGNU Privacy Guard(GPG)を用いてリポジトリの登録を行います。

  1. まずはMariaDB公式サイトのDownload内にあるMariaDB Repositoriesにて、ファイルのリンクを調べます。
  1. 同リンクのChoose a distributionにて自身のOSを選択し、Choose a MariaDB Server versionで執筆時点の最新安定版である10.6を選びます。

  2. するとMirrorおよびコマンドの例が自動的に表示される(日本であれば山形大学, 米沢市 - Yamagata University, Yonezawa)ので、それを元にリポジトリの登録を行います。

  3. まず$ curl -fsSL https://mariadb.org/mariadb_release_signing_key.asc | sudo gpg --dearmor -o /usr/share/keyrings/mariadb-archive-keyring.gpgによって署名鍵を格納します。

    1. これにより署名鍵は/usr/share/keyrings/mariadb-archive-keyring.gpgに格納されました。
  4. 次に$ sudo vim /etc/apt/sources.list.d/mariadb.listでMariaDB用のリポジトリリストを作成します。

mariadb.list

deb [arch=amd64,arm64,ppc64el,s390x signed-by=/usr/share/keyrings/mariadb-archive-keyring.gpg] https://ftp.yz.yamagata-u.ac.jp/pub/dbms/mariadb/repo/10.6/ubuntu focal main
  1. []内は先程インポートした署名鍵検証に使われる鍵を指定しており、これによりリポジトリごとに鍵が参照されます。

  2. そして$ sudo apt updateでアップデートをチェックします。

    1. $ sudo apt info mariadb-serverでチェックすると10.3から10.6に変更されています。
    2. もしリポジトリに関するエラーが発生した場合は別のミラーを選択してください。
  3. MariaDBをインストールする準備が整いましたので$ sudo apt install mariadb-serverでMariaDBをインストールします。

  4. これでMariaDBのインストールが完了しました。

MariaDBセキュリティ初期設定

  1. MariaDBでは$ sudo mysql_secure_installationを実行することで対話型の初期設定が行えます。
NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MariaDB
      SERVERS IN PRODUCTION USE!  PLEASE READ EACH STEP CAREFULLY!

In order to log into MariaDB to secure it, we'll need the current
password for the root user. If you've just installed MariaDB, and
haven't set the root password yet, you should just press enter here.

## MariaDBのrootパスワードが既に存在していれば入力(enterで無し)
Enter current password for root (enter for none):

Setting the root password or using the unix_socket ensures that nobody
can log into the MariaDB root user without the proper authorisation.

You already have your root account protected, so you can safely answer 'n'.

## rootログイン時にパスワード認証ではなくUNIX Socket認証を使うかどうか
## つまり、MariaDBのrootにログインできるのはrootユーザのみ(sudoかsuコマンド)
## 他にもユーザ作成時に"VIA unix_socket"を付けることで同じ認証をそのユーザに適用する
## ただし、TCP接続が必要なアプリケーションを経由するときはこの認証が使えない
## 複数ユーザが単一のアカウントを使い回す場合は"no"
## 例ではUnixSocket認証を使うので"Yes"
Switch to unix_socket authentication [Y/n]

Enabled successfully!
Reloading privilege tables..
 ... Success!

You already have your root account protected, so you can safely answer 'n'.

## rootのパスワードを変更するかどうか
## UnixSocket認証を使っているので"no"。パスワード認証を使う場合は"Yes"で変更する
## ただし、ブラウザでphpMyAdminにrootログインする予定がある際はパスワードが必要
Change the root password? [Y/n]
 ... skipping.

By default, a MariaDB installation has an anonymous user, allowing anyone
to log into MariaDB without having to have a user account created for
them.  This is intended only for testing, and to make the installation
go a bit smoother.  You should remove them before moving into a
production environment.

## 匿名ユーザを削除するかどうか。匿名ログインは必要ないので"Yes"
Remove anonymous users? [Y/n]

Normally, root should only be allowed to connect from 'localhost'.  This
ensures that someone cannot guess at the root password from the network.

## リモートでのrootログインを無効にするかどうか。ローカルしか使わないなら"Yes"
Disallow root login remotely? [Y/n]

By default, MariaDB comes with a database named 'test' that anyone can
access.  This is also intended only for testing, and should be removed
before moving into a production environment.

## testという名前のテスト用データベースを削除するかどうか。不要なので"Yes"
Remove test database and access to it? [Y/n]

Reloading the privilege tables will ensure that all changes made so far
will take effect immediately.

## 特権テーブルをリロードするかどうか。すぐに設定を反映させたいなら"Yes"
Reload privilege tables now? [Y/n]

 ... Success!

Cleaning up...

## これでMariaDBの設定が完了しました。
All done!  If you've completed all of the above steps, your MariaDB
installation should now be secure.

Thanks for using MariaDB!

  1. 初期設定が完了したら$ sudo mysql -u rootでパスワード無し(UniXSocket認証)のままMariaDBにroot接続することができます。
    1. パスワードを設定している場合は$ sudo mysql -u root -pでrootログインできます。
    2. もし、rootのパスワードを変更したい場合は再び$ sudo mysql_secure_installationを実行します。
    3. もしくはMariaDBにログイン後、MariaDB [(none)]> use mysqlでデータベースをmysqlに変更し、MariaDB [(mysql)]> ALTER USER 'root'@'localhost' IDENTIFIED BY "ここにパスワードを入力";を実行することで、rootに対する新しいパスワードを設定できます。

MariaDB動作確認

  1. $ sudo mysql -u rootでMariaDBに接続します。

  2. MariaDB [(none)]> select version();を実行し、バージョンをチェックします。

  3. インストールしたバージョンと同じであれば正しくインストールできています。

    1. ちなみにクライアントのバージョンは$ madiadb --versionでチェックできます。
  4. MariaDBから接続を抜けるにはMariaDB [none]> exitを入力します。

    1. 強制的に抜けるにはctrl+cでも可能。

WordPress用のデータベースを作成

MariaDBが使えるようになったので、WordPress用のデータベースを作成します。

例ではドメイン名(example.comなど)でデータベースとユーザ(rootユーザは使わない)を作ります。

  1. $ sudo mysql -u root -pでMariaDBに接続します。

  2. MariaDB [(none)]> CREATE DATABASE `DB-NAME`;で新たにデータベースを作成します。

    1. DB-NAMEの箇所はWordPressのDBとして使う名前を入れます(例ではexample.comなどのドメイン名)。
  3. 次にユーザ作成ですが、MariaDB [(none)]> GRANT ALL PRIVILEGES ON `DB-NAME`.* TO "USER-NAME"@localhost IDENTIFIED BY "DB-PASS";を実行します。

    1. DB-NAMEには先程作ったばかりのWP用データベース名を入れます。
    2. USER-NAMEはそれに対応するユーザ名(例ではここもドメイン名)を記入します。
    3. DB-PASSにはそのユーザに対するデータベース用のパスワードを新規に入力します。
  4. MariaDB [(phpmyadmin)]> show databases;でデータベースが追加されたか確認します。

  5. MariaDB [(phpmyadmin)]> select user, host from mysql.user;で作成したユーザも確認します。

  6. 作成が確認できれば、MariaDB [(none)]> exitでMariaDBから離脱します。

WordPress導入前の設定

WordPressを導入する前に簡単な設定を行います。

PHP設定

実行するウェブサーバがnginx、SFTPユーザ(ログインユーザ名)をtestとして進めていきます。

  1. まず最初に$ sudo vim /etc/php/8.1/fpm/php-fpm.confでFPMの設定を行います。

php-fpm.conf

;; 省略 ;;

; FPM が利用するイベントメカニズム
; コメントアウトを外す
events.mechanism = epoll

;; 省略 ;;
  1. 次に$ sudo vim /etc/php/8.1/fpm/php.iniでタイムゾーンを設定します。
[Date]
; Defines the default timezone used by the date functions
; https://php.net/date.timezone
; 日付の元となるロケーションを指定(基本的に住んでいるエリア)
date.timezone = "Asia/Tokyo"
  1. 今回ウェブサーバにはNginxを使うため、$ sudo vim /etc/php/8.1/fpm/pool.d/www.confではuserlisten.ownerlisten.groupに属するユーザをnginxに変更し、groupのみSFTPユーザ名(例ではtest)を記述します。これにより、PHP実行の際に生成されるファイルの所有権はnginx:testとなります。

www.conf

; Unix user/group of processes
; Note: The user is mandatory. If the group is not set, the default user's group
;       will be used.
user = nginx
group = test

;; 省略 ;;

; Set permissions for unix socket, if one is used. In Linux, read/write
; permissions must be set in order to allow connections from a web server. Many
; BSD-derived systems allow connections regardless of permissions. The owner
; and group can be specified either by name or by their numeric IDs.
; Default Values: user and group are set as the running user
;                 mode is set to 0660
; www-dataからnginxに変更
listen.owner = nginx
listen.group = nginx

PHPの設定はこれで完了です。

Nginx設定

NginxもWordPress用に設定を行います。

今シリーズの方法に倣い、$ sudo vim /etc/nginx/conf.d/ドメイン名.confドメイン名.conf(例えばexample.com.conf)の設定を行います。

今回の公開ディレクトリとなるrootは/var/www/ドメイン名に設定します。

ただし、WordPressをインストールするディレクトリは/var/www/ドメイン名/appsです(appsの名前は自由)。

理由として、WordPressのインストールディレクトリをずらすことにより、わずかながらセキュリティ対策となるためです(公開ディレクトリ直下だとWP関連のファイルが機械的にアクセスされやすいため)。

そのため、$ sudo mkdir -p /var/www/ドメイン名/appsでディレクトリを作成しておきます。

ドメイン名.conf

## 省略 ##

server {
    # 接続するポート番号を指定
    listen    443 ssl http2;
    listen [::]:443 ssl http2;

    # URLに表示されるドメインまたはIPアドレス
    server_name  ドメイン名;

    # ウェブサイト表示時に参照されるディレクトリ
    root /var/www/ドメイン名;

    # indexページとして読み込むファイル(indexは各locationにも継承される)
    index   index.php index.html index.htm;

    location / {
        # 左から順に参照し、該当が無ければ最後のuri(もしくはcode)に内部転送される
        try_files       $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        # 左から順に参照し、該当が無ければ最後のuri(もしくはcode)に内部転送される
        try_files       $uri $uri/ /index.php?$args /?access=404;

        # PHPに渡すパラメータファイルを指定
        include fastcgi_params;

        # FASTCGIサーバーへのキープアライブ接続をON
        fastcgi_keep_conn on;

        # スラッシュで終わるURIの後にFASTCGIが要求するファイル名
        fastcgi_index   index.php;

        # fastcgi_path_info変数の値をキャプチャする正規表現の定義
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;

        # FastCGIサーバーに渡すパラメータを指定
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        # コード300以上のレスポンスをerror_pageディレクティブで処理
        fastcgi_intercept_errors on;

        # FastCGIサーバーのアドレスをUnixドメインソケットのパスに指定
        fastcgi_pass unix:/run/php/php8.1-fpm.sock;
        }

    #error_page  404              /404.html;

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

}

$ sudo service nginx restartでNginxを再起動します。

WordPressの導入

ここでようやくWordPressの導入です。

WordPressのダウンロード

  1. WordPress公式サイトのリリースから最新版を確認します(執筆時点では5.9)。

  2. 次に$ sudo wget https://ja.wordpress.org/wordpress-5.9-ja.tar.gzで最新のWordPressを圧縮ファイルの状態でダウンロードします。

  3. ダウンロードが終われば、$ tar zxvf wordpress-5.9-ja.tar.gzでファイルを解凍します。

  4. 解凍後は$ cd wordpressに移動します。

  5. そして、$ sudo cp -rp * /var/www/ドメイン名/appswordpressディレクトリ内の全てのファイル(ディレクトリ含む)をvar/www/ドメイン名/appsにパーミッションや所有権(およびタイムスタンプ)をそのままにコピーします。

権限の作成

  1. まず、$ sudo chown -R nginx:test /var/www/ドメイン名により、公開ディレクトリである/var/www/ドメイン名以降の全ファイルの所有者をnginx、グループをtestに変更します。

  2. その次に$ sudo find /var/www/ドメイン名 -type f -exec chmod 664 {} \;を実行することで、/var/www/ドメイン名内にある全ファイルのパーミッションを664に変更します。

    1. 上記コマンドが使えない場合は$ sudo find /var/www/ドメイン名 -type f -print | xargs chmod 664を実行します。
    2. パーミッション664では、所有者(Read,Write)、グループ(Read,Write)、その他(Read)となっています。
    3. より強いセキュリティを求める場合には664の代わりに604を使います。604では所有者(Read,Write)、グループ(none)、その他(Read)となっています。ただし、この場合はFTPユーザが一切何もできなくなります(FTP経由で閲覧も編集できない)。
    4. 上記に対して、SFTPユーザに閲覧権だけでも付与したい場合は644を設定するとSFTPユーザでもファイルの閲覧だけは可能となります。
    5. なお、共用サーバでは全ての利用者に共通のグループが割り当てられているケース もあるため、604に設定した方がいい場合もあります。
  3. ディレクトリもパーミッションを変更する必要があるため、$ sudo find /var/www/ドメイン名 -type d -exec chmod 2775 {} \;により/var/www/ドメイン名内の全ディレクトリのパーミッションを2775に変更します(2775では以降の新規ディレクトリにグループが継承されます)。

    1. 上記コマンドが使えない場合は$ sudo find /var/www/ドメイン名 -type d -print | xargs chmod 2775を実行します。
    2. より強いセキュリティを求める場合には775の代わりに705を使います。705では、所有者(Read,Write)、グループ(none)、その他(Read,Write)となっています。この場合もFTPユーザが一切何もできなくなります(FTP経由で閲覧も編集できない)。
    3. 上記に対して、SFTPユーザに閲覧権だけでも付与したい場合は755を設定するとSFTPユーザでもファイルの閲覧だけは可能となります。
    4. なお、共用サーバでは全ての利用者に共通のグループが割り当てられているケースもあるため、705に設定した方がいい場合もあります。

wp-config設定

  1. $ sudo mv /var/www/ドメイン名/apps/wp-config-sample.php /var/www/ドメイン名/apps/wp-config.phpによりWordPressの設定ファイルを作成します。

  2. そして$ sudo vim /var/www/ドメイン名/apps/wp-config.phpで編集します。

wp-config.php

/** 省略 **/

wp-config.conf
/** WordPress のためのデータベース名 */
define('DB_NAME', 'MariaDBで設定したWordPress用のデータベース名');

/** MySQL データベースのユーザー名 */
define('DB_USER', '上記DBに対応するユーザ名');

/** MySQL データベースのパスワード */
define('DB_PASSWORD', '上記ユーザ名に対応するパスワード');

/** MySQL のホスト名 */
define( 'DB_HOST', 'localhost' );

/** データベースのテーブルを作成する際のデータベースの文字セット */
define( 'DB_CHARSET', 'utf8mb4' );

/** 省略 **/

/**#@+
 * 認証用ユニークキー
 *
 * それぞれを異なるユニーク (一意) な文字列に変更してください。
 * {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org の秘密鍵サービス} で自動生成すること
もできます。
 * 後でいつでも変更して、既存のすべての cookie を無効にできます。これにより、すべてのユーザーを強制的に再ロ
グインさせることになります。
 *
 * @since 2.6.0
 */

/* 以下は https://api.wordpress.org/secret-key/1.1/salt/ で表示されたフレーズを設定する */
/* 一つ一つ行うのは手間なので、これらはコメントアウトして新規に貼り付けると楽 */
define( 'AUTH_KEY',         'put your unique phrase here' );
define( 'SECURE_AUTH_KEY',  'put your unique phrase here' );
define( 'LOGGED_IN_KEY',    'put your unique phrase here' );
define( 'NONCE_KEY',        'put your unique phrase here' );
define( 'AUTH_SALT',        'put your unique phrase here' );
define( 'SECURE_AUTH_SALT', 'put your unique phrase here' );
define( 'LOGGED_IN_SALT',   'put your unique phrase here' );
define( 'NONCE_SALT',       'put your unique phrase here' );

/**#@-*/

/**
 * WordPress データベーステーブルの接頭辞
 *
 * それぞれにユニーク (一意) な接頭辞を与えることで一つのデータベースに複数の WordPress を
 * インストールすることができます。半角英数字と下線のみを使用してください。
 */
/* デフォルトのwp_ではセキュリティ上よくないので変更する */
/* 記号が交じるとエラーが発生するので注意 */
$table_prefix = '適当な半角英数字を入力_';

/** 省略 **/

wp-config.phpはコア設定であることから、非常に大切なファイルです。そのため、セキュリティを高めておく必要があります。

ここでは$ sudo chmod 600 /var/www/ドメイン名/apps/wp-config.phpを実行することで所有者以外は読み書きできないようにします(今後編集する予定もない場合は400に設定)。

WordPressはセキュリティが脆弱だとよく言われていますが、対策すれば一定の安全性は確保できます。セキュリティを高める方法については後述します。

これにより最初の設定は完了したため、$ sudo service nginx restart$ sudo service mysql restart$ sudo service php8.1-fpm restartでそれぞれのサービスを再起動します。

この際にエラーが発生していなければ、ウェブサイト(ドメイン名/apps)にアクセスした時にWordPressの初期設定が表示されます。

説明は省きますが、一応ここでユーザ名とパスワード、メールアドレスを先に設定しておきましょう。

また、ログイン後は設定パーマリンクからパーマリンク設定の共通設定にて基本『以外』を選択しましょう(404ページが機能しないため)。

更に現在の設定ではWordPressが表示されるのはドメイン名/apps限定であることから、 ドメイン名のみで表示を行う設定を行います。

$ sudo vim /var/www/ドメイン名/apps/wp-config.phpでもう一度設定ファイルを編集します。

wp-config.php

/** 省略 **/

/* カスタム値は、この行と「編集が必要なのはここまでです」の行の間に追加してください。 */

define( 'WP_SITEURL', "https://{$_SERVER['HTTP_HOST']}/apps" );
define( 'WP_HOME', "https://{$_SERVER['HTTP_HOST']}" );

/* 編集が必要なのはここまでです ! WordPress でのパブリッシングをお楽しみください。 */

/** 省略 **/

編集が終わったら$ ln -s /var/www/ドメイン名/apps/index.php /var/www/ドメイン名/index.phpを実行することによりappsディレクトリ内にあるindex.phpを公開ディレクトリ直下にシンボリックリンクを配置します。

そしてシンボリックリンクを反映させるために$ sudo service nginx restartでNginxを再起動します。

この状態でドメイン名のみでアクセスした際、WordPressサイトが表示されていれば最初の構築は完了です。

phpMyAdminのインストール

WordPressが動作するようになりましたが、現状データベースはコンソール上でしか編集できないため少々不便です。

そのため、ブラウザでデータベースを管理するためのツールであるphpMyAdminをインストールします。

Ubuntu上のリポジトリでもインストールできますが、バージョンが古いので(Ubuntu20.04では4.9.5)公式サイトから直接ダウンロードを行います。

  1. まず$ cd /usr/shareに移動します。

  2. 執筆時点でのphpMyAdminの最新版は5.1.2です。これを/usr/share$ sudo wget https://files.phpmyadmin.net/phpMyAdmin/5.1.2/phpMyAdmin-5.1.2-all-languages.tar.gzによってダウンロードします。

  3. ダウンロードが終われば/usr/share$ sudo tar zxvf phpMyAdmin-5.1.2-all-languages.tar.gzで圧縮ファイルを解凍します。

  4. 解凍後、ディレクトリが生成されていますが、その名前をphpmyadminに変更するため/usr/share$ sudo mv phpMyAdmin-5.1.2-all-languages phpmyadminを実行します。

    1. 圧縮ファイルは必要無ければ/usr/share$ sudo rm /usr/share/phpMyAdmin-5.1.2-all-languages.tar.gzで削除します。
  5. 次に、ウェブサーバで公開するために$ sudo ln -s /usr/share/phpmyadmin /var/www/ドメイン名/appsでシンボリックリンクを作成します。NginxのlocationでphpMyAdminへのroot(/usr/share/phpmyadmin)を指定しても良いですが、シンボリックリンクの方が楽に設置できます。

    1. この時点でhttp(s)://ドメイン名/apps/phpmyadminというURLでアクセスすることができます(WPのデータベースに対するユーザ名とそのパスワード)が、完全に利用可能となったわけではないので、追加の設定を行います。
  6. 現在のパーミッションではエラーが発生する可能があるため、$ sudo chmod -R 775 /usr/share/phpmyadminで変更しておきます。

  7. 同時に$ sudo chown nginx:SFTPユーザ名 -R /usr/share/phpmyadmin/で所有権も変更します。

環境保管領域が完全に設定されていないを解決する

phpMyAdminにログインした際、phpMyAdmin 環境保管領域が完全に設定されていないため、いくつかの拡張機能が無効になっています。理由についてはこちらをご覧ください。代わりにデータベースの操作タブを使って設定することもできます。 という文章が表示されている場合があります。

これを解決するには特定のデータベースを作り、その管理権限を持ったユーザーを作成する必要があります。

  1. まず$ sudo mysql -u rootでMariaDB(MySQL)にログインします。

    1. rootパスワードありの場合は$ sudo mysql -u root -pを使います。
  2. 次にMariaDB [(none)]> source /usr/share/phpmyadmin/sql/create_tables.sqlを実行してデータベースの作成を行います。

    1. MariaDB [(none)]> show databases;phpmyadminが存在していれば正常です。
  3. それが終われば、MariaDB [(none)]> GRANT SELECT, INSERT, UPDATE, DELETE ON phpmyadmin.* TO "DB-NAME"@localhost;で、DB-NAMEの部分にはWordPressのデータベースに使っているユーザ名を記入します。

  4. MariaDB [(none)]> use phpmyadminで先程作ったデータベースに切り替えます。

    1. MariaDB [(phpmyadmin)]> show tables;pma_という名前のテーブル群が存在することを確認できます。
  5. 完了したらMariaDB [(phpmyadmin)]> select host,user,password from mysql.user;をそのまま実行してアクセス可能なホストおよびユーザとパスワードをチェックします。

  6. 最後にMariaDB [(phpmyadmin)]> exitでMariaDBから離脱します。

    1. flush privilegesINSERTUPDATEDELETEの時に必要になるため、その他のコマンドでは不要です。
  7. その後、phpMyAdminにログインした際に環境保管領域が完全に設定されていないの文章が消えていれば解決です。

暗号化 (blowfish_secret) 用の非公開パスフレーズの設定

上記の他に、暗号化用公開パスフレーズの設定が必要という文章がphpMyAdminに表示される場合があります。

これはphpMyadminのconfig.inc.phpを編集する必要があります。

  1. まず最初に、パスフレーズを生成するために$ openssl rand -hex 32で32文字(もしくはそれ以上)のランダムな文字列を生成します。

  2. そして$ sudo cp /usr/share/phpmyadmin/config.sample.inc.php /usr/share/phpmyadmin/config.inc.phpconfig.sample.inc.phpconfig.inc.phpとしてコピーします。

  3. コピーできれば$ sudo vim /usr/share/phpmyadmin/config.inc.phpで編集を行います。

config.inc.php

/** 省略 **/

/**
 * This is needed for cookie based authentication to encrypt password in
 * cookie. Needs to be 32 chars long.
 **/
$cfg['blowfish_secret'] = 'ここに作成した32文字以上のパスフレーズを入れる'; /* YOU MUST FILL IN THIS FOR COOKIE AUTH! */

/** 省略 **/
  1. 編集後、再びphpMyAdminにログインすれば、同文章が表示されなくなっています。

phpMyAdminのセキュア設定

これまでの設定では、phpMyAdminはそのままウェブで公開されているため、誰もがログイン画面を閲覧できます。

これはセキュリティ上、非常に危険なことを意味しており、セキュリティ対策を行っておく必要があります。

ディレクトリ名を変更する

現在、ウェブで公開されているphpMyAdminはドメイン名/apps/phpmyadminでアクセスすることができます。

これはphpmyadminという名前を直接狙った攻撃を受けてしまいます。そのため、名前を変更しなければなりません。

その方法は$ sudo mv /var/www/ドメイン名/apps/phpMyAdmin /var/www/ドメイン名/apps/$(openssl rand -hex 16)phpmyadminというディレクトリ名を16文字のランダムな文字列に変更するというものです。

変更された名前は$ ls /var/www/ドメイン名/appsでチェックできます。

これにより、名前を直接狙った攻撃を避けることが可能です。しかしこれだけでは当然不十分です。

Basic認証とIP制限を追加する

ディレクトリの名前を変更したとしても、アクセスできれば普通にログイン画面が表示されてしまいます。

たとえphpMyAdminでパスワードを設定していても、それを突破されてはお終いなので、Basic認証を追加することで二重で防御を行います。

ただ、Basic認証はBase64でIDおよびパスワードをエンコードするだけであり、ハッシュ化も暗号化もしていないので、現代のセキュリティにとって十分な強度を保っていません。

しかし、TLS化すると通信の暗号化が提供されるため、この問題は解消されます。

そのため、HTTPS(TLS)では無く、HTTPでの使用は完全に筒抜けとなってしまうため推奨できません。

仮にHTTPで認証を行う場合は、Basic認証ではなく、MD5を用いてハッシュ化を行うDigest認証が推奨されます(今の時代ではHTTPで使う方が珍しいとは思いますが)。

  1. ではまず、$ openssl passwdでBasic認証に使うパスワードを入力します。

    1. 一度入力するとVerifying - Password:が出てくるので、同じパスワードを入力します。
    2. 二度入力後にランダムな文字列が出てきますが、これは必要となりますのでメモしておきます。
    3. なお、SSHログインのパスワードと同じにはできません。
  2. 次に$ sudo vim /etc/nginx/pma_passでBasic認証に使うユーザ名と先程のパスワードの『暗号化された文字列』を入力します。

    pma_pass

    Basic認証に使うユーザ名(自由):暗号化後のパスフレーズ
    
  3. 編集が完了すれば、今度は$ sudo vim /etc/nginx/conf.d/ドメイン名.confでNginxの設定を行います。

    1. コメントアウトしていますが、allowおよびdeny allでのIP制限も行うことができます。接続の許可するIPをallowし、その他のIPはdeny allで全て排除します。これによりセキュリティが更に向上します。

ドメイン名.conf

## 省略 ##

# "/名前"もしくは"/名前/"を含んだURL
location ~ /名前を変更したphpMyAdminの名前(?:$|/) {

    # 閲覧許可するIPを入力する
    # allow 許可するIPアドレスを入力;

    # 許可していないIPはすべて排除
    # deny all;

    # 左から順に参照し、該当が無ければ最後のuri(もしくはcode)に内部転送される
    try_files       $uri $uri/ /index.php?$args;

    # Basic認証を設定
    auth_basic      "login";

    # Basic認証のユーザファイルを指定
    auth_basic_user_file    /etc/nginx/pma_pass;

    location ~ \.php$ {
        # 左から順に参照し、該当が無ければ最後のuri(もしくはcode)に内部転送される
        try_files       $uri $uri/ /index.php?$args;

        # PHPに渡すパラメータファイルを指定
        include fastcgi_params;

        # FASTCGIサーバーへのキープアライブ接続をON
        fastcgi_keep_conn on;

        # スラッシュで終わるURIの後にFASTCGIが要求するファイル名
        fastcgi_index   index.php;

        # fastcgi_path_info変数の値をキャプチャする正規表現の定義
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;

        # FastCGIサーバーに渡すパラメータを指定
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        # コード300以上のレスポンスをerror_pageディレクティブで処理
        fastcgi_intercept_errors on;

        # FastCGIサーバーのアドレスをUnixドメインソケットのパスに指定
        fastcgi_pass unix:/run/php/php8.1-fpm.sock;

    }

}

## 省略 ##

  1. 設定が完了したら$ sudo service nginx restartでNginxを再起動します。

  2. エラーが発生していなければ、次にphpmyadminにアクセスしてみましょう。正しく設定されていれば、この時点でBasic認証によってユーザIDとパスワードが要求されています。そして、正しいIDとパスワードを入力できたらphpMyAdminのログイン画面へと移動します。

これら設定により、phpMyAdminは無対策時と比べてある程度の安全性を確保することができます。

WEBサーバとDBサーバに分ける(予備)

前回の記事ではプライベートネットワークを用い、静的コンテンツを配信する専用サーバを作成しました。

これを応用すると、WordPressを実行表示するサーバ(NginxとPHP)と、MariaDB(MySQL)を実行するデータベースサーバに分けて稼働させることができます。

サーバが2台必要となりますが、この方法を用いることでサーバの負荷を分散するだけでなく、ウェブサイトに問題が発生した際、それぞれが分離していることで発生原因を特定しやすいというメリットがあります。

このセクションでは、プライベートネットワークに存在する役割の異なる2台のサーバを使い、WordPressが利用可能となるまでの手順を紹介します。

なお、DBサーバ側はデータベースのみ実行するため、MariaDB(MySQL)とphpMyAdmin(必要であれば)のインストールを行うだけであり、NginxもPHPも必要ありません。

逆にウェブ(+アプリケーション)サーバ側はNginxとPHPおよびMariaDB(MySQL)が必要です。

また、既存のWordPressデータベースを使用する場合は、WordPressのDBをphpMyAdminからSQLファイルをエクスポートしておきましょう。

その際、エクスポート方法詳細を選び、生成オプションにはDROP TABLE / VIEW / PROCEDURE / FUNCTION / EVENT / TRIGGER コマンドを追加するにチェックを入れます。

ファイルのサイズが気になる場合は圧縮gzip方式を選べばサイズを縮小して保存できます。

プライベートネットワークで接続する

これは前回の記事と全く同じなので、そちらを参照してください。

この設定は$ ping 相手サーバのプライベートIPアドレスをお互いに行い、どちらも0% packetlossとなるポイントまで進めます。

データベース接続設定

ウェブサーバ側がDBサーバに接続するための設定を行う必要があります。

  1. 『DBサーバ』側で$ sudo mysql -u rootを実行してデータベースにログインします。

    1. rootパスワードありの場合は$ sudo mysql -u root -pを実行します。
  2. ログインしたらMariaDB [(none)]> GRANT ALL PRIVILEGES ON `DB-NAME`.* TO "USER-NAME"@"WEB-SERVER-PRIVATE-IP" IDENTIFIED BY "DB-PASS";でデータベースを作成します。

    1. DB-NAMEの部分は使用するWordPressのデータベース名を入力します(例ではexample.comなどのドメイン名)。
    2. USER-NAMEの箇所には新規ユーザ名を記入します(例ではここもドメイン名)。
    3. WEB-SERVER-PRIVATE-IPの部分はウェブサーバのプライベートIPアドレスに変更します。
    4. DB-PASSにはUSER-NAMEに対応するDBログイン用パスワードを新規に設定します。
    5. GRANT文にはWITH GRANT OPTION(アクセス権の継承)を追加しているケースがよく見られますが、権限管理を簡潔にするため、ここでは付与していません。
  3. 追加できたらMariaDB [(none)]> select user, host from mysql.user;でアクセス可能なホストおよびユーザをチェックします。

  4. MariaDB [(none)]> exitでデータベースから離脱します。

    1. GRANT文では実行後すぐにテーブルが更新されるため、flush privilegesは必要ありません。
  5. 次に『DBサーバ』側で$ sudo ufw allow in proto tcp from ウェブサーバのプライベートIPアドレス to any port 3306を実行し、ウェブサーバがDBサーバのデータベースに接続できるように、UFW(ファイアウォール)の設定を行います。

  6. それに加えて、同じく『DBサーバ』側で$ sudo vim /etc/mysql/mariadb.conf.d/50-server.cnfを編集します。

50-server.cnf

## 省略 ##

# Instead of skip-networking the default is now to listen only on
# localhost which is more compatible and is not less secure.
# bind-addressをコメントアウトする
# bind-address            = 127.0.0.1

## 省略 ##
  1. 編集が完了したら$ sudo service mysql restartでMariaDBを再起動します。

  2. 次に『ウェブサーバ』側で$ sudo mysql -u 作成したDBに対応するユーザ名 -h DBサーバプライベートIPアドレス -pを実行してDBサーバのデータベースにアクセスします。

    1. データベースに無事接続できれば、ウェブサーバ(MariaDB)とDBサーバ(MariaDB)は繋がっています。
  3. もしphpMyAdminを使う場合で環境保管領域に対応するには、『DBサーバ』側で$ sudo mysql -u rootでMariaDBにログインしたあと、MariaDB [(none)]> source /usr/share/phpmyadmin/sql/create_tables.sqlを使ってphpmyadminのテーブルを作成します。

  4. 次にMariaDB [(none)]> GRANT SELECT, INSERT, UPDATE, DELETE ON phpmyadmin.* TO `USER-NAME`@'WEB-SERVER-PRIVATE-IP';で、USER-NAMEはWPのデータベースを使用しているユーザ名、WEB-SERVER-PRIVATE-IPにはウェブサーバのプライベートIPアドレスを入力します。

  5. 作成後はMariaDB [(none)]> exitで離脱し、$ sudo vim /usr/share/phpmyadmin/config.inc.phpconfig.inc.phpを編集します。

    1. もしconfig.inc.phpが存在しない場合は$ sudo cp /usr/share/phpmyadmin/config.sample.inc.php /usr/share/phpmyadmin/config.inc.phpconfig.sample.inc.phpconfig.inc.phpとしてコピーします。

config.inc.php

/** 省略 **/

/**
 * First server
 */
$i++;
/* Authentication type */
$cfg['Servers'][$i]['auth_type'] = 'cookie';
/* Server parameters */
$cfg['Servers'][$i]['host'] = 'ウェブサーバのプライベートIPアドレス;
$cfg['Servers'][$i]['compress'] = false;
$cfg['Servers'][$i]['AllowNoPassword'] = false;

/** 省略 **/
  1. hostをウェブサーバのプライベートIPアドレスに変更することで、ウェブサーバからphpMyAdminを編集することができます。

  2. そしてこれが最後の作業であり、『ウェブサーバ』側で$ sudo vim /var/www/ドメイン名/wp-config.phpによって、wp-config.phpを編集します。

wp-config.php

// ** MySQL 設定 - この情報はホスティング先から入手してください。 ** //
/** WordPress のためのデータベース名 */
define('DB_NAME', 'MariaDBで設定したWordPress用のデータベース名');

/** MySQL データベースのユーザー名 */
define('DB_USER', '上記DBに対応するユーザ名');

/** MySQL データベースのパスワード */
define('DB_PASSWORD', '上記ユーザ名に対応するパスワード');

/** MySQL のホスト名 */
define( 'DB_HOST', 'DBサーバのプライベートIPアドレス' );

/** データベースのテーブルを作成する際のデータベースの文字セット */
define( 'DB_CHARSET', 'utf8mb4' );

/** 省略 **/

/**#@+
 * 認証用ユニークキー
 *
 * それぞれを異なるユニーク (一意) な文字列に変更してください。
 * {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org の秘密鍵サービス} で自動生成すること
もできます。
 * 後でいつでも変更して、既存のすべての cookie を無効にできます。これにより、すべてのユーザーを強制的に再ロ
グインさせることになります。
 *
 * @since 2.6.0
 */

/* 以下は https://api.wordpress.org/secret-key/1.1/salt/ で表示されたフレーズを設定する */
/* 一つ一つ行うのは手間なので、これらはコメントアウトして新規に貼り付けると楽 */
define( 'AUTH_KEY',         'put your unique phrase here' );
define( 'SECURE_AUTH_KEY',  'put your unique phrase here' );
define( 'LOGGED_IN_KEY',    'put your unique phrase here' );
define( 'NONCE_KEY',        'put your unique phrase here' );
define( 'AUTH_SALT',        'put your unique phrase here' );
define( 'SECURE_AUTH_SALT', 'put your unique phrase here' );
define( 'LOGGED_IN_SALT',   'put your unique phrase here' );
define( 'NONCE_SALT',       'put your unique phrase here' );

/**#@-*/

/**
 * WordPress データベーステーブルの接頭辞
 *
 * それぞれにユニーク (一意) な接頭辞を与えることで一つのデータベースに複数の WordPress を
 * インストールすることができます。半角英数字と下線のみを使用してください。
 */
/* デフォルトのwp_ではセキュリティ上よくないので変更する */
/* 記号が交じるとエラーが発生するので注意 */
$table_prefix = '適当な半角英数字を入力_';

/** 省略 **/
  1. これにより、ウェブサーバとDBサーバを分離しつつ、お互いに接続を行いながらWordPressを運用することが可能になりました。

WordPress高速化設定

WordPressは便利なツールですが、PHPというアプリケーションを使って動的にページを生成することから、動作が重いとよく言われています。

単純にサーバを強化することである程度は解消しますが、様々なチューニングを行うことでもWordPressを高速化させることができます。

このセクションでは、その方法をいくつか紹介します。

FastCGI Cacheの導入

FastCGI CacheはNginx特有のキャッシュシステムです。

WordPressはPHPを用いていますが、動的にページを生成する(アクセスごとに生成)ため、その生成にかかる時間がネックとなります。

ここでFastCGI Cacheが活躍します。

このキャッシュシステムを利用すると、一度生成された内容をNginxがサーバ側でキャッシュし、その内容をクライアントにそのまま返すことから、デメリットとなる時間が大幅に短縮され、ページの表示が大変高速になります。

サーバでキャッシュを保存するという性質から、これはクライアントだけでなく、サーバ側の負担も軽減されるため、非常に大きなメリットがあります。

今回の設定ではモバイルとPC別にキャッシュを分けるようにしています。

これにより、モバイルとPCでウェブサイトの表示を変更している場合でも、正しくキャッシュを表示できます(不要な場合は該当箇所をコメントアウトしてください)。

※FastCGI Cacheでのキャッシュを消去するWordPressのプラグインを使う場合、今回設定しているモバイルキャッシュも消す設定を加える必要があります。

  1. まずは$ sudo vim /etc/nginx/nginx.confでNginxのコア設定を編集します。内容はリバースプロキシの時とやや類似しています。

nginx.conf

## 省略 ##

### httpディレクティブ内に記述する ###

# FastCGI Cache Settings

    # プロキシキャッシュのパスを指定し、階層レベルを1:2
    # キャッシュゾーン名をwp_cache、そのゾーンのメモリ使用を1MB、全キャッシュの最大量を1GB
    # アクセスが1日無ければ該当キャッシュを削除
    fastcgi_cache_path /var/cache/nginx/fastcgi-cache levels=1:2 keys_zone=wp_cache:1m max_size=1g inactive=1d;

    # キャッシュ時に使用するキーを設定
    # 今回の例ではモバイルとPCでキャッシュを分けるための設定を加えている($mobile_)
    fastcgi_cache_key "$mobile_$scheme$request_method$host$request_uri";

    # キャッシュを効かせるために特定のリクエストヘッダを無効にする
    fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

# /FastCGI Cache Settings

## 省略 ##
  1. 次に$ sudo vim /etc/nginx/conf.d/ドメイン名.confでも編集します。
## 省略 ##

### メインのServerディレクティブ内に記述する ###

# FASTCGI CACHE SETTINGS

    # レスポンスを読み取るために使用されるバッファの数とサイズ
    fastcgi_buffers 4 256k;

    # 受信したレスポンスの最初の部分を読み取るために使用されるバッファサイズ
    fastcgi_buffer_size 128k;

    # レスポンスがビジー(応答待ち)時の最大バッファサイズ
    fastcgi_busy_buffers_size 256k;

    # バッファリング時の一時ファイル書き込み最大サイズ
    fastcgi_temp_file_write_size 256k;

    # キャッシュを返送しないパラメータを指定
    fastcgi_cache_bypass $skip_cache;

    # キャッシュしないパラメータを指定
    fastcgi_no_cache $skip_cache;

    # キャッシュのゾーン名を指定
    fastcgi_cache wp_cache;

    # ステータスごとの最大キャッシュ時間(1日、1時間、5分)
    fastcgi_cache_valid 200 302 1d;
    fastcgi_cache_valid 301 1h;
    fastcgi_cache_valid any 5m;

    # 同一のリクエストが同時に要求された場合、各リクエストを一つにまとめる
    fastcgi_cache_lock on;

    # fastcgi_cache_lockのタイムアウト時間。指定した時間以上経過でキャッシュせずに返却
    fastcgi_cache_lock_timeout 5s;

    # 指定したパラメータが発生した際に古いキャッシュを使用
    fastcgi_cache_use_stale error timeout invalid_header updating http_500;

    # 設定したリクエストヘッダを付与(X-Cache)
    # $upstream_cache_statusはキャッシュの状態を表している
    # MISS(キャッシュ無し)、HIT(キャッシュ使用)、BYPASS(キャッシュを返送しない)
    add_header X-Cache $upstream_cache_status;

    # ユーザーエージェント(利用者の使用プログラム)によって変化するVaryヘッダを付与
    # PCとモバイルで分けている場合に使用する
    add_header Vary "User-Agent";

    # $skip_cacheの初期値を"0"に設定し、初期状態でFastCGI Cacheを有効化
    set $skip_cache 0;

    # モバイルでアクセスした際のキャッシュ設定を追加する
    set $mobile_ "";
    if ($http_x_wap_profile ~ "^[a-z0-9\"]+") {
        set $mobile_ "mobile.";
    }
    if ($http_profile ~ "^[a-z0-9\"]+") {
        set $mobile_ "mobile.";
    }
    if ($http_user_agent ~ "^.*(?:2\.0\ MMP|240x320|400X240|AvantGo|BlackBerry|Blazer|Cellphone|Danger|DoCoMo|Elaine/3\.0|EudoraWeb|Googlebot-Mobile|hiptop|IEMobile|KYOCERA/WX310K|LG/U990|MIDP-2\.0|MMEF20|MOT-V|NetFront|Newt|Nintendo\ Wii|Nitro|Nokia|Opera\ Mini|Palm|PlayStation\ Portable|portalmmm|Proxinet|ProxiNet|SHARP-TQ-GX10|SHG-i900|Small|SonyEricsson|Symbian\ OS|SymbianOS|TS21i-10|UP\.Browser|UP\.Link|webOS|Windows\ CE|WinWAP|YahooSeeker/M1A1-R2D2|iPhone|iPod|Android|BlackBerry9530|LG-TU915\ Obigo|LGE\ VX|webOS|Nokia5800).*") {
        set $mobile_ "mobile.";
    }
    if ($http_user_agent ~ "^(?:w3c\ |w3c-|acs-|alav|alca|amoi|audi|avan|benq|bird|blac|blaz|brew|cell|cldc|cmd-|dang|doco|eric|hipt|htc_|inno|ipaq|ipod|jigs|kddi|keji|leno|lg-c|lg-d|lg-g|lge-|lg/u|maui|maxo|midp|mits|mmef|mobi|mot-|moto|mwbp|nec-|newt|noki|palm|pana|pant|phil|play|port|prox|qwap|sage|sams|sany|sch-|sec-|send|seri|sgh-|shar|sie-|siem|smal|smar|sony|sph-|symb|t-mo|teli|tim-|tosh|tsm-|upg1|upsi|vk-v|voda|wap-|wapa|wapi|wapp|wapr|webc|winw|winw|xda\ |xda-).*") {
        set $mobile_ "mobile.";
    }
    if ($http_user_agent ~ "^(?:DoCoMo/|J-PHONE/|J-EMULATOR/|Vodafone/|MOT(EMULATOR)?-|SoftBank/|[VS]emulator/|KDDI-|UP\.Browser/|emobile/|Huawei/|IAC/|Nokia|mixi-mobile-converter/)") {
        set $mobile_ "mobile.";
    }
    if ($http_user_agent ~ "DDIPOCKET\;|WILLCOM\;|Opera\ Mini|Opera\ Mobi|PalmOS|Windows\ CE\;|PDA\;\ SL-|PlayStation\ Portable\;|SONY/COM|Nitro|Nintendo") {
        set $mobile_ "mobile.";
    }

    # GETリクエスト以外はFastCGI Cacheを使わない
    if ($request_method != "GET") {
        set $skip_cache 1;
    }

    # クエリ文字列を含む場合はFastCGI Cacheを使わない
    if ($query_string != "") {
        set $skip_cache 1;
    }

    # WordPressでのログイン中やコメント直後、投稿がパスワード保護されている場合にはFastCGI Cacheを使わない
    if ($http_cookie ~ "wordpress_logged_in_|comment_author_|wp-postpass") {
        set $skip_cache 1;
    }

    # WordPress関連のファイルではFastCGI Cacheを使わない
    if ($request_uri ~ "/(?:wp-json(?:$|/)|wp-admin(?:$|/)|xmlrpc\.php|wp-(?:app|cron|login|register|mail)\.php|wp-.*\.php|(?:feed|rss2?|rdf|atom)(?:$|/)|index\.php|wp-comments-popup\.php|wp-links-opml\.php|wp-locations\.php|sitemap(_index)?\.xml|[a-z0-9_-]+-sitemap([0-9]+)?\.xml)") {
        set $skip_cache 1;
    }

## 省略 ##
  1. 設定後、$ sudo service nginx restartでNginxを再起動します。

エラーが発生していなければ、ウェブサイトアクセス時にFastCGI Cacheが動作してキャッシュを構築します。

キャッシュを無効化していない限り、明らかに動作が早くなったことを確認できるはずです(設定ではログイン中だと無効化しているので注意)。

ただし、これはPHPで動的に生成されたページのみをキャッシュするため、静的ページやCSSや画像などの静的ファイル、そしてNginxとPHPを経由しないページではFastCGI Cacheは使うことができません。

また、FastCGI Cacheは個人情報が含まれるページ(ログインページなどの専用ページ等)もキャッシュすることができるため、そのような情報が入っているページは基本的にキャッシュしてはいけません。

FastCGI Cacheの命名規則

FastCGI Cacheの命名規則はfastcgi_cache_keyの設定に基づいています。

例えばfastcgi_cache_key "$scheme$request_method$host$request_uri";に設定している場合、https://example.com/test.phpというURLをキャッシュした場合にはfastcgi_cache_key "httpsGETexample.com/test.php";といった形になります。

このときのキャッシュキーはhttpsGETexample.com/test.phpです。

つまり、$schemehttphttpsftpなどを出力し、$request_methodGETPOST等のリクエストを表し、$hostはIPやドメイン名、そして$request_uriはホスト名以降のパスやクエリを意味します。

そして生成されたキーはMD5を通してハッシュ化されます。

httpsGETexample.com/test.phpであれば、MD5ハッシュとして3783e700451e905dba61c1fb80a74a65が表示されます。

続いて、fastcgi_cache_path /var/cache/nginx/fastcgi-cache levels=1:2という設定ではlevels=1:2が指定されており、これは最大2階層のディレクトリにそれぞれキャッシュのMD5ハッシュの一部が付与されます。

今回の例では1:2であることから、ディレクトリの第1階層にはキャッシュに対応するMD5ハッシュのうち、最後の1文字が付与されます。

そして第2階層では第1階層の文字を除いた、MD5ハッシュの最後2文字で命名されています。

最終的なキャッシュディレクトリの全体構造は以下のとおりです。

/var/cache/nginx/fastcgi-cache/5/a6/3783e700451e905dba61c1fb80a74a65

これを上手く利用することで、一見複雑に見える構造でも、思ったより簡単にキャッシュ制御を行うことができます。

FastCGI Cacheの削除設定

便利なFastCGI Cacheですが、現在の設定では時間経過でしかキャッシュが消去されません。

つまり、基本的にキャッシュが残っている限りは記事を更新してもそれが反映されません。

これはこれで不便なので、FastCGI Cacheが消去される条件を追加で設定します。

方法は大きく分けて3つあり、一つはWordPressテーマ内のfunction.php内に書き込む方法、もう1つが外部PHPファイルを新規に作成し、それを直接実行する方法、そして3つ目がプラグインのNginx Helperを使う方法です。

なお、サーバによる別のキャッシュが効いている時や負荷状況によっては、キャッシュの削除が即座に反映されない場合があります。

また、Nginxの作成したキャッシュディレクトリにPHPがアクセスできることを前提としています(同じ実行ユーザ)。

functions.phpを用いたキャッシュ削除

まずはfunctions.phpでの方法を紹介します。

これはプラグインを使いたくない人におすすめで、記事編集後にトップページと編集した記事のキャッシュを削除します。

functions.php

// 記事編集時、自動でFastCGIキャッシュを削除
// How to Setup FastCGI Caching with Nginx on your VPS
// https://www.digitalocean.com/community/tutorials/how-to-setup-fastcgi-caching-with-nginx-on-your-vps
// 28日目:nginx+FastCGIでRCCのサイトを高速化!
// http://www.rcc.ritsumei.ac.jp/2019/1228_10813/
function nginx_cache_purge($post_id){

// 更新した記事のURLを取得
$post_link = get_permalink($post_id);

// FastCGI Cacheのpathを指定
// pathの最後に"/"を付けること
$cache_path = "/var/cache/nginx/fastcgi-cache/";

// ウェブサイトがhttpかhttpsか
$scheme = "https";

// ウェブサイトのホスト名
$host = "example.com";

// 置換でパーマリンクのみを残す
$replace = str_replace($scheme . "://" .  $host, '', $post_link);

// MD5でハッシュ化(トップページ用)
$hash_top = md5($scheme . "GET" . $host . "/");

// MD5でハッシュ化(トップページモバイル用)
$hash_top_m = md5("mobile." . $scheme . "GET" . $host . "/");

// MD5でハッシュ化(記事ページ用)
$hash_page = md5($scheme . "GET" . $host . $replace);

// MD5でハッシュ化(記事ページモバイル用)
$hash_page_m = md5("mobile." . $scheme . "GET" . $host . $replace);

// トップページのキャッシュを削除
array_map('unlink', glob($cache_path . substr($hash_top, -1) ."/". substr($hash_top,-3,2) ."/". $hash_top));

// トップページのキャッシュを削除(モバイル版のキャッシュ)
array_map('unlink', glob($cache_path . substr($hash_top_m, -1) ."/". substr($hash_top_m,-3,2) ."/". $hash_top_m));

// 更新した記事のキャッシュを削除
array_map('unlink', glob($cache_path . substr($hash_page, -1) ."/". substr($hash_page,-3,2) ."/". $hash_page));

// 更新した記事のキャッシュを削除(モバイル版のキャッシュ)
array_map('unlink', glob($cache_path . substr($hash_page_m, -1) ."/". substr($hash_page_m,-3,2) ."/". $hash_page_m));

}
/* 新規投稿、更新等のステータス変更で削除処理が発動 */
add_action( 'new_to_publish', 'nginx_cache_purge' );
add_action( 'publish_to_publish', 'nginx_cache_purge' );
add_action( 'pending_to_publish', 'nginx_cache_purge' );
add_action( 'draft_to_publish', 'nginx_cache_purge' );
add_action( 'auto-draft_to_publish', 'nginx_cache_purge' );
add_action( 'future_to_publish', 'nginx_cache_purge' );
add_action( 'publish_to_trash', 'nginx_cache_purge' );
add_action( 'post_updated' , 'nginx_cache_purge');
add_action( 'edit_post' , 'nginx_cache_purge');

以上の設定により、新規投稿や記事の更新等を行うと同時に、トップページと編集した記事のキャッシュが自動で削除されます。

外部PHPファイルでキャッシュ削除

2つ目のPHPファイルを作成実行する方法ですが、上記の条件によらずキャッシュを一括削除したい場合には、purge-cache.php(名前は自由)という以下のような内容のファイルを作成し、/purge-cache.phpにアクセスすればすべてのFastCGI Cacheを手動で全削除することができます。

purge-cache.php

<?php

$allow_ips = [
    // ログイン画面にアクセスを許可するIPアドレスを定義(未定義の場合は全ての接続元を許可する)
    // cf. '123.123.123.123', ...

];

// WordPressの機能を使うために"wp-load.php"を読み込む
require_once dirname( __FILE__ ) . '/wp-load.php';

// IP制限していない、もしくは許可されたIPの場合
if ( empty( $allow_ips ) || ( ! empty( $allow_ips ) && in_array( $_SERVER['REMOTE_ADDR'], $allow_ips, true ) ) ) {

    // キャッシュをディレクトリ含め全削除する
    array_map('unlink', glob("/var/cache/nginx/fastcgi-cache/*/*/*"));
    array_map('rmdir', glob("/var/cache/nginx/fastcgi-cache/*/*"));
    array_map('rmdir', glob("/var/cache/nginx/fastcgi-cache/*"));

    // WordPress管理ページのURLを取得
    $url = get_admin_url();

    // WordPress管理ページへリダイレクトする
    header('Location: ' . $url, true, 302);

} else {
// アクセスを拒否された場合

    // ステータスコードを404に変更する
    status_header( '404' );

    // テンプレート内の404ページにリダイレクトする
    include( get_query_template( '404' ) );

    // 処理を停止する
    exit();

}

これに加えてfunctions.phpにキャッシュ削除用のAdminBarMenuボタンを追加することで、ワンクリックで全削除が可能になります。

functions.php

// 上部AdminBarMenuにキャッシュ削除用ボタンを追加
function add_admin_bar_menu($wp_admin_bar){

    // ユーザ削除権限を持ったユーザのみ、キャッシュ削除ボタンを表示
    if (current_user_can('delete_users')){
        $wp_admin_bar->add_node( array(
            'id'    => 'delete_cache',
            'title' => 'FastCGI Cacheを全削除する',
            'href'  => '/apps/purge-cache.php',
        ));
    }
}
// "99"と多めに設定することでAdminBarリストの後半にボタンを表示させる
add_action( 'admin_bar_menu', 'add_admin_bar_menu', 99 );

もしNginxでFastCGI Cacheを有効にしている場合は正しい表示ができなくなるため、キャッシュ削除用ファイルをキャッシュしない設定が必要です。

$ sudo vim /etc/nginx/conf.d/ドメイン名.confで編集を行います。

ドメイン名.conf

## 省略 ##

# 設置する位置によっては"$skip_cache"が上書きされてしまうので注意
# キャッシュ削除用ファイルにはFastCGI Cacheを使わない
if ($request_uri ~ "/purge-cache\.php$") {
    set $skip_cache 1;
}

## 省略 ##

編集が完了したら$ sudo service nginx restartで再起動します。

他にもコマンドラインを使いつつ個別に削除したい場合は、以下のPHPファイルを作成して任意の場所に配置します。

cli-purge-cache.php

<?php

$allow_ips = [
    // ログイン画面にアクセスを許可するIPアドレスを定義(未定義の場合は全ての接続元を許可する)
    // cf. '123.123.123.123', ...

];

// IP制限していない、もしくは許可されたIPの場合
if ( empty( $allow_ips ) || ( ! empty( $allow_ips ) && in_array( $_SERVER['REMOTE_ADDR'], $allow_ips, true ) ) ) {

    // FastCGI Cacheの保管場所を指定
    $cache_path = '/var/cache/nginx/fastcgi-cache/';

    // URLの解析
    $url = parse_url($_POST['url']);

    // URLではない場合
    if(!$url)
    {
        echo 'Invalid URL entered';
        die();
    }

    // 解析したURLのschemeを取得
    $scheme = $url['scheme'];

    // 解析したURLのホスト名を取得
    $host = $url['host'];

    // 解析したしたURLのリクエストURIを取得
    $requesturi = $url['path'];

    // 取得した情報を使い、MD5でハッシュ化する
    $hash = md5($scheme.'GET'.$host.$requesturi);

    // 指定したURLのキャッシュを削除する
    var_dump(unlink($cache_path . substr($hash, -1) . '/' . substr($hash,-3,2) . '/' . $hash));

} else {
// アクセスを拒否された場合

    // ステータスコードを404に変更する
    status_header( '404' );

    // テンプレート内の404ページにリダイレクトする
    include( get_query_template( '404' ) );

    // 処理を停止する
    exit();

}

配置後、$ curl -d 'url=http://www.example.com/test.php' https://ホスト名/cli-purge-cache.phpのようにキャッシュ削除したいURLと削除用PHPファイル(cli-purge-cache.php)を含んだコマンドを実行することで、個別にキャッシュを削除することができます。

また、cli-purge-cache.phpはキャッシュの削除有無でtrue(削除完了)またはfalse(削除不可)を出力します。

Nginx Helper

最後に3つ目の方法であるWordPressプラグインのNginx Helperでは、比較的簡単にFastCGI Cacheの管理を行うことができます。

ただし、プラグインを使う前に少し設定が必要となります。

  1. まずNginx HelperプラグインのSettingsで管理ページに移動し、Purging OptionsEnable Purgeを選択します。

  2. すると複数の項目が出てきますが、その中のCaching Methodではnginx Fastcgi cache (requires external settings for nginx) を選択します。

  3. 次にPurge MethodではDelete local server cache filesを選びます。

    1. もしNginxのngx_cache_purgeモジュールを導入している場合はUsing a GET request to PURGE/url (Default option) を選んでください。
  4. ngx_cache_purgeモジュールを使用していない場合はwp-config.phpにFastCGI Cacheを保存している場所を指定しなければなりません。

wp-config.php

/** 省略 **/

/** NGINX HELPER **/
define( 'RT_WP_NGINX_HELPER_CACHE_PATH', '/var/cache/nginx/fastcgi-cache');

/** 省略 **/
  1. 編集後、$ sudo service php8.1-fpm restartでPHP-FPMを再起動させます。

  2. これにより、Nginx Helperを使用できるようになりましたが、これまでのFastCGI Cache設定ではモバイル版のキャッシュも取得しているため、追加の記述が必要となってきます。

  3. Nginx Helperプラグイン自体を変更しなければならないため、Nginx Helperのプラグインを停止させてから、プラグインファイルエディターで編集画面に移動します。

  4. 編集を行うファイルはnginx-helper/admin/class-fastcgi-purger.phpです。この中に一行だけコード($this->delete_cache_file_for( 'mobile.' . $_url_purge );)を追記します。

    1. 追記場所が分からない場合はctrl+fを使って$this->delete_cache_file_for( $_url_purge );を検索するとヒットします。

class-fastcgi-purger.php

/** 省略 **/

        // モバイルキャッシュの削除を追加
        $this->delete_cache_file_for( 'mobile.' . $_url_purge );

				$this->delete_cache_file_for( $_url_purge );

				if ( $feed ) {

					$feed_url = rtrim( $_url_purge_base, '/' ) . '/feed/';
					$this->delete_cache_file_for( $feed_url );
					$this->delete_cache_file_for( $feed_url . 'atom/' );
					$this->delete_cache_file_for( $feed_url . 'rdf/' );

				}
				break;

/** 省略 **/
  1. 保存後、プラグインを再度有効にすることで、モバイルキャッシュの削除に対応したNginx Helperを使うことができます。

PHP-FPMのチューニング

WordPressを動作させるために不可欠であるPHPは、表示速度に関してダイレクトに影響します。

PHP8となった現在のように、バージョンが上がるにつれてPHPはますますパフォーマンスが向上するようになりましたが、チューニングを行うことで更に高速化できます。

サーバ上でPHPを実行するアプリケーションの一つとして、PHP-FPM(FastCGI Process Manager)と呼ばれるPHPのFastCGI実装があります。

今回はこのPHP-FPMに関してチューニングを行います。

$ sudo vim /etc/php/8.1/fpm/pool.d/www.confで複数の項目を編集します。

www.conf

[www]

;; 省略 ;;

; プロセスマネージャが子プロセスの数を制御する方法(static,dynamic,ondemand)
; "static"では"pm.max_children"に指定したプロセス数のみが起動する
; "dynamic"では、"pm.max_children"で最大プロセス数を指定しつつ、関連する他の設定でも制御される
; "ondemand"は必要に応じてプロセスが起動。リクエスト時に"pm.start_servers"設定数のプロセスが起動する
; プロセス起動のオーバーヘッドを最小限に抑える場合は"static"(CPU性能が低い場合に向いている)
; 通常時のメモリ消費量を抑えつつ柔軟に処理を行いたい場合は"dynamic"(高スペック向け)
;
pm = static

; 子プロセスの(最大)数。サーバに対して割り当てが大きすぎるとメモリが枯渇して実行が遅延する
; 割り当ての算出には サーバの使用可能メモリ量 / プロセスの平均メモリ使用量 を使う
; もしくは サーバの使用可能メモリ量 / PHPのmemory_limit(php.ini内)
; 例:使用可能メモリ量が1GB、Memory limitが128MBの場合、"1000/128 = 3.90625" 四捨五入して4を採用
; 使用可能メモリ量の確認は"$ free -h"コマンドを使い、"available"をチェックする
; ただし、CPUの論理コア数が少ない場合はCPUがボトルネックになるため、その場合はCPUコア数を指定する
pm.max_children = 4

; PHP-FPM開始時に起動する子プロセスの数
; pm = dynamic にのみ使用される
; 割り当ての算出には"pm.max_children"設定数の25%を使う
; 例:pm.max_childrenが4の場合、"4 * 0.25 = 1"
pm.start_servers = 1

; アイドル状態の子プロセス最小起動数
; pm = dynamic にのみ使用される
; 割り当ての算出には"pm.max_children"設定数の25%を使う
; 例:pm.max_childrenが4の場合、"4 * 0.25 = 1"
pm.min_spare_servers = 1

; アイドル状態の子プロセス最大起動数
; pm = dynamic にのみ使用される
; アイドル状態になると指定した数のプロセスになるまで減少する
; 割り当ての算出には"pm.max_children"設定数の75%を使う
; 例:pm.max_childrenが4の場合、"4 * 0.75 = 3"
pm.max_spare_servers = 3

; それぞれの子プロセスが再起動を行うリクエスト数
; 設定すると子プロセスのメモリが肥大化することを回避する
; 再起動させない場合は0を指定
; 1日に1回再起動させる場合、"1日の平均リクエスト数 / pm.max_children" で算出する(static)
; 1日の平均リクエスト数 / pm.max_spare_servers(dynamic)
; 例:pm=staticで1日1000pv、pm.max.childrenが4の場合、"1000 / 4 = 250"
pm.max_requests = 250

;; 省略 ;;

このチューニングは適当にやっていては良いパフォーマンスが得られないため、サーバにあった設定を探す必要があります。

また、PHPが使用するメモリを増減させたい場合には$ sudo vim /etc/php/8.1/fpm/php.inimemory_limitを編集する必要があります。

php.ini

; Maximum amount of memory a script may consume
; https://php.net/memory-limit
; PHPのスクリプトが確保できる全体の最大メモリ量
; とにかく大きくすれば良いものではなく(最大制限をかける理由はメモリを食いつぶすことの防止)
; 少なすぎても多すぎても良くないため、必要に応じて調整することが重要
memory_limit = 128M

MariaDBのチューニング

初期状態でもパフォーマンスの高いMariaDBですが、いくつかの設定を行うことで更にその性能を向上させることが可能です。

チューニングを行う際にはMySQLTunerという非常に便利な補助ツールが存在しているため、ありがたく使わせていただきましょう。

執筆時点での最新版はVersion 1.8.3です。$ sudo wget https://github.com/major/MySQLTuner-perl/archive/refs/tags/1.8.3.tar.gz最新版のMySQLTuner-perlをダウンロードします。

ダウンロードが完了したら$ tar zxvf 1.8.3.tar.gzでファイルを解凍します。

解凍後、$ cd MySQLTuner-perl-1.8.3でMySQLTunerのディレクトリに移動します。

そして~/MySQLTuner-perl-1.8.3$ perl ./mysqltuner.plを実行すると、管理者権限のユーザ名とパスワードを要求されるため、それを入力すればMariaDB(MySQL)の診断が開始されます。

 >>  MySQLTuner 1.8.3 - Major Hayden <major@mhtx.net>
 >>  Bug reports, feature requests, and downloads at http://mysqltuner.pl/
 >>  Run with '--help' for additional options and output filtering

[--] Skipped version check for MySQLTuner script

## MariaDB(MySQL)の管理者権限ユーザ名を入力(例ではroot)
Please enter your MySQL administrative login:root

## 上記にユーザに対応するパスワードを入力
Please enter your MySQL administrative password:パスワード

[OK] Operating on 64-bit architecture

-------- Log file Recommendations ------------------------------------------------------------------
[!!] Log file  doesn't exist

-------- Storage Engine Statistics -----------------------------------------------------------------
[--] Status: +Aria +CSV +InnoDB +MEMORY +MRG_MyISAM +MyISAM +PERFORMANCE_SCHEMA +SEQUENCE
[--] Data in Aria tables: 32.0K (Tables: 1)
[--] Data in InnoDB tables: 2.1M (Tables: 31)
[OK] Total fragmented tables: 0

-------- Analysis Performance Metrics --------------------------------------------------------------
[--] innodb_stats_on_metadata: OFF
[OK] No stat updates during querying INFORMATION_SCHEMA.

-------- Security Recommendations ------------------------------------------------------------------
[OK] There are no anonymous accounts for any database users
[OK] All database users have passwords assigned
[--] There are 620 basic passwords in the list.

-------- CVE Security Recommendations --------------------------------------------------------------
[OK] NO SECURITY CVE FOUND FOR YOUR VERSION

-------- Performance Metrics -----------------------------------------------------------------------
[--] Up for: 2d 0h 1m 14s (70K q [0.407 qps], 4K conn, TX: 166M, RX: 10M)
[--] Reads / Writes: 92% / 8%
[--] Binary logging is disabled
[--] Physical Memory     : 976.8M
[--] Max MySQL memory    : 3.2G
[--] Other process memory: 0B
[--] Total buffers: 417.0M global + 18.9M per thread (151 max threads)
[--] P_S Max memory usage: 0B
[--] Galera GCache Max memory usage: 0B
[OK] Maximum reached memory usage: 473.7M (48.50% of installed RAM)
[!!] Maximum possible memory usage: 3.2G (335.02% of installed RAM)
[!!] Overall possible memory usage with other process exceeded memory
[OK] Slow queries: 0% (0/70K)
[OK] Highest usage of available connections: 1% (3/151)
[OK] Aborted connections: 0.31%  (14/4482)
[OK] Query cache is disabled by default due to mutex contention on multiprocessor machines.
[OK] Sorts requiring temporary tables: 0% (0 temp sorts / 5K sorts)
[OK] No joins without indexes
[!!] Temporary tables created on disk: 64% (2K on disk / 3K total)
[OK] Thread cache hit rate: 99% (33 created / 4K connections)
[OK] Table cache hit rate: 95% (59K hits / 62K requests)
[OK] table_definition_cache(400) is upper than number of tables(323)
[OK] Open file limit used: 0% (59/32K)
[OK] Table locks acquired immediately: 100% (1K immediate / 1K locks)

-------- Performance schema ------------------------------------------------------------------------
[--] Performance schema is disabled.
[--] Memory used by P_S: 0B
[--] Sys schema is installed.

-------- ThreadPool Metrics ------------------------------------------------------------------------
[--] ThreadPool stat is enabled.
[--] Thread Pool Size: 1 thread(s).
[--] Using default value is good enough for your version (10.6.5-MariaDB-1:10.6.5+maria~focal)

-------- MyISAM Metrics ----------------------------------------------------------------------------
[!!] Key buffer used: 18.2% (24M used / 134M cache)
[OK] Key buffer size / total MyISAM indexes: 128.0M/0B

-------- InnoDB Metrics ----------------------------------------------------------------------------
[--] InnoDB is enabled.
[OK] InnoDB File per table is activated
[OK] InnoDB buffer pool / data size: 128.0M/2.1M
[!!] Ratio InnoDB log file size / InnoDB Buffer pool size (75 %): 96.0M * 1/128.0M should be equal to 25%
[--] Number of InnoDB Buffer Pool Chunk : 1 for 1 Buffer Pool Instance(s)
[OK] Innodb_buffer_pool_size aligned with Innodb_buffer_pool_chunk_size & Innodb_buffer_pool_instances
[OK] InnoDB Read buffer efficiency: 99.96% (1040206 hits/ 1040673 total)
[!!] InnoDB Write Log efficiency: 872.5% (4441 hits/ 509 total)
[OK] InnoDB log waits: 0.00% (0 waits / 4950 writes)

-------- Aria Metrics ------------------------------------------------------------------------------
[--] Aria Storage Engine is enabled.
[OK] Aria pagecache size / total Aria indexes: 128.0M/352.0K
[!!] Aria pagecache hit rate: 87.7% (15K cached / 1K reads)

-------- TokuDB Metrics ----------------------------------------------------------------------------
[--] TokuDB is disabled.

-------- XtraDB Metrics ----------------------------------------------------------------------------
[--] XtraDB is disabled.

-------- Galera Metrics ----------------------------------------------------------------------------
[--] Galera is disabled.

-------- Replication Metrics -----------------------------------------------------------------------
[--] Galera Synchronous replication: NO
[--] No replication slave(s) for this server.
[--] Binlog format: MIXED
[--] XA support enabled: ON
[--] Semi synchronous replication Master: OFF
[--] Semi synchronous replication Slave: OFF
[--] This is a standalone server

-------- Recommendations ---------------------------------------------------------------------------
General recommendations:
    Reduce your overall MySQL memory footprint for system stability
    Dedicate this server to your database for highest performance.
    Configure your accounts with ip or subnets only, then update your configuration with skip-name-resolve=1
    When making adjustments, make tmp_table_size/max_heap_table_size equal
    Reduce your SELECT DISTINCT queries which have no LIMIT clause
    Performance schema should be activated for better diagnostics
    Before changing innodb_log_file_size and/or innodb_log_files_in_group read this: https://bit.ly/2TcGgtU
Variables to adjust:
  *** MySQL's maximum memory usage is dangerously high ***
  *** Add RAM before increasing MySQL buffer variables ***
    tmp_table_size (> 16M)
    max_heap_table_size (> 16M)
    performance_schema = ON enable PFS
    innodb_log_file_size should be (=32M) if possible, so InnoDB total log files size equals to 25% of buffer pool size.

調整する必要のある項目は以下の通りです。

  • MySQL's maximum memory usage is dangerously high
    • データベースにメモリを多く使いすぎています
  • Add RAM before increasing MySQL buffer variables
    • MySQLバッファを増やす前にメモリを追加する(つまりメモリ不足)
  • tmp_table_size (> 16M)
    • 一時使用のテーブルサイズを16MBよりも上にする
  • max_heap_table_size (> 16M)
    • heapテーブルのサイズを16MBよりも上にする
  • performance_schema = ON enable PFS
    • パフォーマンスモニタリングツールであるperformance schemaを有効化にする
  • innodb_log_file_size should be (=32M) if possible, so InnoDB total log files size equals to 25% of buffer pool size.
    • innodb_log_file_sizeinnodb_buffer_pool_sizeの25%くらいに割り当てる

これら情報を参考にしつつ、$ sudo vim /etc/mysql/my.cnfでMariaDBの設定ファイルを編集します。

なおチューニングを行う際のPCスペックは最初に掲示した(32GB MVMe,1CPU,1GB Memory)とおりであり、やや厳しい構成です。

そのため、実際にチューニングを行う時は各々のスペックごとに調整してください。

my.cnf

[mysqld]

#
# * Log
#

# エラーログの場所を指定
log_error = /var/log/mysql/error.log

# ログ出力に中止した接続等を含めない
log_warnings = 1

# バイナリログの場所を指定
log_bin = /var/log/mysql/mysql-bin

# バイナリログの基準となるサイズ
max_binlog_size = 100M

# バイナリログの保存日数
expire_logs_days = 10

# バイナリログにSTATEMENTとROWを混在させる
binlog_format = MIXED

# slow_queryの出力設定("1"で出力する)
slow_query_log = 1

# slow_queryと判定する秒数
long_query_time = 3

# slow_queryログの場所
slow_query_log_file = /var/log/mysql/slow-queries.log

#
# * Fine Tuning
#

# MariaDBが受け付けるクライアントの最大接続数
# 数値が高いほど潜在的に消費するメモリが多くなる(1000だと潜在的にメモリが3GBほど必要)
max_connections = 30

# スレッドをコネクションの切断後にもキャッシュしておく数
# "max_connections / 3"で算出する
thread_cache_size = 10

# ソートを実行する各セッションに対して指定した数値を割り当て
sort_buffer_size = 1M

# 挿入の際に使用される各スレッドのキャッシュツリーのサイズ
bulk_insert_buffer_size = 32M

# 全てのスレッドでオープンするテーブルの数
# max_connectionsの2倍程度で開始、数時間経過後に"Open_tables"と"Opened_tables"の実行結果で調整する
# MariaDB [(none)]> show global status like 'Open_tables';
# MariaDB [(none)]> show global status like 'Opened_tables';
# Open_tablesの数が"table_open_cache"に届かない数値の場合は減らすことを検討できる
# また、Opened_tablesの数が急速に増加している場合は"table_open_cache"を増やすことを検討できる
table_open_cache = 1000

#
# * MyISAM
#

# キーをキャッシュする際に使用されるバッファのサイズ
# MyISAM自体ほとんど使わないので少なめに
key_buffer_size = 512K

#
# * Query Cache Configuration
#

# クエリキャッシュは更新頻度の高いサイトでは恩恵が少ない
# つまり読み取りが多く書き込みが少ないサイトでは効果が高い
# 時間経過によるキャッシュヒット率を確認しつつ細かい調整が必要
# なお、MySQLではバージョン8.0から廃止されており、使用は推奨されていない

# クエリキャッシュに使用する最大値
query_cache_size = 0

# 指定したサイズを超えたものはキャッシュを行わない
query_cache_limit = 0

# クエリキャッシュのタイプ(0:off, 1:ONでSELECT SQL_NO_CACHE以外, 2:DEMAND SELECT SQL_CACHEのみ)
query_cache_type = 0

#
# * InnoDB
#

# InnoDBのバッファプールサイズ
# ここの数値がデータベースの速度に大きく影響するので重要
# 基本的に全メモリの50%~80%程を割り当てる
innodb_buffer_pool_size = 512M

# InnoDBの更新ログのサイズ
# "innodb_buffer_pool_size"の25%を割り当てる
innodb_log_file_size = 128M

# InnoDBの更新ログに使用するバッファサイズ
innodb_log_buffer_size  = 32M

# InnoDBのバッククラウンド作業時に使用するI/Oサイズ
innodb_io_capacity = 1000

# innodb_io_capacityの最大サイズ
innodb_io_capacity_max  = 2000

# データファイルの二重書き込み(耐障害性のため)有無
# 無効(0)にするとわずかにパフォーマンスが向上するが、障害時に該当データを修復できない
innodb_doublewrite = 0

# バッファプールをフラッシュ(ディスク書き込み)後、同グループのdirtyページ
# (ログ等には存在するがテーブルに存在しないデータ)もフラッシュする
# 作業にはメモリの増加やI/Oアクセスが増大することから、無効(0)にする
innodb_flush_neighbors = 0

# ログバッファの書き込み制御
# ログバッファはコミットするたびにInnoDB REDOログに書き込まれる
# "0"ではコミット時に何も行わないのでパフォーマンスが向上する
# "2"ではフラッシュは1秒に1回なのでわずかにパフォーマンスが増加する
# "0"と"2"はサーバ全体がクラッシュするとデータ損失の可能性あり
innodb_flush_log_at_trx_commit = 2

# InnoDBテーブルのAUTO_INCREMENT値を生成するときに使用されるロックモード
# "2"ではinterleavedロックモードが使用され、もっとも高速に動作する
innodb_autoinc_lock_mode = 2

#
# * ETC
#

# MySQLでのDNS逆引き機能を無効化
skip-name-resolve = 1

# DNS解決を回避するためにキャッシュされるホスト名の数
# DNS逆引き機能を無効化しない場合、この数を増やしてパフォーマンスを上げる
# host_cache_size = 10

# MEMORYテーブルの最大サイズ。このサイズを超えたMEMORYテーブルはディスク上に作成
max_heap_table_size = 32M

# スレッド毎に作成される一時的なテーブルの最大サイズ
tmp_table_size = 32M

# Galera書き込みセットを並列に適用するために使用されるレプリカスレッドの数
wsrep_slave_threads = 8

# MySQLサーバーが実行出来るSQL文の最大長
# クライアントとサーバ間の不正なパケットを捕捉したり、メモリ不足要因となる大きなパケットの誤用防止策
# 数値を増やすほど潜在的に使用するメモリ使用量が大きくなるが、メモリに余裕があればデフォルトの16Mまで上げる
max_allowed_packet = 1M

# パフォーマンスモニタリングを行うストレージエンジンの設定
performance_schema = ON

設定が終われば$ sudo service mysql restartで再起動させます。

そしてもう一度~/MySQLTuner-perl-1.8.3$ ./mysqltuner.plで診断を行います。

-------- Log file Recommendations ------------------------------------------------------------------
[OK] Log file /var/log/mysql/error.log exists
[--] Log file: /var/log/mysql/error.log(0B)
[--] Log file /var/log/mysql/error.log is empty. Assuming log-rotation. Use --server-log={file} for explicit file

-------- Storage Engine Statistics -----------------------------------------------------------------
[--] Status: +Aria +CSV +InnoDB +MEMORY +MRG_MyISAM +MyISAM +PERFORMANCE_SCHEMA +SEQUENCE
[--] Data in Aria tables: 32.0K (Tables: 1)
[--] Data in InnoDB tables: 2.1M (Tables: 31)
[OK] Total fragmented tables: 0

-------- Analysis Performance Metrics --------------------------------------------------------------
[--] innodb_stats_on_metadata: OFF
[OK] No stat updates during querying INFORMATION_SCHEMA.

-------- Security Recommendations ------------------------------------------------------------------
[OK] There are no anonymous accounts for any database users
[OK] All database users have passwords assigned
[--] There are 620 basic passwords in the list.

-------- CVE Security Recommendations --------------------------------------------------------------
[OK] NO SECURITY CVE FOUND FOR YOUR VERSION

-------- Performance Metrics -----------------------------------------------------------------------
[--] Up for: 15h 30m 14s (17K q [0.309 qps], 1K conn, TX: 30M, RX: 2M)
[--] Reads / Writes: 96% / 4%
[--] Binary logging is enabled (GTID MODE: ON)
[--] Physical Memory     : 976.8M
[--] Max MySQL memory    : 775.8M
[--] Other process memory: 0B
[--] Total buffers: 688.5M global + 2.9M per thread (30 max threads)
[--] P_S Max memory usage: 72B
[--] Galera GCache Max memory usage: 0B
[OK] Maximum reached memory usage: 697.2M (71.38% of installed RAM)
[OK] Maximum possible memory usage: 775.8M (79.42% of installed RAM)
[OK] Overall possible memory usage with other process is compatible with memory available
[OK] Slow queries: 0% (0/17K)
[OK] Highest usage of available connections: 10% (3/30)
[OK] Aborted connections: 0.37%  (7/1882)
[OK] Query cache is disabled by default due to mutex contention on multiprocessor machines.
[OK] Sorts requiring temporary tables: 0% (0 temp sorts / 1K sorts)
[OK] No joins without indexes
[!!] Temporary tables created on disk: 41% (633 on disk / 1K total)
[OK] Thread cache hit rate: 99% (17 created / 1K connections)
[OK] Table cache hit rate: 86% (17K hits / 20K requests)
[OK] table_definition_cache(400) is upper than number of tables(323)
[OK] Open file limit used: 0% (62/16K)
[OK] Table locks acquired immediately: 100% (1K immediate / 1K locks)
[OK] Binlog cache memory access: 99.13% (456 Memory / 460 Total)

-------- Performance schema ------------------------------------------------------------------------
[--] Memory used by P_S: 72B
[--] Sys schema is installed.

-------- ThreadPool Metrics ------------------------------------------------------------------------
[--] ThreadPool stat is enabled.
[--] Thread Pool Size: 1 thread(s).
[--] Using default value is good enough for your version (10.6.5-MariaDB-1:10.6.5+maria~focal-log)

-------- MyISAM Metrics ----------------------------------------------------------------------------
[!!] Key buffer used: 19.7% (103K used / 524K cache)
[OK] Key buffer size / total MyISAM indexes: 512.0K/0B

-------- InnoDB Metrics ----------------------------------------------------------------------------
[--] InnoDB is enabled.
[OK] InnoDB File per table is activated
[OK] InnoDB buffer pool / data size: 512.0M/2.1M
[OK] Ratio InnoDB log file size / InnoDB Buffer pool size: 128.0M * 1/512.0M should be equal to 25%
[--] Number of InnoDB Buffer Pool Chunk : 4 for 1 Buffer Pool Instance(s)
[OK] Innodb_buffer_pool_size aligned with Innodb_buffer_pool_chunk_size & Innodb_buffer_pool_instances
[OK] InnoDB Read buffer efficiency: 99.78% (212234 hits/ 212704 total)
[!!] InnoDB Write Log efficiency: 107.91% (532 hits/ 493 total)
[OK] InnoDB log waits: 0.00% (0 waits / 1025 writes)

-------- Aria Metrics ------------------------------------------------------------------------------
[--] Aria Storage Engine is enabled.
[OK] Aria pagecache size / total Aria indexes: 128.0M/352.0K
[!!] Aria pagecache hit rate: 90.2% (6K cached / 645 reads)

-------- TokuDB Metrics ----------------------------------------------------------------------------
[--] TokuDB is disabled.

-------- XtraDB Metrics ----------------------------------------------------------------------------
[--] XtraDB is disabled.

-------- Galera Metrics ----------------------------------------------------------------------------
[--] Galera is disabled.

-------- Replication Metrics -----------------------------------------------------------------------
[--] Galera Synchronous replication: NO
[--] No replication slave(s) for this server.
[--] Binlog format: MIXED
[--] XA support enabled: ON
[--] Semi synchronous replication Master: OFF
[--] Semi synchronous replication Slave: OFF
[--] This is a standalone server

-------- Recommendations ---------------------------------------------------------------------------
General recommendations:
    MySQL was started within the last 24 hours - recommendations may be inaccurate
    When making adjustments, make tmp_table_size/max_heap_table_size equal
    Reduce your SELECT DISTINCT queries which have no LIMIT clause
Variables to adjust:
    tmp_table_size (> 16M)
    max_heap_table_size (> 16M)

まだ調整が必要な箇所はありますが、最初に診断を行った時と比べて状態が良くなりました。

特にメモリの負荷が非常に高かったので、診断結果ではそれが解消されています。

OPcacheの導入

OPcache(オペコード・キャッシュ - Opcode cache)はPHPが動作する際のコードを共有メモリにキャッシュすることで、パフォーマンスを向上させるPHPアクセラレータの一つです。

PHP8ではJIT(Just-In-Time)と呼ばれる、プログラムのソースコードをネイティブコードにコンパイルする機能があります。

それをOPcacheによってキャッシュすることにより、従来のPHPよりも非常に高速に動作することが可能となります。

なお、PHP8のインストール時にOPcacheも一緒にインストールされているため、設定ファイルを編集するだけですぐに使用出来ます。

$ sudo vim /etc/php/8.1/fpm/php.iniphp.iniを編集します。

php.ini

;; 省略 ;;

[opcache]
; zend_extensionとしてopcacheを読み込む
zend_extension = opcache

; OPcacheを有効化
opcache.enable = 1

; トレーシングJITを有効化
; 処理が複数回行われる場所を動的に発見し、該当する部分をトレースしてコンパイル対象にする
opcache.jit = on

; コンパイル済みのJITコードを保存する共有メモリの合計サイズ
opcache.jit_buffer_size = 128M

; OPcacheのコマンドラインを有効化
opcache.enable_cli = 1

; OPcacheが使用するメモリのサイズ
opcache.memory_consumption = 128

; インターン(intern)された文字列を格納するために使用されるメモリ量
opcache.interned_strings_buffer = 8

; OPcacheのハッシュテーブルキーの最大数
opcache.max_accelerated_files = 10000

; スクリプトのタイムスタンプをチェックする秒数
; "0"では常にチェックを行う
; "opcache.validate_timestamps"が無効の場合はこの設定が無視される
; 数値が多いほど更新頻度が減るため、パフォーマンスアップに繋がるが、PHPファイル編集後の反映が遅延する
opcache.revalidate_freq = 3

; OPcacheに含まれる全てのPHPDocコメントを破棄してコードを縮小化
; ただし、注釈のためにコメントを活用しているツールが破壊される可能性がある
; (Doctrine、Zend Framework 2 および PHPUnitなど)
; "0"で無効化
opcache.save_comments = 0

; "file_exists()"、"is_file()"および"is_readable()"が呼ばれた際、
; ファイルが既にキャッシュ済かOPcacheからチェックする
; ただし、"opcache.validate_timestamps"が無効な場合は古いファイルが返される可能性あり
; "1"で有効化
opcache.enable_file_override = 1

;; 省略 ;;

設定後、$ sudo service php8.1-fpm restartで再起動を行います。

これによりOPcacheが有効化されました。

なお、OPcacheが動作しているかをブラウザで確認するにはOCP(Opcache Control Panel)を使うと、とても簡単にチェックすることが可能です。

導入にはocp.phpをドメイン以下に設置し、ブラウザでパスを指定するだけで使用出来ます。

しかし、公開ディレクトリに配置するということは、誰でもその情報を見ることが出来てしまいます。

そのため、IPによる制限かパスワードを設定、WordPressであれば/wp-admin/等のログインした者以外アクセスできない場所に設置したり、使用しない時にはファイルを削除するなどの対策を推奨します。

APCuの導入

APCu(APC User Cache)はOPcacheと同じくPHPに働きかける、PHPアクセラレータの一つです。

OPcacheが実行コードをキャッシュするのに対し、APCuはユーザデータを共有メモリにキャッシュすることによってパフォーマンスを向上させます。

APCuの旧バージョンとしてOPcache機能も備えたAPC(Alternative PHP Cache)が存在していましたが、セキュリティ上の問題により現在はAPCは廃止され、ユーザデータキャッシュのみ(OPcache機能は廃止)を備えたAPCuが後継バージョンとなっています。

APCuはOPcacheと異なり、PHP8のインストール直後ではパッケージが同梱されていないため、新しくインストールを行う必要があります。

なお、以前は過去のPHPと下位互換性を持ったAPCu-bc(APCu Backwards Compatibility Module)というパッケージがよく利用されていましたが、PHP8からはbc版はサポートされなくなりました。

PHP8をインストールする際にOndrej Sury氏のppaを登録していれば、$ sudo apt install php8.1-apcu --no-install-recommendsでAPCuをインストールできます。

--no-install-recommendsを付ける理由として、執筆時点のAPCuではPHP7のパッケージもインストールに含めていることから、オプションを追加することでPHP7のインストールを中止しています。

インストールが完了したら$ sudo vim /etc/php/8.1/fpm/php.iniで編集を行います。

php.ini

;; 省略 ;;

[apcu]
 ; 拡張機能としてapcu.soを読み込む
extension = apcu.so

; APCuの有効化
apc.enabled = 1

; APCuのコマンドラインを有効化
apc.enable_cli = 1

; APCuが使用する個別の共有メモリセグメントのサイズ
apc.shm_size = 128M

; APCuのキャッシュが保持される秒数
; "0"の場合、キャッシュメモリが不足した際に完全に削除される
apc.ttl = 3600

; キャッシュがガベージコレクションのリストに掲載される秒数
apc.gc_ttl=3600

; mmapされたメモリ領域をファイルベースか共有メモリベースに決める際、
; mmapモジュールに渡すmktempスタイルのファイルマスク
; ファイルベースのmmapの場合はXを6つ付ける
; POSIXスタイルの場合は.shmをマスク内のどこかに付ける
apc.mmap_file_mask=/tmp/apc.XXXXXX

;; 省略 ;;

設定が完了したら$ sudo service php8.1-fpm restartで再起動します。

APCuもOPcacheと同様にモニタリングツールを使うことで動作確認を行うことができます。

一覧にあるapc.phpを公開ディレクトリに配置し、その後ブラウザでそのファイルにアクセスするとモニタリング状況が表示されます。

こちらのファイルも、IPによる制限かパスワードを設定し、WordPressであれば/wp-admin/等のログインした者以外アクセスできない場所に設置、そして使用しない時にはファイルを削除するなどの対策を推奨します。

次回はWordPressのセキュリティ設定

様々な項目を編集する必要がありますが、今回の設定により、初期状態と比べて非常にパフォーマンスが向上していることを体感できるはずです。

PHPそのもののパフォーマンスも良くなっているため、数年前より更に高速化しています。

次回はWordPressのセキュリティ設定について紹介していきたいと思います。

セキュリティ設定はこの記事でまとめて紹介するつもりでしたが、今回の記事を含めて思った以上に長くなってしまうため、分けることにしました。

WordPressは特にセキュリティについてあれこれ言われることが多いため、できるだけその心配を解消します。

参考資料