ISUCON7予選でやったインフラのお仕事

# はじめに

先日、[ISUCON7の感想とざっくりした振り返り](https://qiita.com/ihsiek/items/8b3fc66793290810113b)を書きましたが、「メンバーから何をやって、どうチューニングしたのか解説したほうが理解深まるんじゃない？みんなも興味あるだろうから書きなよ」と言われたので、自分がやった範囲の仕事を説明してみます。
もし間違いに気づかれた方、容赦なくツッコんでいただけると喜んで速攻修正しますので、よろしくお願いします:blush:

# 作業の流れ

## 1. ~/.ssh/config@local の設定

- ポータル上に3つのインスタンスが書かれていて、最後のインスタンスでMySQLが動いているとレギュレーションに書いてあったけど、ちゃんとソートされていなかった。
- メンバーが混乱するので、サーバの役割を決めて名前付けして ~/.ssh/config を書いた
    - isucon-bench, isucon-web, isucon-mysqlとか、間違えないように名前を付けた

## 2. 初期設定用シェルスクリプトの実行

```shell-session
$ cat ~/isucon7/init.sh | ssh isucon-{bench,web,mysql} /bin/bash
```

```bash:init.sh
#!/bin/bash

# ログインシェルをbashに変更
## 過去問かなにかで、ログインシェルがbashじゃなかったことがあり、自分もメンバーも躓いたので多数決でbashにしました。競技環境を普段の運用環境とそろえるのは大事
chsh -s /bin/bash
sudo chsh -s /bin/bash

# 早めに入れておきたいツールのインストール
## vim入れておかないとconfファイルいじるときに自分が困る。
## htopは後述のAnsibleで入れてもよかったけど、直前に入れることを決めたのでその場しのぎ。
sudo apt install -y vim git htop

# デフォルトのエディタをvimに変更
## Ubuntu環境だとnanoになっていることがあるので、使い慣れたvimへ。
## 嬉々としてnano使う人がチームにいる場合、先に謝っておく。
sudo update-alternatives --set editor /usr/bin/vim.basic

# メンバーの公開鍵を設置
## 共通の秘密鍵でログインするならその公開鍵を設置するだけでもいいですね。
## 既存のautorized_keysがあった場合を想定していったん/tmpで作るようにしてます。
cat << _EOS > /tmp/authorized_keys
ssh-rsa hogehoge
_EOS
sudo cat /home/isucon/.ssh/authorized_keys >> /tmp/authorized_keys
sudo mkdir -p /home/isucon/.ssh
sudo mv /tmp/authorized_keys /home/isucon/.ssh/
sudo chown -R isucon.isucon /home/isucon/.ssh/
sudo chmod 600 /home/isucon/.ssh/authorized_keys

# MySQLのバックアップを取得
## MySQLとは限らないし、MySQLのrootにパスワードが設置されていた問題もあったので、スクリプト流す前に確認したほうがいい。
## 仮にPostgreSQL使われていたとしてもmysqldumpでこけるだけなので一番最後に書いてます。
mysqldump -uroot --all-databases > /tmp/mysql.dump
```

シェルスクリプトをAnsibleに組み込まなかったのは、メンバーがログインできる状態を早く作りたかったから。 
Playbook分けてもいいけど、オペレーションミスを防ぐにはスクリプト分けるほうが無難だと判断した。

## 3. Ansible-playbook の実行

Playbookを貼ると見にくいので、やっていることをざっくりと。 
最終的に出来上がった設定は後述します。

- etckeeper のセットアップ
- ネットワーク周りのカーネルチューニング
- isucon ユーザーがパスワードなしでsudoできるようにする設定
- [alp](https://github.com/tkuchiki/alp)の設置と[alp](https://github.com/tkuchiki/alp)に対応させたNginxのログフォーマットの設定ファイルを格納
- redisのインストール
    - 入っていれば入れなくていいので、入っていなかった場合の保険
    - インスタンスのスペックに制限がある中、memcachedとredisが共存する状況は避けたほうがいいよねとチームで話して、redisに絞りました。
- gitlab.com にソースをpush/pullする鍵を指定
- rbenvのインストール
    - rubyでやることは決めていたのでバージョンの切り替えを柔軟にできるように
    - 本当はRubiniusへの切り替えを試したかったけど、セットアップに時間かかりすぎるので却下。
- ファイルディスクリプタ数の上限解放
- .vimrc, .gitconfigの設置
    - エイリアスとか諸々設定しているのでこれもいつもと同じ手順で作業できるようにするための内容です。

---
ここからは各サーバの設定内容の説明

# Web/DBサーバ共通

```text:/etc/security/limits.conf
# 同時接続数が増えていくと `Too many open files` が頻発するようになる。 
# ISUCONのような競技プロコンでは、サーバの性能を限界まで引き出すことが求められているため、あらかじめ設定可能な最大値を設定しておく。
* soft nofile 65536
* hard nofile 65536
```

```text:/etc/sysctl.conf
# IPv6の無効化
# IPv6でのルーティングがないのであれば、わずかなオーバーヘッドも惜しいので削る。
# というか昔、IPv6が有効な環境で通信が遅くなる事例に見舞われたので条件反射的にやってしまうやつ。
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1

# IPv4のソケットを使いまわす(送受両方)
# 同一IPから複数セッション張られることが予想される場合、tcp_tw_recycle=0 がいいかも
net.ipv4.tcp_tw_recycle=1
net.ipv4.tcp_tw_reuse=1

# FIN-WAIT-2 のソケットを強制クローズする
net.ipv4.tcp_fin_timeout=2

# TCPキープアライブの設定
# アイドル状態のTCP接続を早めに落として連続してベンチたたいても影響がないようにしたかった。
# net.ipv4.tcp_keepalive_probesとnet.ipv4.tcp_keepalive_intvlを設定しなかったので効いていなかった。※65s + 75s × 9回 = 740s の間、アイドル状態の通信が切られない設定になっていた
net.ipv4.tcp_keepalive_time=65

# できるだけ多くの接続を受け付けられるように接続のキューを増やす
net.core.somaxconn=65535
net.ipv4.tcp_max_syn_backlog=65535

# 送受信のバッファサイズも大きめに設定する
net.core.rmem_max=16777216
net.core.wmem_max=16777216

# swapの発生頻度を下げる設定
# 0にしてもよかった
vm.swappiness=10

# カーネルレベルでのファイルディスクリプタ上限数変更
# プロセス単位のチューニングをやったけど、こっちもやっておく
fs.file-max=65535
```

# Webサーバ

```nginx:/etc/nginx/nginx.conf
user www-data;

# CPUのコア数にあわせて設定してくれるのでautoで。
worker_processes auto;
pid /run/nginx.pid;

# worker_connectionsの4倍を設定しています。
## 以下の記事を参考に、HTTP Pipeliningの話も考慮して決めました。
## https://qiita.com/iwai/items/1e29adbdd269380167d2#worker_rlimit_nofile
## systemdを使っている場合、ここで設定しても反映されないので嵌りポイントだと思います。
worker_rlimit_nofile 4096;

events {
  # 後から設定見直すつもりでの仮置き、結局チューニングに入れず。
  worker_connections 1024;

  # 有効にしとけばよかったけど、漏れてたやつ。
  ## リクエストを同時受付して困るケースに当たったことは今のところない。
  # multi_accept on;
}

http {
  # Linuxのsendfile()を使うようにする設定
  ## ファイルディスクリプタとの通信(コンテンツファイルの読み込みとかクライアントへのレスポンスとか)をカーネル空間内でやるようになる。
  ## tcp_nopushはsendfileで取り扱うパケットをまとめるようになるオプションなので合わせて有効にしておきます。  
  sendfile on;
  tcp_nopush on;

  # KeepAliveの設定
  ## ベンチマークの時間が1分、それより長く滞在することはないはずということでこの設定
  keepalive_timeout 60;

  # typesディレクティブのハッシュテーブルに割り当てるバケットの最大サイズを設定
  ## 競技環境でもともと設定されていた。
  ## Ubuntuだとこれがデフォで設定されるっぽい記述を見かけたのでそれかも。
  types_hash_max_size 2048;

  # レスポンスヘッダーのServerにNginxのバージョン記述を表示させなくする。
  ## データの送信量を削るほどの効果は期待できないけど、やっておかないと気持ち悪い。
  server_tokens off;

～中略～

  # Gzipの設定
  ## レスポンスを圧縮することでデータ転送の効率化を行う。
  ## 基本的にはコメントアウトされていた内容を外しただけ。
  ## gzip_staticを有効にするとあらかじめgzip圧縮されているコンテンツはそのまま配信されるようになるので、これだけ追加して、public以下をgzip圧縮しなおした。
  gzip on;
  gzip_static on;
  gzip_disable "msie6";

  gzip_vary on;
  gzip_proxied any;
  gzip_comp_level 6;
  gzip_buffers 16 8k;
  gzip_http_version 1.1;
  gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript application/font-woff application/font-tff application/vnd.ms-fontobject;

  include /etc/nginx/conf.d/*.conf;
  include /etc/nginx/sites-enabled/*;
```

```nginx:/etc/nginx/sites-available/nginx.conf
upstream app {
  # UNIXドメインソケットを使ってpumaと通信するように切り替える
  ## IP直書きでやると、TCPソケットで3-Way Handshakeを確立するまで通信できない分オーバーヘッドが大きい。
  ## 別サーバと通信する必要がなければUNIXドメインソケットを使うようにしている。
  server unix:/home/isucon/isubata/webapp/ruby/tmp/sockets/puma.socket;
  keepalive 64;
}

server {
  # 最終ベンチのタイミングでは、ログ出力のオーバーヘッドが無視できなくなるのでoffにしました。
#  access_log /var/log/nginx/access.log ltsv;
  access_log off;

  listen 80 default_server;

  # クライアントから受け付けるbodyサイズの最大値を拡張する
  ## 投稿機能があるサービスだったのであらかじめ大きく設定しておく。
  client_max_body_size 20M;

  root /home/isucon/isubata/webapp/public;

  location / {
    try_files $uri @app;
  }

  location ~* \.(?:ico|js|eot|svg|ttf|woff2?|css|gif|jpe?g|png)$ {
    # ブラウザキャッシュの期間を設定
    ## ネットワークの帯域が詰まったので設定した。
    ## 304応答だと1リクエスト1点になるし、帯域を少し確保できたらいいよねと話してこの値になったが、出題の意図を踏まえると30dとか設定したほうがよかった。
    ## 複数台構成に切り替えていたらチューニングしたはずの部分。
    expires 3s;

    # ISUCONの過去問で入れてた設定をそのまま転用
    ## CC:publicはISUCONでpublicに設定してダメなケースは思いつかなかった。
    ## ダメな場合はどうせステータスコードが違うとかエラーが出るんだろうし、その時に見直せばいいと思っていた。
    ## etag offは完全にたまたま。複数台構成になったとして、expiresを修正していればつまらなかった。
    add_header Cache-Control "public, must-revalidate, proxy-revalidate";
    etag off;
  }

  location @app {
    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;
  }
}
```

# DBサーバ

いじったところだけ抜粋

```mysql:/etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
# 最大接続数を拡張
## ruby側の参照実装にコネクションがクローズされていなかったタイミングがあったらしい。
## あらかじめ大きめに取ることで暫定対応しておいた名残。
max_connections = 20000

# ログ出力レベルを変更
## ログ出力のオーバーヘッドを減らしたくて、エラーログだけ出力するようにした。
log_error_verbosity = 1

# クエリキャッシュの設定を有効化
## SELECT SQL_NO_CACHEがなければキャッシュするように。
## query_cache_limit, query_cache_sizeはメモリ量と照らして勘案すべきだったけどほかで嵌って導入時点からチューニングできず。
query_cache_type = 1
query_cache_limit = 2M
query_cache_size = 32M

# 
innodb_flush_method = O_DIRECT
# InnoDBログの出力方法を変更する
## Disk I/Oを減らすことが目的。0と2の違いはトランザクションコミット時にログが出力されるかどうか。
innodb_flush_log_at_trx_commit = 2

# バッファプールサイズの拡張
## テーブル＋インデックスあわせてこの量にならなかったので、少し余裕を作りすぎたかも。
innodb_buffer_pool_size = 384M

# バッファプールのウォームアップを有効にする
## デフォルト25%になっているので100%読み込むように。
## 再起動に時間がかかっても構わないので、再起動時に全部読み込むようにしておく
innodb_buffer_pool_dump_pct  = 100
innodb_buffer_pool_dump_at_shutdown= 1
innodb_buffer_pool_load_at_startup = 1
```

# 最後に

このエントリを書いてる中で、人に説明できる段階に昇華できていない項目が意外と多く、散々調べました。
正直、ISUCON参加の前後で自分のインフラに対する理解度が大きく上がってチューニングの幅が広がったと思ってました。が、アウトプットまでやろうとするとさらに理解が必要で、ここで調べたことは忘れないだろうと思います。

ISUCON7予選の結果は45,000程度と揮わなかったので、今回の記事が参考になる人は限定的かもしれませんが、自分の中でアップデートされたときは、またエントリを書いてみようと思います。