[Dd]enzow(ill)? with DB and Python

DBとか資格とかPythonとかの話をつらつらと

ISUCON11予選に参加して32位で敗退してきました。

f:id:denzow:20210912004126p:plain

最近全然書いてなかったけど、そろそろ書かないと復帰できない気がしたので。

タイトル通り、今年もLAPRASのメンバーでISUCON予選に参加してきました。チームメンバーは去年から一人入れ替わり、チーム真人間として参加しました。メンバーは以下でした。

@showwin @KawamataRyo @denzowill

f:id:denzow:20210912003036p:plain

結果は92087点で32位。使用言語はPython。だってPythonの会社なんです。LAPRASは。

f:id:denzow:20210912003226p:plain
使用言語比率

結局Pythonは1組も決勝には行けておらず、Goばっかりという形でした。逆にPythonを選択したチームでは1番、少なくともかなり上位の結果だったのかなと思うと悔しさもあります。

当日のリポジトリはこちらです。

github.com

なお、修正ごとの点数を記録するのを忘れたためどれくらい効果があったかは記憶と当事者の主観です。

当日の進め方

準備

11時くらいまでは各々準備をしながらレギュレーションの読み込み等をしていました。何が加点になって何が減点になるのかは大事です。 今年も3台のインスタンスが各チームに割り当てられる形式だったため、それぞれが1台ずつを専有し @showwin がインフラ(nginx等)を確認し残りの二名は各々アプリケーションの改善に取り組む方針としました。

また、この時点でDatadog APMを設定し、ボトルネックはほぼすべてAPMで確認するようにしておきました。

最初にやったこと

@KawamataRyoとともにコードリーディングを行い、コメントを付与していきました。この時点で明らかなN1や改善が即座に思いつくところはTODOとして置いておき後で処理しやすいようにしました。

github.com

さらに、app.py 1ファイルのままだと二人での作業がぶつかりやすい上に見通しも悪くなるため雑にファイル分割を行い、特に重いエンドポイント等はロジックを別ファイルに切り出しました。

github.com

改善開始

DBにIndexがほとんどない状態だったので @KawamataRyo にざっくりとIndexを追加してもらいつつ /api/trend はレギュレーション上加点箇所ではないのに激重だったため、他の改善点をAPMから見えるようにするため雑にキャッシュを設けました。

github.com

これだけでも若干点数はあがりました。

続いて、 isu_condition.condition のデータ改善を行いました。該当列には hoge=true,foo=false,bar=false といった形式で警告データがいれられているものの、実際には=true の数を数えているだけだったためデータ投入時点で数を数えておきwarn_count カラムを追加することでIndexが利用できるようにしました。

github.com

この修正自体は公式の講評でも触れられており効果的でした。ちなみにこのときに初期データも修正する必要があったためダンプファイルを編集した結果、画像データのバイナリが中途半端に壊れベンチ-マーカのバリデーションに失敗するトラブルに見舞われ右往左往しています。

f:id:denzow:20210912005249p:plain

アイコンの改善

アイコンデータがDBに格納されており、アプリケーション経由で戻していたのでnginxで戻そうとしています。

github.com

しかし、ログイン状態等に応じて404を戻す必要があるなど、nginxだけでは制御しきれなさそうと後に判明したため、ファイルを外に書き出して戻すのみにしました。これだけでもisu テーブルのサイズを抑えることができました。効果はあったかな・・・?

get_isu_list のN+1改善

isuの一覧を取った上で、最新のisu_condition を取得する処理でN+1が発生していたのでSQLで結合して回避するようにしました。

github.com github.com

理論上効果があるはずでしたが、この時点では大きなスコアの変化はありませんでした。どうも結合のあたりで十分にIndexを効かせてパフォーマンスを出せてないようだったため、クエリを2回発行する形に後に変更しています。

github.com

post_isu_conditionの改善

外部のサーバからISUの状態がどんどん飛んでくるのでそれを永続化するpost_isu_conditionがかなり負荷が高くなっています。初期では1/10だけを処理して他は無視する実装になっていますがそれでも重い。

github.com

