pixiv insideは移転しました! ≫ https://inside.pixiv.blog/

ZendOpcacheとAPCuではじめるハイパフォーマンスPHP

先月末から一週間ほど旅に出ていたbokkoです。今回はpixivでのPHPのバージョンアップに関する取り組みの一部を紹介します。

pixivとPHP

pixivではWebアプリケーションの開発で主にPHPを利用しており、今のところPHP5.3とPHP5.4で動いている環境が混在している状態ですが、これをPHP5.5化するプロジェクトが進行中です。

オペコードキャッシュとユーザキャッシュによるPHPアプリケーションの高速化

pixivのようなPVの多いWebサイト(2013年10月現在で38億/月)をPHPで運用する場合、 アプリケーションのパフォーマンスという観点ではもちろんのこと、運用にかかるコストの面でも APCやZendOpcacheが提供しているようなオペコードキャッシュ機能は必須と言えます。

サーバスペックが高いマシン(CPUコア数が16とか24)を使っているということもありますが、 pixivで稼働しているアプリケーションサーバの台数が約20台で済んでいるのはAPCやZendOpcacheのおかげと言っても過言ではないでしょう。

また、pixivではAPCをオペコードキャッシュとしてだけでなくユーザキャッシュとしても利用しているので、 高速なWebアプリケーションを構築する上で無くてはならない存在です。

このあたりのアプリケーションにおけるキャッシュ戦略の話は 先月のPHPカンファレンス2013で発表したスライドに載っているので 興味のある方はご覧ください。

PHP5.5におけるオペコードキャッシュとユーザキャッシュ

しかし、APCはPHP5.5に対応しておらず、(PHPのリリースマネージャ曰く「APC is dead」) オペコードキャッシュ用のエクステンションとしてZendOpcacheがPHP5.5に標準添付されるようになりました。

またZendOpcacheはオペコードキャッシュ機能を提供しているという点ではAPCと同じですが、ユーザキャッシュ機能は提供していません。

なのでAPCをオペコードキャッシュおよびユーザキャッシュとして利用している場合、単にAPCをZendOpcacheに置き換えるだけではPHP5.5に移行することはできません。

APCの代わりに別のミドルウェア(例:memcached)を使うようにアプリケーションを修正するか後述のAPCuで置き換える必要があります。

pixivにはKVSClientというアプリケーションからデータストアへのアクセスを抽象化するライブラリがあるのでAPCのかわりにmemcachedやRedisを使うように修正するのは そんなに難しくないのですが、大抵の場合APC(u)の方がパフォーマンスが良いので後者を選択しました。

ZendOpcache

ZendOpcacheはPHP5.5では最初から組み込まれていますが、それよりも低いバージョンの場合は以下のようにPECLからインストールする必要があります。

1
sudo pecl install ZendOpcache-beta

あとはconfigureの--with-config-file-scan-dirで指定されているディレクトリにZendOpcacheを有効に設定するiniファイルを配置します。(以下は最小設定)

1
2
3
4適用して本番に再投入しました
; zendopcache.ini
zend_extension=opcache.so
; opcacheが利用するメモリのサイズ(MB)
opcache.memory_consumption=512

ZendOpcacheの実行時設定については既にPHPマニュアルに日本語化されたものがあるので、そちらを見るのが良いでしょう。 また、最新の内容を確認したい場合はgithubのREADMEを見ましょう。

APCu

APCuはAPCからオペコードキャッシュ用のコードを取り除いてユーザキャッシュの機能だけを提供するようにしたプロダクトです。これもPECLからインストールできます。

1
sudo pecl install APCu-beta 

あるいはgithubからクローンしてインストールすることもできます。

1
2
3
4
5
6
7
git clone https://github.com/krakjoe/apcu.git
cd apcu
git checkout master
phpize
./configure
make
make install

あとはZendOpcacheの場合と同様にconfigureの--with-config-file-scan-dirで指定されているディレクトリにiniファイルを配置します。

1
2
3
; apcu.ini
extension=apcu.so
apc.shm_size=512M

共有ライブラリ名はapcu.soですが、設定名のプレフィックスはapcuではなくapcな点に注意しましょう。

pixivのPHP5.5移行作業状況

まだ一部ではありますが、pixivでは既にPHP5.5(とZendOpcacheとAPCu)が稼働している環境があります。 結構な量のリクエストが来るところなのですが、今のところ問題なく稼働しています。

もっともしばらくした後APCuまわりで問題が起きて即座に修正パッチを作成 & 再投入 & 本家にプルリクエストするという展開が待っていましたが。

APCuで遭遇した問題とその対策

