Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 3 years have passed since last update.

ISUCON2021の事前講習に参加してきた

Last updated at Posted at 2021-06-17

はじめに

※公式の記事が投稿される可能性もありますが許可も取れたので備忘録を兼ねて記事を作成します。なお、公式による動画公開はないようです。

座学の方は資料公開されています。

きっかけ

【事前講習開催決定】ISUCON 事前講習2021 座学&ハンズオンを開催しますのような募集を見かけたので応募したら当たったので参加しました。

資材情報

https://github.com/rosylilly/isucon11_prior
https://github.com/isucon/isucon11-prior

実施環境

ドワンゴ社が受講者分のサーバを用意してくれたとのこと。
itamaeのコードを見ると実施環境相当が作れると思われる。

CPU情報

$ cat /proc/cpuinfo 
processor	: 0
vendor_id	: GenuineIntel
cpu family	: 6
model		: 85
model name	: Intel(R) Xeon(R) Gold 6248 CPU @ 2.50GHz
stepping	: 7
microcode	: 0x500002c
cpu MHz		: 2494.140
cache size	: 28160 KB
physical id	: 0
siblings	: 1
core id		: 0
cpu cores	: 1
apicid		: 0
initial apicid	: 0
fpu		: yes
fpu_exception	: yes
cpuid level	: 22
wp		: yes
flags		: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon nopl xtopology tsc_reliable nonstop_tsc cpuid pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch cpuid_fault invpcid_single ssbd ibrs ibpb stibp ibrs_enhanced fsgsbase tsc_adjust bmi1 avx2 smep bmi2 invpcid mpx avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xsaves arat pku ospke md_clear flush_l1d arch_capabilities
bugs		: spectre_v1 spectre_v2 spec_store_bypass swapgs itlb_multihit
bogomips	: 4988.28
clflush size	: 64
cache_alignment	: 64
address sizes	: 43 bits physical, 48 bits virtual
power management:

メモリ情報

$ free -h
              total        used        free      shared  buff/cache   available
Mem:          3.8Gi       644Mi       810Mi       1.0Mi       2.4Gi       2.9Gi
Swap:            0B          0B          0B

ストレージ情報

$ df -h
Filesystem      Size  Used Avail Use% Mounted on
udev            2.0G     0  2.0G   0% /dev
tmpfs           394M  1.1M  393M   1% /run
/dev/sda1        39G  3.7G   35G  10% /
tmpfs           2.0G     0  2.0G   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           2.0G     0  2.0G   0% /sys/fs/cgroup
/dev/loop0       56M   56M     0 100% /snap/core18/2066
/dev/loop1       33M   33M     0 100% /snap/snapd/12057
/dev/loop2       68M   68M     0 100% /snap/lxd/20326
/dev/sda15      105M  7.9M   97M   8% /boot/efi
tmpfs           394M     0  394M   0% /run/user/1000
/dev/loop3       33M   33M     0 100% /snap/snapd/12159
tmpfs           394M     0  394M   0% /run/user/1001

事前講習で実施したこと

ベンチマークを確認

# cd /home/isucon/webapp/tools
./restart-and-bench
:: CLEAR LOGS       ====>

:: RESTART SERVICES ====>

:: BENCHMARK        ====>
17:28:33.794854 score: 473(474 - 1) : pass
17:28:33.794864 deduction: 0 / timeout: 13

