Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
ISUCON Cheat Sheet

集合

9:00にオフィス(Sticky Fingers)

食料品は事前に用意しておく。食べ過ぎない。胃に負担をかけないようにする。

運営の櫛井さんからのメールに従って、サポートチャットと予選ポータルサイトにログインする。

ISUCON6 予選ポータルサイト

http://isucon6q.songmu.org/

10:00〜11:00

最初の1時間

11:00〜12:00

まず基本的なことをやる。

12:00〜17:00

この辺りからRedis移行に取り組む。

17:00〜18:00

最後の1時間

Why

課題の理解、プロファイルをして、チューニングの方針を決める。

What

  • githubにコードをpush
  • OSバージョンを確認・インスタンスのスペックを確認
  • 使われているミドルウェアを確認・設定ファイルをgithubにpush (cf. https://github.com/ngtk/orenoie/tree/master/deploy )
    • systemd
    • nginx
    • mysql
    • postgresql
    • memcached
    • redis
  • 課題の理解
    • レギュレーションと当日マニュアルをよく読む。特に重要なのは「スコアの算出方法」「失格条件」
    • ブラウザで課題サイトへアクセスする
    • アクセスポイントの整理
    • データ量と流れの理解
      • テーブル名とカラム
      • データの更新があるか
  • ベンチマーカー初回実行
  • 方針決定とタスク分配

ISUCON予選突破を支えたオペレーション技術 から引用

競技終了前のチェック

競技終了前になにか見落としがないかチェックします。

  • ディスクサイズに余裕があるか。運営サイドでベンチマークを回す時にログ出力でディスクがうまることがないとも限らない。ログを吐きまくってると意外とディスクに余裕がなくなっていたりするため、最後に確認する。
  • ページの表示は正常か。CSSがなぜかあたってないとかないか。
  • ログの出力を切る。アクセスログやスロークエリログ。
  • (厳密には競技終了30分〜60分の間ぐらい) OSごと再起動してもベンチマークが通るかどうか

max connections

Too many connections error が出る時は、max connections を大きくする。 ただし、5_os にしたがって「OS 側での connection, open file limit の設定の更新」をする必要がある(OS側で制限されてると、mysql 側ではその制限の範囲内でしか設定を変えれない)

# /etc/mysql/my.cnf

# /etc/mysql/mysql.conf.d/mysqld.cnf にあることが多い
[mysqld]
max_connections=10000  # <- connection の limit を更新

設定が反映されてるからは、mysql client で繋げてから SHOW variables LIKE "%max_connection%"; で確認。

isucon@ip-172-31-17-45:~$ mysql -u isucon -p -e 'SHOW variables LIKE "%max_connections%";'
Enter password:
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 10000 |
+-----------------+-------+

尚、OS側でちゃんと open files limit が(例えば 65535 に)設定されてると、mysql 側でもそれが確認できる。

isucon@ip-172-31-17-45:~$ mysql -u isucon -p -e 'SHOW variables LIKE "%open_files_limit%";'
Enter password:
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| open_files_limit | 65535 |
+------------------+-------+

設定がうまくいってない時はmysql の start 時のログにも出てるはずなので、見ると良い。

sudo journalctl -u mysql

slow query

long_query_timeを0にするとすべてのクエリを出力される

[mysqld]
slow_query_log
slow_query_log_file = /var/log/mysql-slow.sql
long_query_time = 5

innodb buffer

http://www.slideshare.net/kazeburo/mysql-casual7isucon

innodb_buffer_pool_size = 1GB # ディスクイメージをメモリ上にバッファさせる値をきめる設定値
innodb_flush_log_at_trx_commit = 2 # 1に設定するとトランザクション単位でログを出力するが 2 を指定すると1秒間に1回ログファイルに出力するようになる
innodb_flush_method = O_DIRECT # データファイル、ログファイルの読み書き方式を指定する(実験する価値はある)

mysql2-cs-bind gem

mysql2-cs-bind gemを使うと扱いやすくなる https://github.com/tagomoris/mysql2-cs-bind

参考

ISUCONの勝ち方 YAPC::Asia Tokyo 2015より抜粋

# mysqlのコンソールにて 
> set global slow_query_log = 1; 
> set global long_query_time = 0; 
> set global slow_query_log_file = "/tmp/slow.log"; 
# ベンチマーク実行 
$ pt-query-digest /tmp/slow.log > /tmp/digest.txt 
$ rm /tmp/slow.log 
# 戻すときは 
$ service mysqld restart

dbdump

ダンプ作成

mysqldump -u user dbname | gzip > dbname.dump.gz 

転送

scp isucon@13.78.120.149:~/isucon.dump.gz ~  

ローカルにDB作成

$ mysql -uroot
mysql> create database isuconp;

リストア

gzcat isucon.dump.gz | mysql -u root isuconp
worker_processes  auto;  # コア数と同じ数まで増やすと良いかも

# nginx worker の設定
worker_rlimit_nofile  4096;  # worker_connections の 4 倍程度(感覚値)
events {
  worker_connections  1024;  # 大きくするなら worker_rlimit_nofile も大きくする(file descriptor数の制限を緩める)
  # multi_accept on;  # error が出るリスクあり。defaultはoff。
  # accept_mutex_delay 100ms;
}

http {
  log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $request_time';   # kataribe 用の log format
  access_log  /var/log/nginx/access.log  main;   # これはしばらく on にして、最後に off にすると良さそう。
  # access_log  off; 
  
  # 基本設定
  sendfile    on;
  tcp_nopush  on;
  tcp_nodelay on;
  types_hash_max_size 2048;
  server_tokens    off;
  # open_file_cache max=100 inactive=20s; file descriptor のキャッシュ。入れた方が良い。
  
  # proxy buffer の設定。白金動物園が設定してた。
  # proxy_buffers 100 32k;
  # proxy_buffer_size 8k;
  
  # mime.type の設定
  include       /etc/nginx/mime.types;  
 
  # Keepalive 設定
  keepalive_timeout 65;
  keepalive_requests 500;

  # Proxy cache 設定。使いどころがあれば。1mでkey8,000個。1gまでcache。
  proxy_cache_path /var/cache/nginx/cache levels=1:2 keys_zone=zone1:1m max_size=1g inactive=1h;
  proxy_temp_path  /var/cache/nginx/tmp;
  # オリジンから来るCache-Controlを無視する必要があるなら。。。
  #proxy_ignore_headers Cache-Control;
  
  # Lua 設定。
  # Lua の redis package を登録
  lua_package_path /home/isucon/lua/redis.lua;
  init_by_lua_block { require "resty.redis" }

  # unix domain socket 設定1
  upstream app {
    server unix:/run/unicorn.sock;  # systemd を使ってると `/tmp` 以下が使えない。appのディレクトリに`tmp`ディレクトリ作って配置する方がpermissionでハマらずに済んで良いかも。
  }
  
  # 複数serverへ proxy
  upstream app {
    server 192.100.0.1:5000 weight=2;  // weight をつけるとproxyする量を変更可能。defaultは1。多いほどたくさんrequestを振り分ける。
    server 192.100.0.2:5000;
    server 192.100.0.3:5000;
    # keepalive 60; app server への connection を keepalive する。app が対応できるならした方が良い。
  }

  server {
    # HTTP/2 (listen 443 の後ろに http2 ってつけるだけ。ブラウザからのリクエストの場合ssl必須)
    listen 443 ssl http2;
    
    # TLS の設定
    listen 443 default ssl;
    # server_name example.jp;  # オレオレ証明書だと指定しなくても動いた
    ssl on;
    ssl_certificate /ssl/oreore.crt;
    ssl_certificate_key /ssl/oreore.key;
    # SSL Sesssion Cache
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1m;  # cacheする時間。1mは1分。
  
    # reverse proxy の 設定
    location / {
      proxy_pass http://localhost:3000;
      # proxy_http_version 1.1;          # app server との connection を keepalive するなら追加
      # proxy_set_header Connection "";  # app server との connection を keepalive するなら追加
    }

    # Unix domain socket の設定2。設定1と組み合わせて。
    location / {
      proxy_pass http://app;
    }
    
    # For Server Sent Event
    location /api/stream/rooms {
      # "magic trio" making EventSource working through Nginx
      proxy_http_version 1.1;
      proxy_set_header Connection '';
      chunked_transfer_encoding off;
      # These are not an official way
      # proxy_buffering off;
      # proxy_cache off;
      proxy_pass http://localhost:8080;
    }
  
    # For websocket
    location /wsapp/ {
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
      proxy_pass http://wsbackend;
    }
    
    # Proxy cache
    location /cached/ {
      proxy_cache zone1;
      # proxy_set_header X-Real-IP $remote_addr;
      # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      # proxy_set_header Host $http_host;
      proxy_pass http://localhost:9292/;
      # デフォルトでは 200, 301, 302 だけキャッシュされる。proxy_cache_valid で増やせる。
      # proxy_cache_valid 200 301 302 3s;
      # cookie を key に含めることもできる。デフォルトは $scheme$proxy_host$request_uri;
      # proxy_cache_key $proxy_host$request_uri$cookie_jessionid;
      # レスポンスヘッダにキャッシュヒットしたかどうかを含める
      add_header X-Nginx-Cache $upstream_cache_status;
    }   
    
    # Lua
    location /img {
      # default_type 'image/svg+xml; charset=utf-8';
      content_by_lua_file /home/isucon/lua/img.lua;
    }
    
    # WebDav 設定。使いどころがあれば。
    location /img {
      client_body_temp_path /dev/shm/client_temp;
      dav_methods PUT DELETE MKCOL COPY MOVE;
      create_full_put_path  on;
      dav_access            group:rw  all:r;
     
      # IPを制限する場合
      # limit_except GET HEAD {
      #   allow 192.168.1.0/32;
      #   deny  all;
      # }
    }

    # static file の配信用の root
    root /home/isucon/webapp/public/;

    location ~ .*\.(htm|html|css|js|jpg|png|gif|ico) {
      expires 24h;
      add_header Cache-Control public;
      
      open_file_cache max=100  # file descriptor などを cache

      gzip on;  # cpu 使うのでメリット・デメリット見極める必要あり。gzip_static 使えるなら事前にgzip圧縮した上でそちらを使う。
      gzip_types text/css application/javascript application/json application/font-woff application/font-tff image/gif image/png image/jpeg image/svg+xml image/x-icon application/octet-stream;
      gzip_disable "msie6";
      gzip_static on;  # nginx configure時に --with-http_gzip_static_module 必要
      gzip_vary on;
    }
  }
}

unicorn 側の設定(unix domain socket にするため)

# unicorn_config.rb
listen "/run/unicorn.sock"

puma 側の設定(unix domain socket にするため)

起動オプションで -b [socket] を指定。 cf. https://github.com/puma/puma

$ bundle exec puma -b unix:///var/run/puma.sock

ベンチマークを実行しながら、現在どこにボトルネックがあるのかを調べる。

最適化をすすめていくとボトルネックは違う場所に移るので、適時調べ直す。

基本的な流れ

  • ロードアベレージをみる。topuptime を使う。
  • CPUとI/Oどちらがボトルネックか調べる。sarvmstat で推移をみる。
  • CPUがボトルネックの場合、topsar ででユーザプログラムかシステムプログラムか。プロセスの特定に ps auxw を使う。
  • プロセスの更に詳しく調べる場合は straceoprofile を使う。
  • 通信内容のキャプチャには tcpdump を使う。

あわせて読みたい

netdata のインストール

$ bash <(curl -Ss https://my-netdata.io/kickstart.sh) all

netdata の使い方

ブラウザで http://(対象ホスト):19999 を開いて確認

ngrep の使い方

コマンドの使い方

top

uptime

sar

  • sar -u CPU使用率
  • sar -q ロードアベレージ
  • sar -r メモリ利用状況
  • sar -W スワップ発生状況
  • sar -n ネットワークトラフィック

sar 1 1000 で1秒毎に1000回レポートする。

vmstat

ps

strace

システムコールレベルのアプリケーションの動作を確認する。

oprofile

iftop

ネットワーク負荷

iotop

disk I/O

dstat

カーネルパラメータチューニング

頻出カーネルパラメータ設定

maxconnection を増やす

1 process デフォルトでは 1,024 しか使えない connection 数を増やす。

# /etc/sysctl.conf
net.core.somaxconn = 10000  # <- 追加。32768 (2^15) くらいまで大きくしても良いかも。
net.ipv4.ip_local_port_range = 10000    60999  # port の範囲を広げる

sysctl -p で設定を反映

$ sudo sysctl -p

確認は sysctl -a

isucon@ip-172-31-17-45:~$ sudo sysctl -a | grep maxconn
net.core.somaxconn = 10000

open file limit を増やす

connection (socket) も file descriptor なので、open file limit を増やす必要がある。 systemd の unit file に記載するのがおすすめ。

例えば、mysql の limit を増やしたい場合は /etc/systemd/system/mysql.service[Service] に以下を追加。

# /etc/systemd/system/mysql.service
[Service]
LimitNOFILE=65535

tcp connection の再利用を有効化

# /etc/sysctl.conf
net.ipv4.tcp_tw_reuse = 1

(追加) tcp connection が FIN-WAIT2 から TIME_WAIT に状態が変化するまでの時間

# /etc/sysctl.conf
net.ipv4.tcp_fin_timeout = 10  # デフォルト 60。CPU 負荷を減らせるが、短すぎると危ういかも?

(追加) TIME_WAIT状態にある tcp connection 数の上限

net.ipv4.tcp_max_tw_buckets = 2000000  # デフォルトは 32768 くらい

(追加) パケット受信時にキューにつなぐことのできるパケットの最大数

net.core.netdev_max_backlog = 8192  # デフォルトは 1000 くらい

RACK_ENV=productionになっているかどうかなど、最初の1時間で確認しておく

おそらくsystemdでプロセスが起動しているので /etc/systemd/system/isu-ruby.service の中身を確認すれば良い。

参考

pixivの時は以下のようになっていた。

$ cat /etc/systemd/system/isu-ruby.service
[Unit]
Description=isu-ruby
After=syslog.target

[Service]
WorkingDirectory=/home/isucon/private_isu/webapp/ruby
EnvironmentFile=/home/isucon/env.sh
Environment=RACK_ENV=production
PIDFile=/home/isucon/private_isu/webapp/ruby/unicorn.pid

User=isucon
Group=isucon
ExecStart=/home/isucon/.local/ruby/bin/bundle exec unicorn -c unicorn_config.rb
ExecStop=/bin/kill -s QUIT $MAINPID
ExecReload=/bin/kill -s USR2 $MAINPID

[Install]
WantedBy=multi-user.target
$ cat ~/env.sh
PATH=/usr/local/bin:/home/isucon/.local/ruby/bin:/home/isucon/.local/node/bin:/home/isucon/.local/python3/bin:/home/isucon/.local/perl/bin:/home/isucon/.local/php/bin:/home/isucon/.local/php/sbin:/home/isucon/.local/go/bin:/home/isucon/.local/scala/bin:/usr/bin/:/bin/:$PATH
GOPATH=/home/isucon/gocode
ISUCONP_DB_NAME=isuconp
ISUCONP_DB_USER=isucon
ISUCONP_DB_PASSWORD=isucon

チートシート

# systemctl start [name.service]
# systemctl stop [name.service]
# systemctl restart [name.service]
# systemctl reload [name.service]
$ systemctl status [name.service]
# systemctl is-active [name.service]
$ systemctl list-units --type service --all
$ systemctl list-units --type service
$ systemctl list-unit-files

必ずやること

  • config ファイルの bind ~ はコメントアウト(localhost以外からも繋げるようにするため)

unix socket

unix socket で redis.conf 使う場合

# Specify the path for the Unix socket that will be used to listen for
# incoming connections. There is no default, so Redis will not listen
# on a unix socket when not specified.
#
# unixsocket /tmp/redis.sock
# unixsocketperm 700

privatetmp の仕組みで /tmp 以下のパスだと動かない。 http://www.kunitake.org/chalow/cat_systemd.html

sudo vim /etc/systemd/system/redis.service

PrivateTmp=yes

slow log

#slowlog-log-slower-than 10000
#slowlog-max-len 128
#latency-monitor-threshold 0

高速化のためにするべき設定

DBスナップショットのディスク保存を止める 再起動後にデータが保持されていないといけないので、これはやってはいけない。

################################ SNAPSHOTTING  ################################
#
# Save the DB on disk:
#
#   save <seconds> <changes>
#
#   Will save the DB if both the given number of seconds and the given
#   number of write operations against the DB occurred.
#
#   In the example below the behaviour will be to save:
#   after 900 sec (15 min) if at least 1 key changed
#   after 300 sec (5 min) if at least 10 keys changed
#   after 60 sec if at least 10000 keys changed
#
#   Note: you can disable saving completely by commenting out all "save" lines.
#
#   It is also possible to remove all the previously configured save
#   points by adding a save directive with a single empty string argument
#   like in the following example:
#
#   save ""

save 900 1
save 300 10
save 60 10000

RDBのチェックサム計算を止める

# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.
# This makes the format more resistant to corruption but there is a performance
# hit to pay (around 10%) when saving and loading RDB files, so you can disable it
# for maximum performances.
#
# RDB files created with checksum disabled have a checksum of zero that will
# tell the loading code to skip the check.
rdbchecksum yes

その他

tcp-backlog 511
timeout 0
tcp-keepalive 0
loglevel notice

stop-writes-on-bgsave-error yes

rdbcompression no
rdbchecksum yes
dbfilename dump.rdb

dir /home/isucon/redis


maxmemory 4g
maxmemory-policy noeviction

hiredis

https://github.com/redis/redis-rb

hiredis ドライバを使うと c ext gem が使えるので早くなる。

gem "redis", "~> 3.0.1"
gem "hiredis", "~> 0.4.5"

参考

ansible

localhost に対して実行する時

ansible-playbook -i "localhsot," setup.yml --connection=localhost

remote host に対して実行する時

ansible-playbook -i "10.125.224.56," setup.yml

Inastall

Gemfilegem puma して bundle install するだけ!

unit file(例)

# isu.ruby.service
[Unit]
Description=isu ruby

[Service]
Type=simple
User=isucon
Group=isucon
WorkingDirectory=/home/isucon/webapp/ruby
EnvironmentFile=/home/isucon/env.sh
ExecStart=/usr/local/bin/bundle exec puma --port 8080 -t 400 -e production config.ru

[Install]
WantedBy=multi-user.target
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment