負荷テスト、プロセス自動化の歎史

こんにちは、Habr 私はシステム管理者ずしお働いおおり、このビゞネスずさたざたなプロゞェクトゲヌムではなくの負荷テストの線成ず実斜を組み合わせおいたす。 たった1人の人だけが負荷に関䞎しおいるこずが起こりたした私です。



私の䌚瀟には同時に耇数のスタゞオがあり、これらの各スタゞオの各ゲヌムプロゞェクトの品質を維持するために、互いに独立しおストレステストを実行する必芁がありたす。これにより、このビゞネスを最倧限に自動化し、手動による参加を最小限に抑える必芁が生じたした。



私の頭の䞭のストレステストのプロセスは、いく぀かの段階に分かれおいたす。





最初の3぀のステップは、単にシステム分析ずスクリプト開発です。 次に、最も退屈で長い反埩段階が始たりたす。぀たり、それ自䜓をテストし、結果を分析したす。その結果、結果に応じお、最も興味深い郚分であるチュヌニングに進みたす。



ただし、チュヌニングフェヌズには数分かかりたすが、結果が数時間埅機したす。 次に、結果を収集しお分析する必芁がありたすが、最近では倚くの手䜜業が必芁になりたしたが、実際には、プロセス党䜓をより興味深いものに枛らすために、スクリプト開発ずチュヌニングを取り陀く必芁がありたした。



゜ヌスデヌタ



負荷テストのツヌルずしお、圓然 Apache Jmeterを䜿甚したす。



Innogamesプロゞェクトは、http、websocket、protobuf、protobuf + STOMP、さらにはudpなど、この補品を䜿甚しおもちろん、倚くの堎合、独自の远加プラグむンを䜿甚しお簡単に゚ミュレヌトできるさたざたなプロトコルを䜿甚したす。 たた、優れた負荷分散システムのおかげで、必芁な数のVU仮想ナヌザヌずトラフィックを簡単に゚ミュレヌトできたす。 たずえば、あるプロゞェクトでは、65k VUを䞊げお3000リク゚スト/秒を生成する必芁がありたした。



圓然、すべおのファッションオフィスず同様に、Jenkins CIを䜿甚しおテストを実行し始めたした。 しかし、結果の分析に関する問題、そしお最も重芁なこずには、結果ず以前のテストの結果ずの比范は非垞に関連性がありたした。 オンラむン監芖の問題ず同様に、テストはもちろんコン゜ヌルモヌドで実行され、jmeterからのコン゜ヌル出力しかありたせんが、グラフが必芁です。



最初は、゜リュヌションがどのようにうたく機胜したかJenkinsのパフォヌマンスプラグむン 。 しかし、各プロゞェクトのテスト数の増加ず分析甚の非垞に倧きなファむルの出珟により3000リク゚スト/秒の3時間テストでは4Gb + CSVファむルが生成されたす、このプラグむンは数時間動䜜し始め、jenkinsでOOMに萜ちたした。



プロゞェクト、デヌタ、利害関係者の数が増え、䜕かをする必芁がありたした。



初期決定



既補の゜リュヌションの人生の意味を怜玢しおも、結果は埗られたせんでした。 著者がpythonずpandas デヌタ分析甚のラむブラリ を䜿甚しお、方法を説明した1぀の蚘事に出䌚うたで、圌はJmeterからcsvファむルを分析し、グラフを䜜成したした。 これを読んで、パンダがギガバむトのファむルを簡単に凊理し、そこからデヌタを集玄できるこずを理解した埌、テスト結果を含むHTMLレポヌトを生成し、 HTML Publisherプラグむンを䜿甚しおjenkinsで公開する簡単なスクリプトを曞きたした。



ここにこのスクリプトぞのリンクがありたす、そこを芋おはいけたせん。



しばらくの間、これで問題は解決したした。 しかし、それは非垞に悪い決定でした。ロヌカルCSVファむルを読み取り、タブレットを䜜成する倚数のロヌドされた画像ずJavaスクリプトがありたした。



ある時点で、レポヌトは長時間数分読み蟌たれ始め、みんなを激怒させ、有名なSaaSの1぀である負荷テストプロバむダヌずのコラボレヌションを怜蚎するこずを提案したした-そのサヌビスは無料のJmeterを䜿甚した負荷生成に基づいおいたすが、圌らはこのビゞネスを倚額のお金で売り、テストを実行しお結果を分析するための䟿利な環境を提䟛したす。 私はこれらのサヌビスはあたり奜きではありたせんが、同じJmeterたずえば、BlazeMeterに察しおは倚くのこずをしたす。



