2018/09/13

ISUCON7の予選問題を解いて、スコアを27万点まで上げた

こんにちは。フリーランスのエンジニアのあっとんです。
仕事が一段落したのもあってプチ夏休みという名の仕事が無い期間、ISUCON7 の予選問題をやったりしました。
それを改めてログにまとめる次第です。

結論から言えば、予選突破のボーダーの21万点を越えられました。
ちなみに累計作業時間は58時間くらいで、最終スコアは 270957 でした。
あとリモートワークのお仕事を募集中(2018/09/13現在)です。


どうして ISUCON の問題を触ってみようと思ったのか

  • Twitter でたまに見た単語で気になっていたので
  • ミドルウェアをインストールする事はあってもガリガリにチューニングする経験が無かったので
  • とりあえず数字で善し悪しが分かるので


自分レギュレーションと目標とか

  • スコア的な目標は予選突破ボーダー(21万点くらい)
  • とりあえず最初(ISUCON7予選)はブログとか見て良いのでノウハウを積む
  • ノウハウを積んだ上で本戦問題を時間制限+解説見ずに解く


環境構築とベンチマーカを動かす

まずは環境構築から。
とりあえず GCP の上に 4core 8GB の instance を上げる。
ISUCON7 の予選問題の README.md に従って環境構築。
初期スコアは5000点くらいだったと思います。
ベンチマークをかける側も 4core 8GB で環境構築。


なんとなくの知識で高速化をする

とりあえず知ってる範囲で適当に進めていく。
  • nginx の proxy を経由せずに直接 puma で 80番を bind するように
    • スコアが 3000 くらいになる。 却下。
  • rackup する時に environment を production にする
  • logger を無しにして log を disk に書かないようにする
    • 一応スコアが 5980 くらいにはなる
  • css と js を minify する
    • yui-compressor なるものを使う
    • $ yui-compressor main.css -o minimain.css # とか
    • Validation に失敗するのでこの手は使えない
  • gzip で圧縮してみる
    • .gz を置いて配る
    • そんなにスコアが変わんなかった
  • N+1 を解決したいが SET IN ... に書き換えるのが面倒
    • 普段 ActiveRecord を使っていたので includes で解決して欲しい……
    • (後から結局 sinatra-activerecord を入れることになる)
  • SELECT * の結果に対して first していた部分に LIMIT 1 を付ける
    • 1万点くらいになる
  • メモリの上に DB の全データを載せる
    • tmpfs + rsync で載せてしまう
    • 19340 点になる。倍。
  • mysql の log の WARN を取り除く
    • 22511 点になる。10%増し。


予測するな計測せよ

結局何が一番のネックなのかが分からないとどうしようもない。
ということで計測していく。
profiling には rack-mini-profiler を使ってみた。
さてこれでどこがネックなのか探るか、と思ったら作成されたファイルが読めない。
結論から言えばファイルに書く前に Marshal.dump されているせいで Marshal.load しないと読めなかった。
Marshal.dump を JSON.dump に monkey patch して無理に plain text にして jq で読む。
しかしファイルが多すぎて(200個とか)まともに読めない。
profiler を使うのは諦めて top だけで行くというゴリ押しをする。
(rack-mini-profiler を rack application で動かす、というのも記事にできそうなので後から書くかもしれない)
なお、初期の段階でベンチマークをかけると CPU を mysql が 55% 、puma が 35% くらい食っていて DB ネックだった。


参考文献を参照開始


1Core 1GB にスペックダウン



11万あたりの伸び悩み

その後もチマチマと変更を加えるものの、12万点の壁は越えられず。
CPU使用率も合計100%にならなくなった。
気付くのに結構かかってしまったけれど、オチとしてはメモリ不足。
具体的には残り10k とかになっていた。
というわけでここからは複数台構成にする。


複数台構成



lsyncd を使って画像の同期

解決すべき問題は icons を3台の間でどう整合性を取って返すか。
業務だったら S3 に置くなり CDN を使うのが良さそう。
しかし ISUCON なので、手元にあるサーバでどうにかする必要がある。
画像ファイル自体は1MB以下なのが保証されている。
なので書き込み時に memcached じゃなくて全サーバに書き込むことに。
そうすれば /icons を nginx が Cache-Control 付きで返してくれて良さそう。
(nfs で mount しても良かったのですが、結局 network access が発生するので早くならなそうだったので断念。)
(nfs の cache の config でどうにかなった可能性もゼロではないが……)
最初は inotify + rsync でゴリ押ししようと思ったのですが、 lsyncd なるものを発見
(rdiff-backup とか osync とか csync2 とか unison とか mirror とかあるがどれも古い)
サーバA は更新があったらサーバB に rsync。 B は C へ。 C は A へ。という形に。
これでファイルが upload されたら全サーバが共通のファイルを返せる。
結果、1台だけの時にスコアが 69770 に。3倍にできれば目標達成かな。


