【PHP高速化】データ先読みで高速化 APCu利用【起動時スクリプト実行】


【PHP高速化】データ先読みで高速化 APCu利用
====

## はじめに

###【PHP】データ先読みやってみた

PHP 7.4 で導入された preload は PHP ソースコードの読み込みを高速化する仕組みでしたが、PHPカンファレンス等を見ていたらデータも先読みしてメモリに乗せられるようになったらという発言があったので、それを試しに作ってみました。

PHPでは出来ないようなこと言われたけど、工夫すれば可能です。

memcached を使ってマスターデータなどをメモリに読み込ませることはしてる人も多いと思いますが、APCu を使って共有メモリ上に乗せることが出来ればより高速化（通信のオーバーヘッドが減る）が期待出来るはず

結論！APCu を使えば簡単に出来ると思っていたが、そうでもなかった。
今回は失敗の経緯も含めて記録しておきます（失敗も何かの参考になると思うので）

テスト環境は PHP 8.0.7、CentOS 7.6 です。

## APCu の導入

導入は他の記事を参考に remi から導入しました（今回は手順省略）

## APCu の設定

	/etc/php.d/40-apcu.ini
	apc.enabled=1
	apc.enable_cli=1
	apc.shm_size=256M

動作テストをコマンドラインで実行する為に cli を有効にしました

当初の思惑では cli でデータ先読みを実行するつもりでしたが・・

## APCu の動作確認

apcテストのサンプルスクリプト

	$ cat apc_test.php 
	<?php
	apcu_add('APC_TEST', 'start');
	var_dump(apcu_fetch('APC_TEST'));

動作確認

	$ php apc_test.php 
	string(5) "start"


続けてテストで登録したデータを読み出ししてみると・・

	$ cat apc_test2.php 
	<?php
	#apcu_add('APC_TEST', 'start');
	var_dump(apcu_fetch('APC_TEST'));
	
	$ php apc_test2.php 
	bool(false)


なんと動作失敗 bool（false）
調べるとこれは APCu の仕様で、コマンドラインで実行した場合は、実行したプロセスが終了すると apcu_add したデータが消えてしまう

この仕組みでは APCu でデータの先読みが出来ない！！

APCu のこの仕様には戸惑ったけど、共有サーバーで不用意に共有メモリ内のデータを改ざんできないような仕組みと思われ、仕方のないことと理解

でも今はクラウドの時代だし（１つのサーバを皆で共有するより、１つのプロセスがサーバを占有するような時代という意味）共有メモリの操作も出来た方が便利だと思うのだが、これは今後の課題かな


## 回避策

理由がわかれば回避するのは簡単、ということで回避策を作ってみた

ローカルホストの設定例

	Listen 18008
	<VirtualHost *:18008>
	    ServerName localhost
	    DocumentRoot /var/www/localhost
	    <Location />
	        Order deny,allow
	        Deny from all
	        Allow from localhost
	    </Location>
	    ErrorLog logs/localhost_error_log
	    Customlog logs/localhost_log common
	</VirtualHost>

今回開発テスト用サーバで使用した設定です。勝手に本番サーバに設定しないように注意！
実際に設定する時はインフラ担当などに相談しましょう。


## サンプルスクリプト

・データセット用サンプル

	$ cat /var/www/localhost/predata/bootscript.php 
	<?php
	$start_time = microtime(true);

	// 仮装 KVS データ作成＆登録
	for ($i=0; $i<50000; $i++) {
	    $key = 'kvs_test_' . $i;
	    apcu_store($key, $i);
	}

	var_dump(apcu_fetch('kvs_test_100'));
	$end_time = microtime(true);
	echo "Exec time: " . ($end_time - $start_time) * 1000 . " ms" . PHP_EOL;

今回は acpu_store でダミーデータを登録してます。


・動作確認用スクリプト

	$ cat /var/www/localhost/predata/view_kvs.php 
	<?php
	$start_time = microtime(true);
	
	// 仮装 KVS 読み取り
	$sum = 0;
	for ($i=0; $i<50000; $i++) {
	    $key = 'kvs_test_' . $i;
	    $sum += apcu_fetch($key);
	}

	echo "SUM = " . $sum . PHP_EOL;
	$end_time = microtime(true);
	echo "Exec time: " . ($end_time - $start_time) * 1000 . " ms" . PHP_EOL;