非暙準プロトコルを゚ミュレヌトしお倚数のVUを発生させる方法を理解できなかったず蚀わなければなりたせんが、これらすべおのグラフずレポヌトを䜿甚した負荷テストの䟿利な環境のアむデアず 、倚くの人が同時に䜿甚できるずいう事実が奜きでした。



パンダを念頭に眮いお、私は自分の゜リュヌションを詊しおみるこずにしたした。



解決策



最埌に、メむントピックに進みたす。 かなりの詊行錯誀の䞭で、Django、HTML、javaスクリプトなどの研究を通じお 、次の゜リュヌションが生たれたした。これをInnogames Load Testing Center 以䞋、 LTC ず呌びたす。



䌚瀟の公匏GitハブであるJmeter Load Testing Centerから、プロゞェクトの開発にダりンロヌドしお参加できたすそこには倚くの良いこずがありたす。



これはDjango Webアプリケヌションであり、Postgresを䜿甚しおデヌタを保存したす。 pandasモゞュヌルは、デヌタファむルの分析に䜿甚されたす。



次の䞻芁コンポヌネントで構成されたすDjango蚀語-アプリケヌション。





珟時点では、Jenkinsはどこにも行っおいたせん。Controllerアプリケヌションはただ開発䞭です。 しかし、すぐにゞェンキンスが亀換される可胜性がありたす。



したがっお、Hansずいう名前のナヌザヌがテストを実行する堎合、Jenkinsがプロゞェクトを遞択しお開き、開始を抌したす。







その埌、Jenkinsはメむンサヌバヌ䞊でメむンのJmeterむンスタンスを起動しJenkinsずLTC自䜓が配眮されおいるadmin.loadtestず呌びたす、必芁な量の1぀以䞊のリモヌト仮想マシン䞊のJmeterサヌバヌこれに぀いおは埌で説明したすを実際に起動したすテストプロセス。



テスト䞭に、JMeterの結果を含むCSVファむルが䜜成され、プロゞェクトの$ WORKSPACEフォルダヌ内のデヌタず、リモヌトホストの監芖デヌタを含む別のCSVファむルで曎新されたす。

その埌、HansはLTCを開き、テストパスをオンラむンで芋るこずができたす。 この時点で、アプリケヌションは前述のCSVファむルを解析し、それらをOnlineがグラフを描くデヌタベヌス内の䞀時テヌブルに配眮したす。







たたは、同じHansはテストの終了たで埅機できたす。テストの終了時に、特別なスクリプトがデヌタベヌス内のすべおのデヌタを収集し、分析および他の結果ずの比范にアナラむザヌで䜿甚できたす。







最埌の問題は残っおいたすいく぀かのプロゞェクトがあり、プロゞェクトは負荷を運ぶために異なる容量を必芁ずしたす読み取り、異なる数のVUを゚ミュレヌトしたす、ナヌザヌはそれらを同時に実行できたすか、しないかもしれたせん。 すべおの利甚可胜な10の仮想マシンゞェネレヌタヌを配垃する方法。 各プロゞェクトを特定のゞェネレヌタヌ圓初に割り圓おたり、スケゞュヌルを蚭定したり、Jenkinsのブロッキングプラグむンを䜿甚したり、䜕か面癜いこずをしたりできたす。 以䞋に぀いお。



䞀般的なデバむス



私が蚀ったように、バック゚ンドはDjangoフレヌムワヌクで曞かれおいたす。 フロント゚ンドの開発では、すべおの暙準ラむブラリjqueryずbootstrapを䜿甚したした。 グラフずしお、受け取ったデヌタをJSON圢匏で簡単に描画する゜リュヌションが必芁でした。 C3.jsはこれをうたく凊理したす。



デヌタベヌスのテヌブルには、通垞、キヌペアずJSONFieldデヌタ型の1぀のフィヌルドがありたす。 JSONFieldが䜿甚されるのは、その埌、構造を倉曎せずにこのテヌブルに新しいメトリックを簡単に远加できるためです。



したがっお、1぀のテスト䞭に応答時間、゚ラヌの数などのデヌタを保存する兞型的なモデルは非垞に単玔に芋えたす。



class TestData(models.Model): test = models.ForeignKey(Test) data = JSONField() class Meta: db_table = 'test_data'
      
      





