Hateburo: kazeburo hatenablog

Operations Engineer / Site Reliability / 運用系小姑 / Perl Monger

ISUCON10予選に「チーム中目黒乗り過ごし」で参加し本選出場決まりました

ISUCON10の予選に尊敬するエンジニアであるhanabokuro氏、mtokioka氏と参加して、9位で本戦出場できることになりました。うれしい! ISUCON8は予選落ち、去年は出題をやらせていただいたので、本選出場は3年ぶりになります

今年の問題も、よく練られた問題で開始から終了まで楽しむことができました。運営のみなさま朝早くから遅くまで本当にありがとうございます。

準備

GoはMackerelプラグインミドルウェアでよく書いておりますが、、いわゆるWebアプリケーションはあまり書いてないので、手を動かすために去年のISUCON9予選の問題を手元で動かして取り組んでみてました。

ローカル環境で動かすのは以下の記事が役立ちます。

isucon.net

また、Cloud TraceやProfilerを試すために、takonomura氏の記事やgithub repositoryをすごくすごく参考にしました。ありがとうございます!

www.takono.io

当日

六本木のオフィスで集まってやることに決めていたので、自宅から移動。

これを言いたいが為だけにこのチーム名になっていることは秘密。

開始が12時すぎになったので、半年ぶりにいったオフィスで賞味期限がきれたお菓子などを片付けたりして準備してました。

構成と最終スコア

最終的には3台を使う構成になりました。

f:id:kazeburo:20200914103843p:plain

作業の順序的にベンチマーカーは103に向けて、NginxとGoがそこで動きます。DBを2つに分割し、chair用とestate用それぞれ専用にしました。 この構成でCPU使用率はGoが40%程度、MySQLの負荷は99%以上になるような状況でしたので、まだまだ改善はできたのではないか、やれることはあったはずと思っています。

最終スコアは 3016 でした。

f:id:kazeburo:20200914104420p:plain

予選でつかったrepositoryはこちらになります

github.com

序盤

開始されてからしばらくベンチマークを動かすことができなかったので、当日マニュアルやソースコードを読む時間にあてました。明らかに不足しているインデックスを追加したり、featureやnazotte検索あたりが重そうだなというと手を付ける箇所を検討していました。空いた時間でbotを避けるnginxの設定もいれました。

今回の予選のサーバは3台すべて1coreのサーバで、ベンチマークを動かしたところMySQLが多くのCPUを使っていることがわかったので、profilerやtraceは一旦おいておいて良さそうと判断し、まずpt-query-digestを使い、DBの解析行いました。

初めの段階でCPU時間を使っているクエリは

SELECT * FROM chair WHERE stock > 0 ORDER BY price ASC, id ASC LIMIT 20

SELECT * FROM estate WHERE rent >= 50000 AND rent < 100000 ORDER BY popularity DESC, id ASC LIMIT 25 OFFSET 0

SELECT * FROM estate WHERE (door_width >= 191 AND door_height >= 183) OR (door_width >= 191 AND door_height >= 173) OR (door_width >= 183 AND door_height >= 191) OR (door_width >= 183 AND door_height >= 173) OR (door_width >= 173 AND door_height >= 191) OR (door_width >= 173 AND door_height >= 183) ORDER BY popularity DESC, id ASC LIMIT 20

最初のクエリは単純にindexで済み、3つ目はクエリをもっと単純にできそうでしたが、問題は昇順と降順が混ざる ORDER BY popularity DESC, id ASC でこれを解決するために、MySQL8のアップデートを試すことにしました。

中盤

MySQL8アップデートの作業は他のチューニングとぶつからないように、103のサーバで行いました。MySQL8のアップデートは初めてでしたので、google様に聞いて以下のサイトを参考にしました。画像までありわかりやすかったです。ありがとうございます!

obel.hatenablog.jp