:: ACCESS LOG       ====>
+-------+-----+-----+-----+-----+-----+--------+-----------------------------+-------+-------+--------+-------+-------+-------+-------+--------+-----------+-------------+--------------+------------+
| COUNT | 1XX | 2XX | 3XX | 4XX | 5XX | METHOD |             URI             |  MIN  |  MAX  |  SUM   |  AVG  |  P1   |  P50  |  P99  | STDDEV | MIN(BODY) |  MAX(BODY)  |  SUM(BODY)   | AVG(BODY)  |
+-------+-----+-----+-----+-----+-----+--------+-----------------------------+-------+-------+--------+-------+-------+-------+-------+--------+-----------+-------------+--------------+------------+
|   356 |   0 | 345 |   0 |  11 |   0 | GET    | /api/schedules/[0-9a-zA-Z]+ | 0.004 | 1.164 | 50.540 | 0.142 | 0.000 | 0.016 | 0.136 |  0.271 |  6938.000 |   18880.000 |  2682754.000 |   7535.826 |
|     1 |   0 |   1 |   0 |   0 |   0 | POST   | /initialize                 | 0.040 | 0.040 |  0.040 | 0.040 | 0.040 | 0.040 | 0.040 |  0.000 |    19.000 |      19.000 |       19.000 |     19.000 |
|   338 |   0 | 338 |   0 |   0 |   0 | POST   | /api/reservations           | 0.004 | 0.956 |  9.949 | 0.029 | 0.012 | 0.008 | 0.004 |  0.113 |   157.000 |     157.000 |    53066.000 |    157.000 |
|   285 |   0 | 285 |   0 |   0 |   0 | GET    | /api/schedules              | 0.004 | 0.920 |  6.224 | 0.022 | 0.004 | 0.056 | 0.012 |  0.078 |     2.000 |     982.000 |   211155.000 |    740.895 |
|    65 |   0 |  65 |   0 |   0 |   0 | POST   | /api/signup                 | 0.004 | 0.108 |  1.092 | 0.017 | 0.012 | 0.000 | 0.016 |  0.021 |   129.000 |     161.000 |     9267.000 |    142.569 |
|    66 |   0 |  59 |   0 |   7 |   0 | POST   | /api/login                  | 0.004 | 0.156 |  0.736 | 0.011 | 0.004 | 0.004 | 0.004 |  0.024 |    55.000 |     161.000 |     8798.000 |    133.303 |
|     7 |   0 |   7 |   0 |   0 |   0 | POST   | /api/schedules              | 0.004 | 0.016 |  0.076 | 0.011 | 0.016 | 0.008 | 0.004 |  0.004 |   123.000 |     129.000 |      876.000 |    125.143 |
|   351 |   0 | 351 |   0 |   0 |   0 | GET    | /                           | 0.004 | 0.080 |  1.684 | 0.005 | 0.012 | 0.008 | 0.004 |  0.011 |   195.000 |     195.000 |    68445.000 |    195.000 |
|   350 |   0 | 350 |   0 |   0 |   0 | GET    | /favicon.ico                | 0.000 | 0.140 |  1.528 | 0.004 | 0.000 | 0.000 | 0.000 |  0.014 |   195.000 |     195.000 |    68250.000 |    195.000 |
|   350 |   0 |  66 | 284 |   0 |   0 | GET    | /esm/index.js               | 0.000 | 0.000 |  0.000 | 0.000 | 0.000 | 0.000 | 0.000 |  0.000 |     0.000 | 1442198.000 | 95185068.000 | 271957.337 |
+-------+-----+-----+-----+-----+-----+--------+-----------------------------+-------+-------+--------+-------+-------+-------+-------+--------+-----------+-------------+--------------+------------+

netdataを確認

netdataについてはこちら
Ubuntuならaptでインストールできるとのこと。

isucon-002.isucon.youki.io_netdata_(iPad Pro).png

benchmarkの実装を確認

$ cd /home/isucon/webapp/tools
$ cat restart-and-bench
#!/bin/bash

set -e

# nginx のログを削除
echo ":: CLEAR LOGS       ====>"
sudo truncate -s 0 -c /var/log/nginx/access.log

# 各種サービスの再起動
echo
echo ":: RESTART SERVICES ====>"
sudo systemctl restart mysql
sudo systemctl restart web-ruby
sudo systemctl restart nginx

sleep 5

# ベンチマークの実行
echo
echo ":: BENCHMARK        ====>"
/home/isucon/.x /home/isucon/bin/benchmarker

# alp で解析
echo
echo ":: ACCESS LOG       ====>"
sudo cat /var/log/nginx/access.log | alp ltsv -m "/api/schedules/[0-9a-zA-Z]+" --sort avg -r

isucon2021_prior@isucon.netユーザでアプリにログイン確認

