Webアプリの入口を見渡そう
スロークエリログで、データベースの大渋滞はかなり解消できました。次は視点を一歩引いて、nginxの**access log(アクセスログ)**を見ます。
nginxは、ブラウザから来たリクエストを最初に受け取る案内係です。アクセスログには、どのURLが何回呼ばれ、返すまで何秒かかり、何byte送ったかを残せます。データベースを使わない処理も含めて、サービス全体を同じ物差しで比べられます。
今回見る数字
COUNT: そのURLが呼ばれた回数SUM: 全リクエストの待ち時間を足した値AVG: 1回あたりの平均時間P90、P99: 遅いほうから10%、1%付近の時間SUM(BODY): 送ったデータ量の合計
平均だけを見ると、ごく一部の遅いリクエストを見落とします。反対に最大値だけを見ると、たまたま一度だけ遅かったものに振り回されます。回数、合計、平均、遅い側の値を並べて眺めましょう。
Agentへのプロンプト
次はnginxのaccess logを導入し、その情報だけから説明できる改善をしてください。
- 現在読み込まれているnginx設定と既存ログ形式を確認する
- method、URI、status、response bodyのbyte数、request_time、upstream_response_timeを1行JSONで記録する
- nginx -tで設定を検証してからreloadし、実際の1行がJSONとして読めることを確認する
- 集計ツールは既存のものを探し、必要なら公式配布元からchecksumを検証して導入する
- ベンチ前にaccess logを空にする
- UUIDやquery parameterの違いをまとめ、URLの役割ごとに集計する
- COUNT、SUM、AVG、P90、P99、SUM(BODY)を比較する
- まず変更前を計測し、観測と直接つながる変更だけを入れ、もう一度計測する
- status codeとレスポンス内容を壊していないか確認する
- スコアと、変更前後の回数・時間・転送量を記録する
ログだけでは原因を断定できないものは、推測で直さず「pprofで調べる候補」として残してください。次はnginxのaccess logを導入し、その情報だけから説明できる改善をしてください。
- 現在読み込まれているnginx設定と既存ログ形式を確認する
- method、URI、status、response bodyのbyte数、request_time、upstream_response_timeを1行JSONで記録する
- nginx -tで設定を検証してからreloadし、実際の1行がJSONとして読めることを確認する
- 集計ツールは既存のものを探し、必要なら公式配布元からchecksumを検証して導入する
- ベンチ前にaccess logを空にする
- UUIDやquery parameterの違いをまとめ、URLの役割ごとに集計する
- COUNT、SUM、AVG、P90、P99、SUM(BODY)を比較する
- まず変更前を計測し、観測と直接つながる変更だけを入れ、もう一度計測する
- status codeとレスポンス内容を壊していないか確認する
- スコアと、変更前後の回数・時間・転送量を記録する
ログだけでは原因を断定できないものは、推測で直さず「pprofで調べる候補」として残してください。今回のAgentは、アクセスログ集計ツールalpを公式リリースから導入しました。alpは、同じ役目のURLをまとめ、上の数字を表にしてくれます。ISUのUUIDが違うURLをそのまま数えると別々の行になるため、/api/isu/<UUID>/iconのようにまとめる設定も大切です。
入口から見えたもの
最初のアクセスログ計測は18,812でした。合計待ち時間順に並べると、上位はこうなりました。
| URLの役目 | 回数 | 合計 | 平均 | 転送量 |
|---|---|---|---|---|
| 状態を送るPOST | 約70,000 | 1,911秒 | 27ms | 70byte |
| trend | 8,628 | 307秒 | 36ms | 38.8MB |
| ISUのアイコン | 4,341 | 207秒 | 48ms | 111MB |
| JavaScript、CSS、SVGなど | 各約8,700 | 主要ファイルだけで1,100秒超 | 21〜23ms | - |
状態POSTが圧倒的です。ただし、アクセスログから分かるのは「入口から出口まで待った」ことだけ。Goのどの処理が時間を使ったかはまだ分かりません。これはpprofで調べる候補に残しました。
一方、JavaScriptやCSSは、ベンチ中に内容が変わりません。それなのにGoアプリを通してファイルを読んでいます。ここはアクセスログだけでも、仕事を減らせる理由がはっきりしています。
静的ファイルはnginxへ
JavaScript、CSS、SVGとindex.htmlを、nginxから直接返すようにしました。変更前後でファイルのハッシュが同じことも確認しています。さらに、変わらないassetには1時間の有効期限を付けました。
再計測は19,628。assetの待ち時間は集計上0.000秒になりました。面白いのは、状態POSTのコードを変えていないのに平均が27msから11msへ下がったことです。同じサーバーのGoアプリが静的ファイルを返さなくなり、本来のAPIへCPUを使えるようになったためだと考えられます。
304は「中身を送り直さなくてよい」の合図
ETagは、ファイルにつける版番号のようなものです。ブラウザが持っている版と同じなら、サーバーは304 Not Modifiedだけを返せます。今回の/は、約1万回中9,655回が304でした。速いレスポンスを作るより、レスポンスを作らないほうが強い場面もあります。
次は、大きな画像を送らない
再計測したアクセスログでは、ISUのアイコンが4,763回、117MB送られていました。アイコンは登録後に変更するAPIがなく、同じ利用者へ同じ画像を何度も返しています。
そこで、認証が必要な画像であることを保つためprivateなCache-ControlとETagを付けました。サーバー全体で他人と共有するcacheにはしていません。
| 段階 | スコア | アイコン取得 | アイコン転送量 | 合計時間 |
|---|---|---|---|---|
| client cache前 | 19,628 | 4,763回 | 117MB | 118秒 |
| client cache後 | 20,390 | 1,265回 | 30.5MB | 26秒 |
約4分の1まで減り、スコアも伸びました。これでアクセスログから素直に選べる変更は一区切りです。残った最大の山、状態POSTの中身をpprofでのぞきに行きましょう。
WARNING
「読み取りだから全部cache」は危険です。利用者ごとに違うデータや、更新直後に見えなければならないデータもあります。公式マニュアルの整合性条件を確認し、何がいつ変わるかを説明できるものだけをcacheします。