839の日記

業務とは関係ない趣味の話を書くブログです。

isucon9予選の本番環境を模してAWSで素振りをした

今年はコロナの影響かisuconの案内は出ていないが、isuconっぽいことがしたい気分だったので素振りをしていた。 最初はローカル環境で構築していたが、どうせなら本番環境と同じ条件で素振りしたいよなーと思い始めてAWS環境に同等の環境を準備することにした。

環境構築

今回利用したインスタンスのタイプは「ecs.sn1ne.large」という vCPU数2個、メモリ4GBというスペックで、ストレージには「Ultra クラウドディスク」を使いました。ecs.sn1ne.largeはCPU非共有型のインスタンスで安定した性能が確保されています。インターネット側の通信は最大 100Mbps、VPC内の通信は1Gbpsまで通信ができる環境となります。ubuntu 18.04 LTSをベースに問題のソースコード、データベース(MySQL)、リバースプロキシ(nginx)、アプリケーションサーバ(Go)が起動するOSイメージを参加者に共有し、インスタンスはこのイメージから起動する形としました。

ISUCON9 予選問題の解説と講評 : ISUCON公式Blog

と記載されていたので、AWS側であっていそうなインスタンスとしてc5.largeを選択することにした。 2コア4GBな点は一緒だけど、帯域幅は全体的にちょっと高めになってしまっている。 あまり帯域がネックになる問題ではなかった気もするのでこの辺の差異は目をつむることにした。 ディスクはgp2 40GBを利用した。

特殊なところに関しては後述するけど、おおまかな手順としては

  1. インスタンス上でUbuntu 18.04イメージを使ったisucon9のアプリ環境の初期設定環境をansibleで構築
  2. インスタンス上でUbuntu 18.04イメージを使ったisucon9のベンチマーカー環境をansibleで構築
  3. SSL証明書を設定(webapp/shipment service/payment serviceの3つ)し、nginxのドメインSSL証明書と同じドメインに変更する
  4. EIPを2つ取得してwebapp/benchmarkerのインスタンスに紐付けた上でDNSの設定を行う
  5. ベンチマーカー環境でisucon9の初期設定状態で正しくベンチが取れるか確認
  6. アプリ環境とベンチマーカー環境のAMI作成

までを初期状態のセットとした。

基本的にはREADMEと公式のローカル環境でISUCON9予選の問題を動かすを参考にして構築した。

環境構築は各インスタンスで必要なデータセットの準備を行ってからansibleでlocalhostに対してprovisioningを行うようにした。 ansibleの操作に対してあまり詳しくなかったので vagrant-isucon/Vagrantfile at master · matsuu/vagrant-isucon · GitHub を参考にしてprovisioningを行った。

ansibleの構築が終わった段階ではSSL証明書が期限切れ状態になっているので、これを正規のものに置き換える手動オペレーションを挟んだ。 自分が持っているドメインで適当に *.isucon9.hoge.com みたいなワイルドカード証明書をLet's EncryptのDNS認証で作成し、それをwebapp/benchmarkerのインスタンスに配置した。 ドメインに対してIPを固定したかったのでLBとなるwebappと外部サービスを動かすbenchmarker用に2つのEIPを取得しておいた。 本来であればshipment/payment/benchmarkerはそれぞれ単独のインスタンスにしたほうが良かったかもしれないけど、面倒だったので1つのインスタンスにまとめた。

  • webapp.isucon9.hoge.com -> EIP1
  • shipment.isucon9.hoge.com -> EIP2
  • payment.isucon9.hoge.com -> EIP2

この段階で上記のDNSのAレコードを設定しておいた。

細かいけど、payment/shipmentサービスをprovisioningするときにserviceが起動するようになっており、systemctl stop shipment とかでサービスを止めておく必要がある。 shipment/paymentサービスはbenchmarkerを立ち上げるときに自動で一緒に立ち上がるようになっていたので今回はそれに乗っかった。*1 ここまで来るとbenchmarkerで https://webapp.isucon9.hoge.com を対象にして実行するとベンチマークが通るようになっているはず。 初期スコアがgoアプリでだいたい2300程度で、isucon9予選の本番環境と同じぐらいのスコアだったことを調べ、だいたい同じぐらいになっていると判断した。

この後はAMIを焼いて、webappを3台にしたりbenchmarkerのインスタンスをc5.xlargeに変えたりすれば予選環境を本番ライクに楽しめる環境が構築できた。

素振り結果