isucon-002.isucon.youki.io_login(iPad Pro).png

レギュレーションを確認

$ cat /home/isucon/webapp/doc/MANUAL.md

レギュレーションの内容はこちら

sudoできるか確認

$ sudo echo test

初期状態のgit commit

isucon@isucon-002:~/webapp$ cd ~/webapp/
isucon@isucon-002:~/webapp$ git init
Initialized empty Git repository in /home/isucon/webapp/.git/
isucon@isucon-002:~/webapp$ git add .
isucon@isucon-002:~/webapp$ git commit -m "init"
[master (root-commit) 4fa4f4e] init
 55 files changed, 4560 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 README.md
 create mode 100644 doc/MANUAL.md
 create mode 100644 doc/REGULATION.md
 create mode 100644 frontend/.gitignore
 create mode 100644 frontend/.node-version
 create mode 100644 frontend/package.json
 create mode 100755 frontend/script/build.mjs
 create mode 100644 frontend/script/package.json.cjs
 create mode 100644 frontend/src/app.tsx
 create mode 100644 frontend/src/context.tsx
 create mode 100644 frontend/src/index.html
 create mode 100644 frontend/src/index.tsx
 create mode 100644 frontend/src/loginPage.tsx
 create mode 100644 frontend/src/model.ts
 create mode 100644 frontend/src/rootPage.tsx
 create mode 100644 frontend/src/schedulePage.tsx
 create mode 100644 frontend/src/signupPage.tsx
 create mode 100644 frontend/tsconfig.json
 create mode 100644 frontend/yarn.lock
 create mode 100644 golang/.gitignore
 create mode 100644 golang/.go-version
 create mode 100644 golang/Makefile
 create mode 100644 golang/app.go
 create mode 100644 golang/db.go
 create mode 100644 golang/go.mod
 create mode 100644 golang/go.sum
 create mode 100644 golang/main.go
 create mode 120000 golang/public
 create mode 100644 nodejs/.gitignore
 create mode 100644 nodejs/.node-version
 create mode 100644 perl/.gitignore
 create mode 100644 perl/.perl-version
 create mode 100644 perl/README.md
 create mode 100644 php/.gitignore
 create mode 100644 php/.php-version
 create mode 100644 php/README.md
 create mode 100644 python/.gitignore
 create mode 100644 python/.python-version
 create mode 100644 ruby/.bundle/config
 create mode 100644 ruby/.gitignore
 create mode 100644 ruby/.ruby-version
 create mode 100644 ruby/Gemfile
 create mode 100644 ruby/Gemfile.lock
 create mode 100644 ruby/app.rb
 create mode 100644 ruby/config.ru
 create mode 100644 ruby/config/puma.rb
 create mode 100644 ruby/db.rb
 create mode 120000 ruby/public
 create mode 100644 ruby/tmp/.keep
 create mode 100644 sql/00_setup.sql
 create mode 100644 sql/01_schema.sql
 create mode 100755 tools/initdb
 create mode 100755 tools/restart-and-bench
 create mode 100755 tools/switch-lang
isucon@isucon-002:~/webapp$ 

SQLの中身を確認

isucon@isucon-002:~/webapp/sql$ cat 01_schema.sql
USE `isucon2021_prior`;

DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id`         VARCHAR(255) PRIMARY KEY NOT NULL,
  `email`      VARCHAR(255) NOT NULL DEFAULT '',
  `nickname`   VARCHAR(120) NOT NULL DEFAULT '',
  `staff`      BOOLEAN NOT NULL DEFAULT false,
  `created_at` DATETIME(6) NOT NULL
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;

DROP TABLE IF EXISTS `schedules`;
CREATE TABLE `schedules` (
  `id`         VARCHAR(255) PRIMARY KEY NOT NULL,
  `title`      VARCHAR(255) NOT NULL DEFAULT '',
  `capacity`   INT UNSIGNED NOT NULL DEFAULT 0,
  `created_at` DATETIME(6) NOT NULL
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;

DROP TABLE IF EXISTS `reservations`;
CREATE TABLE `reservations` (
  `id`          VARCHAR(255) PRIMARY KEY NOT NULL,
  `schedule_id` VARCHAR(255) NOT NULL,
  `user_id`     VARCHAR(255) NOT NULL,
  `created_at`  DATETIME(6) NOT NULL
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;

アプリの内容を確認

$ cd /home/isucon/webapp/ruby
$ ls 
$ cat app.rb
require 'sinatra/json'
require 'active_support/json'
require 'active_support/time'
require_relative 'db'

Time.zone = 'UTC'

class App < Sinatra::Base
  enable :logging

  set :session_secret, 'tagomoris'
  set :sessions, key: 'session_isucon2021_prior', expire_after: 3600
  set :show_exceptions, false
  set :public_folder, './public'
  set :json_encoder, ActiveSupport::JSON

  helpers do
    def db
      DB.connection
    end

    def transaction(name = :default, &block)
      DB.transaction(name, &block)
    end

    def generate_id(table, tx)
      id = ULID.generate
      while tx.xquery("SELECT 1 FROM `#{table}` WHERE `id` = ? LIMIT 1", id).first
        id = ULID.generate
      end
      id
    end

    def required_login!
      halt(401, JSON.generate(error: 'login required')) if current_user.nil?
    end

    def required_staff_login!
      halt(401, JSON.generate(error: 'login required')) if current_user.nil? || !current_user[:staff]
    end

    def current_user
      db.xquery('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', session[:user_id]).first
    end

    def get_reservations(schedule)
      reservations = db.xquery('SELECT * FROM `reservations` WHERE `schedule_id` = ?', schedule[:id]).map do |reservation|
        reservation[:user] = get_user(reservation[:user_id])
        reservation
      end
      schedule[:reservations] = reservations
      schedule[:reserved] = reservations.size
    end

    def get_reservations_count(schedule)
      reservations = db.xquery('SELECT * FROM `reservations` WHERE `schedule_id` = ?', schedule[:id])
      schedule[:reserved] = reservations.size
    end

    def get_user(id)
      user = db.xquery('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', id).first
      user[:email] = '' if !current_user || !current_user[:staff]
      user
    end
  end

  error do
    err = env['sinatra.error']
    $stderr.puts err.full_message
    halt 500, JSON.generate(error: err.message)
  end

  post '/initialize' do
    transaction do |tx|
      tx.query('TRUNCATE `reservations`')
      tx.query('TRUNCATE `schedules`')
      tx.query('TRUNCATE `users`')

      id = generate_id('users', tx)
      tx.xquery('INSERT INTO `users` (`id`, `email`, `nickname`, `staff`, `created_at`) VALUES (?, ?, ?, true, NOW(6))', id, 'isucon2021_prior@isucon.net', 'isucon')
    end

    json(language: 'ruby')
  end

  get '/api/session' do
    json(current_user)
  end

  post '/api/signup' do
    id = ''
    nickname = ''

    user = transaction do |tx|
      id = generate_id('users', tx)
      email = params[:email]
      nickname = params[:nickname]
      tx.xquery('INSERT INTO `users` (`id`, `email`, `nickname`, `created_at`) VALUES (?, ?, ?, NOW(6))', id, email, nickname)
      created_at = tx.xquery('SELECT `created_at` FROM `users` WHERE `id` = ? LIMIT 1', id).first[:created_at]

      { id: id, email: email, nickname: nickname, created_at: created_at }
    end

    json(user)
  end

  post '/api/login' do
    email = params[:email]

    user = db.xquery('SELECT `id`, `nickname` FROM `users` WHERE `email` = ? LIMIT 1', email).first

    if user
      session[:user_id] = user[:id]
      json({ id: current_user[:id], email: current_user[:email], nickname: current_user[:nickname], created_at: current_user[:created_at] })
    else
      session[:user_id] = nil
      halt 403, JSON.generate({ error: 'login failed' })
    end
  end

  post '/api/schedules' do
    required_staff_login!

    transaction do |tx|
      id = generate_id('schedules', tx)
      title = params[:title].to_s
      capacity = params[:capacity].to_i

      tx.xquery('INSERT INTO `schedules` (`id`, `title`, `capacity`, `created_at`) VALUES (?, ?, ?, NOW(6))', id, title, capacity)
      created_at = tx.xquery('SELECT `created_at` FROM `schedules` WHERE `id` = ?', id).first[:created_at]

      json({ id: id, title: title, capacity: capacity, created_at: created_at })
    end
  end

  post '/api/reservations' do
    required_login!

    transaction do |tx|
      id = generate_id('reservations', tx)
      schedule_id = params[:schedule_id].to_s
      user_id = current_user[:id]

      halt(403, JSON.generate(error: 'schedule not found')) if tx.xquery('SELECT 1 FROM `schedules` WHERE `id` = ? LIMIT 1 FOR UPDATE', schedule_id).first.nil?
      halt(403, JSON.generate(error: 'user not found')) unless tx.xquery('SELECT 1 FROM `users` WHERE `id` = ? LIMIT 1', user_id).first
      halt(403, JSON.generate(error: 'already taken')) if tx.xquery('SELECT 1 FROM `reservations` WHERE `schedule_id` = ? AND `user_id` = ? LIMIT 1', schedule_id, user_id).first

      capacity = tx.xquery('SELECT `capacity` FROM `schedules` WHERE `id` = ? LIMIT 1', schedule_id).first[:capacity]
      reserved = 0
      tx.xquery('SELECT * FROM `reservations` WHERE `schedule_id` = ?', schedule_id).each do
        reserved += 1
      end

      halt(403, JSON.generate(error: 'capacity is already full')) if reserved >= capacity

      tx.xquery('INSERT INTO `reservations` (`id`, `schedule_id`, `user_id`, `created_at`) VALUES (?, ?, ?, NOW(6))', id, schedule_id, user_id)
      created_at = tx.xquery('SELECT `created_at` FROM `reservations` WHERE `id` = ?', id).first[:created_at]

      json({ id: id, schedule_id: schedule_id, user_id: user_id, created_at: created_at})
    end
  end

  get '/api/schedules' do
    schedules = db.xquery('SELECT * FROM `schedules` ORDER BY `id` DESC');
    schedules.each do |schedule|
      get_reservations_count(schedule)
    end

    json(schedules.to_a)
  end

  get '/api/schedules/:id' do
    id = params[:id]
    schedule = db.xquery('SELECT * FROM `schedules` WHERE id = ? LIMIT 1', id).first
    halt(404, {}) unless schedule

    get_reservations(schedule)

    json(schedule)
  end

  get '*' do
    File.read(File.join('public', 'index.html'))
  end