そこで、雑にグローバル変数でバッファを設けて100件ごとにBulkInsertするように変更しました。これだと各スレッドごとにバッファができてしまう上に、100件に満たない値が最終的に無視されてしまい整合性が保てないのですが、元々の実装でも90%のリクエストは無視してるくせにレスポンスコード200を返しているため整合性はほぼ気にしなくていいだろうと判断して簡易実装しています。

ただ、この実装だと100件ごとのBulkInset時にAPIがタイムアウトし得るため、後にBulkInsert時は別スレッドを生成してそちらにたくす実装にしています。

redisの投入

DBの負荷を少しでも下げるため、isuuserといったあまり変化がない情報はRedisでキャッシュをもたせるようにしました。

github.com

github.com

これは地味ながら効果はありました。

スコアの伸び悩み

レギュレーション上、初期実装で1/10に落とされているISUの状態情報を多く受け入れられるようにすることで点が伸びる想定でしたが1/2程度まで受け入れをしてもいまいち点が伸びない状態でした。この間に@showwinに3台構成にするためnginxやMariaDBの設定を調整してもらいました。

api/trendと向き合う

api/trend は初手で雑なキャッシュをして以降特に触ってなかったのですがさすがに向き合うべきと思い着手しました。ここは以下のような実装でクエリの三重ループになっていました。

  • isucharacterごとにgroup by し存在するcharacter の一覧を取得
  • 手に入れた上でcharacter毎にisu のリストを取得
  • isu 毎に isu_condition の最新のレコードを取得し状態レベルに応じて集計

仕方ないので強引に1クエリにまとめました。

 with latest_condition as (
  select
    jia_isu_uuid,
    timestamp,
    warn_count
  from
  isu_condition
  where
    (jia_isu_uuid, timestamp) in (
      select
        jia_isu_uuid,
        max(timestamp)
      from
        isu_condition
      group BY
        jia_isu_uuid
    )
)
select
  i.character,
  ic.warn_count,
  group_concat(
      i.id
      ORDER BY ic.timestamp desc
  ) as isu_id_list,
  group_concat(
      ic.timestamp
      ORDER BY ic.timestamp desc
  ) as timestamp_list
from
    isu i
left outer join
    latest_condition ic
on  i.jia_isu_uuid = ic.jia_isu_uuid
group by
    i.character,
    ic.warn_count
order by
    i.character,
    ic.warn_count

github.com

group_councatを用いて必要な結果をカンマ区切りで戻すことで一気に取得できるようにしています。ここは書いている本人もあってるか全く自信がなかったので元々の実装の結果と突き合わせしながら延々と作業をしていました。 これはそれなりに効果があったはずですが、講評ではここの更新頻度に応じてユーザ数が増える実装だったらしいので、キャッシュ保持期間の5秒はもう少しチューニングの余地があったと思います。

また、キャッシュがなかった場合はこのクエリを実行するようにしていましたが、クエリ自体もそれなりに重いのでキャッシュが切れた際はとりあえずキャッシュを返しつつスレッドを生やしてキャッシュの再生成をするほうが良かったかもしれません。

isu graphで過度に情報を取得している問題の改善

ISUの状態を示す日次グラフを表示する機能があったのですが、コードがやたら込み入っており @denzow @KawamataRyoは読むのを後回しにしていたのですがインフラ周りをやり終えた@showwinが一瞬で時間範囲が広すぎる問題に気が付き、修正してくれました。

github.com

最後

ここまでで目立った修正はしたつもりだったものの、予選通過ラインとして予測した10万点には届いておらず、最終的にpost_isu_conditionを全開放してもサーバの余裕がある割に点が伸びない状態でした。

github.com

とりあえず思いつく限り細かい修正を行い、最後にAPMの無効化のロギングの停止を行いベンチマーカーを何回か回してる最中に時間切れとなりました。

github.com

振り返り

去年からメンバーが一人入れ替わり、またフルリモートでの参加でしたが予選通過こそできなかったもののそれなりに手応えのある会でした。特に序盤にレギュレーションの読み込みとファイル分割は後半に結構効いていたため来年もまずはこのあたりから固めつつ、来年こそはPythonでの決勝進出を目指します。