この問題はRubyで過去に解いたことがあったのだけど、この1年間は割とGoを触っていたので今回はGo言語で挑戦してみることにした。 上記の環境構築も含めて大体8時間ぐらいやって最高スコアが17340だったので運が良ければ本戦に出られるのかもしれない*2感触を得られたので良かった。

一応解き直すまではisucon9の記事を読んでトップ層の知見を思い出さないようには気をつけていた。*3 方針としてはかなり愚直な改善だけやっていたので、積み重ねればスコアが出るのだなと体験できたのが良かった。

主にやったなーと覚えているのはこの辺。

  • MySQL/redisサーバなどのミドルウェアを3台のサーバに分散
  • アプリのリクエストを3台のサーバに分散
  • nginxサーバで静的コンテンツのキャッシュ設定
  • N+1の改善(ひたすらwhere in句に直す)
  • 外部リクエストの並列化
  • 無駄な外部リクエストのカット
  • Categoryのメモリ化
  • OR句をUNIONに直してインデックスが効くようにする
  • MySQLのコネクション数調整

やり終わった後に一通り記事を読んでみてリクエストの分散化はちょっとほかとやり方が違うなと感じた。 ログインサーバを分散させる人が多かったように思えるけど、自分の場合はほぼすべてのエンドポイントを3サーバに分散させていた。 ただし、ファイルをローカルに書き出す/読み込むAPIはnginxの完全一致で1サーバにルーティングされるように設定していた。 ファイルのローカル書き出しをredisの中に入れて3サーバで共有化するというのもちょっと考えたけど、8時間の枠に収まらなさそうなのでやめた。 最終的にホストの役割は以下の感じにした。

  • webapp-1
    • LB担当
    • app全般担当
      • uploadを操作する系のパスはここに集約
  • webapp-2
    • appのupload以外担当
    • MySQL担当
  • webapp-3
    • appのupload以外担当
    • redis担当

割と9000点ぐらいをさまよっていたけど、transaction_evidencesテーブルのN+1を直したあたりで15000点ぐらいまでぐわっと伸びてここそんなに効くのかーって思ったのが印象的。 あとは外部リクエストに無駄なものがあるという覚えがあったので(これは去年の記憶を持っていたのでチート)、今回を機に仕様をちゃんと読んでみたら、確かにカットできる箇所があったのでカットしたりしていた。

Campaignに関してはずーっと0でやっていて、1にするとどうしてもログイン部分とかが詰まり始めてしまい、このあたりは結局解消することができなかった。 後で記事を読み返すとbcrypt重すぎ問題に関してはみんな苦戦してるっぽく、自分も分散化する程度でしか解決はできなかった。 あとはどうしても外部サービスが応答しなくなってしまったりして、このあたりでリトライ処理を入れたりしたけどあんまり本質的な改善はなかった。 どういう風に解決するのが正道だったんだろうか…。

今回かなり間抜けなことをしてしまったのがコネクション数の変更で、MySQLのクエリでコネクション数を変更していたのだが、ベンチマーク前に各サービスをrestartさせていた際に初期のコネクション数に戻っていることに気づくまでに時間がかかってしまった…。 confの方で設定しないとrestartで戻るということも知らなかったので、これは勉強になった。 本数を増やしてもすぐにtoo many connectionsが出てしまう問題に苦しめられていて、 mysql -e "show global status like 'max_used_connections';" で何本使ってるんだ…って見てみたら151(初期値)になっていたことでようやく気づいた問題だった。

感想

今まで結構Rubyを書いていたけど、Rubyに詳しいというよりは自分はRailsに詳しいだけなので、Sinatraベースの問題だとあんまりRubyの経験が活かせないなとGoをやって思った。 Goで書くときはisuconのように自分でいっぱいロジックを書いて組み合わせて使うことが多いので、isuconのような問題と直面したときも細かい工夫みたいなのを入れるのに慣れているような感覚がある。 あとGoLandで問題を解いていたので、やっぱり関数ジャンプとか補完の機能が便利すぎるな…とは思った。 コンパイル通ってる時点でtypoみたいなアホなミスはない、というのも心強い。

Goでisuconやるならこういうフローでデプロイしないとなーみたいな練習が本番環境ベースで練習できたのでAWSに環境構築して素振りしたのは色々と知見になった。 今年はなんとなくisuconはなさそうな気がしているけど、来年からまた復活してくれたりするならぜひ出たいと思っている。

*1:service経由で動かしていく方式が正道なのかもしれないけど

*2:今回のスコア的には出れるけど、当然2回目なので1回でこのスコアを出さなければいけないし、本番でちゃんと出し切ることは難易度がかなりが上がるということを理解している

*3:とはいえ読んだことがある記事もまぁまぁある