pixiv private isucon 2016 攻略 (1/5)
攻略記事一覧:
pixiv さんが社内で開催したプライベート ISUCON の AMI を公開してくれたので、手順を残しながら攻略していきます。
この記事の対象読者は途中で何をすればいいかわからなくなってしまう ISUCON 初心者です。 Go を利用して攻略していきますが、他の言語で参加する場合でも考え方などは参考になると思います。
最低限の初期設定
ssh の公開鍵を入れたり、エディターや git などのツール類をインストール・設定します。
このあたりは事前に自分用のチートシートやスクリプトを作って、手早くできるようにしておきましょう。
マシン調査
スペック
とりあえず CPU とメモリくらいはちゃんと把握しておきましょう。
$ grep processor /proc/cpuinfo processor : 0 processor : 1 $ free -m total used free shared buffers cached Mem: 998 906 92 12 25 335 -/+ buffers/cache: 545 453 Swap: 0 0 0
CPUは2コアですね。メモリはトータル1GBでスワップなし。要注意です。
ps aux
何が動いているか確認します。 メールサーバーとか、AppArmor のような、競技に明らかに不要そうなデーモンは、ベンチマークスコアを安定させるためとメモリの節約のために落としておいたほうが良いです。
今回は特に何もする必要はありませんでした。
アプリの仕様把握
初期状態に復元する手順を確認してから、実際にアプリをブラウザからさわってみましょう。
初期状態への復元は、 tar でアーカイブしてローカルに持ってきておいたり、 mysqldump を取ったりしておくと良いです。
ただし今回の場合はAMIからやり直せるので、 /initialize
というパスで初期化されると言うことだけアプリのコードで確認し、バックアップは手抜きしました。
ISUCON 本番では初期状態を復元するのに必要なデータをオペミスで失うと即死する恐れがあるので注意しましょう。
ブラウザのデバッグ機能でどういう通信をしているのか見たり、アプリのコードのURLルーティング部分を中心に軽く目を通したり、 mysqldump --no-data isuconp
で取得したスキーマを参考に、ざっくりと全体像を把握していきます。
nginx の基礎設定
アクセスログを事前に用意しておいたフォーマットに変更して、 upstream への keepalive
を付ける。
コア数少ないので worker_processes
を 1 にして、 keepalive
の効率を上げる。
静的ファイルを nginx から返す設定は、アプリの動作を把握してからでも遅くない。 Go 以外を使う人は、完全に静的なファイルが分かった段階ですぐに、Go を使う人はある程度チューニングが進んで nginx を外すかどうかを判断するタイミングで nginx から返すようにしよう。
/etc/nginx/nginx.conf
:
worker_processes 1; ... ## # Logging Settings ## log_format isucon '$time_local $msec\t$status\treqtime:$request_time\t' 'in:$request_length\tout:$bytes_sent\trequest:$request\t' 'acceptencoding:$http_accept_encoding\treferer:$http_referer\t' 'ua:$http_user_agent'; access_log /var/log/nginx/access.log isucon;
/etc/nginx/sites-enabled/isucon.conf
:
upstream app { server 127.0.0.1:8080; keepalive 32; } server { listen 80; client_max_body_size 10m; root /home/isucon/private_isu/webapp/public/; location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://app; } }
あと、今回は Go を使うので ruby と php のサービスも止めてしまいます。 Go のサービスは開発中はコマンドラインで起動したいのでここでは設定しません。
$ sudo systemctl stop isu-ruby $ sudo systemctl disable isu-ruby $ sudo systemctl stop php7.0-fpm $ sudo systemctl disable php7.0-fpm
MySQL の基礎設定
/etc/mysql/my.cnf
を見てみます。ほぼディストリのデフォルトで使っているようなので、メモリ使用量削減とIO待ち削減のために次の設定だけ追加しておきます。
innodb_flush_method = O_DIRECT innodb_flush_log_at_trx_commit = 2
重いクエリを手早く見つけるためのツールを用意する。僕の場合は myprofiler を使う。
$ wget https://github.com/KLab/myprofiler/releases/download/0.1/myprofiler.linux_amd64.tar.gz $ tar xf myprofiler.linux_amd64.tar.gz $ sudo mv myprofiler /usr/local/bin/
myprofiler の使い方は、 myprofiler -user=root
です。クエリが減ってきたら myprofiler -user=root -interval=0.2
くらいにサンプリング間隔を短くすると良いでしょう。それ以外のオプションは myprofiler -h
で確認してください。
ベンチマークの実行とざっくり状況把握
スコア:
{"pass":true,"score":4745,"success":4408,"fail":0,"messages":[]}
top:
Tasks: 81 total, 1 running, 80 sleeping, 0 stopped, 0 zombie %Cpu(s): 94.1 us, 4.2 sy, 0.0 ni, 0.3 id, 0.0 wa, 0.0 hi, 1.3 si, 0.0 st KiB Mem: 1022972 total, 701036 used, 321936 free, 21396 buffers KiB Swap: 0 total, 0 used, 0 free. 346564 cached Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 720 mysql 20 0 1087768 140276 10724 S 142.8 13.7 0:52.09 mysqld 2908 isucon 20 0 582876 124364 8800 S 46.6 12.2 0:12.98 app 2904 www-data 20 0 91680 4800 2988 S 1.7 0.5 0:00.61 nginx
MySQL が app の3倍CPU食ってるのがわかります。
myprofiler:
198 SELECT * FROM `comments` WHERE `post_id` = N ORDER BY `created_at` DESC LIMIT N 124 SELECT COUNT(*) AS `count` FROM `comments` WHERE `post_id` = N 11 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC 9 SELECT * FROM `comments` WHERE `post_id` = N ORDER BY `created_at` DESC 5 SELECT * FROM `posts` WHERE `id` = N 4 SELECT COUNT(*) AS count FROM `comments` WHERE `post_id` IN (...N) 2 SELECT * FROM `users` WHERE `id` = ? 2 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` WHERE `created_at` <= S ORDER BY `created_at` DESC 1 SELECT * FROM users WHERE account_name = S AND del_flg = N 1 SELECT COUNT(*) AS count FROM `comments` WHERE `user_id` = N
comments の SELECT と COUNT が遅い事がわかる。その次に posts の select。
一番最初のチューニング: インデックス
上位3件のクエリについて、インデックスが有るか確認して、なければ張っておく。
mysql> show create table comments\G *************************** 1. row *************************** Table: comments Create Table: CREATE TABLE `comments` ( `id` int(11) NOT NULL AUTO_INCREMENT, `post_id` int(11) NOT NULL, `user_id` int(11) NOT NULL, `comment` text NOT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=100049 DEFAULT CHARSET=utf8mb4 1 row in set (0.00 sec) mysql> show create table posts\G *************************** 1. row *************************** Table: posts Create Table: CREATE TABLE `posts` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `mime` varchar(64) NOT NULL, `imgdata` mediumblob NOT NULL, `body` text NOT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10056 DEFAULT CHARSET=utf8mb4 1 row in set (0.00 sec)
comments.post_id
と posts.created_at
にインデックスがない。重いクエリ上位3つがこれ。
mysql> create index post_id on comments (post_id); Query OK, 0 rows affected (0.19 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> create index created_at on posts (created_at); Query OK, 0 rows affected (0.03 sec) Records: 0 Duplicates: 0 Warnings: 0
とりあえずこれでもう一度計測しておきます。
top:
top - 18:13:32 up 2:13, 1 user, load average: 2.01, 0.84, 0.52 Tasks: 79 total, 1 running, 78 sleeping, 0 stopped, 0 zombie %Cpu(s): 74.0 us, 19.3 sy, 0.0 ni, 3.2 id, 0.2 wa, 0.0 hi, 3.4 si, 0.0 st KiB Mem: 1022972 total, 944288 used, 78684 free, 23344 buffers KiB Swap: 0 total, 0 used, 0 free. 462656 cached Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2908 isucon 20 0 640148 174444 8800 S 109.7 17.1 0:44.93 app 720 mysql 20 0 1093212 212252 10740 S 58.5 20.7 1:56.57 mysqld
app が mysql の 2倍になり、クエリがネックじゃなくなったのがわかります。
スコア:
{"pass":true,"score":14208,"success":13157,"fail":0,"messages":[]}
myprofiler:
36 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC 15 SELECT COUNT(*) AS count FROM `comments` WHERE `user_id` = N 5 SELECT * FROM `comments` WHERE `post_id` = N ORDER BY `created_at` DESC LIMIT N 4 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` WHERE `created_at` <= S ORDER BY `created_at` DESC
上位2件を見ると
posts
からの SELECT に LIMIT が無いcomments.user_id
にインデックスがない
LIMIT がないのはアプリ側で修正が必要なのと、ボトルネックがアプリに移ってるので、アプリ改修に移る。
comments.user_id
はついでにインデックス張っておく。(1番重いクエリじゃないのでベンチは省略)
mysql> create index user_id on comments (user_id); Query OK, 0 rows affected (0.14 sec) Records: 0 Duplicates: 0 Warnings: 0
まとめ
スコア: 4745 (初期状態) -> 14208 (インデックスチューニング)
systemd のコマンドとか nginx の設定とか MySQL の設定ファイルを見るとかインデックス張るとかいろいろ慣れて、ここまではスムーズにできるようになりましょう。
感覚値ですが、一人でやってる場合はここまでを2時間以内には終わらせられるようにしましょう。1時間を切れればスゴイです。
@methane