最終的な構成

3台 nginx にするより、2台 nginx + DB のがスコアが高かった。
あと mysqltuner というスクリプトを見つけて、それで値を調整。
最終的には score 270957 を達成したので脳内予選突破。



まとめ

  • ISUCON7 の予選問題のスコアを 27 万点くらいまで上げました。
  • 作業ノートによると58時間くらいかかっているらしい。
  • これで夏休みの自由研究2018としましょうか。



参考

2018/07/22

Docker Registry の backup と restore

前回は Image の Backup/Restore について書きました。
これで Image を指定してバックアップ可能になったのですが、Registry に登録される Image が1つとは限らない。
ということで Docker Registry ごと全イメージをバックアップしてみる。


環境

  • OS: CentOS Linux release 7.4.1708 (Core)
  • Docker: version 1.13.1, build 774336d/1.13.1
  • Kernel: 3.10.0-693.5.2.el7.x86_64


Docker Registry を tar にする

さて、Registry はコンテナ内部に登録イメージのファイルを持ってます。
ファイルごとバックアップを取りたいので、戦略としてはコンテナのバックアップを取る。
今度使うのは docker export コマンド

  • $ docker run -d -p 5000:5000 --name registry registry:latest
  • $ docker push hogehoge
  • $ docker export registry > registry-backup.tar
これで Registry のコンテナが tar ファイルになります。


Docker Registry を復元する

さて、 tar ファイルから復元したいと思います。
  • $ docker import - registry:latest < registry-backup.tar
今回はタグの情報とかは無いので自分で指定する。
というかコンテナからイメージを作るので実質 docker commit みたいなものか。

tar から作られた registry:latest を実行します。
  • $ docker run -d -p 5000:5000 --name registry registry:latest
お? 動かない。 
/usr/bin/docker-current: Error response from daemon: No command specified.
と言ってくる。CMD の情報が吹っ飛んでるのか。
Github にある Registry の Dockerfile を見ると、'docker-registry' とすれば良いかな。
  • $ docker run -d -p 5000:5000 --name registry registry:latest docker-registry
動かない。今度は
/usr/bin/docker-current: Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused "exec: \"docker-registry\": executable file not found in $PATH
か。そんなコマンドは無いらしい。 
もうコンテナ内に入ってしまうかな。export 前の registry に対して
  • $ docker exec -it registry bash
すると
rpc error: code = 2 desc = oci runtime error: exec failed: container_linux.go:247: starting container process caused "exec: \"bash\": executable file not found in $PATH"
おお、bash すら無いのか。sh も無ければ最早 user interactive に何もできないコンテナだ。

  • $ docker exec -it registry sh
流石に sh はありました。find もある。
find で docker-registry を探しても無い。
他に探索していると registry というのが /bin/registry にあった。
実行すると serve か garbage-collect と help がサブコマンドにある。serve してみます。
  • $ registry serve
文句言ってきました。具体的には
configuration error: configuration path unspecified
と。 docker-registry の configuration path って何だろう。
どこかに mount してたらそれを参照してくれるから設定変更可能、とかそういう意図なんだろうか。

とかとか、いろいろ試した結果、ps で path が分かりました。
Process ID 1 のやつが
registry serve /etc/docker/registry/config.yml
で動いていました。
と、いうことで、実行するコマンドは
$ docker run -d -p 5000:5000 --name registry registry:latest registry serve /etc/docker/registry/config.yml
で Import した Registry を復元できました。やったね。


おまけ

/etc/docker/registry/config.yml を読むと storage.filesystem.rootdirectory が /var/lib/registry
だったので、run する時に -v で /var/lib/registry に Host の Directory を mount しておけば Image の保存が楽だった説が。

それと、Docker EE だと、 DTR (Docekr Trusted Registry) の backup/restore 手順が提供されています
きちんとサポートを受けたい場合は Docker EE を使うのもアリかも。


参考

2018/07/15

Docker Image の backup と restore

さて、Docker Registry を立てたは良いものの、マシンが壊れたりすると悲しい事態になります。
Build 元と Registry で二重になってはいるのですが、両方が同じマシンだと悲しい事態が発生する可能性がありますね。
と、いうことで Build した Docker の Image をファイルに書き出したり、そのファイルからイメージを復元します。


環境

  • OS: CentOS Linux release 7.4.1708 (Core)
  • Docker: version 1.13.1, build 774336d/1.13.1
  • Kernel: 3.10.0-693.5.2.el7.x86_64


