WP-Cronの負荷はさほどでもない?

WP-Cronの負荷はさほどでもない?

WordPress のタスクスケジューラ WP-Cron はそのサイトにアクセスがあるたびに起動することはよく知られており、その負荷を軽減するために WP−Cron を無効にしてサーバーの cron からサイト直下の wp-cron.php を起動する方法が取られます。

で、ふと思ったのは、その場合 cron の最適な実行間隔どれくらいなんでしょう。また、WP-Cron を有効にした場合でも同じ wp-cron.php を使っているんでしょうか。

ということで WordPress のクーロン実装 WP-Cron について調べてみました。

01WordPress の起動プロセスを読んでいくと…

WordPress のバージョンは 6.7.2 です。

WordPress の起動プロセスは index.php → wp-blog-header.php → wp-load.php → wp-config.php → wp-settings.php と進み、wp−setting.php の149行目で wp-includes/default−filters.php を読み込みます。その391行目に

// WP Cron.
if ( ! defined( 'DOING_CRON' ) ) {
	add_action( 'init', 'wp_cron' );
}

があり、init アクションフックに関数 wp_cron() を登録しています。DOING_CRON は、詳しく調べてはいませんが、ざっと見たところではクーロンリクエスト中であれば true になっている定数です。

その後、プロセスは wp−setting.php に戻り includes ディレクトリ内の様々な関数ファイルを読み込むわけですが、その中に includes/cron.php があります。そして wp-setting.php はさらに進み、プラグインやテーマを読み込み、704行目で init アクションを実行し、登録した関数 wp_cron() を実行します。

その関数 wp_cron() はすでに読み込まれている関数ファイル includes/cron.php の中にあります。

02includes/cron.php の関数 _wp_cron() へ…

関数 wp_cron() は cron.php の973行目にあるのですが、これは実質的にはすぐ下の関数 _wp_cron() を呼んでいるだけです。そして、その _wp_cron() の最初には

	if ( str_contains( $_SERVER['REQUEST_URI'], '/wp-cron.php' )
		|| ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON )
	) {
		return 0;
	}

とあり、ここで定数 DISABLE_WP_CRON の値をみて WP-Cron を無効としているかどうかをチェックしています。それに直接 wp-cron.php から呼ばれた場合もここではねています。wp-cron.php がこの wp_cron() を呼ぶことはないと思いますのでよくわかりません。

とにかく WP-Cron はサイト直下の wp-config.php に define('DISABLE_WP_CRON', true); としておけば、これ以降のクーロン処理は実行されずに wp−setting.php に戻るということになります。

WP-Cron を有効としている場合は、cron.php の処理が続き、

	$crons = wp_get_ready_cron_jobs();
	if ( empty( $crons ) ) {
		return 0;
	}

と、すでに実行時間が過ぎているのに実行されていないクーロンジョブを取得しています。関数 wp_get_ready_cron_jobs() も cron.php 内にあり、すべてのクーロンジョブを取得したのち現在時刻と比較して実行時間が過ぎているジョブを返してきます。

ということは実行すべきクーロンジョブがなければここで wp-setting.php に戻るわけですから WP-Cron を有効にしていてかかる負荷というのは wp_get_ready_cron_jobs() を呼ぶ際の負荷ということになります。大した負荷じゃないかも知れませんね。

とにかく Wp-Cron は指定した時間に実行されるわけではなく、アクセスがあったときに実行されていないクーロンジョブを探し出して実行するということになります。

この時点での WordPress のクーロン実装 WP-Cron についてまとめますと、

  • wp-config.php に define(‘DISABLE_WP_CRON’, true) とすれば無効となる
  • デフォルトでは有効となっており、アクセス時に実行時間を過ぎているジョブが処理される
  • 負荷は実行時間を過ぎているジョブを取得する wp_get_ready_cron_jobs() だけと思われる
  • それでも無駄なことはしないほうがいいので WP-Cron を無効するのが正解か

ということになります。

03_wp_cron() から spawn_cron() へ…

引き続き、cron.php を読んでいきます。1017行目から

	foreach ( $crons as $timestamp => $cronhooks ) {
		if ( $timestamp > $gmt_time ) {
			break;
		}

		foreach ( (array) $cronhooks as $hook => $args ) {
			if ( isset( $schedules[ $hook ]['callback'] )
				&& ! call_user_func( $schedules[ $hook ]['callback'] )
			) {
				continue;
			}

			$results[] = spawn_cron( $gmt_time );
			break 2;
		}
	}

という実行時間の過ぎているジョブ $crons をループするコードがあります。$crons というのは、たとえば

  [1743998845]=>
  array(1) {
    ["wp_privacy_delete_old_export_files"]=>
    array(1) {
      ["40cd750bba9870f18aada2478b24840a"]=>
      array(3) {
        ["schedule"]=>
        string(6) "hourly"
        ["args"]=>
        array(0) {
        }
        ["interval"]=>
        int(3600)
      }
    }
  }

これがそのひとつのデータです。

