ISUCON6予選問題のPHP実装を担当しました
こんにちはの方もはじめましての方もエントリーを読んでいただきありがとうございます。
昨日から開催されている ISUCON6 の予選にてPHP実装のコーディングを担当させていただきました t-cyrill と申します。 日頃はWeb屋さんでインフラのお仕事をやっています。
今回はいつものような適当なエントリーではなく少ししっかりとしたエントリーで担当しましたPHP実装について、いくつか簡単な補足をさせていただきます。
はじめに
ISUCON6予選参加者のみなさま、お疲れさまでした。 たくさんのチームの方にPHP実装を触っていただけたようでありがとうございます。
ISUCONということで多くの人に触れていただくコードになるだろうと思い、どのチームの方にも得意不得意の影響があまりないようになるべくよく知られたライブラリを使ったり、元からPHPがうまく動作するような環境を用意できるように準備したつもりです。
いくつか至らない点もあったと思いますが、もし困るところがあまりなかったようでしたら幸いです。
実装に使用したライブラリなど
composer.lock
を見ていただればわかることですが、今回はなるべく初期のPerl実装に近くなるようにライブラリを選びました。
軽量フレームワークの Slim3
、HTTPクライアントの Guzzle6
、テンプレートエンジンの Twig
などPHPな方にはよく見るようなライブラリを元にして作っていきました。
Twigを選択したのは元になったPerlテンプレート内でテンプレート継承を使用していたためですが、今思えば特に必要ではなかったような気もします。
独自実装を使ったところなど
Perl実装から移す際にPHPで用意できなかったrandom_string
など一部の関数は元の実装コードとそれほど違和感がないように独自に実装しました。
これらの関数は glue.php
にまとまっています。
本質的なところではないのですが、 random_string
の実装はかなり雑で、Perl実装のように柔軟なランダム文字列を生成することはできません。
pcreの制限について
webapp/Isuda/app.php
の L49 に怪しげな
// NOTE: avoid pcre limitation "regular expression is too large at offset"
というコメントを見かけたPHPの方は嫌な気持ちになったかもしれません。
あまりこのケースに当てはまることがないので意外と知られていないかもしれませんが、PHPのpreg
系関数が内部で使っている正規表現ライブラリpcre
にはデフォルトで渡せる正規表現パターン文字列の長さに制限があります。この制限を変更するには pcre
ビルド時にオプションを指定する必要があります。
今回初期実装の通りに実装すると正規表現文字数が pcre
の制限にかかります。Perl実装から移すときにここはかなり悩みました。
というのも、正規表現を使わない場合PHP実装がやや有利になってしまいますし、pcre
を使うためには独自にpcre
を用意する必要があり、初期ビルド以外のPHPを用意したチームが本質的ではないところでトラブルに遭遇するのは避けたかったのです。
結局、間を取ってハッシュの生成には preg_replace_callback
を分割した正規表現で複数回呼び出して生成し、本来の置換処理はループ後の strtr
にてまとめて行うように実装しました。
余談ですが、Perl実装の動作は正規表現のevalでPHPでは廃止された機能です。
Slim Containerの拡張
細かなところですが各エンドポイントで $this->dbh
や $this->htmlify
のような呼び出しを実現するため、$app
コンテナを継承した独自のSlim Containerを用意しています。
PHP7で入った無名クラスの機能をこのような形で使えたのは少し面白かったです。
stash
Perlではテンプレートへの変数渡しにstash
を使っており、これを模倣するためコンテナのPimple
を直接突っ込んでおきました。
Slimの依存に含まれていたのでいい感じの入れ物として使いましたがこういう使い方は普段しないのであまりよくないような気もしています。
実装ミスについて
途中で気付かれたチームから報告がありましたが、初期のPHP実装にはベンチマーク結果に影響のない2つのわかりやすい実装ミスがあります。
一つは nginx.php.conf
での mime.types
ロード忘れでブラウザで閲覧した際に多くのブラウザでスタイルが適用されないなど見た目の問題が発生します。
+ include mime.types; + default_type application/octet-stream;
もう一つは POST keyword/keyword
を呼び出すと必ず500エラーが発生するというもので、ミドルウェアの指定個所に間違いがあります。
また、同エンドポイントはkeyword
のパース方法も間違っています。
- $keyword = $req->getParsedBody()['keyword']; + $keyword = $req->getAttribute('keyword'); - })->add('authenticate')->add($mw['set_name']); + })->add($mw['authenticate'])->add($mw['set_name']);
しかしながら、ベンチマーカーはPOST keyword/keyword
が成功するのを試さないので、ベンチマークの結果に影響はありませんでした。
ベンチマーカーがPOST keyword/keyword
を試していないことが公開になると他言語実装でも有利になってしまいますし、(特に1日目2日目で情報が違うことになる)
ベンチマークの結果自体には影響のないものでしたので、ルールにのっとり「ベンチマーカーの結果が全てです」という回答としました。 GoやPython実装のバグとは違いこのバグに対してパッチを提供しなかったのはこのような理由です。
2点ともすぐに気づけるようなミスなだけにPHP実装を試していただいた方には申しわけなかったです。
初期実装のサーバについて
初期実装のサーバは予選時には nginx + phpfpm の構成としました。
もともとはビルトインサーバを使用して簡単に呼び出す予定でしたが、並列処理性に問題があるためベンチの初期チェックを通過できなかったため、Ruby実装がunicornを用意したタイミングでphp-fpmを使うようにsystemd unitを置き換えました。
pm
も"static"に設定されたのでPerlほどではありませんがPHPの初期スコアはそれなりに出るようになっていたと思います。
実装の最適化について
いくつかのチームから解説が出るでしょうし、PHP実装もそちらを参考にしていただければ同じように速くなりますが、初期実装でわざと遅くなりそうにした個所のみ補足します。
PHP-FPMのtcp呼び出し
初期実装で php-fpm
は 9000
と 9001
ポートでlistenしており、nginxからプロキシする形で用意されています。
php-fpm にはunix domain socketでlistenする機能がありますので置き換えることでパフォーマンスがわずかに改善します。
最後に
本選でPHP実装を担当することはおそらくありませんが、本選でもPHP実装が上位チームに入ってくれることを期待しています。 それでは本選でお会いしましょう。
C90夏コミありがとうございました&サークル活動の休止について
C90でゆきいろパラソルのブースに来てくださったみなさま、ありがとうございました。
今回も無事「ひだまりPHPシリーズ」の新刊を出すことができました。
あと、10部ほどコピー本も用意してみました。Kinko'sさんで初めて作ってみたのですが、無事刷れてよかったです。超大変でした!中身はもちろん、今回初めて表紙絵描きました!(前日まで間に合うよ!すごいね)
ImageMagickで作った(!)巨大PDFをUSBに入れて持っていくだけで Kinko's お兄さんがいい感じにプリンタ設定してくれました。すごいね。
Kinkosのコピー機めっちゃ面白かった
— シリル@夏コミお疲れ様でした! (@d_cyrill1129) 2016年8月12日
設営終わりました!本めっちゃあります!たすけて!
— シリル@夏コミお疲れ様でした! (@d_cyrill1129) 2016年8月13日
今日はよろしくお願いします!!! pic.twitter.com/FL8dkPwyBt
そんなこんなで三度目となるサークル参加となったC90も楽しい3日間でした。
今後のサークル活動についてですが、ゆきいろパラソルとしての活動は一旦おやすみとします。
ひだまりPHPシリーズが一通り書けたというのと、今後の広がりがあまり想像できなくなったというのが理由です。すみません。
ほかに書くことが見つかるまで、こちらのブログは引き続き更新していきますのでお付き合いいただければ幸いです。
MySQL 5.6の InnoDB バッファープールのプリロード機能が優秀で頭悪いクエリを投げなくても良くなった
再起動を高速化するための InnoDB バッファープールのプリロード
特に大きなバッファプールを扱う場合にMySQLを再起動すると、起動直後はメモリにDBのデータが乗っていないので大きなディスクIOが必要になるようなイカれたクエリを投げるとめちゃくちゃ遅いという現象が起きます。
MySQL5.5以前はユーザリクエストを振る前にイカれたクエリを実行しておくことでバッファプールに乗せるという手法がそれなりに有効でした。
ただ、イカれたクエリを投げる方法ではバッファプールが再起動前と同じ状態になるわけではないので遅いクエリはどうしても出てしまうし、再起動前に近い状態になるにはそれなりに長い時間が必要でした。
InnoDB バッファプールのプリロード機能は、バッファプールの復元に必要な情報をファイルから復元することでバッファプールを再起動前の状態に復元できる機能です。
バッファプール復元のための情報は終了時か任意のタイミングでファイルに書き出せます。バッファプールの中身をすべて書き出すわけではないので、それほどディスクを圧迫することはありませんし、IO負荷も少なめです。
ファイルにはテーブルスペースIDとページのIDが書き出されます。
シャットダウン時にバッファプールを書き出すには SET GLOBAL innodb_buffer_pool_dump_at_shutdown=ON;
を実行しておきます。
すぐに書き出すには SET GLOBAL innodb_buffer_pool_dump_now=ON;
とするだけです。
デフォルトではデータディレクトリに ib_buffer_pool
という名前で保存されます。
データディレクトリに ib_buffer_pool
がある状態で SET GLOBAL innodb_buffer_pool_load_now=ON;
を実行すると、バッファプールの復元が始まります。
ib_buffer_pool
はテーブルスペースIDとページIDを記録しているだけなので、他のサーバで生成したものを使うこともできます。
(innodb_buffer_pool_sizeが違っていても動作します。メモリから溢れるだけかな・・・いや嘘ですわかりません適当なこと書きました)
(できそうな気はしてたので検索したらやってる方がいました。)
これが便利で 本運用しているデータベースサーバで ib_buffer_pool
を書き出し -> 再起動済みのデータベースサーバにコピー -> バッファプールのロード と実行すれば、
本運用しているサーバと同じようなバッファプールの状態が復元でき、いきなりイカれたクエリが飛んできてもそれなりに高速に処理できるようになっています。
ウォームアップのためにイカれたクエリを投げなくてもよくなって平和になりました。
メモリやSSDも数年前と比べ安価で高速になり大規模データベースを扱いやすくなっていますね。大きなバッファプールを扱わないといけない機会が辛いめでたい。
free -h のことをフリーエッチとかいうのよくない
— シリル@ひだまりPHP 3日目西f26a (@d_cyrill1129) July 27, 2016
Vagrant 1.7からinsecure_private_keyが置き換えられる仕様になっていることに今更ながら気づいた
ごく日常的にvagrantを使っている分には気にならないと思いますが、vagrant 1.7から vagrant ssh
する際に仕様される秘密鍵は vagrant up
の際に置き換えられる仕様になっています。
(1.6以前はInsecure Private Keyと呼ばれる共有の秘密鍵を使っていた)
Insecure Private Keyを使ってvagrantで立てたVMにSSHをする方法を取っているとvagrantのバージョンをあげたときに突然エラーになって「おや?」ってなります。
vagrant up
してvagrant ssh
すると問題なくsshできるので原因に気づきづらい。
ssh -i ~/.vagrant.d/insecure_private_key vagrant_vm_ip # sshできない vagrant ssh # sshできる
置き換える秘密鍵は .vagrant/machines/default/virtualbox/private_key
に置かれるので、
こちらを指定するように変更するか、Vagrantfile
にconfig.ssh.insert_key = false
の記述を追加すれば以前の動作と同じになります。
普段使いでは問題ないですが、自動テストなどで使っているとはまりますね。というかはまりました。
ダレカタスケテ-
— シリル@ひだまりPHP 3日目西f26a (@d_cyrill1129) July 24, 2016
C90入稿完了したので、夏コミ本出ます。お品書きはまた後日出します。よろしくお願いします。
コミックマーケット90情報です
よろしくです。 / サークル「ゆきいろパラソル」は、コミックマーケット90で「日曜日 西地区 "f" 26a」に配置されました! コミケWebカタログにてサークル情報公開中です https://t.co/NimzDVYBnD #C90WebCatalog
— シリル@ひだまりPHP 3日目西f26a (@d_cyrill1129) 2016年6月10日
夏コミの時期が来ましたよ!!!
今回は3日目(日曜日)西f26aのスペースをいただきました。
シンフォニーちゃんが活躍する「ひだまりPHP」シリーズの新刊と既刊の「ひだまりPHP vol.2」を用意する予定です!
あと余裕があればWebサーバの運用周りで何か書ければいいなーと考えてますので、3日目西f26aの「ゆきいろパラソル」をよろしくお願いします!
西館f26aの配置はこんな感じです!当日はよろしくお願いします!!! pic.twitter.com/2tFm2DU5as
— シリル@ひだまりPHP 3日目西f26a (@d_cyrill1129) 2016年6月10日
Elasticsearch 1.7から2.3にアップグレードする
重い腰を上げてElasticsearchのメジャーバージョンアップを行うことになったのでメモ。
続きを読むFluentdを使ってKibanaにデータを入れてるけど一部のログを落としていたのでリプレイしたい
Fluentdを使ってKibanaにデータを入れてるけど一部のログを落としていたのでリプレイしたいってときに、 取りこぼした時間分のログを置いて、Fluentdに読み込ませて送ればいいじゃんっていう気持ちになるけど、 ログの量が多いとクライアントが高速にログを読みすぎてFluentdのバッファーが溢れてしまう。
Fluentdの用途的におかしいので仕方ないのだけどなんとかしたいので、 通常時と同じようにログに追記していくようにする。 そのときに trottle(1) を使い、ストリームの速度を制限する。
cat foobar_log | throttle -l /tmp/throttle.ctl -M 1 > /tmp/foobar_log_replay.log
これで書き込み速度を1MByte/sに制限できる。
-l
は制御用の名前付きパイプを作るオプション。
速度が早すぎたり遅すぎたりした際に調整するのに便利。
throttle -t /tmp/throttle.ctl -B 1
このようにすると、転送は1Byte/sになる。速すぎたときに使う。
流すログの量が多い場合は、バッファーのディレクトリを監視しながら
自動でlimit
を調整できると便利かもしれない。
<source> type tail path /tmp/foobar_log_replay.log tag kibana4.foobar format json time_key time read_from_head true </source> <source> type tail path /var/fluentd/foobar/*.log pos_file /var/fluentd/foobar_log.pos tag kibana4.foobar format json time_key time </source>
かしこ