ループしながら共有メモリにアクセスしてます。
今回はループの中でアクセスしても遅くならないのを確認するということで５万回ループしてます。

同様なことを memcached でやっていたら大変なことになるでしょう・・

動作確認

	$ curl localhost:18008/predata/bootscript.php
	int(100)
	Exec time: 30.582189559937 ms

	$ curl localhost:18008/predata/view_kvs.php
	SUM = 1249975000
	Exec time: 25.286912918091 ms

ループしながら共有メモリにアクセスしているので処理時間がかかってますが、５万ループしても数十ミリ秒で済みます。
memcachedと違って通信のオーバーヘッドが無いので気にならない程度ですね。


### 再起動すると

OS を再起動すると共有メモリに保存されたデータが削除されます。

次に、起動時にデータ登録するスクリプトを自動実行する設定を行います。


## Systemd で自動実行

起動時にスクリプトを自動起動する仕組みとして、今回は簡単に出来る systemd を使用します。

詳しくは systemd で自動起動の仕方を参照してください。


### 起動時に実行するサンプルスクリプト

自動起動用のスクリプト

	$ cat /opt/predata/bootscript.sh 
	#!/bin/sh
	sleep 10
	curl localhost:18008/predata/bootscript.php

curl で localhost にアクセスしてスクリプトを実行します。起動直後は Web の動作が不安定になるので sleep を入れて実行を遅らせるようにしてます。

スクリプトが起動できるように chmod で実行権限を付けておきます。

	$chmod 777 /opt/predata/bootscript.sh 

スクリプトの動作確認

	$ /opt/predata/bootscript.sh 
	int(100)
	Exec time: 7.4429512023926 ms


### systemd 設定

systemd の設定ファイルを作成する

	$ cat /etc/systemd/system/bootscript.service 
	[Unit]
	Description = boot script
	ConditionPathExists = /opt/predata
	
	[Service]
	ExecStart = /opt/predata/bootscript.sh
	Type = oneshot
	RemainAfterExit = yes
	
	[Install]
	WantedBy = multi-user.target

今回使った自動機同様のスクリプトです。Type を oneshot にすると、OS起動時に一度だけスクリプトを実行してくれます。

systemd の設定を有効にする

	$ sudo systemctl daemon-reload

systemd の動作確認を行う

	$ sudo systemctl start bootscript	
	$ sudo systemctl status bootscript

OS 起動時に自動実行の設定を有効にする

	$ sudo systemctl enable bootscript
	
### 再起動して動作確認します。

	$ sudo systemctl status bootscript
	● bootscript.service - boot script
	   Loaded: loaded (/etc/systemd/system/bootscript.service; enabled; vendor preset: disabled)
	   Active: active (exited) since 水 2021-07-07 11:28:12 JST; 30s ago
	  Process: 524 ExecStart=/opt/predata/bootscript.sh (code=exited, status=0/SUCCESS)
	 Main PID: 524 (code=exited, status=0/SUCCESS)
 	  CGroup: /system.slice/bootscript.service

systemctl status で動作したことを確認してます。

	$ curl localhost:10101/predata/view_kvs.php
	SUM = 1249975000
	Exec time: 26.578903198242 ms

再起動しても共有メモリにデータが読み込まれてることが確認出来ました。

## まとめ

注意！今回は簡単に導入できる APCu を使用しましたが、サーバの設定変更が必要です。無闇に本番サーバの設定を変更しないように

今回は想定外の失敗もあったが、データ先読みの効果を体感することが出来ました。

memcachedと比べてどのくらい速くなるかは環境にもよりますが、通信のオーバーヘッドが無くなるので、何も考えずにループの中で使用しても問題ないところがいいですね。

今後の課題としては、共有メモリを操作するプログラムを作るか（結構大変です）
APCuを改造するか（コマンドラインで共有メモリ書き換えない安全機能を削除するとか）まだやれることがあると思います。

以上、何かの参考になれば幸いです。