PHP5.5で動くサーバを本番に投入してしばらくした後、数は少ないものの以下のエラーメッセージが観測されるようになりました。(具体的なキー名や数値は変数にしています)

1
apc_store(): GC cache entry '$key_name' was on gc-list for $time seconds

これを補足した後すぐに本番から対象となるサーバを切り離し、調査を開始しました。

APCuのソースコードを調べていくと上記のエラーメッセージを出力しているのは apc_cache_gcという不要になったキャッシュエントリを削除する関数だと分かりました。 以下はそこのメインループを取り出したものです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
while (*slot != NULL) {
    time_t now = time(0);
    time_t gc_sec = cache->gc_ttl ? (now - (*slot)->dtime) : 0;
    if (!(*slot)->value->ref_count || gc_sec > (time_t)cache->gc_ttl) {
        apc_cache_slot_t* dead = *slot;
        /* good ol' whining */
        if (dead->value->ref_count > 0) {
            apc_warning(
                "GC cache entry '%s' was on gc-list for %d seconds" TSRMLS_CC,
                dead->key.str, gc_sec
            );
        }
        /* set next slot */
        *slot = dead->next;
        /* free slot */
        free_slot(
            dead TSRMLS_CC);
        /* next */
        continue;
    } else {
         slot = &(*slot)->next;
     }
}

この実装を読み解くにはapc.gc_ttlというパラメータの意味を理解する必要があります。apc.gc_ttlについては PHPマニュアルに以下のように記述されています。

キャッシュエントリがガベージコレクションのリストに残り続ける秒数。 ソースファイルのキャッシュ中にサーバープロセスが死んだ場合の安全装置となります。 ソースファイルが変更された場合、メモリに割り当てられている古いバージョンは、 この TTL に達するまで再読み込みされません。 この機能を無効にするにはゼロを設定します。

もう一度エラーメッセージを見てみましょう。($key_nameがキャッシュエントリ名、$timeがキャッシュエントリの生存期間)

1
apc_store(): GC cache entry '$key_name' was on gc-list for $time seconds

PHPマニュアルの説明文と上記のエラーメッセージ、そして実装を照らし合わせると、 「$key_nameというキャッシュエントリを消そうとしたら、apc.gc_ttlで定めた期間を越えてるのにまだそのキャッシュが参照されている箇所があるよ!」という風に解釈することができます。

pixivではapc.gc_ttlにかなり大きめの値を設定しているので、Apache & mod_phpのような環境ではgc_ttlで定めた期間を越えてもまだ参照されているエントリが残っているような状況は通常起こらなさそうに思えますが、 現実にはApachePHPのプロセスが落ちて本来デクリメントされるはずの参照カウントの値がそのまま残るというケースがあり得ます。(APCuのデータは共有メモリに保存される)

というわけで、このシチュエーションは現実の世界では割とよくあるし、この状況が起こってもPHPやAPCuはまだ問題なく動作するから警告エラー(E_WARNING)にせずにnoticeかdebugとして扱うべきだよねという感じで 以下の修正パッチを適用して本番に再投入しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
commit 9d4292c1df6e771700cebb5282deb579a83d987d
Author: cubicdaiya <cubicdaiya@gmail.com>
Date:   Fri Oct 4 00:14:58 2013 +0900
    debug-printing instead of warning-printing
    When cache->gc_ttl is outdated and reference to item remains,
    apc_cache_gc prints warning message.
    But this situation should be treated as not warning but debug or notice.
diff --git a/apc_cache.c b/apc_cache.c
index 6fc9f50..76ea3b4 100644
--- a/apc_cache.c
+++ b/apc_cache.c
@@ -209,7 +209,7 @@ PHP_APCU_API void apc_cache_gc(apc_cache_t* cache TSRMLS_DC)
                                /* good ol' whining */
                            if (dead->value->ref_count > 0) {
-                               apc_warning(
+                               apc_debug(
                                                "GC cache entry '%s' was on gc-list for %d seconds" TSRMLS_CC,
                                                dead->key.str, gc_sec
                                        );

また開発元にこの修正パッチをプルリクエストで投げた結果マージされました。実際のやりとりはこちら。 既にmasterブランチに取り込まれてはいますが、この記事の執筆時点(2013/10/15)ではまだPECLでインストールされるバージョン(4.0.2)に入ってないので注意しましょう。

まとめ

このほかにも紆余曲折はありましたが、pixivでもPHP5.5が動き始めました。

完全移行するにはまだやらなければいけないことが色々残ってるのですが、 結構な量のアクセスが来る環境でPHP5.5とZendOpcacheと(パッチ当てた)APCuの組み合わせが問題なく動作することが確認できたので少し胸をなでおろしています。