ISUCON9予選をDockerだけで動かす

[isucon/isucon9\-qualify](https://github.com/isucon/isucon9-qualify/)を、ローカル環境にDockerを使って構築します。MySQLやGoはインストールすること無くDockerだけで動かします。アプリケーションにはRubyを利用します。(Rubyもインストールする必要はありません)

完成品は下記です。
https://github.com/s2terminal/isucon9-qualify-docker

## 環境
- [ASUS Chromebook Flip C434TA (Core i5モデル)](https://www.asus.com/jp/Laptops/ASUS-Chromebook-Flip-C434TA/)
- ChromeOS Crostini (Linux) Debian 9.11
- Docker 19.03.4

## 構築手順
まず`git clone`します。

```bash
$ git clone git@github.com:isucon/isucon9-qualify.git
$ cd isucon9-qualify
```

### 下準備
リポジトリに含まれていない初期画像ファイルのダウンロードが必要なのですが、1.5GBあるので先に落としておきます。

```bash
$ wget https://github.com/isucon/isucon9-qualify/releases/download/v2/bench1.zip
$ wget https://github.com/isucon/isucon9-qualify/releases/download/v2/initial.zip
```
ダウンロードできたら、解凍してそれぞれ所定のディレクトリに移動します。

```bash
$ unzip bench1.zip
$ unzip initial.zip
$ mv v3_initial_data webapp/public/upload
$ mv v3_bench1 initial-data/images
```

初期データの準備をします。中ではperlのDockerコンテナを使ってSQLを生成するなどしているようです。

```bash
$ cd initial-data
$ make
```

### Ruby環境準備

Rubyアプリケーションを実行する準備をします。`webapp/ruby`ディレクトリにDockerfileを配置します。

```Dockerfile:webapp/ruby/Dockerfile
FROM ruby:2.7.1
WORKDIR /app

RUN apt-get update \
 && apt-get install -y \
    mariadb-client \
    less

COPY ./Gemfile ./
COPY ./Gemfile.lock ./
RUN bundle install

CMD /bin/sh -c "while sleep 1000; do :; done"
```

`mariadb-client`は必須です。他に`less`だけ入れていますが、追加で欲しい物があれば随時入れておきます。ISUCON用ということは中に入ってなんやかんやしたいと思うので、キャッシュを消したりはせずに置いておきます。

`CMD /bin/sh -c "while sleep 1000; do :; done"`で、コンテナが立ち上がりっぱなしになるようにしています。

プロジェクトルートに`docker-compose.yml`を配置します。

```yml:docker-compose.yml
version: '3'
services:
  ruby:
    build: ./webapp/ruby
    volumes:
      - .:/app
    ports:
      - 127.0.0.1:5555:5555
      - 127.0.0.1:7000:7000
      - 127.0.0.1:8000:8000
    environment:
      - MYSQL_HOST=db
      - MYSQL_USER=root
      - MYSQL_PASS=secret
```

### MySQL環境準備
データベースの準備をします。[DBだけDockerを使ってISUCON9の環境構築した \- teru\_0x01\.log](https://cha-shu00.hatenablog.com/entry/2020/04/02/210125)を参考に、`webapp/sql/conf/my.cnf`を作成します。

```bash
$ mkdir webapp/sql/conf
```

```conf:webapp/sql/conf/my.cnf
[mysqld]
pid-file        = /var/run/mysqld/mysqld.pid
socket          = /var/run/mysqld/mysqld.sock
datadir         = /var/lib/mysql
secure-file-priv= NULL
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0

slow_query_log = 1
long_query_time = 5


[client]
default-character-set = utf8mb4


# Custom config should go here
!includedir /etc/mysql/conf.d/
```

`docker-compose.yml`に下記を追記します。

```docker-compose.yml
  db:
    image: mysql:5.7
    volumes:
      - ./webapp/sql:/docker-entrypoint-initdb.d
      - ./webapp/sql/conf:/etc/mysql/conf.d
      - ./datadir:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=secret
      - MYSQL_HOST=127.0.0.1
      - MYSQL_USER=root
      - MYSQL_PASS=secret
    ports:
      - 127.0.0.1:3306:3306
```

### アプリケーションの起動
Rubyコンテナに入って下記実行します。

```bash
$ docker-compose exec ruby /bin/bash
```

初回起動時やDBをリセットした時などは、DBの初期化処理を実行します。

```
# ./webapp/sql/init.sh
```

[isucon9\-qualifyのプロビジョニングファイルのサービス定義ディレクトリ](https://github.com/isucon/isucon9-qualify/blob/master/provisioning/roles/webapp.ruby/files/etc/systemd/system/isucari.ruby.service)を参考に、アプリケーションを起動します。

```
# cd webapp/ruby && bundle exec rackup --port 8000 --host 0.0.0.0
```

http://localhost:8000/ にアクセスして、ISUCARIが表示されたら成功です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/1def0c95-db4e-458b-a2bb-3e26a76e17ba.png" width=320>

もし下記のようなエラー画面が表示された場合はRubyのエラーログを確認します。(自分の場合はDBの接続文字列が間違っていました)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/941950bc-0682-830b-b282-cd3d5e693eff.png" width=320>

git管理する場合は、MySQLのデータマウントに使うディレクトリを`.gitignore`に入れておきます。ついでに私は`tmp/`ディレクトリにダウンロード済ファイルを放り込んでいましたので、お好みで。



```.gitignore:.gitignore
datadir/
tmp/
```

### ベンチマーカーのビルド
ベンチマーカーの実行バイナリはリポジトリに含まれておらず、Goでビルドする必要があります。ビルド用のコンテナを準備するために`docker-compose.yml`に追記します。

```docker-compose.yml
  golang:
    image: golang:1.14.6
    volumes:
      - .:/app
    working_dir: /app
    command: /bin/sh -c "while sleep 1000; do :; done"
```

ベンチマーカーの`Makefile`はプロジェクトルートにありますので、下記コマンドでビルドします。

```bash
$ docker-compose exec golang make
```

他にもGoに依存している所があるかなと思って`docker-compose.yml`に記述して立ち上げておきましたが、このベンチマーカーのビルドの1回しか使いませんでした。1回`$ docker run`するだけで良かったかもしれません。

### ベンチマーク実行
rackを起動した状態で、Rubyのコンテナ内で`$ ./bin/benchmarker`すればベンチマークを実行できます。しかし今回使った環境(Chromebook C434TA Core i5 Crostini + Docker)では重すぎるのか、スコア記録前の`/initialize`へのPOSTでタイムアウトしてしまいスコアが付きませんでした。

調べてみると、`init.sh`のSQLを流すのに1分以上かかっているのが原因のようです。

```bash
$ time ./webapp/sql/init.sh
(中略)
real    1m2.555s
user    0m1.872s
sys     0m0.536s
```

[ISUCON9予選マニュアルには POST /initialize が20秒以内と記載](https://github.com/isucon/isucon9-qualify/blob/master/docs/manual.md#%E3%83%99%E3%83%B3%E3%83%81%E3%83%9E%E3%83%BC%E3%82%AF%E8%B5%B0%E8%A1%8C)されており、[ベンチマーカーの実装も20秒でタイムアウトするようになっている](https://github.com/isucon/isucon9-qualify/blob/master/bench/scenario/scenario.go#L20-L21)ようです。

これは恐らくルール違反なのですが、`init.sh`のSQLを先に手動で流した後、SQL実行をスキップするように修正してからベンチマークを動かすことで、やっとスコアが付きました。

```diff:webapp/sql/init.sh
 export LANG="C.UTF-8"
 cd $CURRENT_DIR
 
-cat 01_schema.sql 02_categories.sql initial.sql | mysql --defaults-file=/dev/null -h $MYSQL_HOST -P $MYSQL_PORT -u $MYSQL_USER $MYSQL_DBNAME
+# cat 01_schema.sql 02_categories.sql initial.sql | mysql --defaults-file=/dev/null -h $MYSQL_HOST -P $MYSQL_PORT -u $MYSQL_USER $MYSQL_DBNAME
```

```
# ./bin/benchmarker
(中略)
{"pass":true,"score":810,"campaign":0,"language":"ruby","messages":[]}
```

スコアは810点でした。[Macbook Proだと3,000ぐらい出る](https://github.com/isucon/isucon9-qualify/issues/569#issuecomment-531079437)ようですが、Chromebook上のLinux仮想環境と、その上にDockerを載せているという二重のオーバーヘッドがパフォーマンスを下げているのでしょうか。

## 参考
- [ローカル環境でISUCON9予選の問題を動かす : ISUCON公式Blog](http://isucon.net/archives/53805209.html)
- [DBだけDockerを使ってISUCON9の環境構築した \- teru\_0x01\.log](https://cha-shu00.hatenablog.com/entry/2020/04/02/210125)

完成品は下記です。
https://github.com/s2terminal/isucon9-qualify-docker/pull/1