ジェットゾウ

ISUCON 7 予選1日目を1位で通過して来ました

dozen ISUCON

ISUCON7にチームMSA(@ken39arg, @mizkei, @suzuki)として参加してきました。

今回は予選に参加したチームが400組を超え、1チームに割り当てられるサーバも3台でしたから土日合わせて1200台ものインスタンスを準備したということになります。 運営チームの方々、ありがとうございました。

チームMSA

@mizkeiさんがチームメイトを募集していて、私も参加しようと思ってはいたんですが誰も組む相手がいない…という状況でしたので、ぜひご一緒しましょうということで参加させてもらいました。あと一人どうしようかと思っているときに来てくれたのが@ken39argさんです。

チーム名は3人の頭文字です。なんでこの順番かは@ken39argさんが知っているかもしれません。

やったこと

インフラ周り以外はノータッチで、アプリの読み込み・改修は完全にkens39argさんとmizkeiさんにお願いしていて、私はインフラのタスクだけをしていました。 なのでその辺に関しては二人のブログを参照されるのが良いと思います。 ken39argさんとmizkeiさんはとにかく実装スピードが速く、またken39argさんはインフラやミドルウェア周りの知識・経験もお持ちなので私がもたついてる場面で一足先に設定ファイルを書いてくれていました。すごいスピードです。

事前準備

複数台のサーバを管理しやすくするために、repoを作ってサクッと環境構築できるchefのレシピを用意しておきました。

  • 3人のユーザを用意
    • http://github.com/{username}.keys が便利です
  • root, appユーザでサーバ間をsshで移動できるようにする
    • ついでにroot, appユーザの公開鍵をdeployキーとして登録しておきます
    • あとhostsで自分たちがやりやすいホスト名を設定、例えばisu1~isu3みたいにしておくと便利
  • 全台でchefを回したり、repoを全台に配るMakefileを用意

当日

  • 事前に用意しておいたchefを回す
  • mysqlのslowlogを出すようにする
  • ミドルウェアの設定・アプリを全台にデプロイするMakefileを作成
  • 構成確認
  • go実装へ切り替え
  • goのコードをrepoに入れる
  • alpのログ取れるようにnginxのログフォーマットをltsvに
  • 最初のベンチを実行
    • pprof, alp, pt-query-digest でログ解析
    • dstatを眺める

とりあえず以上のことをやりました。

初期ユーザ名/パスワードで全員がログインし、分担して作業を進めていきました。私はchefを回したりアプリのデプロイができるように環境を整える仕事をしていました。

構成確認では3台ともCPU 1コア, メモリ 1GB, スワップ 4GBであることを把握しました。全体的にリソースが少ないというのはもちろんですが、スワップがきになるぞ、という感想。一応心の隅に留めておきます。 アプリの構成とレギュレーションで公開されているネットワーク帯域幅も加えてまとめると、以下のようになります。

  • CPU 1コア, メモリ1GB, スワップ4GB のサーバ3台
    • nginx + app x 2
    • mysql x 1
  • WAN帯域 100Mbps
  • LAN帯域 500Mbps

go実装に切り替えて初回のベンチを回したところ、dstatでMySQLサーバがLAN側のネットワークを500Mbps以上も消費していました。レギュレーションによると500Mbpsとされているので、ここがボトルネックであることは明らかです。念のためiperfで計測してみましたが560Mbpsくらい。 これは一体どういうことなんだと思ったのですが、実装とslowlogをみてもらったアプリ担当の二人から画像データをBLOB型のカラムに保存してたと聞いたので、これが原因なのかな?と思いました。

一旦ホワイトボードを囲んで話し合いをした結果、ともかくDBから画像を剥がさないことには何も始まらないという共通の見解を得たため、この対策から取り掛かりました。 初期データの画像ファイルをファイルに書き出すのと画像をファイルシステムに保存するようにアプリを変更する作業をmizkeiさんが行い、ken39argさんがアプリを読んでINDEXを張っていくのと静的ファイルを参照するようにnginxの設定を書き換えるという作業を行なっていました。

