Skip to content

データベースを速くする、その先へ

前の章でindexやcache tableを入れ、上位SQLは計測上ほぼ0秒になりました。それでもアプリは、状態が届くたびに履歴をDBへ書き、画面を開くたびにDBから読みます。

ここでAgentは、少し大きな問いを立てました。

このデータは、ベンチ中ずっとDBに置いておく必要があるだろうか?

memoryに置くとは

memoryは、アプリが動いている間だけ使える高速な作業机です。DBは再起動しても残る倉庫です。倉庫へ毎回取りに行くより、よく使うものを机へ置くほうが速く読めます。

ただし、机には弱点があります。

  • アプリを再起動すると消える
  • 複数サーバーの机は、自動では同じ内容にならない
  • 同時に読み書きすると、順番や壊れ方を考える必要がある
  • 置きすぎるとmemoryが足りなくなる

「memoryは速い」で終わらず、公式仕様と初期化の流れを確認します。

今回の設計

POST /initializeのときだけ、初期データをDBからmemoryへ読み込みます。その後のベンチ中は、次をmemoryで処理しました。

  • userとISUの存在確認
  • ISUごとのcondition履歴
  • 最新condition
  • graph用の時刻範囲検索
  • trend用の最新状態
  • 短時間だけ再利用するJSON

conditionは時刻順のsliceへ置きます。sliceは、同じ種類のデータを順番に並べる箱です。新しいデータは多くの場合末尾へ追加でき、古い時刻が混ざったときだけ並べ直します。

初期化時にはDBから作り直せるため、「ベンチ開始前の状態へ戻す」要件も守れます。競技終了後まで新規データを永続化する仕様ではないことを、公式マニュアルとベンチ動作から確認して選びました。

20,306点から43,682点へ

応用編の再基準と設定調整を挟み、memory化前後は次のようになりました。

段階スコア見えたこと
応用編の再基準21,088DBの大きな山は消え、Go・network・GCが候補
OS・DB・Go設定23,358小幅な伸び。設定だけでは上限を動かしにくい
conditionをmemoryへ43,682pass。ほぼ同じ構造の仕事をDBから外して約1.87倍
再測定42,2444万点台を再現

GCは、使わなくなったmemoryをGoが片づける仕組みです。memory化した直後は、JSONやsliceを大量に作るため、pprofでGCが見えるようになりました。ボトルネックを消すと、次の山が姿を見せます。

「先に大きく確保」が裏目に出た

sliceは満杯になると、より大きな箱を作って中身を移します。それを避けようと、新規ISUごとに16,384件分を先に確保しました。速そうに聞こえますよね。

結果は42,244点から34,599点へ低下しました。

理由は、1台あたりの無駄が小さく見えても、新規ISUの数を掛けると大きなmemoryになるからです。実測に合わせて1,024件へ縮め、GC設定も比較すると47,190点まで戻りました。

ここには、ISUCONで何度も使える教訓があります。

1回分の費用ではなく、最大件数を掛けた費用を考える。

JSONを読む費用

pprofでは次にencoding/jsonが目立ちました。Goの標準JSON parserは、さまざまなJSONを正しく読める頼もしい道具です。しかしcondition POSTの形は決まっています。

そこで、よく来る正規の形だけを少ないallocationで読むfast pathを作りました。allocationは、新しいmemory領域を用意することです。文字列やsliceを毎回作る量が減ると、parser自身だけでなくGCの仕事も減ります。

入力順が違う、escape文字があるなど、想定外の形は標準parserへ戻します。速い道だけを作り、正しく読める範囲は狭めません。

この変更とtrend cacheなどを合わせ、55,572点。その後bufferとsessionの再利用を調整し、1台構成の最高57,208点になりました。

一番多い入口だけ、薄くする

3台へ分けた後もpprofとaccess logを取り直すと、condition POSTだけが約10万回ずつworkerへ来ていました。形が決まった短いrequestを受けるために、毎回同じWeb frameworkの経路を通っています。

そこでworkerでは、port 3000にcondition POST専用の小さなHTTP serverを置きました。ここでいう「小さい」は、機能を削ったという意味です。

  • request lineと必要なheaderを読む
  • condition POSTなら専用handlerへ渡す
  • それ以外はport 3002の従来Echoアプリへつなぐ
  • JSONの形が想定外なら、これまでの安全なparserへ戻す

専用serverで77,109点、lock範囲とresponse JSONを見直して80,671点になりました。ただし、すべてのAPIを手書きにしたわけではありません。回数が圧倒的に多く、入力が決まっている一本道だけを薄くしました。

この変更は初心者が最初に選ぶものではありません。slow queryやindexを直す前にHTTP parserを書き始めると、速くなる前に講習が終わります。pprofでGoのHTTP処理が見え、access logで対象routeの回数が分かってから出すカードです。

大きな構造変更を頼むプロンプト

text
計測結果から、現在のDBアクセスを小さく直し続けるべきか、構造を変えるべきか判断してください。

- 公式仕様から、再起動後も永続化が必要なデータと、initializeで復元できるデータを分ける
- user、ISU、condition、trend、graphの読み書き経路を図にする
- memory化する場合は、初期load、同時read/write、時刻順、重複、再初期化を設計する
- 変更前後で同じAPI responseになるtestを追加する
- memory量は「1件の大きさ×最大件数」で見積もる
- benchmark、pprof、vmstat、HTTP statusを再計測する
- 標準JSON parserを外す場合は、互換用fallbackを残す
- 改善しなかった設定や事前確保は戻し、理由を記録する

いきなり実装せず、守る仕様と失敗時の戻し方を先に説明してください。
計測結果から、現在のDBアクセスを小さく直し続けるべきか、構造を変えるべきか判断してください。

- 公式仕様から、再起動後も永続化が必要なデータと、initializeで復元できるデータを分ける
- user、ISU、condition、trend、graphの読み書き経路を図にする
- memory化する場合は、初期load、同時read/write、時刻順、重複、再初期化を設計する
- 変更前後で同じAPI responseになるtestを追加する
- memory量は「1件の大きさ×最大件数」で見積もる
- benchmark、pprof、vmstat、HTTP statusを再計測する
- 標準JSON parserを外す場合は、互換用fallbackを残す
- 改善しなかった設定や事前確保は戻し、理由を記録する

いきなり実装せず、守る仕様と失敗時の戻し方を先に説明してください。

memory化は強いカードです。でも、仕様を読み、正しさをtestし、memory量を測って初めて採用できます。大きな変更ほど、ガードレールも大きくしましょう。