概要
題名通りですが、本番環境でローディングが長いと1分30秒以上かかる重めの処理を速度改善する(より早くする)という業務に携わったので、そのときに感じたことや学びになったことをまとめていきます。
※最終的には元々の処理時間の49%分実行速度を早めることに成功しました。
(本番データを入れたローカル環境での実例:47.2秒 -> 23.9秒)
※前提
わざわざGoにリプレイスしなくても、PHPのままでn+1等を解消したりインデックスを適切に貼ればいい可能性もありましたが、今回既に自身が参画させていただいた時点では、PHPからGoに書き換える(リプレイス)ことが確定していたのでGoにリプレイスする前提で書いていきます。
対応した作業の流れ
- 現状どういう問題や課題があるのかを伺う
- 本番環境のとある画面でローディングが1分半かかっていて1年ぐらい先方から苦情がきているとのこと
- 現在の仕様を担当(PM)の方にお伺いし、ソースコードの解析を行う
- ソースコードの確認
- 現行システムのボトルネックを計測
- 処理のうち約80パーセントの時間がn+1問題だったりを抱えるループ処理でかかっており、主にここを解決すればいいことが判明
- 実際に修正対応を行う
- N+1問題の解消(可能な範囲で)
- リファクタリング(不要なSQL発行をなくす)
- PHP=>Goへの書き換え
- 並列処理(ゴルーチン)の実装
結果
Laravel vs Go 総合平均比較
フレームワーク | 測定1 | 測定2 | 測定3 |
---|---|---|---|
Laravel | 41秒 | 40秒 | 49秒 |
Go(並列処理なし) | 34秒 | 32秒 | 41秒 |
Go(並列処理あり) | 20秒 | 16秒 | 27秒 |
差分(Go(並列処理あり) - Laravel) | -21.20秒 | -23.70秒 | -21.89秒 |
学んだこと その1
n+1問題を発生させるのは基本的に御法度
特に初学者の頃はn+1問題なんて発生していても別にしょうがないというか、見た目がシンプルに書けるしこのままでも良くないか?と考えていました。
特にSQLの処理速度が0.02~0.1秒程度の実装で全体的にデータが少ない案件の場合、N+1を防ぎつつもそのままで良いいのでは?と思ってたりしました。
ただ、今回のシステムでは、テーブルのデータが100万件を超えるケースがあり、1回のSQLクエリの実行に約0.5秒(joinを複数しているため)かかっていました。仮に検索条件で2000件程度まで絞り込んだとしても、ループ処理により0.5秒 × 2000回 = 1000秒(約16分40秒)となり、この場合処理時間が大幅に増加してしまいます。
学んだこと その2
Goルーチンで並列数を多くすればするほど速度が速くなると思っていたが勘違いだった。
下記はうる覚えのデータですが、並列処理数と処理実行時間の関係性は確かこんな感じのデータでした。
Worker数(並列処理の数) | 測定 |
---|---|
5個 | 19秒 |
10個 | 19秒 |
20個 | 20秒 |
40個 | 21秒 |
単純に並列処理数10個で20秒なのであれば、40個で 4倍の5秒台を切るのか??と思っていたのですが、結果は 21秒でそんな単純な話ではないことに技術的な関心を覚えました
学んだこと その3 単純にGoの並列処理をするだけでもかなり速度が速くなった
大前提、n+1問題を発生させたり汚いコードを書くのは良くないとは思いますが、単純にゴルーチンで並列処理するだけでもデータからわかる通り、速度が32パーセントも早くなったのには驚きました。
ちなみにゴルーチンの並列処理実装はとても簡単でした。Goのコードを並列処理に書き換えるサンプルコードを貼り付けておきます。
並列処理 前
// 逐次処理で重い処理を実行
results := make([]int, len(data))
for i, val := range data {
results[i] = heavyProcessing(val)
}
並列処理 実装後
// 並列処理で実行
results := make([]int, len(data))
var wg sync.WaitGroup
for i, val := range data {
wg.Add(1)
go func(i, val int) {
defer wg.Done()
results[i] = heavyProcessing(val)
}(i, val)
}
wg.Wait()
終わりに
単純にシステムのメインで利用されている機能を短期間で影響ないようにPHPからGoに書き換えるのだけでも結構大変ではありましたが、ボトルネックの特定だったりゴルーチンを利用したり、Goを利用したりと、普段ではできない経験をさせていただいて大変勉強になりました。