概要
この記事は、Azure上でサーバ側typescriptで、
Express+TypeORMを使って業務アプリ構築する時の
パフォーマンス向上系の試したことのまとめとなります。
フロントエンドはVueとかVuetifyも使っているのは前提です。
特にこの技術スタックを使ってなくても同じ問題を面することもあるので
参考になれば幸いです。
経験があるメンバーから見ると、どれも基礎の基礎かもしれませんが、
日々のコーディングやレビューするときにやっぱり忘れがちになるので、
自分の振り返りにもなります。
背景
製品のMVPを限られた時間内で全力集中して一気に作ってから、
パフォーマンスを改善する、という無茶なことをやりました。
実はギリギリ大量ユーザ、ヘビーユーザが使い始める前にやってかなり危ない話で、
もう一度やろうとしたら十分の予算をあらかじめとって、
作る途中でもパフォーマンスの品質を作り込みたいです。
課題、対策一覧
TypeORMのsave
saveはそれぞれのものに対してselectしてinsert/updateしていますよ!
TyepORMの便利は機能であるsaveですが、大きな落とし穴でもあります。
よく考えないてあっちこっちなんでも使うと、色々やられます。
それは、下記のような書き方で複数(N)レコードに対してsaveするときに、
N回selectとN回insert/updateを発行しているのです。
つまり2N回queryが発行されています。
await entityManager.getRepository(SomeEntity).save(someObjects);
- insertでやるパターン:someObjectsはDBにないことがわかったのであれば、saveではなくinsertで一回queryできます。
- updateで一括やるパターン:あるいはsomeObjectsは全部DBにある、更新するものも同じのであれば、updateで一回queryできます。
- 別々updateでやるパターン:あるいはsomeObjectsはDBにあるが、更新するものはそれぞれバラバラの場合、updateをそれぞれ発行しても合計N回で済むので、2Nにはならないはずです。
- insertとupdate両方あるパターン:someObjectsはDBにあるのとないのと両方ある場合は、まずselectしてidとかで比較して、ないものはsave、あるならupdateとすることによって、合計のquery回数が2Nにならないこともできます。
もちろ、単数のobjectやsomeObjectsの数は少ないとか、パフォーマンスはそんなに考慮しなくて済む場合もありますので、saveは使用禁止までにはならないと思いますが、大量データの場合注意しましょう。
TyepORMのrelations
[eager loading]と[with expression(relations)]はjoinしているから大きくなりすぎる可能性がありますよ!
one to manyのリレーションのSomeEntityとanotherEntityがあるとします。
そして'one'の方の1個のレコードが巨大になる可能性があるとします。
そしてmanyの方の数はNとします。
例えばtextフィールドとかjsonフィールドとかがあると、
MB単位になる可能性もあります。
すると下記のコードでやると、
いきなりN倍のメモリが使う巨大なqueryの結果が戻ってくるになって、
データ伝送に時間がかかるとか、
下手するとOOMが起きるかもしれません。
もっと怖いのは、さらにNのmanyとMのもう一つのentity結合しようとすると、
もうMxN倍のデータ量になるので。。。
await appDataSource.getRepository(SomeEntity).findOneOrFail({
where: { id: someId },
relations: ['anotherEntity', 'anotherEntity.subEntity'] }))
あらかじめ上記ケースであることをわかっている場合、
SomeEntityをselectしてから、
foreign keyを辿ってanotherEntityの複数レコードをselectという、
わざと2回query発行することにしましょう。
DBから必要のないフィールド
必要のないフィールド取ってくると時間とメモリがかかるよ!
当たり前ですが、下記の様な書き方がよくて、
逆にselectを書かない状態で、実際に使うfieldAとfieldB以外に、
fieldCという非常の大きなものがあると、
それを無駄にDBから出していないかをチェックしないと、
かなりパフォーマンス低下になります。
await appDataSource
.getRepository(SomeEntity)
.find({
select: {
id: true,
fieldA: true,
fieldB: true,
},
where: { someKey: { key }},
});
listとgetの役割
listとgetの役割をきちんと決めないとlistが重くなることがありますよ!
よくあるパターンとして、
フロントエンドに一回全部のリソースの概要を送ってから、
一つあるいは幾つかそのうちのものに対して細かく処理したり加工して複雑なものをもらうという。
なのでリソースをリストするときには余計な加工とかしているかどうかをチェックして、
シンプルにすることでパフォーマンス向上を図ることができます。
例
画像のリストを取得するときに、それぞれ画像のSASを組む必要があるのでしょうか。
例えばファイル名の一覧を出す場合とか、だといらないですね。
AzureのSASのは大量にやると意外と時間がかかる話もありまして。
大きな画像のプリロード
プリロードがあるとサクサクになりますよ!
infinit scrollの様な発想で、
今のスクロールで表示しているものと、
もうちょっと上下に表示しそうなものだけブラウザにロードすることで、
一気にロードすることなく、ブラウザのOOMを防ぐことができます。
そして数行のプリロードもあるので、
すぐ表示されずにストレスを感じることもありません。
スクロールバーが画面に下にいくことによって、
ロードされた画像の数が増えると、
上部から徐々にロードした写真のメモリを解放していく動きもあるので、
ブラウザ側のOOMも防ぐことに役立ちます。
また、大きな画像(10mb以上だとか)を1枚単位で次へ次へと表示するときに、
次に見る可能性のある範囲内の写真をプリロードの仕組みを実装してあげるとUX的には一気にサクサク感が出ます。
画像のSASが変わる
大きな画像のSASが変わると再度時間かかってloadされるケースがありますよ!
画像に対する操作をしたが、画像自身が変わらない場合、
サーバ側再度SASを組んで送ってきてフロントエンドのSASが更新すると、
画像の再読み込みが発生するので無駄が大きくなる場合があります。
要するには無駄な処理を省きましょう的なことです。
画像が変わってなければ新しいSASを組んで送るのをやめたり、
来ても使うURIを更新しないなどの工夫をするということになります。
サーバ側のOOM
OOMのトラブルシューティングは大変ですよ!
サーバ側のexpressでOOMになると、スタックログがないので、
どのソースコードのどの辺で失敗したかはわからないというのはよくあるので、
いまだに良い解決方法がなくて、
負荷テストでOOMが発生するたびに疑わしい箇所に
logをいっぱい入れて発生箇所特定したりメモリ状況も出してみるとか。
この辺スタックログ出せる様なツールやミドルウェアがあれば欲しいですね。
streamingの考え方
考え方を活用できれば色々良いことがありますよ!
いつもの話ですけど、OBJを丸ごとフォーマット変換してアップロードすると、
下手するとOBJの2倍のメモリを使ってしまう可能性もあるので、
よくやるのはOBJをファイルに入れてからstreamingするとか。
同じ思想で、大きなファイルを一気に読み込みするのではなく、
skiprowsとかchunksizeとかで分けて少しずつ処理していくとかという考え方があります。
サーバ側やフロント側のjsだと、ネットワークを一気にPromise.allで大量に使うと、
ブロッキングになって他の操作や処理ができなくなるケースがあるので、
複数のバッチを分けてループしていくことで、
途中で他の処理もできる様にするとかも
目的は違いますけど、似た様な考えですね。
重い処理
重い処理やバッチであるべきものは頑張って別サービスやAPIサーバと別にするのは頑張る価値が大きいですよ!
レポート作成や画像の大量処理など重い処理は
作るときのスケジュールとかインフラの難易度、アーキテクチャの難易度があって、
非同期などになるとユーザとのやり取りや通知なども発生するので、
ステー区ホルダとの調整で面倒臭くなりがちです。
ですが、切り出しておかないと、処理が重い分、
メモリの消耗が激しいとか、ネットワークいっぱい使うとか、
cpu貼り付けるのとで同居している他のサービスに影響が出るとか、
色々課題点が出てきます。
急いでAPIサーバにバッチを作っても、
OOMが発生してAPIサーバのメモリを増やす羽目になったり、
APIサーバのcpuリソースが基本低く限定されているのでバッチが遅くてtimeoutになったり、
あるいはインテンシブ的なCPU利用が発生して他の処理をブロックしてしまうとか、
悪いこといっぱいありますので、
ここはやっぱり手を抜かずにきちんと別サービスにしましょう。
まとめ
々のコーディングやレビューするときにやっぱり忘れがちになります。
スピード勝負の中、
いちいちロジックを最適化するのはある程度無駄があるが、
(なんでも綺麗に仕上げる、とことん頑張るのは無駄があると言いたい、)
少なくとも大量処理だけでも注目して、適切なオプティマイズが必要です。