hnwの日記

APCuは速いけど初期設定がイマイチだというお話

ISUCON本戦で惨敗してきた皆さんこんにちは。昨日のやけ酒は最高でしたね!今日はISUCON予選のときに気づいたAPCuのイマイチな点を紹介します。


APCuというのはPHP extensionで実装されているKVS(Key Value Store)で、localhost内でのデータキャッシュに利用されます。かなり高速な上、APCの時代から考えると利用実績も十分あるため、PHPでは定番extensionの一つといえるでしょう。


ところで、APCuに大量のデータ(10万エントリ以上)を格納するとデータの取得や更新が遅くなることがあります。APCuでは格納するエントリ数の「ヒント」をあらかじめ設定値で指定するようなつくりになっているのですが、この設定があまり知られておらず、デフォルト値もかなり小さいため、遅いまま使っている環境が多いように推測しています。

APCuの設定

APCuの設定値に、「apc.entries_hint」というものがあります。これはAPCの「apc.user_entries_hint」と同じもので、APCuに格納するデータ総数の推測値を指定するものです。

apc.entries_hint


A "hint" about the number variables expected in the cache. Set to zero or omit if you're not sure. (Default: 4096)


APCu INSTALL


この「apc.entries_hint」の指定を元に、APCuがデータを格納するときに使うハッシュのスロット数が決定されます。また、このスロット数はエントリ数が増えても拡張されることはありません。同じハッシュスロットに格納されているデータはlinked listで格納されるため、データが増えれば増えるほどlinked listが長くなります。


apc.entries_hintの初期値は4096であるため、たとえばAPCuに40万エントリを格納した場合、linked list部分の長さが平均で100になってしまいます。データの参照や更新のたびにハッシュ値の計算に加えてキーの重複チェックのため100エントリ分の文字列比較が必要になるため、速度面で効率が悪くなってしまいます*1


apc.entries_hintにデータの総数を指定しておけばlinked list部分の長さが平均1になり、速度とメモリ効率の観点から理想に近づくと言えます。多少ブレても影響は軽微なので、若干小さめの値を指定するくらいのノリで良いと思います。

APCu 4.0.6までの制約

ところで、APCu 4.0.6まではapc.entries_hintにどれだけ大きい値を指定しようと、ハッシュスロット数は19457までに制限されていました*2


この制約はデータを20000個前後しか格納しない前提であれば問題ないのですが、現実にはもっと大量のデータを格納することがあるはずです。そこで、下記のようなPull Requestを投げ、より大きい設定値に対応できるようにしました。


これは無事採用され、APCu 4.0.7からはハッシュスロット数の最大値が983063になっています。

apc.entries_hintを大きくしたときのメモリ消費量

このスロット数が1増えるたびに、64bit環境では64バイトのメモリを消費します。つまり、2万スロットであれば約1.2MBのメモリを消費します。100万スロットでも約64MBです。大抵の環境では、hintを大きくするリスクは小さいと言えそうです。


もっとも、これ以外に格納するデータに対応する分のメモリも必要です。APC/APCuでは1エントリごとに管理用の構造体で960Byteを消費します。更にキーと値に対応するデータの分もメモリを消費しますので、数百万エントリを格納する前提の場合は十分なメモリを用意しておきましょう。

現在使っているエントリ数を確認する方法

APCuに付属しているapc.phpでも表示されますし、下記のようなスクリプトでも確認できます。

<?php
$ret=apc_cache_info('user', true);
var_dump($ret['nentries']); /* 現在使っているエントリ数 */


今回の話に関わらず、この数字が単調増加していないかどうか普段からモニタリングしておいた方が良いでしょう。

まとめ

  • APCuの設定値apc.entries_hintに格納エントリ数の推定値を指定すると性能が上がる
    • デフォルト値は4096
  • APCu 4.0.6以下ではapc.entries_hintに2万以上の値を指定しても無視される
    • 4.0.7から100万付近が上限になった


補足しておくと、このapc.entries_hintの修正が必須になるような環境はかなり珍しいかと思います。上にも書いた通り、多少遅くなったとしてもAPCuは十分高速なので、他のボトルネックに比べれば無視できるはずです。ただ、APCuに数百万オーダーのデータを格納している場合は試しに設定変更してみる価値があると思います。

*1:効率が悪いといっても、この程度ならlocalhostmemcachedを使うよりは高速です。同一プロセス内で処理が完結するのは速度面で有利なのです

*2:ハッシュ値のmodを取ったときの偏りを減らすため、hintより大きい素数をリストから探すような仕組みになっています