ISUCON5から学ぶいろいろ

ISUCON5のエントリを見てるとたくさん得られることがある。コンテストといっても、実際に課題になった技術は単に知っているだけのものとは違って、手を動かして実際に実装してみないといけないな、と思わせるには十分なインパクトがある。単に知っているだけのことと、自分ではなくても誰かが実装して失敗して諦めて成功して、という過程を見ていると、自分が知っていたことがとても浅いことに気付いたりして、思わぬ収穫というより大きな収穫がある。

並列化

  • お題は、クライアントからのリクエストが、複数の外部APIサーバへのリクエストになるマイクロサービス。Goであれば、goroutineを使って書けるので並列化は難しくない。
  • Goでセマフォ(ロックカウント>1)を実現する場合は、sync.WaitGroupを使う必要があるのだと思っていたけど、sync.Mutexでもいけるようだ?

ISUCON5本戦にてスコアトップの18万点でfailしました - Qiita

HTTP/2

  • HTTPSを使っている外部APIサーバがあって、同じトークンでリクエストを並列化するとToo Many Requestsが返ってしまうという罠で、HTTPSで待ち受けているのは理由があって、これは実はHTTP/2を話せるサーバなので、HTTP/2を使えば並列化が可能というトリック。
  • HTTP/1.1では、リクエストとレスポンスの対応がその順序でしか取れない(ためにHTTPパイライン上におけるHoL問題が発生する)が、HTTP/2ではストリーム単位でIDが振られるので(TCP上におけるHoL問題は残るものの)並列化がうまくいく。
  • HTTP/2はクライアント側のフィックスされた実装がまだまだ言語毎に出揃っていないことが露呈した様子。node.jsはnghttp2がある。

Last-Modified / If-Modified-Since

  • レスポンスにLast-Modifiedヘッダが付加されていたコンテンツ(あるいは付加されていない場合もあり得る?)は、クライアント側からリクエストを投げる際にIf-Modified-Sinceによって、コンテンツ自体に変化があったかどうかを問い合わせる。変化していない場合に、クライアントはサーバから304 Not Modifiedを受け取ることで、クライアントはキャッシュしているコンテンツを利用することができる。つまり、外部APIサーバには500msecの遅延が挿入されていた、ということだから、これによって1msecから数msec程度にまでRTTが短縮されることになる。
  • HTTPヘッダのCache-Controlは効果としてはこれに良く似ており、max-ageでキャッシュとして有効な期間をあらかじめ指定でき、この期間を過ぎるまではサーバに問い合わせを行わなず即座にキャッシュされたコンテンツを利用できる。またno-cacheは、キャッシュしたコンテンツに対するETAGをサーバに送って検証することで、Last-Modifiedと同様の効果を得ることができ、no-storeは、クライアント側にキャッシュさせないよう指示する。

Sinatra ENV = production

  • sinatra/sinatra · GitHubIn the "production" and "test" environments, templates are cached by default.という記載があった。配信するtemplateとしてwww.yahoo.co.jpのHTMLを使い、ローカルでSinatraのウェブサーバをRACK_ENV=production指定有り・無しで起動して、abテストをパラメータは適当に決めて実行してみた結果、3倍くらい速くなっていることが分かる。
$ RACK_ENV=production ruby WebView.rb

$ sudo ab -n 1000 -c 2 http://127.0.0.1:4567/bench
Time per request:       5.543 [ms] (mean)
Time per request:       2.772 [ms] (mean, across all concurrent requests)
Transfer rate:          6767.21 [Kbytes/sec] received
$ ruby WebView.rb

$ sudo ab -n 1000 -c 2 http://127.0.0.1:4567/bench
Time per request:       15.486 [ms] (mean)
Time per request:       7.743 [ms] (mean, across all concurrent requests)
Transfer rate:          2422.46 [Kbytes/sec] received

キャッシュ戦略

  • 静的コンテンツと思いきや、数秒毎に更新されるAPIがあるので、キャッシュに有効期限を付ける必要がある。memcachedにはTTL(Expiration)を指定できるので、それで対応したチームがほとんどという印象。
  • キャッシュが切れた直後に、並列化された外部APIアクセスが一斉にバックエンドに対して発生する現象を、CacheのThundering Herdと呼ぶ。キャッシュの期限が短い場合、そのタイミングでバックエンド側へのアクセスが増えるために性能が上がらないという結果になる。バックエンド側の同一エンドポイントへのアクセスを束ねるか、常にキャッシュを最新に保つスレッドをバックグラウンドで動かしておくか、という対策が必要になる。
  • キャッシュの更新というと、最近ホット*1なVarnishのPURGEを思い付くが、PURGEは予めTTLを設定していなくとも任意のタイミングでキャッシュをクリアするためのAPI

ISUCON 5 決勝の天気APIの解説 - Qiita

MySQL Engine = BLACKHOLE

  • id:songmuさんの過去のエントリ(ISUCON1)で神の一手として登場。Engine=BLACKHOLEはWriteもReadも常に成功するが、実際にはテーブルに対する変化はメモリ上でもディスク上でも、何も起こらない。ただ、ステートメントベースのバイナリログにはクエリが残るので、レプリケーション先のスレーブでReadに対するウォームアップ(メモリにロード)が出来る。

おそらくはそれさえも平凡な日々: #isucon で優勝させてもらってきました

Gzip

  • サイズの大きなコンテンツを圧縮することによって、トラヒック流量の削減に貢献できる。これによってCPU使用率は上がるので、トレードオフを考えてサイズの大きいコンテンツに対して適用することで効果がある。

静的コンテンツ配信

  • ユーザに依存しない静的なコンテンツの配信を、アプリサーバからリバースプロキシに委ねることによって、アプリサーバの負荷を低減する。

*1:CDNスタートアップのFastlyが使っていることでも更に株が上がった感がある Fastly社に行ってきた ー 動的コンテンツの定義を変えるCDN? | API guy