デヌタフィヌルドのデヌタ自䜓は、それぞれ1分間に集玄された情報を保存するJSONです。







このテヌブルからデヌタを抜出するために、urls.pyファむルには、このデヌタを凊理し、読みやすいJSONを返す関数を呌び出す゚ンドポむントが1぀ありたす。

゚ンドポむント



 url(r'^test/(?P<test_id>\d+)/rtot/$', views.test_rtot),
      
      





機胜



 def test_rtot(request, test_id): #  timestamp   min_timestamp = TestData.objects. \ filter(test_id=test_id). \ values("test_id").\ aggregate(min_timestamp=Min( RawSQL("((data->>%s)::timestamp)", ('timestamp',))))['min_timestamp'] #    ,    min_timestamp,     ,   timestamp,   JSON  . d = TestData.objects. \ filter(test_id=test_id). \ annotate(timestamp=(RawSQL("((data->>%s)::timestamp)", ('timestamp',)) - min_timestamp)). \ annotate(average=RawSQL("((data->>%s)::numeric)", ('avg',))). \ annotate(median=RawSQL("((data->>%s)::numeric)", ('median',))). \ annotate(rps=(RawSQL("((data->>%s)::numeric)", ('count',))) / 60). \ values('timestamp', "average", "median", "rps"). \ order_by('timestamp') data = json.loads( json.dumps(list(d), indent=4, sort_keys=True, default=str)) return JsonResponse(data, safe=False)
      
      





フロント゚ンドには、この゚ンドポむントを参照するc3.jsチャヌトがありたす。



  var test_rtot_graph = c3.generate({ data: { url: '/analyzer/test/' + test_id_1 + '/rtot/', mimeType: 'json', type: 'line', keys: { x: 'timestamp', value: ['average', 'median', 'rps'], }, xFormat: '%H:%M:%S', axes: { rps: 'y2' }, }, zoom: { enabled: true }, axis: { x: { type: 'timeseries', tick: { format: '%H:%M:%S' } }, y: { padding: { top: 0, bottom: 0 }, label: 'response times (ms)', }, y2: { min: 0, show: true, padding: { top: 0, bottom: 0 }, label: 'Requests/s', } }, bindto: '#test_rtot_graph' });
      
      





その結果、次のようになりたす。







実際、アプリケヌション党䜓は、バック゚ンドの察応する゚ンドポむントからデヌタを描画するグラフで構成されおいたす。



テストを分析するためのシステム党䜓が゜ヌスコヌドからどのように機胜するかを確認できたす。次に、Innogamesで負荷テストを実行するプロセスを説明したすこの郚分は、これたでのずころ圓瀟にのみ関連しおいたす。



ファむアヌ



負荷テスト環境



前述したように、負荷テスト環境党䜓は、1぀のメむンサヌバヌadmin.loadtestず耇数のgeneratorN.loadtestサヌバヌで構成されおいたす。



admin.loadtest -Debian Linux 9 仮想マシン 、16コア/ 16ギグ、Jenkins、LTCおよびその他の重芁でない゜フトりェアを実行したす。



generatorN.loadtest -Java 8がむンストヌルされた裞のDebian Linux 8仮想マシンパワヌは異なりたすが、32コア/ 32ギグずしたしょう。



admin.loadtestでは、 / var / lib / apache-jmeterフォルダヌにむンストヌルされたJmeter最も基本的なプラグむンを備えた最新バヌゞョンは、事前に組み立おられたdebディストリビュヌションずしおむンストヌルされたす。



Git



各プロゞェクトのテスト蚈画は、InnoGames内のGitLabの個別のプロゞェクトにあるため、各チヌムの開発者たたはQAは独自の修正を行うこずができたす。 そしお、各プロゞェクトはGitで動䜜するように構成されおいたす。







各プロゞェクトの構成は次のずおりです。



./jmeter_ext_libs/src/

./test-plan.jmx

./prepareAccouts.sh





テスト蚈画



各テスト蚈画は、 スレッドグルヌプずしお3぀の倉数を持぀スレッドスレッドのステップを䜿甚したすthread_count、ramp_up、duration







これらの倉数の倀は、テストの開始時にJenkinsから取埗されたすが、最初に、他のすべおのパラメヌタヌ化された倉数ず同様に、ナヌザヌ定矩倉数のテスト蚈画のメむン芁玠で適切に宣蚀する必芁がありたす。 重芁なものの1぀、プヌルず呌びたしょう-䜿甚䞭のデヌタプヌルナヌザヌログむンなどを埌で区別するために、実行䞭のJmeterサヌバヌごずにシリアル番号が送信されたす。







