プログラミング 読書

「達人が教える Webパフォーマンスチューニング ISUCONから学ぶ高速化の実践」を読んだ

2023年4月29日

達人が教えるWebパフォーマンスチューニング〜ISUCONから学ぶ高速化の実践」を読んだ。

private-isuを題材にチューニングを追体験できて楽しかった、実践的だった。

GitHub - catatsuy/private-isu: 社内ISUCON https://github.com/catatsuy/private-isu

top、alp、pt-query-digestでひとつずつボトルネックを解消してて勉強になった。
MacOS上でdocker-composeして試してたのでtopはできなかったけど。

docker statsやdocker-compose topはtopっぽいけどなんか違った。

手元のMacbook ProがM1なのでvagrantも動かなくてMac上にmysqlとmemcachedいれて適当に動かしてたけどどこかで詰まってしまってdocker-composeで続けた。

地味にmysql-slow-logをベンチマーカーを実行するたびに削除して再生成したいけどうまくいかなくて困ってたけど、pt-query-digest --sinceでいい感じに対応できた。

#!/bin/sh

docker run --network host -i private-isu-benchmarker /opt/go/bin/benchmarker -t http://host.docker.internal -u /opt/go/userdata
alp json \
  --sort sum -r \
  -m "/posts/[0-9]+,/@\w+,/image/\d+" \
  -o count,method,uri,min,avg,max,sum \
  --filters 'Time < TimeAgo("2m")' \
  < ../webapp/logs/nginx/access.log
pt-query-digest --since=`gdate "+%s" -d "542 minutes ago"` ../webapp/logs/mysql/slow.log

これをbenchmarker/bench.shとかに保存して何かを変更するたびに実行してた。

手元ではベンチマーカーの実行時間が2分以内だったので、alpもpt-query-digestも直近2分間だけログを取得して集計している。

alpは--pos=posfileで読み込んだ最終行を記録しておき、次回実行時にそこから読み込める仕組みがあるのでより正確なんだけど、pt-query-digestにはないので2分間縛りにした。

542 minutes agoなのは9時間ずれてるのを修正するのが面倒だったのでこうしてる。

MacOSではn minutes agoという表現を使うためにcoreutilsにはいってるGNU dateが必要。

brew install coreutils

静的ファイル配信したり、プリペアードステートメントを辞めたり、外部コマンドを辞めたり、N+1をSTRAIGHT_JOINやFORCE INDEXしたり、インデックス作ったりしてどんどんスコアが伸びていくのが楽しい。

unicornをUNIX domain socket経由にするのとか、unicornのYJITを有効にするのとかはdocker-composeだとちょっと時間かかりそうだったので断念したりしたけど。

docker-composeにadminerいれてインデックス作ったり、EXPLAINみたりしてた。テーブルもサクサクみれて便利。

services:
  nginx:
    image: nginx:1.24
    volumes:
      - ./etc/nginx/conf.d:/etc/nginx/conf.d
      - ./public:/public
      - ./logs/nginx:/var/log/nginx
    ports:
      - "80:80"
    links:
      - app

  app:
    # Go実装の場合は golang/ PHP実装の場合は php/
    build: ruby/
    environment:
      ISUCONP_DB_HOST: mysql
      ISUCONP_DB_PORT: 3306
      ISUCONP_DB_USER: root
      ISUCONP_DB_PASSWORD: root
      ISUCONP_DB_NAME: isuconp
      ISUCONP_MEMCACHED_ADDRESS: memcached:11211
    links:
      - mysql
      - memcached
    volumes:
      - ./public:/home/public

    init: true
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 1g

  mysql:
    image: mysql:8.0
    environment:
      #- "TZ=Asia/Tokyo"
      - "MYSQL_ROOT_HOST=%"
      - "MYSQL_ROOT_PASSWORD=root"
    volumes:
      - mysql:/var/lib/mysql
      - ./etc/my.cnf:/etc/my.cnf
      - ./sql:/docker-entrypoint-initdb.d
      - ./logs/mysql:/var/log/mysql
    ports:
      - "3306:3306"
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 1g

  memcached:
    image: memcached:1.6

  adminer:
    image: adminer
    restart: always
    ports:
      - 9000:8080