その間に私が何をしていたかというと、何もしてません。DBに入っている初期画像データをファイルに落とすというのが当初の担当だったんですが、失敗し続けていたのでmizkeiさんにバトンタッチしました。ken39argさんが書いてくれたnginxの設定をレビューしたくらいですかね…。

画像データを引っこ抜くのなんてわけないだろうと思ってmysqlコマンドを使ったシェルのワンライナーを書いたんですが全くうまくいかず、これなら絶対うまく行くと思ってcurlでアイコン画像のエンドポイントを叩いて画像ファイルを保存してみても上手くいかずでしょんぼりしました。

画像をDBから剥がした結果、以下のような構成になりました。 *がついているサーバがベンチマーカのリクエストを受け付けます。

  • *isu1: nginx (GET /icons POST /profile 以外) + app
  • *isu2: nginx (GET /icons POST /profile 以外) + app
  • isu3: nginx (GET /icons POST /profile のみ) + app + DB

isu1とisu2ではnginxでGET /iconsPOST /profileをisu3に流してしまい、画像のアップロードと画像ファイルの配信はisu3のnginx + appが担当するようにしました。 これでLAN帯域のボトルネックが消えたため、スコアが上がったはず(覚えてない)。

ここからアプリ担当のken39argさんとmizkeiさんがものすごい勢いでアプリのリファクタリングに入りました。 この間のスコア計測中にDBサーバでページアウトが発生し始めましたが、メモリ消費は600MBだったのでvm.swappinessを確認すると60でした。メモリを使い切るまでページアウトしないように全部のサーバにおいて1をセット。 いくつかアプリの改善を経てスコアが上昇してはいたのですが、アプリサーバもDBサーバもCPUが遊んでおりスコアが伸び悩みます。 dstatからWAN帯域がサチっているということがわかったので、静的ファイルのキャッシュをするようnginxの設定をしたところ、304を返せるようになりスコアが劇的に上昇。スコアが伸び悩む中黙々とアプリの改善をしてきた効果が現れました。

その後もアプリの改善は続き、スコアはどんどん伸びていきました。初めてMySQLサーバのCPUリソースがボトルネックとなり、再びDBだけのサーバとするため以下のような構成を取ることにしました。

  • *isu1: nginx + app
  • *isu2: nginx + app
  • isu3: DB

isu1, isu2はそれぞれがPOST /profileを受け付け、ローカルにアイコン画像を保存します。アップロードされた画像はどちらか一方にしか保存されないため、ローカルにないファイルへのリクエストが来た場合はもう一方のサーバを見に行くようnginxを設定することにしました。 この設定を適用した後、アイコン画像が表示されなくなってしまったため、お互いのサーバを参照する設定がまずかったのかと思いisu2だけにアイコン画像を保存する設定に変更しました。しかし、実際にはisu3にあった初期画像ファイルをisu1, isu2にコピーした時にコピー先を間違えていたのが原因でした。 上記の問題で関係のないアプリの変更PRにも不具合があるように見えてしまい、不具合がないアプリの見直しをmizkeiさんにさせてしまうという、アプリを巻き込んでハマってしまうという大事故を起こしました。本当に申し訳なかったです。

ようやくこの構成がまともに動くようになるとスコアは46万点を記録しました。アプリサーバは遊んでいるがMySQLサーバはCPUを食っていてIOwaitが出始めているという状況だったので、innodb_buffer_pool_sizeしかいじっていなかったMySQLのチューニングをする流れになりました。 innodb_buffer_pool_sizeを増やしてベンチマークをかけるとスコアががくんと落ちたため、まずかったかと急いで戻して再度ベンチマークをかけるとさらにスコアが落ち、そんなことをしている間に2万点まで落ち込み大騒ぎになりました。

