ISHOCON2のWriteup

2018-08-26



ISHOCON2に参加してきたぞ、成績は7位/28人くらいだ。そこで今日はちょいとwriteup(詳細な記録、具体的にどんな対策をしたか)の一つでも書いてみよう。なお言語はもちろんRuby! Goなどが圧倒的に強いがスクリプト言語でも基本的な対応をやるだけで35000点くらい(初期値2000点)は取れる。

そもそもISHOCONってなんだ? ISUCONじゃないのか? という人が多いかもしれないので先に説明しておこう。ISHOCONはいわゆる社内ISUCONの一つで株式会社scoutyの@showwinさんが主催する個人参加のイベントだ。ISHOCONの問題はベンチマーカー含めて公開されているので興味があるなら実際に触ってみよう。

普通のISUCONよりも難易度が低めでアプローチすべき場所が限られているため初心者が単独で触ってもかなりやりやすいはず。
https://github.com/showwin/ISHOCON2

はじめに

スポンサーの株式会社Scouty、主催のshowwinさん、他の参加者のみんなお疲れ様です。
あとチームメンバーを集められなくてこのままだとISUCON8に参加できないので誰か助けて。٩( ᐛ )و
(追記: @Goryudyumaさんと@bgpat_さんのチームに入れてもらいチームBremenとして参加できることになりました)

ざっくりとした流れ

まず何はともあれ最初に行うのは「計測」だ、具体的にはsinatraとmysqlで各URLが何回呼び出されてどの程度の時間がかかっているか。ボトルネックとなっているSQLは何かを探す。

この時点でわかったのは投票を行うためのPOST '/vote'が最も多く呼び出されており、またリクエストあたりの時間もかかっているということ。今回の大会ではもっぱらこの処理を如何に高速化するか、というところが重要となる。ここでは投票データをデータベースではなく完全にオンメモリ化する作業が中心で最も効果が高い。

nginxでも最初からSSL化されていたためHTTP2化したり、静的ファイルはnginx側で圧縮ファイルを返したりといった基本的な対応を行ったがこれらはスコアに影響をほとんど与えなかった。そもそも今回の題材では配信する静的ファイルはcss一個だけであり、こういった対策は効果が非常に薄い。何も考えずに出来る対応だから手始めにやったが時間が貴重ならやるべきではなかった。
(追記: 他の参加者によると実際にはこれらの施策も有効だった。スコアの伸びを感じなかったのは自分が最序盤に何も考えず設定したため、その時点ではボトルネックがnginxではなくアプリにあったためっぽい)

MySQLチューニングもそれなりに行う。キャッシュ化などの設定を追加したりインデックスの付与を行ったり。これは効果を上げたが最終的にほとんどのデータはオンメモリで扱われることになったため無駄といえば無駄である。

レコードのオンメモリ化

今回の題材におけるメイン作業である。
計測結果を見れば分かる通り、SQLで最も重いのはvotesテーブル、投票データを格納するテーブルへのINSERT文だ。これがダントツで重い。そして今回のISHOCON2では再起動チェックがない、つまり競技中のベンチマーカーさえ通ればOKだ。そこでかなり雑にデータをオンメモリー化する。ベンチマーカー後にvotesテーブルを見ると4000件程度しか入っていない。念のため試しにIRBでハッシュを要素にした配列を数万件作ってselectなどを回してみると問題がないことがわかった。そこで本当に愚直にプロセス中のメモリ内に全ての投票データを格納させるように書き換える。

- params[:vote_count].to_i.times do
-   result = db.xquery('INSERT INTO votes (user_id, candidate_id, keyword) VALUES (?, ?, ?)',
-     user[:id],
-     candidate[:id],
-     params[:keyword])
- end
+ @@votes << { user_id: user[:id], candidate_id: candidate[:id], keyword: params[:keyword], vote_count: params[:vote_count].to_i }

見ての通り、投票数の分だけレコードを作るのも無意味なので単にvote_countとして保存している。これで後は他の箇所のvotesに対するSELECT文をゴリゴリと置換していけばいい。

   def election_results
-    query = <<SQL
- SELECT c.id, c.name, c.political_party, c.sex, v.count
-   FROM candidates AS c
-   LEFT OUTER JOIN
-    (SELECT candidate_id, COUNT(*) AS count
-   FROM votes
-   GROUP BY candidate_id) AS v
- ON c.id = v.candidate_id
- ORDER BY v.count DESC
- SQL
-      db.xquery(query)
+      return @@result if @@cache[:result]
+      @@result = ALL_CANDIDATES.dup
+      @@result.each do |cand|
+        cand[:count] = @@votes.select { |vote| vote[:candidate_id].to_i == cand[:id].to_i }.sum {|g| g[:vote_count].to_i }
+      end
+      @@cache[:result] = true
+      @@result.sort! { |a,b| b[:count] <=> a[:count] }
    end

例えばこの投票結果を生成するメソッドはデータベースのアクセスを全て廃止してメモリだけで完結している。ALL_CANDIDATESは候補者のデータが数十件入っている。このデータは完全に静的で変更されることがなく数も少ないので予めデータベースから読み込んで使い回す。

ついでにキャッシュフラグを作っておき、投票が新規に行われていなければ(キャッシュフラグがtrueのままなら)前回の処理結果をそのまま返す。この題材のベンチマーカーは基本的に「ユーザーが集団で投票する」というアクションの後に「ユーザーが投票結果を確認しにいく」というフローで走るのでこういうこともそれなりに有効。

usersテーブルも完全な静的データであるため、オンメモリ化を検討したがレコード数が余りにも多かったため、直接メモリに乗せることを躊躇した。これは後で乗せても良かったのかを検証しておきたい。とりあえずRedisへ name_address_mynumber をキーにid_votecountをバリューとして格納しようとしたがディスクがカツカツだったため上手く行かず試行錯誤中にタイムアップ。

HTMLレンダリングの高速化

これは意外にやってない人が多かったかもしれない。オンメモリ化でデータベースのボトルネック度が低下したので相対的にERBのHTMLレンダリング速度をあげることが有効になってくる。最初に考えたのはより高速なレンダリングエンジンであるhamlなどへの移植だが、今回の題材では所詮POST '/vote'に関連するレンダリングだけが重要だ。

なので、より愚直に文字列結合だけで行う。viewファイルを読んでいけばわかるがレンダリングが必要な動的部分は2カ所だけ。「候補者のレコードからselect/optionタグで選択肢を生成する」「エラーメッセージを表示する」の2つだ。言うまでもなく、前者は実質的に動的じゃない。だって候補者データは固定だもの。実際にレンダリングされた結果のHTMLで該当部分を埋める。

するとエラーメッセージだけが動的だとわかる。なので{静的HTML上部分} + {エラーメッセージ} + {静的HTML下部分}の文字列結合で作れる。もちろんこれも全部オンメモリ化しておいて、リクエストされた時には単にメモリ上の文字列を返すだけの処理に変える。これでディスクアクセスやパースに必要な時間が全部消える。


 このエントリーをはてなブックマークに追加


<< Rubyへのコミット: RubyHackChallenge  

[28]