PHPのインメモリKeyValueストア
PHPサーバーへのDOS攻撃的な繰り返しアクセスに対して503エラーを出したいと思って、Redisのようなインメモリ型のデータベースが使えないかと調べていたら、APCuという拡張ライブラリがあるということを知り設定してみました。
環境は、Windows11とDebian11のPHP8.1です。
Windowsの場合
PECLのページよりWindows用のバイナリをダウンロードします。
Windowのアイコンと「DLL」と表記のあるリンクから進むとPHPのバージョン、TSかNTS、32か64bitによりバイナリがわかれています。
PHPのTSやNTSのバージョンの違いについては過去にWindowsへPHPをインストールした際の記事を読んでいただければと思います。
ここではPHP8.1、TS、64bit版をダウンロードしました。
ダウンロードしたzipファイルを展開すると、php_apcu.dllがあるので拡張ライブラリフォルダにコピーします。これは通常PHPルートのextフォルダとなります。
php.iniに拡張ライブラリの設定を記述します。初めて拡張ライブラリを設定する際はextension_dirの設定もします。
php.ini
...
; On windows:
; コメントアウト解除
extension_dir = "ext"
...
; 新規追加
extension=apcu
Debianの場合
以前筆者はLaravelをデプロイした際にPHP8.1をビルドしたので、そこへ設定するためにビルドをします。
APTレポジトリのバージョンでインストールする場合は php-apcuをインストールするだけです。
# wget https://pecl.php.net/get/apcu-5.1.22.tgz # tar vzxf apcu-5.1.22.tgz ... # cd apcu-5.1.22 # phpize ... # ./configure --enable-apcu ... # make ... # make test ... # cp modules/apcu.so /usr/local/lib/php/extensions
途中、次のようなメッセージがでたらおそらくautoconfが存在しないことが原因です。
autoconfはconfigureスクリプトを自動生成するプログラムです。
また、makeが実行できない場合は、built-essentialをインストールします。
# apt install autoconf build-essential
ライブラリ(apcu.so)のコピーは自身の環境に合わせて設定してください。その後php.iniに拡張ライブラリの読み込みの設定をします。
php.ini
...
; 拡張ライブラリのディレクトリを指定
extension_dir = "/usr/local/lib/php/extensions/"
...
; 新規追加
extension=apcu
APCu稼働の確認
Windowsの場合もDebianの場合もphp.iniの設定まで完了したら、Apacheを再起動します。phpinfoを読み込んで、次のような記述があれば設定は成功です。
php.iniの設定とステータス確認
APCuを使用可能にすると、php.iniにおいて、いくつかそれ用の設定ができるようになります。公式ページにはAPCuは大半の環境でデフォルトの設定のまま使って問題ないとありますので、設定変更の可能性が高そうな項目だけ紹介しておきます。
- apc.enabed
APCuの有効化と無効化です。"1"で有効、"0"で無効です。
- apc.shm_size
メモリのサイズです。デフォルトでは"32M"がセットされています。
- apc.ttl
メモリに設定した値の有効期限です。デフォルトは"0"ですが、"0"の場合メモリ不足になった場合すべてのキャッシュが削除されます。
また、チューニングをする場合にはapc.phpファイルが役に立ちます。Linux用のtarを展開した中に含まれるPHPのコードで単体でホストしてブラウザから読みだすことでAPCuのステータスを確認できます。
APCuの使い方
たとえば30秒間の間に同じリモートアドレスから10回以上のアクセスがあった場合に503を返すコードは次のようになります。
sample.php
<?php
define('TTL',30);
define('MAX_ACCESS',10);
$addr = $_SERVER['REMOTE_ADDR'];
// キーが存在するか確認
if(apcu_exists($addr)) {
// 存在した場合は値を取得
$cnt = apcu_fetch($addr);
$cnt = $cnt+1;
if (MAX_ACCESS <= $cnt) {
// カウントアップ(TTLを更新する)
apcu_store($addr,$cnt,TTL);
// 503
header('HTTP/1.1 503 Service Temporarily Unavailable');
die('503 Service Temporarily Unavailable');
}
// カウントアップ
$result=false;
apcu_store($addr,$cnt,TTL);
// デバッグ情報
echo "$addr:$cnt";
} else {
// 既にキーに対する値が存在する場合はエラーになる
apcu_add($addr,1,TTL);
// デバッグ情報
echo "$addr:access";
}
操作に関しては主に次のような関数があります。
- apcu_add($キー,$値,$TTL)
新規にデータを追加します、キーが既存の際はエラーになります。
- apcu_store($キー,$値,$TTL)
データを保存します、キーが既存の際は上書きします。
- apcu_delete($キー)
キャッシュから削除します。
- apcu_clear_cache()
キャッシュをすべてクリアします。
- apcu_entory($キー, $コールバック関数,$TTL)
キー値が存在した場合キー値を返します。
キーが存在しない時コールバックを呼びます。コールバック関数ではキー値を受け取り、保存値を返すようにします。未存在時はコールバックで返した値がキーにセットされます。この時の有効期限は$TTLに設定した時間になります。
apcu_entoryでは排他ロックがかかるためアトミック性が保たれます。
- apcu_exists($キー)
キーとして保存した値がある場合true、それ以外はfalseが返ります。
- apcu_fetch($キー)
キー値を取り出します。失敗時はfalseが返ります。
他に、メタ情報を取得する関数として、apcu_cache_info()や、apcu_key_info($キー)、などが存在します。