インストール中に、認証方式を新しい認証方式にするか、5.7と互換性にあるモードにするか聞いてくるらしい、というのがわかったのがとくに嬉しいところでした。実際にやってみたところ、最後にmy.cnfの設定を、今の設定を残すか、新しい設定で上書きするか聞いてくれたのも便利でした。設定のdiffもみれ、何が起きるのかも明確でした。別のところで悩みなくなかったのでここは5.7互換モードを選び、新しい設定で上書きしました。

apt-get install のあとmysqlコンソールでデータが読めるか確認したところ、何事もなく読めたので、ベンチマーカの向き先を 103 にして実行。スコアは半分程度にさがりましたが無事にベンチマークが成功しているので、クエリとindexのチューニングで改善すると考え、MySQL8で続行しました。

create index idx_pop on isuumo.chair(popularity desc);
create index idx_pop on isuumo.estate(popularity desc);

ORDER BYに関してはこの2つで多くは解消しました。

mysql> explain SELECT * FROM chair WHERE price >= 3000 AND price < 6000 AND height >= 80 AND height < 110 AND width >= 80 AND width < 110 AND depth >= 150 AND stock > 0 ORDER BY popularity DESC, id ASC LIMIT 25 OFFSET 25\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: chair
   partitions: NULL
         type: index
possible_keys: idx_price,idx_depth,idx_height,idx_width,stock
          key: idx_pop
      key_len: 4
          ref: NULL
         rows: 286
     filtered: 0.16
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

idx_popが選ばれて、rowsが小さくなってます。MySQL8賢い。信じてた。

MySQL8のアップデート後一旦さがったスコアは、indexの整理、ドアの大きさのクエリの改善、nazotte検索のN+1の解消、SELECT COUNT(*) を goroutineで取得などをいれることで良くなり、1000を超えるスコアになってきていました。

nazotte検索のN+1は

SELECT id,latitude,longitude FROM estate WHERE latitude <= ? AND latitude >= ? AND longitude <= ? AND longitude >= ?

のクエリのあと

SELECT * FROM estate WHERE 
   (id = ? AND ST_Contains(ST_PolygonFromText(%s), ST_GeomFromText('POINT(%f %f)'))) 
OR (id = ? AND ST_Contains(ST_PolygonFromText(%s), ST_GeomFromText('POINT(%f %f)')))
OR (id = ? AND ST_Contains(ST_PolygonFromText(%s), ST_GeomFromText('POINT(%f %f)')))
..

と2回のクエリで取得するようにしています。ベンチマーカーからのリクエストではHitするのが多くても数百件でしたのでこれで十分な性能がでると判断しました。

ここまで1台でやってきてましたが、MySQLの負荷を低減させるのが難しく複数台構成への変更を行いました。chairとestateテーブルはJOINを行わなず、複雑なトランザクションなどもないので、テーブルごとにDBを分割する方法をとりました。自分はレプリケーション案を出していましたが、分割案ではレプリケーション遅延などを考える必要もなくシンプルに負荷分散ができるこちらで行くことに決め、101、102のサーバのMySQLのアップグレード作業を行いました。

分割後、スコアは2000程度まで上昇。これがだいたい18:30ぐらいでした。

終盤

ここから、細かく次のようなチューニングを行い、

  • estateとchairのbuk insert
  • buyChair時のクエリを削減
  • getLowest のcache
  • chair と estateの検索をcache
  • nazotte検索の最初のクエリをcovering indexになるように調整

20時をまわったところで一旦再起動試験、ufwやapparmorを無効にして再起動して問題がないことを確認。 20時半に3000点を超えたところで、作業を終了にしました。

よかったところと反省点

初手から複数台構成やキャッシュ戦略を取らず、pt-query-digestやkataribeを使い、ログを解析しながら手を動かして行った点、複数台構成を最初に行わなかったことで大きな変更の検証や、MySQL8へのアップグレードを使ってないサーバを使って試せたのはよかった。逆に、MySQLの負荷が最後まで下がらず、負荷を大幅に押さえるような手段が取れなかったのは、まだまだ精進しなければならないところ。本選に行くチャンスを得たので、反省含め頑張っていきたい。

今回、生中継があったので奥さん、息子、娘も家でちょこちょこ見て応援してくれていたようです!ありがとう!!!