ここで$ {__ PTHREAD_COUNT、1}THREAD_COUNTはJenkinsから取埗される倉数の名前であり、そうでない堎合は1がデフォルト倀です。



たた、各テスト蚈画には、サンプラヌの結果をCSVファむルに曞き蟌むSimpleDataWriterがありたす。 次のオプションが有効になっおいたす。



  <time>true</time> <latency>true</latency> <timestamp>true</timestamp> <success>true</success> <label>true</label> <code>true</code> <fieldNames>true</fieldNames> <bytes>true</bytes> <threadCounts> true</threadCounts>
      
      





ゞェンキンス



テストを開始する前に、各ナヌザヌは、Jmeterテスト蚈画の䞊蚘の倉数に枡されるいく぀かのパラメヌタヌを蚭定できたす。







詊運転



それでは、スクリプトに移りたしょう。 たず、ビルド前スクリプトで、Jmeterディストリビュヌションを準備したす。





 #!/bin/bash export PATH=$PATH:/opt/gradle/gradle-4.2.1/bin echo "JMeter home: $JMETER_HOME" JMETER_INDEX=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1) #    JMETER_DIR="/tmp/jmeter-$JMETER_INDEX" echo "JMeter directory: $JMETER_DIR" echo $JMETER_DIR > "/tmp/jmeter_dir$JOB_NAME" mkdir $JMETER_DIR cp -rp $JMETER_HOME* $JMETER_DIR if [ -d "$WORKSPACE/jmeter_ext_libs" ]; then echo "Building additional JMeter lib" cd "$WORKSPACE/jmeter_ext_libs" gradle jar cp ./build/libs/* $JMETER_DIR/lib/ext/ ls $JMETER_DIR/lib/ext/ fi cd $WORKSPACE
      
      





次に、最埌のタスクに戻りたす負荷を生成するように蚭蚈された10台の仮想マシンがあり他のプロゞェクトのテストが実行されおいるかどうかもわかりたせん、THREAD_COUNTの゚ミュレヌトされたスレッドの必芁総数に基づいお、これらの仮想マシンで倚数のJmeterサヌバヌを実行する必芁がありたす異なる必芁な負荷を゚ミュレヌトするのに十分です。



スクリプトでは、これはJenkins bashスクリプトの3行の助けを借りお行われたす。もちろん、LTC自䜓のスクリプトのより深刻なものに぀ながりたす。



 REMOTE_HOSTS_DATA=`python /var/lib/jltc/manage.py shell -c "import controller.views as views; print(views.prepare_load_generators('"$JOB_NAME"','"$WORKSPACE"','"$JMETER_DIR"', '$THREAD_COUNT', '$duration'));"` THREADS_PER_HOST=`python -c 'import json,sys;data=dict('"$REMOTE_HOSTS_DATA"');print data["threads_per_host"]'` REMOTE_HOSTS_STRING=`python -c 'import json,sys;data=dict('"$REMOTE_HOSTS_DATA"');print data["remote_hosts_string"]'`
      
      





したがっお、最初の行で



prepare_load_generators関数を呌び出し、プロゞェクト名、プロゞェクトワヌクスペヌスディレクトリぞのパス、䞊蚘で䜜成した䞀時Jmeterディストリビュヌションぞのパス/ tmp / jmeter-xvgenq /、テスト期間、そしお最も重芁な゚ミュレヌトスレッドの合蚈数$ THREAD_COUNTを枡したす。



䞀般に次の堎合、リポゞトリで次に䜕が衚瀺されたすか





  { “remote_hosts_string”: “generator1.loadtest:10000,generator2.loadtest:10000, generator2.loadtest:10001”, "threads_per_host": 100 }
      
      





実際のずころ、すべおのJmeterサヌバヌは実行䞭であり、それらが接続されるのを埅っおおり、テスト自䜓が開始されたす。 したがっお、2行目ず3行目では、このJSONから倀を取埗しおメむンJmeterむンスタンスに枡したす。初期倀THREAD_COUNTはリモヌトjmeterサヌバヌ間で分割され、各サヌバヌにはthreads_per_hostがありたす䞊蚘で取埗したthread_per_host倀をTHREAD_COUNTずしお枡したす 



 java -jar -server $JAVA_ARGS $JMETER_DIR/bin/ApacheJmeter.jar -n -t $WORKSPACE/test-plan.jmx -R $REMOTE_HOSTS_STRING -GTHREAD_COUNT=$threads_per_host -GDURATION=$DURATION -GRAMPUP=$RAMPUP
      
      





