言語はチーム3人全員が触れたことのある PHP にした。
時刻 | できごと | 補足 |
---|---|---|
13:09 | Score 3473 | 初期状態のスコアを計測 |
13:38 | DB server に slowlog 設定 | |
14:26 | message.channel_id と image.name に INDEX をはる | |
14:26 | php-fpm を tcp から domain socket に変更 | |
14:50 | Score 4513 | |
15:03 | /message の N+1 問題修正 | |
15:07 | Score 4564 | |
15:27 | /history の N+1 問題修正 | |
15:28 | Score 4687 | |
15:41 | DBに入っているアイコン画像をファイルにキャッシュ | |
16:42 | Score 8208 | |
18:53 | php-fpm のプロセス数調整、不要なforkしないように | |
18:53 | Score 16768 | |
19:26 | カーネルパラメータ調整 | |
19:26 | Score 17673 | |
19:41 | 画像自体を imagemagick で圧縮してみたが、負荷走行前チェックで失敗するのでrevert | |
19:43 | アイコン画像のキャッシュが存在する場合は nginx だけで処理するように | |
19:43 | Score 16111 |
(castaneaiが) やったことの詳細
message.channel_id と image.name に INDEX をはる
CREATE TABLE に INDEX(...)
をつけただけ。少しだけ(+1000ぐらい?)だが、スコアが上がったと思われる。
断定できないのは、ほぼ同時にphp-fpm の tcp -> unix socket も入ったため、どちらが効いたのかはっきりしないため。
--- a/db/isubata.sql +++ b/db/isubata.sql @@ -12,7 +12,7 @@ CREATE TABLE image ( id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, name VARCHAR(191), data LONGBLOB, + INDEX(`name`) ) Engine=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE channel ( @@ -29,7 +29,7 @@ CREATE TABLE message ( user_id BIGINT, content TEXT, created_at DATETIME NOT NULL, + INDEX(channel_id) ) Engine=InnoDB DEFAULT CHARSET=utf8mb4;
N+1 問題修正
ループの中で何回も SELECT
しないようにして、最初に WHERE IN (...)
でまとめて取って連想配列で紐付けるように変更。
@@ -347,13 +342,14 @@ $app->get('/history/{channel_id}', function (Request $request, Response $respons $stmt->execute([$channelId]); $rows = $stmt->fetchall(); + $userIds = getKeys($rows, 'user_id'); + $stmt = whereIn($dbh, 'user', $userIds); + $userMap = fetchMap($stmt); $messages = []; foreach ($rows as $row) { $r = []; $r['id'] = (int)$row['id']; - $stmt = $dbh->prepare("SELECT name, display_name, avatar_icon FROM user WHERE id = ?"); - $stmt->execute([$row['user_id']]); - $r['user'] = $stmt->fetch(); + $r['user'] = $userMap[$row['user_id']]; $r['date'] = str_replace('-', '/', $row['created_at']); $r['content'] = $row['content']; $messages[] = $r;
DBに入っているアイコン画像をファイルにキャッシュ
アイコン画像をファイルにキャッシュして、キャッシュが存在すればDBに触らずにそのままファイルの中身を返す。
--- a/php/index.php +++ b/php/index.php @@ -491,19 +491,31 @@ function ext2mime($ext) $app->get('/icons/{filename}', function (Request $request, Response $response) { $filename = $request->getAttribute('filename'); - $stmt = getPDO()->prepare("SELECT * FROM image WHERE name = ?"); - $stmt->execute([$filename]); - $row = $stmt->fetch(); - $ext = pathinfo($filename, PATHINFO_EXTENSION); - $mime = ext2mime($ext); - - if ($row && $mime) { - $response->write(base64_encode($row['data'])); - $response->withHeader('Content-Transfer-Encoding', 'base64'); - return $response->withHeader('Content-type', $mime); - } - return $response->withStatus(404); + // --- cache + $cacheDir = realpath(dirname(__FILE__)) . '/cache'; + if (!is_writable($cacheDir)) { + chmod($cacheDir, 0777); + } + $cachePath = $cacheDir . '/' . $filename; + if (!file_exists($cachePath)) { + $stmt = getPDO()->prepare("SELECT * FROM image WHERE name = ?"); + $stmt->execute([$filename]); + $row = $stmt->fetch(); + file_put_contents($cachePath, $row['data']); + } + // --- + + $data = file_get_contents($cachePath); + + $ext = pathinfo($filename, PATHINFO_EXTENSION); + $mime = ext2mime($ext); + if ($base64data && $mime) { + $response->write($data); + return $response->withHeader('Content-type', $mime); + } + return $response->withStatus(404); });
アイコン画像のキャッシュが存在する場合は nginx だけで処理するように
エラーは出てないものの、これでスコアはほぼ変わらなかったので、何か間違っていたかもしれない。
- キャッシュの保存先を
/home/isucon/isubata/webapp/public/icons
(nginx document root 配下)に変更 - nginx conf でそこに存在する場合はnginxから返すように
location / {
...
set_proxy_header ...
set_proxy_pass http://127.0.0.1:9000;
}
↓
location / { ... try_files $uri $uri/ @proxy; } location @proxy { set_proxy_header ... set_proxy_pass http://127.0.0.1:9000; }
今回のよかったこと
- デプロイスクリプトをチームメンバーに準備してもらった(シェル叩くだけで、デプロイが完了する)(最高)
無駄に時間がかかってしまった作業
2台あるうち片方がPHP実装になっておらず、1時間以上ベンチ結果を勘違いしていた
動いているphp が ~/local/... 以下にあり、拡張機能のつなぎ込み方がわからなかった
知識不足を感じた項目
- HTTP Cache Control
- nginx の設定
- 既読管理の設計
感想
去年は「正規表現をどうにかしたら良いのだろうな~」って思えたが、今年はそもそもどこがボトルネックなのかわからなかった。 画像系だろうねって話はでてたけど、画像の圧縮は許されていなかったので、では画像のどこ・・?となってうーん・・・わからん!!となって終了した。
後から上位チームのwrite-upを見ると、HTTPのCache-Control周りの話が多く、そのあたりについて何も知識がなかったのでだめだった・・と実感。