WordPress WP-Cron を止めて OS層の Cron で実行する方法

WordPress内で定期的な処理を実行するために cron ライクの WP-Cron という実装がありますが、複数のWordPressインスタンス(サーバー)構成で可用性を向上している場合に、各サーバーで WP-Cron が動いてしまうことで、処理の重複実行がされてしまうのではないかと気になりました。そこで、WP-Cronの実行を分離することで解決を図ろうと思い、調べたことを記載します。

結論:二重実行を気にする必要はない

出オチのようになってしまいましたが、以下の理由から二重実行を気にする必要はない(むしろ、二重実行され続けている)と理解してよさそうです。

  • wp-cron.php の内部に重複実行を排除するロック処理があったので、複数のインスタンスで同時に起動しても問題ない

  • WP-Cron は cron をシミュレートしているだけなので、正確な時刻通りに実行されるものではなく、動作のトリガーにはユーザーのアクセス(WordPress への HTTP アクセス)を必要とする。

    Note:WP-Cron does not run constantly as the system cron does; it is only triggered on page load.
    Scheduling errors could occur if you schedule a task for 2:00PM and no page loads occur until 5:00PM.
    Cron | Plugin Developer Handbook | WordPress Developer Resources

当然、アクセスが多いサイトでは秒間何件ものレベルでアクセスが発生しているので、無駄な判定処理を減らす目的においては工夫の余地があります。

Cronjob

WordPress サイトで定期的に決まった時間、日付、または実行感覚でジョブをスケジュールするために使用されます。WordPress での Cron ジョブの例としては、投稿の公開スケジュール設定や更新の確認、ほかには事前設定されたスケジュールで実行されるバックアップ用のプラグインなども一例です。

Linux では cron と親しまれている定期実行ジョブですが、WordPress ではこれをシミュレートするために使用される WP-Cron によって処理されます。

WP-Cron のパフォーマンスの問題

サイトへのトラフィックの量によっては、組み込みの WP-Cron を使用するとページの読み込み時間に影響を与え始める可能性があります。そのようなケースではWP-Cron ( wp-cron.php) を無効にし、代わりにOS (Linux) レイヤーの cron を使用して性能影響を軽減する手段があります。

これまでに記載した通り、WP-Cron は実際の cron の仕事ではないことを理解することが重要です。OS の cron を模倣するために WordPress が作成したものであり、WP-Cron は継続的に実行されません。どういうことかというと、ユーザー(クライアント)が WordPress へアクセスする都度実行され、WP-Cronとして登録された各ジョブを実行すべきタイミングがきているかを都度判断しているに過ぎません。性能要求の厳しいサイトではPHPの Worker 数などのチューニングパラメタに注意する必要があります。

全く逆のケースとして、WordPressサイトへのトラフィックが全く発生しないサイト場合、スケジュール処理が間に合わないことも生じえます(アクセスがされないということは、WP-Cronを実行するトリガーがないため)。とはいえ積極的に公開しなくても検索エンジンのクローラーが巡回してくる時代ですので、全く動かないこともないとは思いますが・・・。

WP-Cron の実行を確実にする方法

余程の理由がなければ(一般的な用途であれば)この対策をする必要はないと思いますが、WP-Cron を無効にして代わりにOS の cron を使用する手段があります。これは公式ドキュメントにも記載されている方法です。

Hooking WP-Cron Into the System Task Scheduler | Plugin Developer Handbook | WordPress Developer Resources

WP-Cron を無効にする方法

WP-Cron を無効にするには、 wp-config.phpに次の1行を追加します。これにより、WordPress サイトの各種ページの読み込み時に WP-Cron のジョブが実行されなくなります。

define('DISABLE_WP_CRON', true);

システム cron をスケジュールする方法

次に、wp-cron.phpを実行するようにスケジューリングする必要があります。

WordPress の構成が「マルチサイト」モードの場合は追加でひと癖ありますが、単一サイトでの実行であればサイト配下の wp-cron.php プログラムに対してHTTPリクエストをするだけです。具体的には以下のようにします。

# wget コマンドの場合
wget -q -O - https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

# curl コマンドの場合
curl --silent https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

crontab -e で Cronを登録します。

*/5 * * * * /usr/bin/curl --silent https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

実行時間は5分だと細かいようにも見えますが、もともと(アクセスさえあれば)1分間に何回でも実行されていたわけなので、実行間隔が長すぎるとも言えます。この値は提供サイトの状況によって適宜見直しをしてください。

(参考)Kubernetes の Cronjob で実行する場合

やっていることはただの curl ですので、起動するイメージは curl コマンドが使えれば何でも問題ありません。以下の例では alpine イメージを利用しています。