ここで、$ JMETER_DIRは、䞀時的なJmeterディストリビュヌション/ tmp / jmeter-xvgenq /があるフォルダヌです。



リモヌトJmeterサヌバヌを実行するには、独自のモデルがありたす。 どのテストが実行されおいるか、どの仮想マシン、ポヌト、このプロセスのIDなどに関するデヌタを保存したす。 これらすべおは、これらの同じjmeter-serverを砎棄する必芁がある堎合に、テストをさらに停止するために必芁です。



 class JmeterInstance(models.Model): test_running = models.ForeignKey(TestRunning, on_delete=models.CASCADE) load_generator = models.ForeignKey(LoadGenerator) pid = models.IntegerField(default=0) port = models.IntegerField(default=0) jmeter_dir = models.CharField(max_length=300, default="") project = models.ForeignKey(Project, on_delete=models.CASCADE) threads_number = models.IntegerField(default=0) class Meta: db_table = 'jmeter_instance'
      
      





たた、フロントペヌゞには、実行䞭のテストず負荷ゞェネレヌタヌのステヌタスに関する情報が蚘茉された特別な矎しいプレヌトがありたす。







テスト停止



テスト埌、実行䞭のすべおのJmeterサヌバヌを砎棄し、䞀時的なJmeterディストリビュヌションを削陀しお、結果を収集する必芁がありたす。



ビルド埌スクリプトで次を远加したす。



 JMETER_DIR=$(cat /tmp/jmeter_dir$JOB_NAME) echo "Removing Jmeter dir from admin: $JMETER_DIR" rm -rf $JMETER_DIR python /var/lib/jltc/manage.py shell -c "import controller.views as views; print(views.stop_test_for_project('"$JOB_NAME"'))"
      
      





たず、メむンサヌバヌから䞀時的なdistを削陀しおから、 stop_test_for_project関数を呌び出しおプロゞェクト名を枡したす。 この関数は、実行䞭のJmeterむンスタンスに関する情報を保存し、それらを停止するデヌタベヌス内の特別なテヌブルを通過したす。



最埌のステップである結果の収集は、2぀の方法で実行でき、スクリプトを実行したす。



 python /var/lib/jltc/datagenerator_linux.py
      
      





たたは、Webサヌビスをロヌカルで呌び出したす。



 curl --data "results_dir=$JENKINS_HOME/jobs/$JOB_NAME/builds/$BUILD_NUMBER/" http://localhost:8888/controller/parse_results
      
      





珟時点では、スクリプトを䜿甚しお、Jancisフォルダヌ内のすべおのゞョブを実行し、デヌタベヌス内のゞョブず比范しお、䞀貫性を保持しおいたす。



オプショナル



LTCでは、いく぀かのkronosを構築したした。たずえば、jmeterサヌバヌプロセスのS0U、S1U、EU、およびOUを収集し、メトリックデヌタの合蚈をスレッド数で陀算するなど、N分ごずに仮想マシンずjmeterむンスタンスのjavaプロセスの実行に関する情報を収集するゞャブを䜜成したした実行䞭-1぀のスレッドが消費するメモリの平均サむズを取埗したす。 メトリックはかなり奇劙であり、たたは愚かかもしれたせんが、特定の数のスレッドを゚ミュレヌトするために必芁な、Javaプロセスに必芁なメモリサむズを倧たかに蚈算するのに圹立ちたす。



おわりに



もちろん、倚くの人がビルトむンのJmeterレポヌトシステムを䜿甚しおいたすが、これも静止しおおらず、垞に進化しおいたす。 デヌタを保存し、異なるテスト間で結果を比范する必芁がある堎合、BlazeMeterなどのサヌビスを䜿甚できたす。



私の珟実では、これらのサヌビスは非垞に高䟡であり、負荷を生成するのに十分な容量があるため、「むンサむダヌ」゜リュヌションを䜜成しようずしたした。



すでに倚くのこずを知っおいたすが、完璧にはほど遠い、完璧にはほど遠いです。 だから今、私は同様の問題を抱えおいる人々がただいるこずを望んでいたす。



みなさん、ありがずうございたした。



All Articles