end

GitHub上にpushのためのプライベートリポジトリを作成

GitHub上でDeploy keysを追加&push

Deploy keysの追加はWeb上で行う。書き込み権を付与する。

$ git remote add origin git@github.com:<account>/<privaterepo>.git
$ git branch -M main
$ git push origin main

リファクタリングの解説を聞きながら写経

リファクタリング→ベンチマークの繰り返し。
リファクタリング内容の模範解答はこちら
ほぼ写経になってしまった。。。

アプリの修正と動作確認

$ sudo systemctl stop web-ruby
$ cd /home/isucon/webapp/ruby
$ MYSQL_QUERY_LOGGER=1 bundle exec rackup

リファクタリング中に良く打つコマンド

CPU リソース: htop
アクセスログ: sudo tail -f /var/log/nginx/access.log
cd /home/isucon/webapp
ベンチ: ./tools/restart-and-bench

ベンチマークの並列数を上げる

$ /home/isucon/.x /home/isucon/bin/benchmarker -parallelism=5

リファクタリング内容の理解その1

すでにメモリに載っている(変数に値がある)場合のSQLクエリ発行を省略している
https://github.com/rosylilly/isucon11_prior/commit/c4209d14d9481da4627082a7f81e945541b9501c

リファクタリング内容の理解その2

join句を活用することで「get_userメソッド」を呼ばなくて済むようになり結果的にSQLクエリ発行を省略している

リファクタリング内容の理解その3

気が向いたら内容理解する...

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?