ISUCON9予選一日目に🐶として参加しました
ISUCON9にチーム名「🐶」として予選参加しました。
1人チームで言語はPHPを選択し、最終スコアは11,590でした(ベストは12,780)。
スコア的には予選通過ラインだったようですが、追試をパス出来ず予選通過とはなりませんでした。無念…😇
追試で落ちて本戦進出できない不幸な方がこれ以上に出ないように&来年の自分のためにも忘れないように記事にしておきます。
スコアの推移はこんな感じです。
最終的にはappサーバー×1、dbサーバー×1です。
(appサーバーを2台にしたかったけど、間に合わなかった…)
ほとんど準備らしい準備はしてなかったのですが、1人参加ということで最低限タイムスケジュールだけ決めておきました。
--
10:00〜 レギュレーション読む
10:05〜 インスタンス作成
10:10〜 ブラウザで実際にアプリをさわる
10:15〜 デフォルト状態でベンチ回す
10:20〜 実装をPHPに切り替える
10:25〜 PHPでベンチ回す(チューニングいれない)
10:30〜 Xhprof、netdata、kataribe、myprofierなどモニタリングやチューニングに利用するツールをいれる
11:00〜 1台でチューニング
12:00〜 昼休憩
12:30〜 1台でチューニングの続き
15:00〜 複数台化
17:00〜 再起動チェック
17:30〜ベンチガチャタイム
--
1人でやると3人でやるときに比べてコミュニケーションコストが0になるというメリットがありますが、どこかに詰まってしまうとカバーしてくれる仲間がいませんので時間的にも精神的にも辛くなり致命的になります。
そのため、まずはデフォルト状態でベンチ回す、言語切替でチューニングいれないでベンチ回す、なにかチューニングいれたらその度にベンチ回す、といった感じでベンチがコケた時の原因を特定しやすいようにしておくという、基本的なところはちゃんと行うように気をつけました。
コードをさわるのは自分だけなのでデプロイもサーバー上の/home/isucon/isucari/webapp/php以下をローカルと同期し、コード編集して保存する度に自動的にsftpでアップロードするようにPhpStormで設定しておきます。
一応webサーバーを複数台した用にPhpStormと同期しているサーバーから指定したサーバーにソースコードをrsyncでばらまくシンプルなデプロイ用のシェルだけ用意しておきました。
チューニングにあたってはkataribeでアクセスログ解析で重たいエンドポイントの確認して、アプリ側もXhprof&XHGuiでプロファイリングを行いました。
PHP7でXhprofを導入するのは下記に記事書いてますので参考にどうぞ。
https://qiita.com/takahashi-yugo/items/8f141e6ca0259a2bd2c8
もともとサーバーに用意されている/home/isucon/local/phpにあるphpバイナリは言語実装をPHPに切り替えるタイミングでapt-getでphpを新たにシステムに入れてsystemdのisucariのサービスで参照しているphp-fpmもapt-getでいれたものに変更しました。
これは後々チューニングでmemcache、redis、apcuやXhprof用にmongoのエクステンションを入れる事を想定していたのでいちいちビルドしていると面倒だからです。
一応/home/isucon/local/phpで特殊なエクステンションとか使われてないよねとかはphp -mで確認した上で変更を行いました(まぁ各言語の実装をあわせないといけないのであまり特殊なエクステンションは使ってないだろうとは思いつつ)。
Xhprof&XHGuiの導入は何回も飽きるほどやって来ましたので特に詰まることはなく、モニタリングツールやapt-getで色々導入する部分は事前にUbuntu16でリハーサルしておいて問題ないことを確認しておいたので特に問題ありませんでした。
主におこなったチューニングポイントは下記です。
・PHP入れ直したタイミングでXdebugは無効に(お約束)
・mysqld.cnfにとりあえずで下記を投入
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 2
innodb_buffer_pool_size = 2G
innodb_log_file_size = 512M
innodb_log_buffer_size = 16M
innodb_doublewrite = 0
・sysctrl.confに下記を投入
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 10
・userデータをAPCuにのせ、参照時はオンメモリで返せるように
・userのデータはオンメモリ化したのでN+1部分は許容
・categoriesもAPCuでオンメモリ化
・configsもAPCuでオンメモリ化
・/users/transactions.jsonで叩いている重たいクエリを調整しSELECTした結果はAPCuにのせる
・/items/editでitemsを更新後に無駄にitemデータを参照しているところを削除
・DBを別サーバーに分離
・外部APIのレスポンスをキャッシュ
・ある程度チューニングできたところでcampaignを増やし、最終的には4に。
・php-fpmのプロセス数を増やす
・最後はXhprofを無効にし、nginxのアクセスログや不要なプロセスを落とす
/users/transactions.jsonのSQL調整の部分は、下記のように調整した上で
SELECT * FROM `items` WHERE `seller_id` = ? OR `buyer_id` = ?) AND `status` IN (?,?,?,?,?) AND (`created_at` < ? OR (`created_at` <=? AND `id` < ?)) ORDER BY `created_at` DESC, `id` DESC LIMIT ?'
↓
SELECT * FROM `items` WHERE (`seller_id` = ? OR `buyer_id` = ?) AND `status` IN (?,?,?,?,?) ORDER BY `created_at` DESC, `id` DESC
取得したデータはAPCuに載せちゃいます。SQLを変更したことで1st pageの時でもpagingの時でもクエリが同じになりキャッシュを効きやすくします。
その上でアプリ側で (`created_at` < ? OR (`created_at` <=? AND `id` < ?)) の部分の処理をおこなったりLIMITの部分をarray_sliceで代替することで同じデータになるように調整します。
SQLで取得するデータは多くなるのですが、SELECTする結果が変わってしまうような更新がされるまではAPCuにのせる前提でしたし、クエリをシンプルにしてMySQL側の処理を軽くしたかった感じです。
このあたりはredisにキャッシュさせようか悩みましたが、キー設計などがすぐにはいいアイデアが浮かばなかったので結局キャッシュまわりは全部APCuになりました。PHPはpreforkでプロセスでわかれちゃって、Goみたいにメモリ上にデータ保持しておくということは出来ませんが、APCuだとそれに近いことができるのでアーキテクチャ的な弱点をカバーします。APCuだと難しいとか構造的に適さないとか複数サーバーで共有したいという時にはredisかmemcache使うという方針です。
後は/uploadにアップロードされる画像をnginxで返してブラウザ側でキャッシュされるようにもしましたが、あまりスコアには影響ありませんでした。
外部APIのレスポンスキャッシュ、DBサーバーの分離、campagin値を上げる、php-fpmのプロセス数を増やすあたりがスコアへの影響が大きかったです。
特にphp-fpmのプロセス数はあげる度にスコアが地味にあがっていくので最終的には150(!)くらいにまで上げました。
こんな設定、あのスペックのサーバーだったら業務ではまずやりませんが、isuconなので動けばいいや精神です。(こんな設定でもちゃんとさばけるPHPすごい)。
MySQLを別サーバーにした時はMySQLの権限まわり調整したけどなぜかappサーバーからつながらず、少し手間取ってしまった。google先生にきいてbind-addressということに気づけたけど、このあたり時間かかってたらやばかったです。
DB周りでどのクエリが重たいのかはmyprofierを使って確認しました。
https://github.com/KLab/myprofiler
このツールはすごくシンプルなので使い勝手がよく、いつもお世話になってます。
netdataはさくっとインストールできて便利なのですが、最初に全体のイメージ掴んだら後は終始アプリがボトルネックだったのであまり見なくなりました。だいたいはtopみてました。
前回もソロ参加でしたし、PHPでのチューニングや実装スピードには不安はなかったです。
スコアに直接的に影響があったチューニングができるまで15時くらいまでかかってしまったり、序盤で初期で用意されているupload以下の画像を謎に消えてしまうトラブルに見舞われたりして他のサーバーからもってくるみたいなことやってて、もう今年は無理かなぁと諦めかけましたが、最終的にはなんとかスコアは1日目だと14位、全日だと26位の位置までいけたのでソロでやったわりにはまぁ上出来かなぁ。
ただ、ベンチが5回に1回くらいの確率で/buyが500になることでfailedになっていたのですが、毎回ではないから勝手に外部APIの問題かなーと決めつけてしまって、終盤気味で時間がなかったこともあり、とりあえず深く追わず他のチューニングや再起動チェックを優先してしまいました。実際には/buyはデッドロックが起きたときに500になっていたようです。
結果的にこの/buyのエラーで追試が失敗してしまい、本線進出は逃してしまいました。
追試時にデッドロックが起きなかったらいけてたかもですが、failedになった原因を確認するという超基本的な部分を怠ったのが原因なのでここは猛省ポイントです。
ソロ参加は無謀な印象がありますが、自分的にはコミュニケーションコストや実装を複数人でやる際の細かな調整、実装方針の話し合い、PRマージしたり競合解決したりみたいなところが一切なくなるし、方針さえミスらなければ生産性的にはいいんじゃないかなと思ってます。
特に近年アプリが結構でかくなってきているので一人でガッツリやりきるパワーがあるならソロおすすめです。
でもwebサーバー、アプリ、DBの全体的な知識を求められたり、キャッシュ戦略なんかも全部自分で考えたり、失敗したときの責任も全部自分なのでまぁまぁつらいです(そこが醍醐味ですが)。なので、追試ミスをやらかしたのはさすがに落ち込んで、とりあえずやけ酒して家族に慰めてもらいました。
今回のミスはしっかり反省して来年につなげるぞ…!