Vuls開発中に出会ったGORMあれこれ
はじめまして、中岡(@MaineK00n)と申します。
2021年6月の現在はフューチャー発OSSのVuls開発をメインに、CSIG1でアルバイトをしています。
TL;DR
- O/Rマッパ(Object-relational mapping)を触るときは発行されるクエリが意図したものか確認する
- 推測するな、計測せよ
GORMを触る、Vuls#1202
本題に入る前に、Vulsのスキャンについて簡単に説明します。
Vulsでは、以下のようにsqlite3などの形式で保存されたVulnerability DBからCVE情報を集めて、スキャン先の脆弱性を解析しています。
FYI: Vulsのスキャンアーキテクチャはこちらに詳しく書かれています。
<img src=”/images/20210609a/vuls-arch.png” alt=”Vulsアーキテクチャ図” loading=”lazy”
Reference: future-architect/vuls
さて,GORMを触るきっかけは、このPR(Vuls#1202)です。このPRは、VulsでDebianをスキャンしたとき、CVE情報のベースをOVALからDebian Security Trackerへ変更しようというものです。実装してみると、変更前はスキャン時間の90%ileが3.37sだったのですが、変更後は11.15sまで増加してしまいました。
原因を調査すると、Vulsと連携してDebian Security Trackerの情報を取り扱うgost(knqyf263/gost)にあることが分かりました。詳しくは、追加された機能(gost#47)によってDBに保存したDebianに関するCVE情報を取得する部分が増加したことでした。
パフォーマンスを改善しなければ、この機能は導入できないということになりました。そのため、EXPLAINしてINDEXを確認したりなどをして、解決策を色々探していました
その中で、2020年8月にGORM 2.0がリリースされていることを思い出しました。Release Noteによると、GORM v2はフルスクラッチされており、パフォーマンスが改善されているそうなのです。
まず、私はGORM v2をサポートして、どれくらい改善するかを検証することにしました(gost#60)。
期待のGORM v2、そのパフォーマンス
GORMをv1からv2にしたところ、発行されるクエリは大きく変わっていませんでした。
それでは、簡易なパフォーマンス測定をやってみることにします。
検証(Package: expat)
Debian busterのPackage: expatに関するunfixed/fixedなCVE情報を検索することを100回繰り返して、レスポンスにかかる時間の90%ileを取ってみました。
- unfixed cves
発行クエリ数 | 90%ile | |
---|---|---|
GORM v1 | 4 | 0.006682 |
GORM v2 | 4 | 0.010948 |
- fixed cves
発行クエリ数 | 90%ile | |
---|---|---|
GORM v1 | 46 | 0.023047 |
GORM v2 | 46 | 0.015868 |
検証(Package: linux)
もしかして発行されるクエリ数が少なすぎるかなと思って、Package: linuxに関するunfixed/fixedなCVE情報でも検証してみました。
すると、この場合はGORM v2にすることによるはっきりとしたパフォーマンスの向上が確認できました。
- unfixed cves
発行クエリ数 | 90%ile | |
---|---|---|
GORM v1 | 298 | 0.035935 |
GORM v2 | 307 | 0.027996 |
- fixed cves
発行クエリ数 | 90%ile | |
---|---|---|
GORM v1 | 4930 | 0.43754 |
GORM v2 | 4945 | 0.298629 |
ちなみに、GORM v2をサポートした状態でのVulsによるスキャン時間は11.5sから9.17sになりました。
まだまだ高速化が必要です😢
さらなる高速化に向けて
発行されるクエリのうち、ボトルネックになっていたのはJOIN句が入ったこのクエリです。
SELECT |
JOIN句を使わずに同様の検索が出来ないかを考え、クエリをチューニングしました(gost#61)。
クエリチューニング版では、以下のように、JOIN句をやめ、シンプルにdebian_packages
からdebian_cve_id
を求め、軽いクエリを多く実行することにしました。
SELECT |
検証(クエリチューニング)
先程と同様に、Debian busterにあるPackage: expat、linuxに関するunfixed/fixedなCVE情報を検索することを100回繰り返して、レスポンスにかかる時間の90%ileでクエリチューニングの効果を評価したいと思います。
- expat unfixes cves
発行クエリ数 | 90%ile | |
---|---|---|
GORM v1 | 4 | 0.006682 |
GORM v2 | 4 | 0.010948 |
GORM v2 + query tuning | 49 | 0.003467 |
- expat fixes cves
発行クエリ数 | 90%ile | |
---|---|---|
GORM v1 | 46 | 0.023047 |
GORM v2 | 46 | 0.015868 |
GORM v2 + query tuning | 49 | 0.003759 |
- linux unfixes cves
発行クエリ数 | 90%ile | |
---|---|---|
GORM v1 | 298 | 0.035935 |
GORM v2 | 307 | 0.027996 |
GORM v2 + query tuning | 5287 | 0.267383 |
- linux fixes cves
発行クエリ数 | 90%ile | |
---|---|---|
GORM v1 | 4930 | 0.43754 |
GORM v2 | 4945 | 0.298629 |
GORM v2 + query tuning | 5287 | 0.280019 |
結果としては、クエリチューニングをすることで、発行されるクエリ数は増えていますが、linux unfixed cvesの場合以外、高速化できているように見えます。
執筆時(2021/05/27)では、gostにおいて、CVE情報を検索可能なDebianのパッケージは3059件あり、それらのパッケージに対してfixed/unfixesなCVE情報を検索したとき、発行されるクエリ数はどのように分布するのかを調べてみました。
すると、ほとんどの検索の場合、0-499個のクエリしか発行しないことが分かりました。
つまり、linux unfixes cvesの場合のように、GORM v2 + query tuning
のクエリ数だけがとても増加して、検索にかかる時間が大きくなる事象によるロスより、それ以外の場合による高速化が大きく現れやすいということです。
これが、Vulsのスキャン時間にどれくらいの影響を与えるかを、スキャン時間の90%ileで見てみます。
スキャン先のDebianにはパッケージが218個インストールされています。
参考に、追加機能が導入されてないVuls v0.15.10も追加しました。
結果より、GORM v1, GORM v2の時点では、機能を導入に対するトレードオフとしては見合わなかったのですが、クエリチューニングをすることによって、追加機能がないものより高速にスキャンできるようになりました🎉
baseline(Vuls v0.15.10) | GORM v1 | GORM v2 | GORM v2 + query tuning |
---|---|---|---|
3.37 | 11.5 | 9.17 | 2.49 |
Gost、Ubuntuサポートをする
gostはUbuntuをサポートしていなかったので(TODOにはあった)、ついでと思ってUbuntuのサポートをしました(gost#62)。
これをもとにして、VulsでUbuntuをスキャンするときにgostからのCVE情報が追加される予定です(Vuls#1243)。
執筆時点(2021/05/27)では、gostでGORM v2をサポートするPRはまだMergeされてませんので、最初はDebianでのクエリチューニングを基にして、GORM v1で対応しました。gostの対応はサクッと終わったので、続いてVulsとの連携を実装しました。
さて、Vulsとの連携も実装できたので、Vulsのスキャン先を用意します。スキャン先として用意したUbuntu環境にインストールされているパッケージは2662個です。
この環境に対してVulsでスキャンをすると、スキャン時間が5m38sと遅い……(機能導入前であれば2sぐらい)
そこで、他のPRでGORM v2をサポートするので、Ubuntuサポート版もGORM v2に対応しました。
さて、GORM v2にしたときのVulsのスキャン速度は……?
7s
約48倍も速くなりました!!!一応、実際に発行されるクエリを見てみましたが、発行されるクエリに差分は、ほぼありませんでした。
約48倍も速くなった要因は、スキャン先にインストールされているパッケージ数がDebianの場合よりも多く、GORM v2にアップデートすることによるパフォーマンスの向上が顕著に現れたと考えています。
GORM v1とGORM v2におけるPreloadの挙動
さて、GORM v1で実装していて、テストをしていると、Debianの場合はRDBとRedisでレスポンスを比較して、差分は出ないのですが、UbuntuではあるパッケージにGETリクエストを投げたときのレスポンスがRDBとRedisで異なることに気が付きました。
とりあえず、Preloadの順番を変更すると、レスポンスが正しく返ってきました。
err := r.conn. |
そして、GORM v2にアップデートした後、GORM v1のときに変更したPreloadの順番ってGORM v2でも同様の挙動をするのかが気になりました🤔
Preloadを調整したcommitをrevertして、レスポンスをチェックすると、アレレ? ちゃんと正しいレスポンスが返ってきていました……
しかし、どうして正しいレスポンスが返ってくるようになったのでしょうか。
私、気になります!
そこで、Preload調整前のGORM v1とPreload調整後のGORM v1、Preload調整後のGORM v2、Preload調整をrevertしたGORM v2の4つが発行するクエリの一部を比較しました。
結果としては、今回の場合、GORM v1においては、Preloadの順を変更すると、WHERE IN
に影響を及ぼすようです。GORM v2になると、Preloadの順番に左右されない挙動をしていました。
- GORM v1, Preload調整前
SELECT |
- GORM v1, Preload調整後
SELECT |
- GORM v2, Preload調整後
SELECT |
- GORM v2, revert Preload調整
SELECT |
まとめ
自戒の念を込めて。
O/Rマッパを使うのはいいですが、ちゃんと意図したクエリ、レスポンスになっているかはちゃんと確認したほうが良いです。
そして、パフォーマンスの改善をするときは、ちゃんとボトルネックを色々な角度から測定して、トータルで改善することを確認してから実行すると良いでしょう。
- 1.CSIG(CyberSecurityInnovationGroup)は、セキュリティ関連のコンサルティングや実装などを行っています ↩