ここで競技終了まで1時間30分というところです。DBサーバに問題があるのかとずっと調べていましたが、原因はアプリサーバで発生していたページアウトでした。アプリがメモリを食っているということで、アプリ担当の二人に調べてもらったところ、アイコン画像のアップロード部分でioutil.ReadAllを使っているのが原因だろうということをアプリ担当の二人が突き止めたのは競技終了まで40分の時点でした。

再起動試験をする余裕なども考えると安全圏ではないという結論に至り、アプリの修正はされませんでした。アプリを再起動すれば1回のベンチには耐えることを確認した為、このままにすることにしました。

競技開始してからスコア計測時は必ず全サーバのモニタリングをしていたはずなのに最後にスワップを見落として全員を道に迷わせたのが辛かったです。計測中はdstatをひたすらみてたのに、スワップに気づいたのはなんとなく打ったfreeコマンドでした… まあ、freeで気付いた時はスコア計測中ではなかったのでdstat -afで表示しているpage in/outの値は判断しようがないです、が、その場合でもメモリ使用量でアプリが異常にメモリを消費していることに気づくべきですし、スコア計測中は page out が頻発していたのを見逃していたということですのでどのみちダメダメだな。

終わってみて思うこと

1位で通過できたのは非常に嬉しかったんですが、ブログを書いたりgit logを見返しているうちに自分の仕事ぶりを冷静に振り返ることになります。

手が遅いというのはかなり感じたんですが、速度以外にも能力不足な点が多くあり、二人の足を引っ張る場面が何度かありました。インフラ側がしっかりしてれば防げたトラブル、加点要素がいくつも思い当たりますし、インフラの仕事も全部こなせなかったので情けないですね。

そういえば私は普段ken39argさんやmizkeiさんと一緒に働いているわけではないのですが、競技中にやりにくさとかは全く感じませんでしたし、そうなるんじゃないかという不安もチーム結成直後から予選終了まで一度もなかったです。これはとても良いことだったなと思います。

今回は予選から複数台構成での戦いということでしたが、チューニング以前に複数のサーバに役割を持たせてサービスを運用することにまず魅力を感じるので、例年では本選に行かないと味わえない複数台構成でのISUCONが予選から体験できるのは最高だと思いました。

それと、終わった後にisu3にnginxだけ置くための設定ファイルを用意していたので入れていたかもしれないとメンバーに言ったのですが、実際あの時の私がそれを入れるようなことは絶対になかったと思うので、嘘をつきました。 MySQLのiowaitを解消したら入れてたかもしれないですね、みたいな言い方をしていたんですが、おそらくMySQLのチューニングでiowaitが解消されてまたCPUが天井に張り付くのをみたら、MySQLのCPUリソースは1ミリも割きたくないのでこのままにする、という選択をしていたと思います。

3台構成にする利点はWAN帯域を300Mbps確保できることですが、終盤に2台でベンチマーカからリクエストを受け付ける構成でもスコア計測中のボトルネックはDBサーバのCPUで、WAN帯域を200Mbps使い切ってはいなかったと思います。 ただし、スコア計測が始まってすぐはキャッシュされるアイコン画像などもきちんと返す必要があるため、帯域を使い果たしていたはず。 その立ち上がりの詰まりを取り除けると、早い段階から負荷がかかる(段階的に負荷を上げてくるという特性も把握してなかった)ため60秒で効率的にベンチを回せるためスコアが伸びるということだと思います(ほかのチームのブログにそんなことが書いてあったような気がします)。 私はスコア計測の初期にWANが刺さっていることと、ベンチマーカの特性の両方を把握していなかったので、DBサーバのCPUを削って帯域を広げるのは全く考えにありませんでした。 3台構成にしたチームの話を聞いたから、後出しでこんなものを用意していましたと言ってしまったんですが、これはかなり格好悪いですね。

自分の未熟さが良くわかったし大いに反省したので、これから伸ばして行きたいです。

1ヶ月後の本選も頑張ります。チームの皆さん、よろしくお願いします。🐘

dozen
どぜんです