Docker Image をファイルへバックアップする

Docker そのものはファイル群を /var/lib/docker の下に持ってます。
Image は default だと overlay2 で作成されます。
overlay2 は差分でファイルを管理するファイルシステムなので、そういったメタ情報もバックアップしないといけない。
つまりは cp とか rsync だと事故る可能性があります。

さて、何を使うのが正しいかと言えば公式です。具体的には docker save コマンド
Image を構成する全レイヤをきちんと tar に書き出してくれます。
実行例として、hogehoge Image をバックアップするのはこんな感じ。
  • $ docker image save hogehoge > backup.tar
この tar をバックアップ先 HDD 等に持っていけば良い。


Docker Image をファイルから復元する

バックアップする方法は分かりました。
次はバックアップからの復元。
docker load コマンドを使います。具体的には
  • $ docker image load < backup.tar
で終わり。
Image の repository name や tag も tar に入っているので、それらの情報も全部復元されます。楽。


参考

2018/07/08

自前の Docker Registry を Systemd で管理する

前回の記事では自前で Docker Registry を立ててみました。
ですが、これだと docker run しただけなので、マシンを再起動すると上がってこない。
ということで systemd の unit にしてしまって自動起動させるようにしました。


環境

  • OS: CentOS Linux release 7.4.1708 (Core)
  • Docker: version 1.13.1, build 774336d/1.13.1
  • Kernel: 3.10.0-693.5.2.el7.x86_64


systemd の unit を書く

ググると誰かが書いた unit がいくつかヒットしました。これとかこれとか。
これらを参考に unit を書きます。とはいっても、考慮する具体的な内容は
  • registry の container の name は決めうち
    • unit を書くのを楽にするため
    • volume は面倒なので mount しない
  • registry の dependency には docker がある
    • サービスの起動順番の指定
くらいのものです。

ということで、できたのが
です。こいつを
  • /usr/lib/systemd/system/docker-registry.service
に配置。それから怒涛の systemctl 。
  • $ sudo systemctl daemon-reload
    • unit の reload で認識させる
  • $ sudo systemctl start docker-registry
    • systemd 経由で docker-registry を起動してみる。
    • docker ps するときちんと生きてます。
  • $ sudo systemctl restart docker
    • docker を再起動してもちゃんと生きてる。
    • dependency の設定も上手くいってそうです。
  • $ sudo systemctl enable docker-registry
    • んで、マシン起動時に起動するように
  • $ sudo reboot
    • そして再起動。
    • 再起動後にもきちんと registry が上がってました。良し良し。

ってな感じで再起動しても docker-registry が上がるようになりました。
めでたしめでたし。


参考

2018/07/01

自前の Docker Registry を立てて、 Build した Image を保存する

さて、Docker を使っていて Image を作った後、他の人に配るにはいくつか方法があります。
export/import したりとか、dockerhub に置いたりとか、自前で Registry を立てたりとか。
今回は自前で Docker Registry を立ててみようと思います。


環境(docker client と registry server の両方は同じ環境)

  • OS: CentOS Linux release 7.4.1708 (Core)
  • Docker: version 1.13.1, build 774336d/1.13.1
  • Kernel: 3.10.0-693.5.2.el7.x86_64


Registry を立てる

Image が提供されているのでコマンド一発でおしまい。
  • registry-server$ docker run -d -p 5000:5000 --name registry registry:2
便利ですね。


Config とか

立てるのは簡単ですが、外から使うにはいくつか設定がいります。
まずは firewall で 5000 番を開放します。
  • registry-server$ sudo firewall-cmd --add-port=5000/tcp --permanent
  • registry-server$ sudo firewall-cmd --reload
これでOK。ということで push してみましょう。
  • docker-client$ docker image tag hoge 192.168.0.1:5000/hoge
  • docker-client$ docker push 192.168.0.1:5000
怒られます。
Get https://192.168.0.1:5000/v1/_ping: http: server gave HTTP response to HTTPS client
クライアント側が https で繋ぎにいってるのに http を返してきてる、と。
調べたところ、http でアクセスする registry と指定すれば良いらしいです。

と、いうことで指示通りに /etc/docker/daemon.json に書く。
  • docker-client$ sudo echo '{ "insecure-registries":["192.168.0.1:5000"] }' > /etc/docker/daemon.json
  • docker-client$ sytemctl restart docker
  • docker-client$ docker push 192.168.0.1:5000
これで通りました。良し良し。

今日日 https 必須感もありますし、https にするのが正しい気もしますが、動いたのでまずは一段落。


参考