Skip to content

EkeMinusYou/isucon-template

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

最初にやること

SSHの設定

~/.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環境を各サーバーにセットアップ

サーバーでShellとNeovim環境をセットアップ。全てのサーバーで実行する

make setup-shell SETUP_HOST=isucon-1
ssh isucon-1 "sudo -i -u isucon"
sudo passwd isucon
./setup.sh

Goのビルドのセットアップ

Makefileでbuildのターゲットを準備すること

build:
	go build -o isuconquest ./... 

Nginx/MySQL/Webappをローカルにコピーして、Git管理にする

Makefileの以下をアプリ名に書換えてから、makeを実行する

APP_NAME:=isuports
make setup-sysctl
make setup-nginx
make setup-mysql
make setup-webapp

Docker剥がし

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でalp用のログ出力

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/mysql.conf.d/mysqld.cnf で以下のように書く。

slow_query_log		= 1
slow_query_log_file	= /var/log/mysql/mysql-slow.log
long_query_time = 0

Goのprofile

以下のように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)
	}()
}

Google Trace/Profiler を使う

以下のように定義する

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の向き先を変える

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;
  }

別サーバーのDBにアクセスする

対象のサーバーの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"@"%";

pgoを有効にする

以下のようにmake targetを書き換える

isuports:
	cp cpu.pprof default.pgo || true
	go build -o isuports -pgo=auto ./... 

ER図を出力

サーバーで以下を実行し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';

Unix Domain Socket

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)
}

秘伝のタレ

nginx.conf

worker_rlimit_nofileworker_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;
}

mysqld.cnf

ディスクイメージをメモリー上にバッファする

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

bulk upsertのqueryを作成

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
}

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published