Upload
masahiro-nagano
View
30.423
Download
1
Embed Size (px)
DESCRIPTION
Citation preview
ISUCONで学ぶ Webアプリケーションのパフォーマンス向上のコツ
実践編 完全版ISUCON夏期講習
2014/8/20Masahiro Nagano
この資料を読む前に以下の記事をお読みください
http://blog.nomadscafe.jp/2014/08/isucon-2014-ami.html
チューニングにあたり@acidlemon さんのblog記事を参考にしています
「ざっくりと #isucon 2013年予選問題の解き方教えます」
http://isucon.net/archives/32976287.html
挑戦してみました
最終スコア
9079
やってみたことを紹介します
初期スコア
1664ruby実装にて
(1) 環境整備
静的コンテンツをReverse Proxy で配信
Reverse Proxy: クライアントからの接続を受け、Applicationサーバに処理を中継する。画像,js,css などの静的コンテンツを返す役割もある
Application Server: ユーザからのリクエストを受けて適切なページを構築・レスポンスを行う
<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>
/etc/httpd/conf.d/isucon.conf
スコア
1664 => 1719
Nginx 化
• オープンソースのWebサーバ。高速に動作し、メモリ使用量がすくないなどの特徴があります
Apache vs. Nginx
worker worker worker
worker worker worker
worker worker worker
リクエスト
コンテキストスイッチが大量発生
リクエスト
worker
1個のプロセスで効率よく通信を処理
$ sudo yum install nginx$ sudo service httpd stop
[program:nginx]directory=/command=/usr/sbin/nginx -c /home/isu-user/isucon/nginx.confautostart = true
command
run.ini
nginx.conf: https://gist.github.com/kazeburo/7b0385cce1b0a4565581
スコア
1719 => 1764
(2) Perl にしますワタシハパールチョットデキル
Perl の起動方法
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
run.iniTCPではなくUNIX domain
socketを使う
プロセスを長生きさせる
プロセスはあげすぎない
TCPの接続は高コスト
ReverseProxy
AppServer
リクエスト毎にthree way handshake
スコア
1764 => 1891
(3) アプリをみよう
“/” “/recent/xxx”
“/memo/xxxx” “/mypage”
“/” “/recent/xxx”
“/memo/xxxx” “/mypage”
DBへの問い合わせが重い
markdown の変換にプロセス起動
DBへの問い合わせが若干重い
(4) 外部プロセス起動
+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) }
webapp/perl/lib/Isucon3/Web.pm
ここがmarkdownコマンドを起動している
“/memo/xxxx”
XS(C)で高速にmarkdownを処理するモジュール
スコア
1891 => 2233
(5) N+1 クエリ
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}, );}
webapp/perl/lib/Isucon3/Web.pm
100回ルーーーープ
“/”
use the join, luke
id user_id id name
memosテーブル usersテーブル
id user_id name
memos JOIN users ON memos.user_id = user.id
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');
webapp/perl/lib/Isucon3/Web.pm
“/”, “/recent”
スコア
2233 => 2398
(6) インデックス
SELECT * FROM memos WHERE is_private=0 ORDER BY created_at DESC LIMIT 100
id is_private
...
0
0
1
0
1
memosテーブル
id is_private
...
0
0
0
ソート
webapp/perl/lib/Isucon3/Web.pm
indexがないと
抽出
CPU負荷高い
indexをつくる
cat <<'EOF' | mysql -u isucon isuconALTER TABLE memos ADD INDEX (is_private,created_at);EOF
init.sh
B-Tree
0 1is_private
created_at
older newer older newer
B-Tree
0 1is_private
created_at
older newer older newer
B-Tree
0 1is_private
created_at
older newer older newer
B-Tree
0 1is_private
created_at
older newer older newer
B-Tree
0 1is_private
created_at
older newer older newer
順に取得するだけ
スコア
2398 => 2668
(7) タイトル事前生成
これ
mysql> show create table memos\G*************************** 1. row *************************** Table: memosCreate 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=utf81 row in set (0.00 sec)
mysql
titleカラムが存在しない!
<: $memo.content.split('\r?\n').first() :>webapp/perl/views/index.tx
splitでCPU使用contentの転送で通信
タイトルは本文から都度生成
cat <<'EOF' | mysql -u isucon isuconALTER TABLE memos ADD COLUMN title text;UPDATE memos SET title = substring_index(content,"\n",1);EOF
init.sh
titleカラムの追加し、事前生成
POST時にも生成$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');
webapp/perl/lib/Isucon3/Web.pm
“/”, “/recent”memos.* だと contentを取ってしまう
スコア
2668 => 3060
(8) OFFSET = 破棄
SELECT * FROM memos ORDER BY created_at LIMIT 100
OFFSET 10000とても大きなOFFSET
”/recent/100”100ページ目
MySQLのOFFSET処理のイメージ
id title user ... . id title user ... . id title user ... . id title user ... .
id title user ... . id title user ... . id title user ... . id title user ... .
id title user ... . id title user ... . id title user ... . id title user ... .
id title user ... .
id title user ... . id title user ... . id title user ... . id title user ... .
id title user ... .
1 2 3 4
5 6 7 8
9 10 11 12
13
10000
10001 10002 10003 10004
MySQLのOFFSET処理のイメージ
id title user ... . id title user ... . id title user ... . id title user ... .
id title user ... . id title user ... . id title user ... . id title user ... .
id title user ... . id title user ... . id title user ... . id title user ... .
id title user ... .
id title user ... . id title user ... . id title user ... . id title user ... .
id title user ... .
1 2 3 4
5 6 7 8
9 10 11 12
13
10000
10001 10002 10003 10004
頑張ってソート
必要な個数まで到達
MySQLのOFFSET処理のイメージ
id title user ... . id title user ... . id title user ... . id title user ... .
id title user ... . id title user ... . id title user ... . id title user ... .
id title user ... . id title user ... . id title user ... . id title user ... .
id title user ... .
id title user ... . id title user ... . id title user ... . id title user ... .
id title user ... .
1 2 3 4
5 6 7 8
9 10 11 12
13
10000
10001 10002 10003 10004
頑張ってソート
必要な個数まで到達
廃棄
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
9999
id id id id id id id id id id id id id ・・・・・
id id
10000
id id id id
10001 10002 10003 10004
・・・・・
MySQLのOFFSET処理のイメージ
1 2 3 4 5 6 7 8 9 10 11 12 13
9999
id id id id id id id id id id id id id ・・・・・
id id
10000
id id id id
10001 10002 10003 10004
廃棄読むデータも、捨てるデータも少ない
“id” だけにすると高速になるもう一つの理由
“Covering Index”
MySQLのインデックスとデータの持ち方
titleuser
....
titleuser
...
titleuser
...
titleuser
...
titleuser
...
titleuser
....
titleuser
....
titleuser
....
PRIMARY KEYCLUSTERED INDEX
リーフノードにデータを含む
small largeid id id id id id id id
MySQLのインデックスとデータの持ち方
SECONDARY KEYprimary keyじゃないkey
リーフノードにPRIMARY KEYが含まれ、データはCLUSTERED INDEX
から取得
id id id id id id id id
is_private
created_atolder newer older newer
SELECT * の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 * の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 * の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 * の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 * の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 * の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 * の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 * の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 * の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 * の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 * の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 * の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 * の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 * の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 * の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
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 の場合
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
titleuser
....
PRIMARY KEY
id id id id id id id id
SECONDARY KEY
id id id id id id id id
is_private
created_at
= “Covering Index”indexだけで探索が終わる
Covering Indexで高速に絞り込んだidの
titleなど、他のデータを取得する方法
SELECT id FROM memos WHERE is_private = 0 ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 100000
クエリ1
(1) IN 句
SELECT * FROM memos WHERE id IN (10000,10001,10002,1003,....) ORDER BY created_at DESC, id DESC
クエリ2 ID羅列
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
クエリ
(2) SELF JOIN
サブクエリーを使用し派生テーブル”t”を作成派生テーブル”t”と
元のテーブルをJOIN
スコア
3060 => 4234よりクエリの少ないSELF JOINを使いました
(9) その他インデックス
cat <<'EOF' | mysql -u isucon isuconALTER TABLE memos ADD INDEX (is_private,created_at), ADD INDEX mypage(user,created_at), ADD INDEX memo_private(user,is_private,created_at)EOF
init.sh
my $memos = $self->dbh->select_all( "SELECT id FROM memos WHERE user=? $cond ORDER BY created_at", $memo->{user},);
webapp/perl/lib/Isucon3/Web.pm
“/memo/xxx”
元は”*”だが、Covering Indexを狙って”id”に変更
スコア
4234 => 5309
(10) OFFSET殲滅データ構造を変更
予めソート済みのmemoのリストがあり、BETWEEN句で
アクセスができればOFFSETで破棄される
データはいなくなり、エコ
id memo1 42 63 8... ...... ...
10525 2127410526 2127710527 21280
... ...10626 2147710627 21480
... ...20627 41345
public_memosテーブル
OFFSET 10000の代わりにBETWEEN 10001 AND 10100
is_private=0 のmemoのidリストolder
newer
memoの個数にもなる!
B-TreeでイメージPRIMARY KEY
older newerid id id id id id id id
memo
memo
memo
memo
memo
memo
memo
memo
BETWEEN 10001 AND 10100
cat <<'EOF' | mysql -u isucon isuconDROP 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
init.sh
* innodb_autoinc_lock_mode の影響でInnoDBではauto increment が連続した値にならない可能性がある
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);
webapp/perl/lib/Isucon3/Web.pm
“/” or “/recent/xxx”
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);}
webapp/perl/lib/Isucon3/Web.pm
post “/memo”
is_private = 0 ならpublic_memosにもinsert
スコア
5309 => 8720
あと、セッション周りのクエリを減らしたりすると
スコア
8720 => 9079
Cache がなくても SQL やインデックスのチューニングでここまで変わる、この問題は面白いなぁと思いました。
出題の@fujiwaraさん、@acidlemonさんをはじめKAYACの皆様にあらためて感謝