キーポイント
- GUIのパフォーマンスツールは、長期間の使用には問題があるかもしれない。
- 可読性と構成可能性が、長く使えるパフォーマンス・テストの鍵だ。
- パフォーマンス・メトリクスがどのように実装されているかを常にチェックする。
- コードとしての性能テストは、GUIベースのアプローチに代わる有望な方法である。
JMeterとGatlingは、もっとも人気のあるパフォーマンステストツールの1つだ。この2つのツールを比較するコンテンツはすでにたくさんある。では、なぜまた記事を書くのか?私は、少し違った角度からこの2つのツールを比較してみようと思う。私は両方をかなり長い間使っているので、そろそろ私の経験をまとめる時期だと思う。
完璧な世界でのパフォーマンステストはどうあるべきか?
開発者としての私の見解では、アプリケーションのパフォーマンスをテストすることは、それを構築した開発者の責任だ。したがって、アプリケーションのソースコードに一度も触れたことのないテスターの専門チームが、そのパフォーマンスをチェックしなければならないようなアプローチはうまくいかない。テスターが良いパフォーマンステストを作れないという意味ではない。むしろ、性能テスターが自由に使えるのであれば、アプリケーションのテストプロセスは、テスターと開発者の間で、特に最初の段階で、緊密に協力して実施されなければならないことを強調したいのである。
時間が経つにつれて、より多くの責任がテストチームに移されるかもしれないが、結果を分析するために開発者が必要であることに変わりはない。特に、結果が当初の予想と異なっているような状況では、そうなる。もちろん、これは開発者にとって非常に良いことだ。なぜなら、ユニットテスト、統合テスト、またはE2E(End-to-End)テストを書く場合と同様に、自分たちのソリューションの品質に関するフィードバックループが生まれるからである。
性能テストは、ソフトウェア開発においてもっともコストのかかるテストの一つであることは間違いない。テストを作成するための貴重なテスターや開発者の時間に加え、テストを実施するための専用の環境も(可能であれば)必要だ。アプリケーションは非常にダイナミックに変化するため、性能テストもその変化を追跡し、常に最新の状態に保つ必要がある。これは時に、そのようなテストを最初に作成するよりもさらにコストがかかるのだ。
上記の理由から、パフォーマンス・テストに関して私が最初にアドバイスするのは、プロセス全体を長期的な文脈で考えることだ。理想的には、パフォーマンステストは継続的デプロイメント(CD)サイクルの一部であるべきである。これは常に可能というわけではないし、常に意味をなすわけでもない。しかし、私の観察によれば、パフォーマンス・テストの最初の段階で近道をしてしまうと、将来的に大きな犠牲を払うことになりかねないのだ。
性能テストに使うツールはどう選ぶ?
パフォーマンス・テストのためのツール選びは最初のジレンマの1つとなるだろうが、この記事が正しい選択をするための一助となれば幸いだ。
繰り返しになるが、開発者の視点から見ると、優れたパフォーマンステストツールには4つの主要な属性を期待すべきだ。
- 可読性
- 構成可能性
- 正しい計算
- 分散負荷の生成
"それだけ?"と思われるかもしれない。正直なところ、そうだ。これらは、ツールを選択する際にもっとも重要な点である。もちろん、テストシナリオの作成、本番環境のトラフィックのシミュレーションなどについては、ほとんどのツールが多かれ少なかれ同様のオプションを提供しているが、具体的で便利な機能はたくさんある。そこで、これらのポイントに焦点を当て、便利で保守性の高いツールを選ぶ際に本当に重要なことは何かを示したい。
この4つの基本要件を満たしたツールであれば、初めてその機能の全領域の分析に進むことができる。そうでなければ、長期的にはそれほど面白くも便利でもない興味深い機能に誘惑されて、最適とは言えないテストツールを選択してしまうだろう。
テストの可読性
さて、まず最初のポイントから。GUIとソースコードでアプリケーションの可読性を比較するのは、かなり変わったアプローチだが、そこから何が生まれるか見てみよう。JMeterの非常にシンプルなビジネスフローは、次のようになる。
一見したところ - かなり良い。すべてが明確で、あるテストが何をテストしているのかを理解するのは簡単だ。しかし、新しいステップを追加したり、現在のステップをパラメータ化したり、動作を変更したりして、シナリオを拡張し始めると、すぐに、これは非常に退屈な作業であるという結論に達するだろう。マウスをたくさん使い、何がどこに隠れているのかを把握し、さらに悪いことに、個々のクエリ間の暗黙的な呼び出し(共有変数など)をすべて覚えておかなければならないのだ。
私の経験では、遅かれ早かれ、GUIからの編集は快適ではなくなり、XMLで書かれた、(XMLの典型的な)全く読めないソースコードに切り替えることになるだろう。XMLでは、少なくとも「replace with」などの文字列に基づいた基本的なリファクタリングのテクニックを使うことができる。
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.3"></jmeterTestPlan>
<hashTree></hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true"></TestPlan>
<stringProp name="TestPlan.comments"></stringProp>
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.tearDown_on_shutdown">True</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
<elementProp name="TestPlan.user_defined_variables" elementtype="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true"></elementProp>
<collectionProp name="Arguments.arguments"></collectionProp>
</elementProp>
<stringProp name="TestPlan.user_define_classpath"></stringProp>
...
That's a very, very long XML.
...
完全なXMLコードはこちらで入手可能だ。
全く同じシナリオをGatlingで表現すると、次のようになる。
val scn =
scenario("Example scenario")
- exec(http("go to main page").get("/"))
- exec(http("find computer").get("/computers?f=macbook"))
- exec(http("edit computer").get("/computers/6"))
- exec(http("go to main page").get("/"))
- repeat(4, "page") {
exec(http("go to page").get("/computers?p=${page}"))
}
- exec(http("go to create new computer page").get("/computers/new"))
- exec(
http("create new computer")
- post("/computers")
- formParam("name", "Beautiful Computer")
- formParam("introduced", "2012-05-30")
- formParam("discontinued", "")
- formParam("company", "37")
)
JMeterと非常によく似ている。つまり、何がテストされているのか、全体の流れはどうなっているのかを見ることができるのだ。しかし、これは、ほとんど普通の文章のように読めるソースコードであることを忘れてはならない。プログラマーの聖杯だ。すぐに思いつくのは、ソースコードを扱うのであれば、既知のあらゆるリファクタリング手法を使ってシナリオを拡張したり、可読性を向上させたりできることだろう。
言うまでもなく、Scala(Gatlingで使用)は強い型付け言語なので、シナリオを正しく構築するための問題のほとんどは、コードをコンパイルする段階ですでに捕捉される。JMeterでは、シナリオが起動されたときにのみエラーが表示され、結果のフィードバックループを確実に遅くする。
ソースコードを支持するもう一つの論拠は、そのようなテストのバージョンアップが非常に簡単で、多くのプログラマー(たとえ異なるチームであっても)にレビューしてもらうことができることだ。もし、何千行ものXMLでそれを行わなければならないのであれば、幸運なことである。
構成可能性
認証やユーザー作成などのロジックを共有する複数のパフォーマンステストを作成する必要がある場合、構成可能性は非常に重要だ。JMeter では、瞬く間にコピー&ペーストの惨状ができあがってしまう。この単純なテスト計画でさえも、繰り返される断片があるのだ。
時間が経てば、そのような場所はもっと増えるだろう。個々のリクエストだけでなく、ビジネスロジックのセクション全体が重複することになる。この問題は、Module Controllerを使ったり、GroovyやBeanShellで独自の拡張を作ったりすることで対応できる。私の経験では、かなり不便で、エラーになりやすいと思う。
Gatlingでは、基本的にプログラミングのスキルがあれば,再利用可能なフラグメントの構築ができる。最初のステップは、いくつかのメソッドを抽出して、複数回利用できるようにすることだろう。
private val goToMainPage = http("go to main page").get("/")
private def findComputer(name: String) = http("find computer").get(s"/computers?f=${name}")
private def editComputer(id: Int) = http("edit computer").get(s"/computers/${id}")
private def goToPage(page: Int) = http("go to page").get(s"/computers?p=${page}")
private val goToCreateNewComputerPage = http("go to create new computer page").get("/computers/new")
private def createNewComputer(name: String) =
http("create new computer")
.post("/computers")
.formParam("name", name)
.formParam("introduced", "2012-05-30")
.formParam("discontinued", "")
.formParam("company", "37")
val scn =
scenario("Example scenario")
.exec(goToMainPage)
.exec(findComputer("macbook"))
.exec(editComputer(6))
.exec(goToMainPage)
.exec(goToPage(1))
.exec(goToPage(1))
.exec(goToPage(3))
.exec(goToPage(10))
.exec(goToCreateNewComputerPage)
.exec(createNewComputer("Awesome computer"))
次に、シナリオを細かく分割し、組み合わせて、より複雑なビジネスフローを作ることができる。
val search = exec(goToMainPage)
.exec(findComputer("macbook"))
.exec(editComputer(6))
val jumpBetweenPages = exec(goToPage(1))
.exec(goToPage(1))
.exec(goToPage(3))
.exec(goToPage(10))
val addComputer = exec(goToMainPage)
.exec(goToCreateNewComputerPage)
.exec(createNewComputer("Awesome computer"))
val scn =
scenario("Example scenario")
.exec(search, jumpBetweenPages, addComputer)
長期的に性能テストを維持しなければならないのであれば、高い構成可能性が他のツールに対するアドバンテージになることは間違いない。私の観察によれば、ソースコードにテストを書くことができるツール、例えばGatlingとScala、LocustとPython、WRK2とLuaだけがこの基準を満たすようだ。もしテストがXMLやJSONなどのテキスト形式で保存されるなら、これらの形式の構成可能性によって常に制限されることになる。
正しい計算
パフォーマンステスターなら知っておくべき格言がある。" 嘘には三種類ある:嘘、まっかな嘘、そして統計 "だ。もしまだ知らないのであれば、きっと痛い目にあいながら学ぶことになるだろう。なぜこの文章が性能テストの分野でマントラであるべきなのかについては、別の記事を書くことができるだろう。一言で言えば、中央値、算術平均、標準偏差は、この分野では全く役に立たない指標だ(追加的な洞察としてのみ使用できる)。AzulのCTO兼共同設立者であるGil Teneによるこの素晴らしいプレゼンテーションで、より詳しく知ることができる。したがって、もしパフォーマンステストツールがこのような静的なデータしか提供しないのであれば、すぐに捨てることができる。
パフォーマンスを測定し、比較するために意味のある指標は、パーセンタイルだけだ。しかし、それらがどのように実装されたのか、ある程度疑って使うことも必要である。非常に多くの場合、算術平均と標準偏差に基づいて実装されているが、もちろん、それらも同様に役に立たない。
上のプレゼンテーションから、パーセンタイルの正しさを検証する方法を学ぶことができる。
また、実装のソースコードを自分でチェックするのも一つの方法だろう。残念なことに、ほとんどの性能テストツールのドキュメントには、パーセンタイルの計算方法は記載されていない。たとえそのようなドキュメントがあったとしても、それを使う人はほとんどおらず、その結果、例えばDropwizard Metricsの実装のような罠にはまるかもしれないのだ。
正しい数学/統計学がなければ、パフォーマンステストの文脈におけるすべての作業は、完全に無価値になりえる。
私のテストでは、時間の経過とともに変化するパーセンタイルのグラフに頼ることが非常に多く、これはGatlingとJMeterの両方で取得が可能だ。そのおかげで、テストされたシステムが、テスト全体にわたって性能上の問題がないかどうかを判断することができるのだ。
個々のテストの結果を比較するためには、グローバルなパーセンタイルが必要だ(どちらのツールでも利用可能だ)。しかし、私はかつて、JMeterのグローバル・パーセンタイルの精度について、かなり興味深い問題から立ち直ったことがある。Gatlingは、その実装において、HdrHistogramライブラリを使用してパーセンタイルを計算し、精度と必要なメモリの間で非常に合理的な妥協点を提供している。
分散型テスト
性能テストツールの性能に関する記事がいくつかある。これは、あるレベルまでは重要かもしれない。なぜなら、テスト対象のシステムを適切に「プッシュ」するために、巨大なトラフィックを発生させたいのは間違いないからである。問題は、現在のアプリケーションが、1台のマシン上で動作するシングルインスタンスであることは非常に稀であることだ。私たちは、多くのインスタンスと多くのマシン(多くの場合、動的に拡張可能なクラウドソリューション)で動作する分散システムを扱っているのだ。性能テストを実行する1台のマシンでは、このような環境をテストするのに十分な負荷を発生させることはできないだろう。したがって、どのツールが1台のマシンからより多くのトラフィックを発生させるかに注目するのではなく、多くのマシンから同時に分散テストを実行するオプションがあるかどうかを確認する方が良い。
この点において,JMeterとGatlingは引き分けだ。JMeterと同様に、Gatlingでもテストを手動で分散させることができる。さらに、FloodやGatling Enterpriseのような、それを自動的に行ってくれる既存のソリューションを使うこともできる。私は、間違いなく2番目の選択肢をお勧めする。なぜなら、それは多くの貴重な時間を節約できるからだ。
まとめ
この記事はJMeterへの批判と受け取られるかもしれないが、私はこれら2つのツールを使っているので、そのような意図はない。私は以前、パフォーマンスをテストするための唯一の賢明なツールとしてJMeterを見ていたが、Gatlingを使い始めたとき、JMeterに戻る意味を見いだせなくなった。
グラフィカルなインターフェースを持つツールは、最初のうちは使いやすいかもしれないが、コードとしてのパフォーマンス・テストという考え方の方が、私には魅力的に感じられる。GatlingのDSLが本当に快適で便利なのは言うまでもない。テストは読みやすく、メンテナンスもずっと簡単だ。
多くの人がGatlingに懐疑的だ。なぜなら、新しいプログラミング言語であるScalaを学ぶ必要があり、Scalaは難しい言語、使いにくい言語というイメージがあるからである。しかし、これほど真実から遠いことはない。Scalaには長所と短所があるが、Gatlingでは、基本的な構文の知識だけが必要だ。一方、もしあなたが、仕事でScalaを使いたいとずっと思っていたが、いろいろな理由で使えなかったとしたら、おそらくパフォーマンス(および自動)テストは、この言語をあなたのエコシステムにそっと導入する良い方法だろう。Gatling 3.7から、Javaと一緒に使えるようになったことに注意してほしい。これについては、次回の記事で取り上げる予定だ。期待してほしい。