そして、上のコードの7行目の $schedules はイベントの繰り返しスケジュールであり、これもひとつの例ですが、デフォルトで登録されている1時間に1回のイベントはこうなっています。

  ["hourly"]=>
  array(2) {
    ["interval"]=>
    int(3600)
    ["display"]=>
    string(14) "1時間に1回"
  }

$schedules はこうした配列の親要素ですので、上のコードの $schedules[ $hook ][‘callback’] はたとえば $schedules[‘wp_privacy_delete_old_export_files’][‘callback’] となります。これで間違っていないとは思いますが、これで何をチェックしているのかよくわかりません。ただ、この if 文が TRUE になるイベントはありませんので結局のところ、この後の処理は関数 spawn_cron() に移ることになります。

先に進む前に spawn_cron() の結果をみておきますと、結果は $results に入り break 2 となっていますのでループを2つ抜け、起動プロセスは wp-setting.php に戻っていきます。

04spawn_cron() から wp-cron.php をコールする…

_wp_cron() から呼ばれた関数 spawn_cron() をみていきます。

ここでは再び現在時刻を確認したり、DOING_CRON をチェックしたりしてクーロン処理にロックがかかっていないかをチェックし、再び wp_get_ready_cron_jobs() で実行されていないジョブを取得しています。

なぜこんなややこしいことをしているのでしょう? この spawn_cron() は _wp_cron() からしか呼ばれていないにもかかわらず別関数にとして同じ処理を繰り返しています。今の私のスキルではわかりませんがなにか意味があるのでしょう。

続いて定数 ALTERNATE_WP_CRON が TRUE の場合はゴニョゴニョして(現時点ではスルーです…)wp-cron.php を呼び、そうでない場合(こちらが本流…)は cron ロックを掛けます。具体的には $doing_wp_cron に現在時刻を設定し、set_transient() でテーブル wp_options に option_name=_site_transient_doing_dron の値として保存されます。

そしてやっと wp-cron.php をコールすることになります。

	$cron_request = apply_filters(
		'cron_request',
		array(
			'url'  => add_query_arg( 'doing_wp_cron', $doing_wp_cron, site_url( 'wp-cron.php' ) ),
			'key'  => $doing_wp_cron,
			'args' => array(
				'timeout'   => 0.01,
				'blocking'  => false,
				/** This filter is documented in wp-includes/class-wp-http-streams.php */
				'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
			),
		),
		$doing_wp_cron
	);

	$result = wp_remote_post( $cron_request['url'], $cron_request['args'] );

wp_remote_post() を使ってPOSTメソッドで wp-cron.php を呼んでいます。

05wp-cron.php は非同期で実行される

wp-cron.php はどうなっているのでしょう。

wp-cron.php の 27行目に

if ( function_exists( 'fastcgi_finish_request' ) ) {
	fastcgi_finish_request();
} elseif ( function_exists( 'litespeed_finish_request' ) ) {
	litespeed_finish_request();
}

というコードがあり、なんだろうと思いましたら、これ、非同期処理をする設定みたいです。

PHP で非同期処理? と思いググってみましたらいろいろライブラリーがありますね。実際、XServer では fastcgi_finish_request() が呼ばれており、これ以降非同期処理になるようです。チェックのためにこの変数は何を持っているんだろうと var_dump とかを使っても確認できません。

確かに時間のかかるジョブがあるかも知れません。その後、処理は再び wp_get_ready_cron_jobs() で実行指定時間を過ぎているクーロンジョブを取得して、cron ロックがかかっていなければロックを掛け、ジョブがあればその数だけループしてクーロンスケジュールが更新できればアクションで指定された処理を実行するということになります。

06WP-Cron のまとめ

結局、WP-Cron はサイトにアクセスがあった時に実行されていないクーロンジョブを取得し、あればサイト直下の wp-cron.php を1回だけ呼ぶということであり、wp-cron.php はあらためてクーロンジョブを取得してそれぞれ処理を実行するということになります。

ですのでサイトにアクセスした時にクーロンジョブがなければ大した負荷はかかりませんが、あればwp-cron.php を呼ぶまでのかなり面倒くさい作業がありますのでそれなりの負荷はかかるということになります。

それならば WordPress のクーロン実装 WP-Cron を無効にしてサーバーの cron からサイト直下の wp-cron.php を呼んだほうが効率がいいということになります。

その実行間隔は時間指定のクーロンジョブがなければ最も短い間隔で実行させればいいということになります。WordPress のデフォルトでは1時間が最短ですので理論的には1時間間隔で cron を走らせれば実行されていないジョブが1時間毎に実行されることになります。

私の場合は10分間隔で処理する作業がありますのでサーバーの cron から10分間隔で wp-cron.php を走らせています。

ところでこの wp-cron.php は外部から誰でも実行できてしまいますがセキュリティ上はどうなんでしょう?

アクセス制限を掛けておいたほうがいいですね。wp-cron.php は php で動かせばいいですし、WP-Cron は無効にしていますので wp-config.php などと一緒に .htaccess でアクセスできないようにしておきましょう。

<FilesMatch "^(wp-config\.php|wp-cron\.php|xmlrpc\.php)">
    order deny,allow
    deny from all
</FilesMatch>

ということで WP-Cron の解析でした。ただ、まだよくわからないところもあります。