volumes:
  mysql:

こんな感じでインデックス作ったり

こんな感じで実行して、EXPLAINのリンクをクリックすると横長に見やすく表示されます。

修正例は以下に掲載されていて、行き詰まったらdiffみてあ〜〜なるほど〜〜〜〜とかひとりでぼやいてた。

tatsujin-web-performance/README.md at main · tatsujin-web-performance/tatsujin-web-performance · GitHub https://github.com/tatsujin-web-performance/tatsujin-web-performance/blob/main/appendix-A/README.md

課題を作るのも大変だけどベンチマーカーを作るのはもっと大変だってことがわかって感心した。

alpとpt-query-digestをコンパスにして一番負荷がかかってる処理を解決する、この流れが体験できてよかった。

そんなにアクセスあるサイト担当してるわけじゃないけど業務で実践してノウハウを貯めていきたい(postgres使ってる案件はMySQLに移行しないとな…。あとRailsだとプリペアードステートメント辞めれるんだろうか?とかSTRAIGHT_JOINとかFORCE INDEXとかどうやってやるか調べないととか)

以下は読書メモ。

Prometheus
node_exporter
nginxのaccess_logにレスポンスタイム追加してJSON化
alp
Fluentd+Amazon Redshift
ab
mysql slow_query_log
mysqldumpslow
unicornのworker_processesをCPUコア数の5倍に
k6
pt-query-digest
N+1問題
FORCE INDEX
STRAIGHT_JOIN
JOIN_ORDER
SELECT *をやめる
ADMIN PREPARE、プリペアドステートメントをやめる
max_connectionsを数千
innodb_buffer_pool_sizeを物理メモリの80%
innodb_flush_method=O_DIRECT
innodb_flush_log_at_trx_commit=2
レプリケーションしないならバイナリログを無効化
disable-log-bin=1
sync_binlog=1000
nginx
client_max_body_size=10m;
静的ファイルの配信とexpires 1d;
worker_processes=auto
gzip on;
ngx_http_gzip_static_module
ngx_http_gunzip_module
gzip_comp_level=6
gzip圧縮しないと帯域使用量が5倍になる
アプリケーションサーバ上でもgzip圧縮する
Brotli
keepalive 32;
keepalive_requests 10000;
ssl_session_cache
TLS HTTP/2 HPACK
listen 443 ssl http2;
ssl_protocols TLSv1.2 TLSv1.3;
kTLS
worker_rlimit_nofile
sendfile on
tcp_nopush on
memcached
Redisはシングルスレッド
単純なGET/SET以外は全体の処理がブロックしてしまう
問題を考えても導入するメリットが上回る場合のみキャッシュ
データが更新された時にキャッシュも更新するのは二重管理になるので十分短いTTLを設定する
Thundering herd problem
キャッシュの残り時間が下回ったら一定確率で再構築
nginxのproxy_cache_lock
バッチ処理でキャッシュ生成
comments.1234.countにコメント数を10秒キャッシュ
memcached statusでevictions、get_hitsなどを監視
外部コマンドではなくライブラリを利用する
プロセスをコピーして実行するので遅い、リソースを食う
HTTPクライアント
コネクションを使い回す
タイムアウトを短くする
コネクション数を制限する
nginxのX-Accel-Redirect
CDN、s3でキャッシュ
Linux NAPI、Receive Side Scaling
TLS暗号化復号化のハードウェアオフロード
eBPF extended Berkelay Packet Filter
Max Open Files LimitNOFILE=1006500
mysqlのopen_files_limitの決め方
net.core.somaxconn
net.ipv4.ip_local_port_range
UNIX domain socket 同ホストならポート枯渇しない
Amazon Linux2のMTUは9001

-プログラミング, 読書