ついでに -w "status:%{http_code} time:%{time_total}\n" により、ログに少しだけ情報を追加しています。

apiVersion: batch/v1
kind: CronJob
metadata:
  name: job-wordpress-cron-fire
  namespace: wordpress
spec:
  schedule: "*/5 * * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: wp-cron
              image: alpine/curl:3.14
              imagePullPolicy: IfNotPresent
              command: ["/bin/sh", "-c"]
              args:
                - |
                  echo "Execute WP-Cron..." &&
                  curl --silent -o /dev/null -w "status:%{http_code} time:%{time_total}\n" https://example.com/wp-cron.php?doing_wp_cron                  

          restartPolicy: Never

(参考)マルチサイト構成で WP-Cron を動かす

WPをコマンドラインで操作するための WP-CLI というツールがあります。これを利用することでバッチの活用の幅を広げることができます。

https://wp-cli.org/

https://wp-cli.org/

マルチサイト構成の場合は、所属するサイトそれぞれのドメイン(URL)に対して GET /wp-cron.php?doing_wp_cron のHTTPリクエストを発生させる必要があります。そこで、その複数サイトを WP-CLI コマンドを用いて取得する例が以下のバッチです。サイト数が少なければ、対象となるサイトを手で列挙しても意味は変わりませんので、ケースバイケースで利用するのがよいでしょう。

#!/bin/bash
 
# This script runs all due events on every WordPress site in a Multisite install, using WP CLI.
# To be run by the "www-data" user every minute or so.
#
# Thanks https://geek.hellyer.kiwi/2017/01/29/using-wp-cli-run-cron-jobs-multisite-networks/
 
PATH_TO_WORDPRESS="/path/to/wordpress"
if [ $1 == "--debug" ]; then
DEBUG=true
else
DEBUG=false
fi
DEBUG_LOG=/var/log/wp-cron
 
if [ "$DEBUG" = true ]; then
        echo $(date -u) "Running WP Cron for all sites." >> $DEBUG_LOG
fi
 
for URL in = $(wp site list --field=url --path="/path/to/wordpress" --deleted=0 --archived=0)
do
        if [[ $URL == "http"* ]]; then
                if [ "$DEBUG" = true ]; then
                        echo $(date -u) "Running WP Cron for $URL:" >> $DEBUG_LOG
                        wp cron event run --due-now --url="$URL" --path="$PATH_TO_WORDPRESS" >> $DEBUG_LOG
                else
                        wp cron event run --quiet --due-now --url="$URL" --path="$PATH_TO_WORDPRESS"
                fi
        fi
done

[Source: Running WordPress cron on a multisite instance - Chris Hardie’s Tech and Software Blog]

WP-Cronの実装の理解

ソースコードの熟読には至らないので表面的な理解になりますが、おおよそ以下の仕様で説明できると思います。

  • wp-includes/cron.php に WP-Cron の実行処理がある。このファイルは wp-settings.php からrequire されている。したがって、Webサイト(WordPress)へのアクセスがある都度起動される。
    • WordPress/cron.php at 6.0.3 · WordPress/WordPress
    • wp_cron() が action として登録される
    • _wp_cron() が呼び出される。 DISABLE_WP_CRON 定数が wp-config.php で設定されている場合は return 0; を返すので実行されない。続行された場合、Cron実行用として登録されている処理をループし、 spawn_cron() にて呼び出される。
    • spawn_cron() 関数では最終的に wp-cron.php?doing_wp_cron へのリクエストを実行している。
      • 途中、 ALTERNATE_WP_CRON 定数での分岐処理がある。 true の場合にはリダイレクトで $_SERVER['REQUEST_URI'] に遷移させつつも、その後に wp-cron.php を読み込む処理をしている。アクセスしてきたクライアントへはリダイレクトで本来ページに飛ばしつつ、Cron 実行処理によるレスポンス低下を回避するような狙い(?)。どのようなケースで用いるのかのケース確認は省略。
  • wp-cron.php に本体の処理がある。
    • WordPress/wp-cron.php at 6.0.3 · WordPress/WordPress
    • 上記の通り wp-includes/cron.php を通じて読み込まれるが、手動でHTTPアクセスをして起動することもできる。外部から Cron を実行する手段としてはこれがよく用いられる
    • Webサイト(WordPress)へのアクセスが多ければ、その分だけ多重起動することになるが、ロック処理があるので処理が重複して実行されることはない。ただし、状態確認のための SQL アクセスが発生するので、Webサイト設計によっては無駄にアクセスを遅延させるだけの一要因になり得る。
      • 正確には、SQLアクセスの有無はオブジェクトキャッシュの状況による。 wp_using_ext_object_cache() にて判定される。

参考リンク