ISUCONで学ぶ Webアプリケーションのパフォーマンス向上のコツ...

Post on 15-Jan-2015

30.417 views 1 download

description

 

Transcript of ISUCONで学ぶ Webアプリケーションのパフォーマンス向上のコツ...

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の皆様にあらためて感謝