ISUCONで学ぶ 
Webアプリケーションの 
パフォーマンス向上のコツ 
実践編 完全版 
ISUCON夏期講習 
2014/8/20 
Masahiro Nagano
この資料を読む前に 
以下の記事をお読みください 
http://blog.nomadscafe.jp/2014/08/isucon-2014-ami.html
チューニングにあたり@acidlemon さんの 
blog記事を参考にしています 
「ざっくりと #isucon 2013年予選問題の 
解き方教えます」 
http://isucon.net/archives/32976287.html
挑戦してみました
最終スコア 
9079
やってみたことを 
紹介します
初期スコア 
1664 
ruby実装にて
(1) 環境整備
静的コンテンツを 
Reverse Proxy で配信 
Reverse Proxy: クライアントからの接続を 
受け、Applicationサーバに処理を中継す 
る。画像,js,css などの静的コンテンツを返す 
役割もある 
Application Server: ユーザからのリクエス 
トを受けて適切なページを構築・レスポン 
スを行う
/etc/httpd/conf.d/isucon.conf 
<VirtualHost *:80> 
DocumentRoot /home/isu-user/isucon/webapp/public 
RewriteEngine on 
RewriteCond REQUEST_URI !^/favicon.ico$ 
RewriteCond REQUEST_URI !^/(img|css|js)/ 
RewriteRule /(.*)$ http://localhost:5000/$1 [P] 
</VirtualHost>
スコア 
1664 => 1719
Nginx 化 
• オープンソースのWebサーバ。高速に動 
作し、メモリ使用量がすくないなどの 
特徴があります
Apache vs. Nginx 
リクエスト 
worker worker worker 
worker worker worker 
worker worker worker 
コンテキストスイッチが 
大量発生 
リクエスト 
worker 
1個のプロセスで 
効率よく通信を処理
command 
$ sudo yum install nginx 
$ sudo service httpd stop 
run.ini 
[program:nginx] 
directory=/ 
command=/usr/sbin/nginx -c /home/isu-user/isucon/ 
nginx.conf 
autostart = true 
nginx.conf: https://gist.github.com/kazeburo/7b0385cce1b0a4565581
スコア 
1719 => 1764
(2) Perl にします 
ワタシハパールチョットデキル
Perl の起動方法 
run.ini 
TCPではなくUNIX domain 
socketを使う 
command=/home/../isucon/env.sh carton exec -- 
start_server --path /tmp/app.sock --  
plackup -s Starlet  
--max-workers 4  
プロセスはあげすぎない 
--max-reqs-per-child 50000  
-E production -a app.psgi 
プロセスを長生きさせる
TCPの接続は高コスト 
Reverse 
Proxy 
リクエスト毎に 
three way handshake 
App 
Server
スコア 
1764 => 1891
(3) アプリをみよう
“/” “/recent/xxx” 
“/memo/xxxx” “/mypage”
“/” “/recent/xxx” 
DBへの問い合わせが重い 
“/memo/xxxx” “/mypage” 
markdown の変換に 
プロセス起動 
DBへの問い合わせが 
若干重い
(4) 外部プロセス起動
webapp/perl/lib/Isucon3/Web.pm 
“/memo/xxxx” 
+use Text::Markdown::Hoedown qw//; 
sub markdown { 
my $content = shift; 
- my ($fh, $filename) = tempfile(); 
- $fh->print(encode_utf8($content)); 
- $fh->close; 
- my $html = qx{ ../bin/markdown $filename }; 
- unlink $filename; 
- return $html; 
+ Text::Markdown::Hoedown::markdown($content) 
} 
ここがmarkdownコマンドを 
起動している 
XS(C)で高速にmarkdownを 
処理するモジュール
スコア 
1891 => 2233
(5) N+1 クエリ
webapp/perl/lib/Isucon3/Web.pm 
“/” 
my $memos = $self->dbh->select_all( 
'SELECT * FROM memos WHERE is_private=0 ORDER BY 
created_at DESC, id DESC LIMIT 100' 
); 
for my $memo (@$memos) { 
$memo->{username} = $self->dbh->select_one( 
'SELECT username FROM users WHERE id=?', 
$memo->{user}, 
); 
} 
100回ルーーーープ
use the join, luke
memosテーブルusersテーブル 
id user_id id name 
memos JOIN users ON memos.user_id = user.id 
id user_id name
webapp/perl/lib/Isucon3/Web.pm 
my $memos = $self->dbh->select_all( 
'SELECT memos.*,users.username 
FROM memos JOIN users ON memos.user = users.id 
WHERE memos.is_private=0 
ORDER BY memos.created_at DESC, 
memos.id DESC 
LIMIT 100' 
); 
“/”, “/recent”
スコア 
2233 => 2398
(6) インデックス
indexがないと 
SELECT * FROM memos WHERE is_private=0 ORDER BY 
created_at DESC LIMIT 100 
memosテーブル 
id is_priv 
ate 
... 
0 
0 
1 
0 
1 
id is_priv 
ate 
... 
0 
0 
0 
ソート 
webapp/perl/lib/Isucon3/Web.pm 
抽出 
CPU負荷高い
indexをつくる 
init.sh 
cat <<'EOF' | mysql -u isucon isucon 
ALTER TABLE memos ADD INDEX (is_private,created_at); 
EOF
B-Tree 
is_private 0 1 
created_at 
older newer older newer
B-Tree 
is_private 0 1 
created_at 
older newer older newer
B-Tree 
is_private 0 1 
created_at 
older newer older newer
B-Tree 
is_private 0 1 
created_at 
older newer older newer
B-Tree 
is_private 0 1 
created_at 
older newer older newer 
順に取得するだけ
スコア 
2398 => 2668
(7) タイトル事前生成
これ
mysql 
mysql> show create table memosG 
*************************** 1. row *************************** 
Table: memos 
Create Table: CREATE TABLE `memos` ( 
`id` int(11) NOT NULL AUTO_INCREMENT, 
`user` int(11) NOT NULL, 
`content` text, 
`is_private` tinyint(4) NOT NULL DEFAULT '0', 
`created_at` datetime NOT NULL, 
`updated_at` timestamp NOT NULL DEFAULT, 
PRIMARY KEY (`id`), 
) ENGINE=InnoDB AUTO_INCREMENT=41311 DEFAULT CHARSET=utf8 
1 row in set (0.00 sec) 
titleカラムが存在しない!
タイトルは本文から 
都度生成 
webapp/perl/views/index.tx 
<: $memo.content.split('r?n').first() :> 
contentの転送で通信splitでCPU使用
titleカラムの追加し、 
init.sh 
事前生成 
cat <<'EOF' | mysql -u isucon isucon 
ALTER TABLE memos ADD COLUMN title text; 
UPDATE memos SET 
title = substring_index(content,"n",1); 
EOF
POST時にも生成 
webapp/perl/lib/Isucon3/Web.pm 
$self->dbh->query( 
  'INSERT INTO memos 
(user, title, content, is_private, created_at) 
VALUES (?, ?, ?, ?, now()) 
', 
$user_id, 
(split /r?n/, $content)[0], 
$content, 
$is_private, 
);
webapp/perl/lib/Isucon3/Web.pm 
my $memos = $self->dbh->select_all( 
'SELECT memos.id, memos.title, memos.is_private, 
memos.created_at, users.username 
FROM memos JOIN users ON memos.user = users.id 
WHERE memos.is_private=0 
ORDER BY memos.created_at DESC, 
memos.id DESC 
LIMIT 100' 
); 
“/”, “/recent” 
memos.* だと contentを 
取ってしまう
スコア 
2668 => 3060
(8) OFFSET = 破棄
”/recent/100” 
100ページ目 
SELECT * FROM memos ORDER 
BY created_at LIMIT 100 
OFFSET 10000 
とても大きなOFFSET
MySQLのOFFSET処理のイメージ 
1 2 3 4 
id title user ... . id title user ... . id title user ... . id title user ... . 
5 6 7 8 
id title user ... . id title user ... . id title user ... . id title user ... . 
9 10 11 12 
id title user ... . id title user ... . id title user ... . id title user ... . 
13 
id title user ... . 
10000 
id title user ... . 
10001 10002 10003 10004 
id title user ... . id title user ... . id title user ... . id title user ... .
MySQLのOFFSET処理のイメージ 
頑張ってソート 
1 2 3 4 
id title user ... . id title user ... . id title user ... . id title user ... . 
5 6 7 8 
id title user ... . id title user ... . id title user ... . id title user ... . 
9 10 11 12 
id title user ... . id title user ... . id title user ... . id title user ... . 
13 
id title user ... . 
10000 
id title user ... . 
10001 10002 10003 10004 
id title user ... . id title user ... . id title user ... . id title user ... . 
必要な個数まで到達
MySQLのOFFSET処理のイメージ 
頑張ってソート 
1 2 3 4 
id title user ... . id title user ... . id title user ... . id title user ... . 
5 6 7 8 
id title user ... . id title user ... . id title user ... . id title user ... . 
9 10 11 12 
id title user ... . id title user ... . id title user ... . id title user ... . 
13 
id title user ... . 
10000 
id title user ... . 
10001 10002 10003 10004 
id title user ... . id title user ... . id title user ... . id title user ... . 
必要な個数まで到達 
廃棄
MOTTAINAI
捨てるデータを減らす
取得するデータを制限 
SELECT id FROM memos 
ORDER BY created_at LIMIT 100 
OFFSET 10000
MySQLのOFFSET処理のイメージ 
1 2 3 4 5 6 7 8 9 10 11 12 13 
id id id id id id id id id id id id id ・・・・・ 
・・・・・ 
9999 
10000 
id id 
10001 10002 10003 10004 
id id id id
MySQLのOFFSET処理のイメージ 
1 2 3 4 5 6 7 8 9 10 11 12 13 
id id id id id id id id id id id id id ・・・・・ 
10001 10002 10003 10004 廃棄 
・・・・・ 
9999 
10000 
id id 
id id id id 
読むデータも、捨て 
るデータも少ない
“id” だけにすると 
高速になるもう一つの理由 
“Covering Index”
MySQLのインデックスと 
データの持ち方 
title user ... title user ... 
user ... PRIMARY title user ... 
title user ... 
title user ... 
title user ... title user ... title KEY 
CLUSTERED INDEX 
リーフノードに 
データを含む 
small large 
id 
id 
id 
id 
id 
id 
id 
id
MySQLのインデックスと 
データの持ち方 
SECONDARY KEY 
primary keyじゃないkey 
リーフノードに 
PRIMARY KEYが含まれ、 
データはCLUSTERED INDEX 
から取得 
is_private 
id id id id id id id id 
created_at 
older newer older newer
SELECT * の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT * の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT * の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT * の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT * の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT * の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT * の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT * の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT * の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT * の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT * の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT * の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT * の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT * の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT * の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at 
何度も繰り返す
SELECT id の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT id の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT id の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT id の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT id の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT id の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT id の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT id の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at
SELECT id の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at 
indexだけで 
探索が終わる
SELECT id の場合 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
title user ... . 
PRIMARY KEY 
id 
id 
id 
id 
id 
id 
id 
id 
SECONDARY KEY 
id id id id id id id id 
is_private 
created_at 
indexだけで= “Covering Index” 
探索が終わる
Covering Indexで高速に 
絞り込んだidの 
titleなど、他のデータを 
取得する方法
(1) IN 句 
クエリ1 
SELECT id FROM memos 
WHERE is_private = 0 
ORDER BY created_at DESC, id DESC 
LIMIT 100 OFFSET 100000 
クエリ2 ID羅列 
SELECT * FROM memos 
WHERE id IN (10000,10001,10002,1003,....) 
ORDER BY created_at DESC, id DESC
(2) SELF JOIN 
クエリ 
SELECT memos.id, memos.title, memos.is_private, 
memos.created_at, users.username 
FROM memos, users, 
(SELECT id FROM memos WHERE is_private = 0 ORDER 
BY created_at DESC, id DESC LIMIT 100) AS t 
WHERE t.id = memos.id 
AND users.id = memos.user 
サブクエリーを使用し 
派生テーブル”t”と派生テーブル”t”を作成 
元のテーブルをJOIN
スコア 
3060 => 4234 
よりクエリの少ないSELF JOINを使いました
(9) その他インデックス
init.sh 
cat <<'EOF' | mysql -u isucon isucon 
ALTER TABLE memos ADD INDEX (is_private,created_at), 
ADD INDEX mypage(user,created_at), 
ADD INDEX memo_private(user,is_private,created_at) 
EOF
webapp/perl/lib/Isucon3/Web.pm 
my $memos = $self->dbh->select_all( 
"SELECT id FROM memos WHERE user=? $cond ORDER 
BY created_at", 
$memo->{user}, 
); 
“/memo/xxx” 
元は”*”だが、Covering Indexを 
狙って”id”に変更
スコア 
4234 => 5309
(10) OFFSET殲滅 
データ構造を変更
予めソート済みのmemoの 
リストがあり、BETWEEN句で 
アクセスができれば 
OFFSETで破棄される 
データはいなくなり、エコ
public_memos 
テーブル 
id memo 
1 4 
2 6 
3 8 
... ... 
... ... 
10525 21274 
10526 21277 
10527 21280 
... ... 
10626 21477 
10627 21480 
... ... 
20627 41345 
is_private=0 のmemoの 
idリストolder 
OFFSET 10000の代わりに 
BETWEEN 10001 AND 10100 
memoの 
個数にもなる! 
newer
B-Treeでイメージ 
PRIMARY KEY 
older newer 
id 
id 
id 
id 
id 
id 
id 
id 
memo 
memo 
memo 
memo 
memo 
memo 
memo 
memo 
BETWEEN 10001 AND 10100
init.sh 
cat <<'EOF' | mysql -u isucon isucon 
DROP TABLE IF EXISTS public_memos; 
CREATE TABLE public_memos ( 
id INT NOT NULL AUTO_INCREMENT, 
memo int DEFAULT NULL, 
PRIMARY KEY (id) 
) ENGINE=MyISAM DEFAULT CHARSET=utf8; 
INSERT INTO public_memos (memo) SELECT id FROM memos 
WHERE is_private=0 ORDER BY created_at ASC, id ASC; 
EOF 
* innodb_autoinc_lock_mode の影響で 
InnoDBではauto increment が連続した値にならない可能性がある
webapp/perl/lib/Isucon3/Web.pm 
my $total = $self->dbh->select_one( 
'SELECT MAX(id) FROM public_memos' 
); 
my $memos = $self->dbh->select_all( 
'SELECT memos.id, memos.title, memos.is_private, 
memos.created_at, users.username 
FROM memos,users, 
(SELECT memo FROM public_memos WHERE id 
BETWEEN ? AND ? ORDER BY id DESC) AS t 
WHERE t.memo = memos.id 
AND users.id=memos.user', 
$total-99, $total 
); 
“/” or “/recent/xxx”
post “/memo” 
webapp/perl/lib/Isucon3/Web.pm 
my $memo_id = $self->dbh->last_insert_id; 
if ( ! scalar($c->req->param('is_private')) ) { 
$self->dbh->query('INSERT INTO public_memos 
(memo) VALUES (?)',$memo_id); 
} 
is_private = 0 なら 
public_memosにもinsert
スコア 
5309 => 8720
あと、セッション周りの 
クエリを減らしたりすると
スコア 
8720 => 9079
Cache がなくても SQL や 
インデックスのチューニングで 
ここまで変わる、この問題は 
面白いなぁと思いました。 
出題の@fujiwaraさん、@acidlemonさん 
をはじめKAYACの皆様にあらためて感謝

ISUCONで学ぶ Webアプリケーションのパフォーマンス向上のコツ 実践編 完全版

  • 1.
    ISUCONで学ぶ Webアプリケーションの パフォーマンス向上のコツ 実践編 完全版 ISUCON夏期講習 2014/8/20 Masahiro Nagano
  • 2.
  • 3.
    チューニングにあたり@acidlemon さんの blog記事を参考にしています 「ざっくりと #isucon 2013年予選問題の 解き方教えます」 http://isucon.net/archives/32976287.html
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
    静的コンテンツを Reverse Proxyで配信 Reverse Proxy: クライアントからの接続を 受け、Applicationサーバに処理を中継す る。画像,js,css などの静的コンテンツを返す 役割もある Application Server: ユーザからのリクエス トを受けて適切なページを構築・レスポン スを行う
  • 10.
    /etc/httpd/conf.d/isucon.conf <VirtualHost *:80> DocumentRoot /home/isu-user/isucon/webapp/public RewriteEngine on RewriteCond REQUEST_URI !^/favicon.ico$ RewriteCond REQUEST_URI !^/(img|css|js)/ RewriteRule /(.*)$ http://localhost:5000/$1 [P] </VirtualHost>
  • 11.
  • 12.
    Nginx 化 •オープンソースのWebサーバ。高速に動 作し、メモリ使用量がすくないなどの 特徴があります
  • 13.
    Apache vs. Nginx リクエスト worker worker worker worker worker worker worker worker worker コンテキストスイッチが 大量発生 リクエスト worker 1個のプロセスで 効率よく通信を処理
  • 14.
    command $ sudoyum install nginx $ sudo service httpd stop run.ini [program:nginx] directory=/ command=/usr/sbin/nginx -c /home/isu-user/isucon/ nginx.conf autostart = true nginx.conf: https://gist.github.com/kazeburo/7b0385cce1b0a4565581
  • 15.
  • 16.
    (2) Perl にします ワタシハパールチョットデキル
  • 17.
    Perl の起動方法 run.ini TCPではなくUNIX domain socketを使う command=/home/../isucon/env.sh carton exec -- start_server --path /tmp/app.sock -- plackup -s Starlet --max-workers 4 プロセスはあげすぎない --max-reqs-per-child 50000 -E production -a app.psgi プロセスを長生きさせる
  • 18.
    TCPの接続は高コスト Reverse Proxy リクエスト毎に three way handshake App Server
  • 19.
  • 20.
  • 21.
  • 22.
    “/” “/recent/xxx” DBへの問い合わせが重い “/memo/xxxx” “/mypage” markdown の変換に プロセス起動 DBへの問い合わせが 若干重い
  • 23.
  • 24.
    webapp/perl/lib/Isucon3/Web.pm “/memo/xxxx” +useText::Markdown::Hoedown qw//; sub markdown { my $content = shift; - my ($fh, $filename) = tempfile(); - $fh->print(encode_utf8($content)); - $fh->close; - my $html = qx{ ../bin/markdown $filename }; - unlink $filename; - return $html; + Text::Markdown::Hoedown::markdown($content) } ここがmarkdownコマンドを 起動している XS(C)で高速にmarkdownを 処理するモジュール
  • 25.
  • 26.
  • 27.
    webapp/perl/lib/Isucon3/Web.pm “/” my$memos = $self->dbh->select_all( 'SELECT * FROM memos WHERE is_private=0 ORDER BY created_at DESC, id DESC LIMIT 100' ); for my $memo (@$memos) { $memo->{username} = $self->dbh->select_one( 'SELECT username FROM users WHERE id=?', $memo->{user}, ); } 100回ルーーーープ
  • 28.
  • 29.
    memosテーブルusersテーブル id user_idid name memos JOIN users ON memos.user_id = user.id id user_id name
  • 30.
    webapp/perl/lib/Isucon3/Web.pm my $memos= $self->dbh->select_all( 'SELECT memos.*,users.username FROM memos JOIN users ON memos.user = users.id WHERE memos.is_private=0 ORDER BY memos.created_at DESC, memos.id DESC LIMIT 100' ); “/”, “/recent”
  • 31.
  • 32.
  • 33.
    indexがないと SELECT *FROM memos WHERE is_private=0 ORDER BY created_at DESC LIMIT 100 memosテーブル id is_priv ate ... 0 0 1 0 1 id is_priv ate ... 0 0 0 ソート webapp/perl/lib/Isucon3/Web.pm 抽出 CPU負荷高い
  • 34.
    indexをつくる init.sh cat<<'EOF' | mysql -u isucon isucon ALTER TABLE memos ADD INDEX (is_private,created_at); EOF
  • 35.
    B-Tree is_private 01 created_at older newer older newer
  • 36.
    B-Tree is_private 01 created_at older newer older newer
  • 37.
    B-Tree is_private 01 created_at older newer older newer
  • 38.
    B-Tree is_private 01 created_at older newer older newer
  • 39.
    B-Tree is_private 01 created_at older newer older newer 順に取得するだけ
  • 40.
  • 41.
  • 42.
  • 43.
    mysql mysql> showcreate table memosG *************************** 1. row *************************** Table: memos Create Table: CREATE TABLE `memos` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user` int(11) NOT NULL, `content` text, `is_private` tinyint(4) NOT NULL DEFAULT '0', `created_at` datetime NOT NULL, `updated_at` timestamp NOT NULL DEFAULT, PRIMARY KEY (`id`), ) ENGINE=InnoDB AUTO_INCREMENT=41311 DEFAULT CHARSET=utf8 1 row in set (0.00 sec) titleカラムが存在しない!
  • 44.
    タイトルは本文から 都度生成 webapp/perl/views/index.tx <: $memo.content.split('r?n').first() :> contentの転送で通信splitでCPU使用
  • 45.
    titleカラムの追加し、 init.sh 事前生成 cat <<'EOF' | mysql -u isucon isucon ALTER TABLE memos ADD COLUMN title text; UPDATE memos SET title = substring_index(content,"n",1); EOF
  • 46.
    POST時にも生成 webapp/perl/lib/Isucon3/Web.pm $self->dbh->query(   'INSERT INTO memos (user, title, content, is_private, created_at) VALUES (?, ?, ?, ?, now()) ', $user_id, (split /r?n/, $content)[0], $content, $is_private, );
  • 47.
    webapp/perl/lib/Isucon3/Web.pm my $memos= $self->dbh->select_all( 'SELECT memos.id, memos.title, memos.is_private, memos.created_at, users.username FROM memos JOIN users ON memos.user = users.id WHERE memos.is_private=0 ORDER BY memos.created_at DESC, memos.id DESC LIMIT 100' ); “/”, “/recent” memos.* だと contentを 取ってしまう
  • 48.
  • 49.
  • 50.
    ”/recent/100” 100ページ目 SELECT* FROM memos ORDER BY created_at LIMIT 100 OFFSET 10000 とても大きなOFFSET
  • 51.
    MySQLのOFFSET処理のイメージ 1 23 4 id title user ... . id title user ... . id title user ... . id title user ... . 5 6 7 8 id title user ... . id title user ... . id title user ... . id title user ... . 9 10 11 12 id title user ... . id title user ... . id title user ... . id title user ... . 13 id title user ... . 10000 id title user ... . 10001 10002 10003 10004 id title user ... . id title user ... . id title user ... . id title user ... .
  • 52.
    MySQLのOFFSET処理のイメージ 頑張ってソート 12 3 4 id title user ... . id title user ... . id title user ... . id title user ... . 5 6 7 8 id title user ... . id title user ... . id title user ... . id title user ... . 9 10 11 12 id title user ... . id title user ... . id title user ... . id title user ... . 13 id title user ... . 10000 id title user ... . 10001 10002 10003 10004 id title user ... . id title user ... . id title user ... . id title user ... . 必要な個数まで到達
  • 53.
    MySQLのOFFSET処理のイメージ 頑張ってソート 12 3 4 id title user ... . id title user ... . id title user ... . id title user ... . 5 6 7 8 id title user ... . id title user ... . id title user ... . id title user ... . 9 10 11 12 id title user ... . id title user ... . id title user ... . id title user ... . 13 id title user ... . 10000 id title user ... . 10001 10002 10003 10004 id title user ... . id title user ... . id title user ... . id title user ... . 必要な個数まで到達 廃棄
  • 54.
  • 55.
  • 56.
    取得するデータを制限 SELECT idFROM memos ORDER BY created_at LIMIT 100 OFFSET 10000
  • 57.
    MySQLのOFFSET処理のイメージ 1 23 4 5 6 7 8 9 10 11 12 13 id id id id id id id id id id id id id ・・・・・ ・・・・・ 9999 10000 id id 10001 10002 10003 10004 id id id id
  • 58.
    MySQLのOFFSET処理のイメージ 1 23 4 5 6 7 8 9 10 11 12 13 id id id id id id id id id id id id id ・・・・・ 10001 10002 10003 10004 廃棄 ・・・・・ 9999 10000 id id id id id id 読むデータも、捨て るデータも少ない
  • 59.
  • 60.
    MySQLのインデックスと データの持ち方 titleuser ... title user ... user ... PRIMARY title user ... title user ... title user ... title user ... title user ... title KEY CLUSTERED INDEX リーフノードに データを含む small large id id id id id id id id
  • 61.
    MySQLのインデックスと データの持ち方 SECONDARYKEY primary keyじゃないkey リーフノードに PRIMARY KEYが含まれ、 データはCLUSTERED INDEX から取得 is_private id id id id id id id id created_at older newer older newer
  • 62.
    SELECT * の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 63.
    SELECT * の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 64.
    SELECT * の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 65.
    SELECT * の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 66.
    SELECT * の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 67.
    SELECT * の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 68.
    SELECT * の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 69.
    SELECT * の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 70.
    SELECT * の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 71.
    SELECT * の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 72.
    SELECT * の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 73.
    SELECT * の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 74.
    SELECT * の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 75.
    SELECT * の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 76.
    SELECT * の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at 何度も繰り返す
  • 77.
    SELECT id の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 78.
    SELECT id の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 79.
    SELECT id の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 80.
    SELECT id の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 81.
    SELECT id の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 82.
    SELECT id の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 83.
    SELECT id の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 84.
    SELECT id の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at
  • 85.
    SELECT id の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at indexだけで 探索が終わる
  • 86.
    SELECT id の場合 title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . title user ... . PRIMARY KEY id id id id id id id id SECONDARY KEY id id id id id id id id is_private created_at indexだけで= “Covering Index” 探索が終わる
  • 87.
    Covering Indexで高速に 絞り込んだidの titleなど、他のデータを 取得する方法
  • 88.
    (1) IN 句 クエリ1 SELECT id FROM memos WHERE is_private = 0 ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 100000 クエリ2 ID羅列 SELECT * FROM memos WHERE id IN (10000,10001,10002,1003,....) ORDER BY created_at DESC, id DESC
  • 89.
    (2) SELF JOIN クエリ SELECT memos.id, memos.title, memos.is_private, memos.created_at, users.username FROM memos, users, (SELECT id FROM memos WHERE is_private = 0 ORDER BY created_at DESC, id DESC LIMIT 100) AS t WHERE t.id = memos.id AND users.id = memos.user サブクエリーを使用し 派生テーブル”t”と派生テーブル”t”を作成 元のテーブルをJOIN
  • 90.
    スコア 3060 =>4234 よりクエリの少ないSELF JOINを使いました
  • 91.
  • 92.
    init.sh cat <<'EOF'| mysql -u isucon isucon ALTER TABLE memos ADD INDEX (is_private,created_at), ADD INDEX mypage(user,created_at), ADD INDEX memo_private(user,is_private,created_at) EOF
  • 93.
    webapp/perl/lib/Isucon3/Web.pm my $memos= $self->dbh->select_all( "SELECT id FROM memos WHERE user=? $cond ORDER BY created_at", $memo->{user}, ); “/memo/xxx” 元は”*”だが、Covering Indexを 狙って”id”に変更
  • 94.
  • 95.
  • 96.
    予めソート済みのmemoの リストがあり、BETWEEN句で アクセスができれば OFFSETで破棄される データはいなくなり、エコ
  • 97.
    public_memos テーブル idmemo 1 4 2 6 3 8 ... ... ... ... 10525 21274 10526 21277 10527 21280 ... ... 10626 21477 10627 21480 ... ... 20627 41345 is_private=0 のmemoの idリストolder OFFSET 10000の代わりに BETWEEN 10001 AND 10100 memoの 個数にもなる! newer
  • 98.
    B-Treeでイメージ PRIMARY KEY older newer id id id id id id id id memo memo memo memo memo memo memo memo BETWEEN 10001 AND 10100
  • 99.
    init.sh cat <<'EOF'| mysql -u isucon isucon DROP TABLE IF EXISTS public_memos; CREATE TABLE public_memos ( id INT NOT NULL AUTO_INCREMENT, memo int DEFAULT NULL, PRIMARY KEY (id) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; INSERT INTO public_memos (memo) SELECT id FROM memos WHERE is_private=0 ORDER BY created_at ASC, id ASC; EOF * innodb_autoinc_lock_mode の影響で InnoDBではauto increment が連続した値にならない可能性がある
  • 100.
    webapp/perl/lib/Isucon3/Web.pm my $total= $self->dbh->select_one( 'SELECT MAX(id) FROM public_memos' ); my $memos = $self->dbh->select_all( 'SELECT memos.id, memos.title, memos.is_private, memos.created_at, users.username FROM memos,users, (SELECT memo FROM public_memos WHERE id BETWEEN ? AND ? ORDER BY id DESC) AS t WHERE t.memo = memos.id AND users.id=memos.user', $total-99, $total ); “/” or “/recent/xxx”
  • 101.
    post “/memo” webapp/perl/lib/Isucon3/Web.pm my $memo_id = $self->dbh->last_insert_id; if ( ! scalar($c->req->param('is_private')) ) { $self->dbh->query('INSERT INTO public_memos (memo) VALUES (?)',$memo_id); } is_private = 0 なら public_memosにもinsert
  • 102.
  • 103.
  • 104.
  • 105.
    Cache がなくても SQLや インデックスのチューニングで ここまで変わる、この問題は 面白いなぁと思いました。 出題の@fujiwaraさん、@acidlemonさん をはじめKAYACの皆様にあらためて感謝