~/.ssh/config
のサンプル
Host isucon-1
Hostname 13.231.219.177
IdentityFile ~/.ssh/isucon-practice.pem
User ubuntu
RequestTTY yes
StrictHostKeyChecking no
Host isucon-2
Hostname 18.179.30.73
IdentityFile ~/.ssh/isucon-practice.pem
User ubuntu
RequestTTY yes
StrictHostKeyChecking no
Host isucon-3
Hostname 54.178.138.79
IdentityFile ~/.ssh/isucon-practice.pem
User ubuntu
RequestTTY yes
StrictHostKeyChecking no
sshしつつユーザー切り替え
ssh isucon-1 "sudo -i -u isucon"
サーバーでShellとNeovim環境をセットアップ。全てのサーバーで実行する
make setup-shell SETUP_HOST=isucon-1
ssh isucon-1 "sudo -i -u isucon"
sudo passwd isucon
./setup.sh
Makefileでbuildのターゲットを準備すること
build:
go build -o isuconquest ./...
Makefileの以下をアプリ名に書換えてから、makeを実行する
APP_NAME:=isuports
make setup-sysctl
make setup-nginx
make setup-mysql
make setup-webapp
etc/systemd/system/$(サービス名)
に以下のように追記する。Dockerで定義されていた環境変数は、Environmentで定義する。
WorkingDirectory=/home/isucon/webapp/go
ExecStart=/home/isucon/webapp/go/isuports
Environment=ISUCON_DB_HOST=192.168.0.12
Environment=ISUCON_DB_PORT=3306
Environment=ISUCON_DB_USER=isucon
Environment=ISUCON_DB_PASSWORD=isucon
Environment=ISUCON_DB_NAME=isuports
webapp/go配下のビルドするMakefileのサンプル
isuports:
go build -o isuports ./...
これらの設定は最終的に元に戻すこと
これらの設定を行えば、make before-bench
をベンチマーク実行前、make after-bench
をベンチマーク実行後にそれぞれ実行することで、計測結果がそれぞれ以下に格納される。
- alp
- nginxのアクセスログ。alpで解析する
- slowquery
- mysqlのslowquery。pt-query-digestなどで解析する
- profile
- golangのprofile。pdfファイルが出力される
nginx/nginx.conf
でログ出力を以下のように書き換える
http {
log_format json escape=json '{"time":"$time_local",'
'"host":"$remote_addr",'
'"forwardedfor":"$http_x_forwarded_for",'
'"req":"$request",'
'"status":"$status",'
'"method":"$request_method",'
'"uri":"$request_uri",'
'"body_bytes":$body_bytes_sent,'
'"referer":"$http_referer",'
'"ua":"$http_user_agent",'
'"request_time":$request_time,'
'"cache":"$upstream_http_x_cache",'
'"runtime":"$upstream_http_x_runtime",'
'"response_time":"$upstream_response_time",'
'"vhost":"$host"}';
access_log /var/log/nginx/access.log json;
mysql/mysql.conf.d/mysqld.cnf
で以下のように書く。
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 0
以下のようにProfileの設定をする。
package main
import (
"flag"
"log"
"os"
"os/signal"
"runtime/pprof"
"syscall"
)
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
func main() {
flag.Parse()
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
log.Fatal(err)
}
err = pprof.StartCPUProfile(f)
if err != nil {
log.Fatal(err)
}
go func() {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT)
<-sig
pprof.StopCPUProfile()
f.Close()
os.Exit(0)
}()
}
Run()
}
systemdでファイル名を指定する
ExecStart=/home/isucon/webapp/go/isuports -cpuprofile cpu.pprof
pdf出力のためgarphvizをinstall
brew install graphviz
また、以下でinteractiveに解析することも出来る
go tool pprof -source_path $PWD/webapp/go profile/cpu.pprof
fgpprofでI/O含めた結果を見る場合
package main
import (
"flag"
"log"
"os"
"os/signal"
"runtime/pprof"
"syscall"
"github.com/felixge/fgprof"
)
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
var fcpuprofile = flag.String("fcpuprofile", "", "write fgprof cpu profile to file")
func main() {
flag.Parse()
var cpuCloser func()
var stopFGProf func() error
if *cpuprofile != "" {
cpu, closer := startCPUProfile(*cpuprofile)
defer cpu.Close()
cpuCloser = closer
}
if *fcpuprofile != "" {
fcpu, stop := startFGProfile(*fcpuprofile)
defer fcpu.Close()
stopFGProf = stop
}
handleSignals(cpuCloser, stopFGProf)
Run()
}
func startCPUProfile(cpuprofile string) (*os.File, func()) {
cpu, err := os.Create(cpuprofile)
if err != nil {
log.Fatal(err)
}
err = pprof.StartCPUProfile(cpu)
if err != nil {
log.Fatal(err)
}
return cpu, pprof.StopCPUProfile
}
func startFGProfile(fcpuprofile string) (*os.File, func() error) {
fcpu, err := os.Create(fcpuprofile)
if err != nil {
log.Fatal(err)
}
stop := fgprof.Start(fcpu, fgprof.FormatPprof)
return fcpu, stop
}
func handleSignals(cpuCloser func(), stopFGProf func() error) {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT)
go func() {
<-sig
if cpuCloser != nil {
cpuCloser()
}
if stopFGProf != nil {
stopFGProf()
}
os.Exit(0)
}()
}
以下のように定義する
package main
import (
"context"
"log"
"os"
"cloud.google.com/go/profiler"
)
func main() {
ctx := context.Background()
projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
if projectID != "" {
cfg := profiler.Config{
Service: "isuconquest",
ServiceVersion: "1.0.0",
ProjectID: projectID,
}
if err := profiler.Start(cfg); err != nil {
log.Fatalf("failed to start profiler: %v", err)
}
res, err := resource.New(ctx,
resource.WithTelemetrySDK(),
)
if err != nil {
log.Fatalf("resource.New: %v", err)
}
exporter, err := texporter.New(texporter.WithProjectID(projectID))
if err != nil {
log.Fatalf("texporter.New: %v", err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
)
defer tp.Shutdown(ctx)
otel.SetTracerProvider(tp)
log.Printf("Tracing to project %s", projectID)
} else {
log.Printf("GOOGLE_CLOUD_PROJECT not set, not tracing")
}
Run()
}
また、tracingのためEchoの場合以下でレイテンシを記録する
if os.Getenv("GOOGLE_CLOUD_PROJECT") != "" {
e.Use(otelecho.Middleware("isuconquest"))
}
nginx/sites-available/${サービス名}
の以下を書き変える。proxy_passで 127.0.0.1
となっている箇所を、対象のプライベートIPに変更する。
location / {
try_files $uri /index.html;
}
location ~ ^/(api|initialize) {
proxy_set_header Host $host;
proxy_read_timeout 600;
proxy_pass http://127.0.0.1:3000;
}
location /auth/ {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:3001;
}
対象のサーバーのmysql/mysql.conf.d/mysqld.confのbind-address
を無効にする
# bind-address = 127.0.0.1
アプリのsystemdの設定で環境変数を対象のサーバーのIPアドレスに置換える
Environment=ISUCON_DB_HOST=172.31.32.96
Environment=ISUCON_DB_PORT=3306
Environment=ISUCON_DB_USER=isucon
Environment=ISUCON_DB_PASSWORD=isucon
Environment=ISUCON_DB_NAME=isucon
MySQLのisuconユーザーがlocalhost以外からの接続を受け入れているか確認する。%になっていたらOK
mysql -u isucon -p
mysql> SELECT user, host FROM mysql.user;
+------------------+-----------+
| user | host |
+------------------+-----------+
| isucon | % |
| debian-sys-maint | localhost |
| mysql.infoschema | localhost |
| mysql.session | localhost |
| mysql.sys | localhost |
| root | localhost |
+------------------+-----------+
6 rows in set (0.00 sec)
なっていなかったら、以下のコマンドでユーザーと権限を追加する
CREATE USER "isucon"@"%" IDENTIFIED BY "isucon";
GRANT ALL PRIVILEGES ON *.* TO "isucon"@"%";
以下のようにmake targetを書き換える
isuports:
cp cpu.pprof default.pgo || true
go build -o isuports -pgo=auto ./...
サーバーで以下を実行しtblsをインストールし、ドキュメントを出力する。isuportsの部分は対象のDB名なのでアプリの名称によって、適宜変えること。
brew install k1LoW/tap/tbls
source ~/.zshrc
tbls doc mysql://${USER}:${PASS}@localhost:3306/isuports ./dbdoc
その後、ローカルで以下を実行して、ローカルにファイルをコピーして、git管理にする
mkdir -p dbdoc
rsync -az -e ssh ubuntu@isucon-1:/home/isucon/dbdoc/ dbdoc/ --rsync-path="sudo rsync"
また以下でテーブルのメタ情報がわかる
mysql> select table_name, engine, table_rows, avg_row_length from information_schema.tables where table_schema = 'isucon';
nginx.confで以下のように設定
upstream webapp {
server unix:/tmp/webapp.sock;
}
server {
location ~ ^/(api|initialize) {
proxy_set_header Host $host;
proxy_read_timeout 600;
proxy_pass http://webapp;
}
Go言語で以下のように書く
socketFile := "/tmp/webapp.sock"
if _, err := os.Stat(socketFile); err == nil {
os.Remove(socketFile)
}
listener, err := net.Listen("unix", socketFile)
if err != nil {
e.Logger.Fatalf("failed to listen: %v", err)
return
}
err = os.Chmod(socketFile, 0777)
if err != nil {
e.Logger.Fatalf("failed to chmod: %v", err)
return
}
e.Listener = listener
server := new(http.Server)
if err := e.StartServer(server); err != nil {
e.Logger.Fatalf("failed to serve: %v", err)
}
worker_rlimit_nofile
はworker_connections
の4倍程度で設定する
worker_rlimit_nofile 4096;
events {
worker_connections 1024;
}
基本設定
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
types_hash_max_size 2048;
server_tokens off;
}
nginxとupstreamのkeepalive設定。app側も対応必要(Go言語ならデフォルトでOK)
upstream app {
server 192.100.0.1:5000;
keepalive 60;
}
server {
location /api/ {
proxy_set_header Host $host;
proxy_read_timeout 600;
proxy_pass http://app;
# この二つの設定はkeepaliveに必須
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
multi_accept_onにしておく
events {
worker_connections 2304;
multi_accept on;
}
ディスクイメージをメモリー上にバッファする
innodb_buffer_pool_size = 1GB
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
isuconでクラスタ構成を使わない場合disable-log-binを1にする
disable-log-bin = 1
接続数を上げる
max_connections=10000
adminDB.SetMaxOpenConns(50)
MySQLの場合は、etc/systemd/system/mysql.service.d/limits.conf
で以下のように書いて、make deploy-mysql
[Service]
LimitNOFILE=1006500
etc/systemd/sysctl.conf
で以下を書いて、make deploy-sysctl
を実行で全サーバーに適用
net.core.somaxconn=65535
net.ipv4.tcp_max_syn_backlog=65535
net.ipv4.ip_local_port_range=10000 60999
net.ipv4.tcp_tw_reuse=1
net.core.rmem_max=16777216
net.core.wmem_max=16777216
sqlxが対応していないため、ベタにqueryを作成するやり方
func BulkUpsertUserItems(db *sqlx.Tx, userItems []*UserItem) (string, error) {
values := []string{}
for _, item := range userItems {
values = append(values, fmt.Sprintf("(%d, %d, %d, %d, %d, %d, %d)", item.ID, item.UserID, item.ItemID, item.ItemType, item.Amount, item.CreatedAt, item.UpdatedAt))
}
query := fmt.Sprintf("INSERT INTO %s (%s) VALUES %s ON DUPLICATE KEY UPDATE %s",
"user_items",
"id, user_id, item_id, item_type, amount, created_at, updated_at",
strings.Join(values, ", "),
"amount = VALUES(amount), updated_at = VALUES(updated_at)",
)
return query, nil
}