<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>JMeter 的實作紀錄 on Tarragon</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/jmeter/hands-on/</link><description>Recent content in JMeter 的實作紀錄 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 28 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/jmeter/hands-on/index.xml" rel="self" type="application/rss+xml"/><item><title>JMeter 在本機跑負載測試的實測紀錄：Docker image、test plan 的元件、CSV Data Set、CLI mode 與 JTL、dashboard 與 statistics.json、JMeter 之外的門檻判定、remote testing 的 client 與 server</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/jmeter/hands-on/jmeter-local-lab/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/jmeter/hands-on/jmeter-local-lab/</guid><description>&lt;p>實測日期是 2026-10-28，環境是一台 macOS 筆電上的 Docker（server 29.8.2，Docker 可用 8 顆 CPU 與 8 GB 記憶體），JMeter 5.6.3 跑在以 &lt;code>eclipse-temurin:21-jre&lt;/code>（&lt;code>java -version&lt;/code> 印出 21.0.12.1）自建的 image 裡，受測服務是同一個 Docker network 裡另一個 container 跑的 Python 標準函式庫程式；這次的操作是從手寫一份 test plan 開始，用 CLI mode 執行、產生報告、在 JMeter 之外判定門檻，最後開一台 client 與兩台 server 做 remote testing。&lt;/p>
&lt;p>這篇記錄在本機用 Docker 跑 JMeter 的完整操作：自建 JMeter image 並核對發行檔的雜湊值；test plan 裡各元件負責的事（HTTP Request Defaults、CSV Data Set、Thread Group、只登入一次的 Once Only Controller、依隨機數挑請求的 If Controller、放使用者停頓時間（think time）的 Flow Control Action）；CLI mode 的參數與 JTL 的欄位；這次遇到的 Throughput Controller 與 CSV Data Set 週期對齊、讓某個商品編號從來沒被請求到的情形與改法；有失敗的 sample（JMeter 對一次請求的量測紀錄）時 exit code 仍然是 0；HTML dashboard 與 &lt;code>statistics.json&lt;/code>，以及用 &lt;code>-g&lt;/code> 從 JTL 重新產生報告；一支讀 &lt;code>statistics.json&lt;/code> 判定門檻的程式；remote testing 的 client 與 server 怎麼啟動、&lt;code>-J&lt;/code> 與 &lt;code>-G&lt;/code> 的差別、JTL 怎麼分出來自哪一台 server；最後是清理。受測服務與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/locust/hands-on/locust-local-lab/" data-link-title="Locust 在本機跑負載測試的實測紀錄：受測服務、locustfile、headless 執行與 CSV、內建的 exit code、quitting 事件裡的門檻判定、--reset-stats 與 on_start、master 與 worker 的分散執行" data-link-desc="要在本機用 Docker 照一次實測跑 Locust：寫 locustfile、headless 執行、讀終端機的統計表與 CSV、在 quitting 事件裡判定延遲門檻、開 master 與多個 worker 分散執行時讀；也用在受測服務變慢而 exit code 還是 0、加了 --reset-stats 之後登入請求還在統計裡、master 回報 worker failed to send heartbeat 的時候">Locust 在本機跑負載測試的實測紀錄&lt;/a> 用的是同一支程式，兩篇的負載模型也對齊（每個使用者登入一次、商品頁與會失敗的 endpoint 四比一、每一輪停 1 到 3 秒），讀者可以拿同一個受測服務比較兩個工具的輸出。這些操作背後的判斷，也就是 CI 用這次的量測擋下效能退化（效能 regression gate，下文簡稱 gate）時，門檻判定為什麼放在 JMeter 之外、逐筆 assertion 與統計門檻怎麼分工、gate 失敗時依什麼順序排除誤判，寫在 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/jmeter/jmeter-performance-regression-gate/" data-link-title="JMeter 在效能 regression gate 的用法：JMeter 之外的門檻判定、逐筆 assertion 與統計門檻的分工、thread group 的負載模型、施壓端設定與基準的可比性、PR 與 nightly 的分工、證據保存與 gate 失敗的排查順序" data-link-desc="用 JMeter 實作效能 regression gate 時的判斷：門檻判定放在 JMeter 之外的哪一段、逐筆 assertion 與分佈門檻的分工、負載模型與施壓端設定怎麼影響前後兩版的比較、要保存的執行條件，以及 gate 失敗時怎麼把誤判和真正的退化分開">JMeter 在效能 regression gate 的用法：JMeter 之外的門檻判定、逐筆 assertion 與統計門檻的分工、thread group 的負載模型、施壓端設定與基準的可比性、PR 與 nightly 的分工、證據保存與 gate 失敗的排查順序&lt;/a>；用 JMeter 逐級加壓找飽和點的做法寫在 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/vendors/jmeter/jmeter-capacity-probing/" data-link-title="JMeter 在容量探測的用法：thread group 固定的負載變數、逐級加壓、knee 的判讀、施壓端與分散施壓的檢查、多 protocol 的涵蓋範圍與排查順序" data-link-desc="用 JMeter 找飽和點時的判斷：thread group 與 timer 量的是哪一種負載模型、Open Model Thread Group 的逐級加壓、從 dashboard 判讀 knee、施壓端與 TLS 連線的失真、分散施壓，以及容量結論要附的條件">JMeter 在容量探測的用法：thread group 固定的負載變數、逐級加壓、knee 的判讀、施壓端與分散施壓的檢查、多 protocol 的涵蓋範圍與排查順序&lt;/a>。GUI 的操作、HTTP(S) Test Script Recorder 的錄製與 plugin 都不在這次的實測裡。&lt;/p></description><content:encoded><![CDATA[<p>實測日期是 2026-10-28，環境是一台 macOS 筆電上的 Docker（server 29.8.2，Docker 可用 8 顆 CPU 與 8 GB 記憶體），JMeter 5.6.3 跑在以 <code>eclipse-temurin:21-jre</code>（<code>java -version</code> 印出 21.0.12.1）自建的 image 裡，受測服務是同一個 Docker network 裡另一個 container 跑的 Python 標準函式庫程式；這次的操作是從手寫一份 test plan 開始，用 CLI mode 執行、產生報告、在 JMeter 之外判定門檻，最後開一台 client 與兩台 server 做 remote testing。</p>
<p>這篇記錄在本機用 Docker 跑 JMeter 的完整操作：自建 JMeter image 並核對發行檔的雜湊值；test plan 裡各元件負責的事（HTTP Request Defaults、CSV Data Set、Thread Group、只登入一次的 Once Only Controller、依隨機數挑請求的 If Controller、放使用者停頓時間（think time）的 Flow Control Action）；CLI mode 的參數與 JTL 的欄位；這次遇到的 Throughput Controller 與 CSV Data Set 週期對齊、讓某個商品編號從來沒被請求到的情形與改法；有失敗的 sample（JMeter 對一次請求的量測紀錄）時 exit code 仍然是 0；HTML dashboard 與 <code>statistics.json</code>，以及用 <code>-g</code> 從 JTL 重新產生報告；一支讀 <code>statistics.json</code> 判定門檻的程式；remote testing 的 client 與 server 怎麼啟動、<code>-J</code> 與 <code>-G</code> 的差別、JTL 怎麼分出來自哪一台 server；最後是清理。受測服務與 <a href="/blog/backend/06-reliability/vendors/locust/hands-on/locust-local-lab/" data-link-title="Locust 在本機跑負載測試的實測紀錄：受測服務、locustfile、headless 執行與 CSV、內建的 exit code、quitting 事件裡的門檻判定、--reset-stats 與 on_start、master 與 worker 的分散執行" data-link-desc="要在本機用 Docker 照一次實測跑 Locust：寫 locustfile、headless 執行、讀終端機的統計表與 CSV、在 quitting 事件裡判定延遲門檻、開 master 與多個 worker 分散執行時讀；也用在受測服務變慢而 exit code 還是 0、加了 --reset-stats 之後登入請求還在統計裡、master 回報 worker failed to send heartbeat 的時候">Locust 在本機跑負載測試的實測紀錄</a> 用的是同一支程式，兩篇的負載模型也對齊（每個使用者登入一次、商品頁與會失敗的 endpoint 四比一、每一輪停 1 到 3 秒），讀者可以拿同一個受測服務比較兩個工具的輸出。這些操作背後的判斷，也就是 CI 用這次的量測擋下效能退化（效能 regression gate，下文簡稱 gate）時，門檻判定為什麼放在 JMeter 之外、逐筆 assertion 與統計門檻怎麼分工、gate 失敗時依什麼順序排除誤判，寫在 <a href="/blog/backend/06-reliability/vendors/jmeter/jmeter-performance-regression-gate/" data-link-title="JMeter 在效能 regression gate 的用法：JMeter 之外的門檻判定、逐筆 assertion 與統計門檻的分工、thread group 的負載模型、施壓端設定與基準的可比性、PR 與 nightly 的分工、證據保存與 gate 失敗的排查順序" data-link-desc="用 JMeter 實作效能 regression gate 時的判斷：門檻判定放在 JMeter 之外的哪一段、逐筆 assertion 與分佈門檻的分工、負載模型與施壓端設定怎麼影響前後兩版的比較、要保存的執行條件，以及 gate 失敗時怎麼把誤判和真正的退化分開">JMeter 在效能 regression gate 的用法：JMeter 之外的門檻判定、逐筆 assertion 與統計門檻的分工、thread group 的負載模型、施壓端設定與基準的可比性、PR 與 nightly 的分工、證據保存與 gate 失敗的排查順序</a>；用 JMeter 逐級加壓找飽和點的做法寫在 <a href="/blog/backend/09-performance-capacity/vendors/jmeter/jmeter-capacity-probing/" data-link-title="JMeter 在容量探測的用法：thread group 固定的負載變數、逐級加壓、knee 的判讀、施壓端與分散施壓的檢查、多 protocol 的涵蓋範圍與排查順序" data-link-desc="用 JMeter 找飽和點時的判斷：thread group 與 timer 量的是哪一種負載模型、Open Model Thread Group 的逐級加壓、從 dashboard 判讀 knee、施壓端與 TLS 連線的失真、分散施壓，以及容量結論要附的條件">JMeter 在容量探測的用法：thread group 固定的負載變數、逐級加壓、knee 的判讀、施壓端與分散施壓的檢查、多 protocol 的涵蓋範圍與排查順序</a>。GUI 的操作、HTTP(S) Test Script Recorder 的錄製與 plugin 都不在這次的實測裡。</p>
<h2 id="受測服務">受測服務</h2>
<p>受測服務是一支只依賴 Python 標準函式庫的 HTTP 程式，<code>GET /items/&lt;id&gt;</code> 正常回應（<code>DELAY_MS</code> 環境變數可以加上固定延遲），<code>GET /flaky</code> 每 10 個請求回一次 500，<code>POST /login</code> 回一個 token。這篇不重列程式：開始之前先打開 <a href="/blog/backend/06-reliability/vendors/locust/hands-on/locust-local-lab/" data-link-title="Locust 在本機跑負載測試的實測紀錄：受測服務、locustfile、headless 執行與 CSV、內建的 exit code、quitting 事件裡的門檻判定、--reset-stats 與 on_start、master 與 worker 的分散執行" data-link-desc="要在本機用 Docker 照一次實測跑 Locust：寫 locustfile、headless 執行、讀終端機的統計表與 CSV、在 quitting 事件裡判定延遲門檻、開 master 與多個 worker 分散執行時讀；也用在受測服務變慢而 exit code 還是 0、加了 --reset-stats 之後登入請求還在統計裡、master 回報 worker failed to send heartbeat 的時候">Locust 在本機跑負載測試的實測紀錄</a> 的〈受測服務：標準函式庫寫的 HTTP 程式〉，把那段程式存成 <code>target/app.py</code>，那一節也寫了它為什麼這樣設計。</p>
<p>這篇的程式碼區塊裡，<code>#</code> 開頭的那幾行是說明或預期的輸出；macOS 內建的 zsh 在互動模式下預設不把 <code>#</code> 當成註解，貼上之前先在同一個終端機分頁執行一次 <code>setopt interactivecomments</code>（只對這個 shell 有效），否則那幾行會印出 <code>command not found: #</code>；用 bash 時不需要這一步，bash 沒有 <code>setopt</code> 這個指令。<code>-v</code> 掛載本機的目錄，在 macOS 的 Docker Desktop 上這個目錄要在 Settings 的 File sharing 列出的路徑裡（預設是 <code>/Users</code>、<code>/Volumes</code>、<code>/private</code>、<code>/tmp</code>、<code>/var/folders</code>），不在的話 <code>docker run</code> 回 <code>Mounts denied</code>（依 Docker Desktop 官方文件的設定說明，這次沒有實跑）；OrbStack 不限定路徑，<code>/Users</code> 以外的目錄也掛得進去。在 Linux 主機上，container 以 root 寫出的 <code>jmeter/out/</code> 與 <code>jmeter/jmeter.log</code> 歸 root 所有，CI 重用工作目錄時下一次清理要 sudo（這一點從 container 以 root 執行推出，沒有在 Linux 上實跑）。啟動方式相同：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker network create loadlab
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker run -d --rm --name target --network loadlab <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  -v <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">/target:/app:ro&#34;</span> python:3.12-slim python /app/app.py
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 從同一個 network 裡確認服務在回應，印出 {&#34;id&#34;: &#34;42&#34;} 就是起來了</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">docker run --rm --network loadlab curlimages/curl:8.10.1 -s http://target:8080/items/42</span></span></code></pre></div><p><code>loadlab</code> 是這次建的 Docker network，<code>target</code> 是受測服務的 container 名稱，也是 JMeter 在同一個 network 裡連它用的主機名；<code>$PWD/target</code> 換成讀者放 <code>app.py</code> 的目錄。這篇的指令都在 <code>target/</code>、<code>jmeter/</code> 兩個目錄的上一層執行。自建的 network 讓 container 之間用名稱連線，後面的 <code>target</code>、<code>js1</code>、<code>jclient</code> 都靠它解析；改用 Docker 預設的 bridge network 時，container 之間不能用名稱連，要改填 IP，這次沒有實跑。</p>
<h2 id="自建-jmeter-的-image">自建 JMeter 的 image</h2>
<p>這次用 Java 的 JRE image 加上 Apache 的官方發行檔自己建，JMeter 的版本、Java 的版本與下載來源都寫在自己的 Dockerfile 裡。下面的內容存成 <code>jmeter/Dockerfile</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">FROM</span><span class="s"> eclipse-temurin:21-jre</span><span class="err">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="err"></span><span class="k">ARG</span> <span class="nv">JMETER_VERSION</span><span class="o">=</span><span class="m">5</span>.6.3<span class="err">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="err"></span><span class="k">RUN</span> apt-get update <span class="o">&amp;&amp;</span> apt-get install -y --no-install-recommends curl ca-certificates <span class="o">&amp;&amp;</span> rm -rf /var/lib/apt/lists/* <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span> <span class="o">&amp;&amp;</span> curl -fsSL https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-<span class="si">${</span><span class="nv">JMETER_VERSION</span><span class="si">}</span>.tgz -o /tmp/jmeter.tgz <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span> <span class="o">&amp;&amp;</span> curl -fsSL https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-<span class="si">${</span><span class="nv">JMETER_VERSION</span><span class="si">}</span>.tgz.sha512 -o /tmp/jmeter.tgz.sha512 <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">&#34;</span><span class="k">$(</span>cut -d<span class="s1">&#39; &#39;</span> -f1 /tmp/jmeter.tgz.sha512<span class="k">)</span><span class="s2">  /tmp/jmeter.tgz&#34;</span> <span class="p">|</span> sha512sum -c - <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span> <span class="o">&amp;&amp;</span> tar -xzf /tmp/jmeter.tgz -C /opt <span class="o">&amp;&amp;</span> rm /tmp/jmeter.tgz*<span class="err">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="err"></span><span class="k">ENV</span> <span class="nv">PATH</span><span class="o">=</span>/opt/apache-jmeter-<span class="si">${</span><span class="nv">JMETER_VERSION</span><span class="si">}</span>/bin:<span class="nv">$PATH</span><span class="err">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="err"></span><span class="k">WORKDIR</span><span class="s"> /mnt/jmeter</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker build -t lab-jmeter jmeter/
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># ASCII 字的 banner 最後一行結尾印出 5.6.3</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">docker run --rm lab-jmeter jmeter --version
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 後面的結果與報告都寫在 out/ 底下；JMeter 只建報告那一層資料夾，不建 out/ 本身</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">mkdir -p jmeter/out</span></span></code></pre></div><p><code>JMETER_VERSION</code> 寫死成 5.6.3，是建 image 當天 archive.apache.org 上最新的版本；基底的 Java 21 取的是當天的 LTS 版，沒有比較其他 Java 版本，而 <code>eclipse-temurin:21-jre</code> 這個 tag 會跟著 patch 版更新（這次是 21.0.12.1），要讓兩次執行的基底相同，改用帶完整版本的 tag 或 <code>@sha256:&lt;建 image 當天的 digest&gt;</code>（這次沒有實跑）。讀者重做時先查 archive.apache.org 的 <code>jmeter/binaries/</code> 目錄，版本不同時兩次結果要記下各自的版本才比得了。Dockerfile 裡的 <code>sha512sum -c</code> 拿官方同目錄的 <code>.sha512</code> 檔核對下載的檔案，雜湊對不上時 <code>docker build</code> 失敗、不會建出 image；省略這一步也建得出來，代價是下載被截斷或被替換時不會被發現。這次從 archive.apache.org 下載大約花了兩分鐘（依網路與當時 archive 的負載而變），官方的鏡像站（<code>dlcdn.apache.org</code>）分散在各地，這次沒有實測它的下載時間，但它只放最新版，版本更新之後舊版的網址就失效，所以 Dockerfile 用 archive。<code>jmeter/out</code> 不先建的話，第一次執行的 <code>-e -o out/report1</code> 回 exit 1，訊息是 <code>Cannot write to '/mnt/jmeter/out/report1' as folder does not exist and parent folder is not writable</code>。<code>jmeter/</code> 是放 Dockerfile 與 test plan 的目錄，<code>WORKDIR /mnt/jmeter</code> 是之後把它掛進 container 的位置，test plan 裡用相對路徑讀的 <code>items.csv</code> 從這裡找。</p>
<p>替代做法有兩種，這次都沒有實跑：在本機裝 Java 與 JMeter 的發行檔（官方文件〈Getting Started〉），換到的是可以開 GUI 編輯 test plan，付出的是本機的 Java 版本與 JVM 參數會影響結果；用社群維護的 image（Docker Hub 上有幾個），換到的是不必自己寫 Dockerfile，付出的是 JMeter 版本、預裝的 plugin 與 JVM 參數由維護者決定，要先讀它的 Dockerfile。</p>
<h2 id="test-plan-的元件">test plan 的元件</h2>
<p>test plan 是一個 <code>.jmx</code> 檔，平常在 JMeter 的 GUI 裡編輯、存檔就是這個格式；這次沒有開 GUI，直接手寫 XML，GUI 打開它會顯示同樣的樹狀結構。讀這棵樹之前要先知道三個名詞：JMeter 用一個 thread 代表一個虛擬使用者；thread 把 thread group 底下的元件由上到下執行一次叫一輪（iteration），跑完再從頭開始下一輪；sampler 是實際送出一個請求的元件（樹裡的 <code>POST /login</code>、<code>GET /items/[id]</code>、<code>GET /flaky</code>），每執行一次產生一筆 sample。元件由上而下是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">shop（Test Plan）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">├── HTTP Request Defaults    主機 ${__P(host,target)}、port ${__P(port,8080)}
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">├── items.csv（CSV Data Set）  每一輪讀一行，變數名取檔案第一行 item_id
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">└── shoppers（Thread Group）  thread 數、ramp-up（逐步啟動 thread 的期間）、持續時間都從屬性讀
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    ├── roll 1-5（Random Variable）     每一輪產生 1 到 5 的隨機數
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    ├── login once（Once Only Controller）
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    │   └── POST /login  ── token（JSON Extractor，$.token 存進 ${token}）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    ├── items 4/5（If Controller：roll &lt;= 4）
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    │   └── GET /items/${item_id}  ── auth（Header Manager：Bearer ${token}）
</span></span><span class="line"><span class="ln">10</span><span class="cl">    ├── flaky 1/5（If Controller：roll == 5）
</span></span><span class="line"><span class="ln">11</span><span class="cl">    │   └── GET /flaky
</span></span><span class="line"><span class="ln">12</span><span class="cl">    └── think（Flow Control Action）── think 1-3s（Uniform Random Timer）</span></span></code></pre></div><p><code>.jmx</code> 的全文，存成 <code>jmeter/shop2.jmx</code>，同一個目錄放 <code>items.csv</code>（第一行是表頭 <code>item_id</code>，下面五行是 <code>101</code> 到 <code>105</code>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="ln">  1</span><span class="cl"><span class="cp">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span>
</span></span><span class="line"><span class="ln">  2</span><span class="cl"><span class="nt">&lt;jmeterTestPlan</span> <span class="na">version=</span><span class="s">&#34;1.2&#34;</span> <span class="na">properties=</span><span class="s">&#34;5.0&#34;</span> <span class="na">jmeter=</span><span class="s">&#34;5.6.3&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln">  3</span><span class="cl">  <span class="nt">&lt;hashTree&gt;</span>
</span></span><span class="line"><span class="ln">  4</span><span class="cl">    <span class="nt">&lt;TestPlan</span> <span class="na">guiclass=</span><span class="s">&#34;TestPlanGui&#34;</span> <span class="na">testclass=</span><span class="s">&#34;TestPlan&#34;</span> <span class="na">testname=</span><span class="s">&#34;shop&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln">  5</span><span class="cl">      <span class="nt">&lt;elementProp</span> <span class="na">name=</span><span class="s">&#34;TestPlan.user_defined_variables&#34;</span> <span class="na">elementType=</span><span class="s">&#34;Arguments&#34;</span> <span class="na">guiclass=</span><span class="s">&#34;ArgumentsPanel&#34;</span> <span class="na">testclass=</span><span class="s">&#34;Arguments&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln">  6</span><span class="cl">        <span class="nt">&lt;collectionProp</span> <span class="na">name=</span><span class="s">&#34;Arguments.arguments&#34;</span><span class="nt">/&gt;</span>
</span></span><span class="line"><span class="ln">  7</span><span class="cl">      <span class="nt">&lt;/elementProp&gt;</span>
</span></span><span class="line"><span class="ln">  8</span><span class="cl">      <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;TestPlan.functional_mode&#34;</span><span class="nt">&gt;</span>false<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln">  9</span><span class="cl">      <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;TestPlan.serialize_threadgroups&#34;</span><span class="nt">&gt;</span>false<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln"> 10</span><span class="cl">    <span class="nt">&lt;/TestPlan&gt;</span>
</span></span><span class="line"><span class="ln"> 11</span><span class="cl">    <span class="nt">&lt;hashTree&gt;</span>
</span></span><span class="line"><span class="ln"> 12</span><span class="cl">      <span class="nt">&lt;ConfigTestElement</span> <span class="na">guiclass=</span><span class="s">&#34;HttpDefaultsGui&#34;</span> <span class="na">testclass=</span><span class="s">&#34;ConfigTestElement&#34;</span> <span class="na">testname=</span><span class="s">&#34;HTTP Request Defaults&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 13</span><span class="cl">        <span class="nt">&lt;elementProp</span> <span class="na">name=</span><span class="s">&#34;HTTPsampler.Arguments&#34;</span> <span class="na">elementType=</span><span class="s">&#34;Arguments&#34;</span> <span class="na">guiclass=</span><span class="s">&#34;HTTPArgumentsPanel&#34;</span> <span class="na">testclass=</span><span class="s">&#34;Arguments&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 14</span><span class="cl">          <span class="nt">&lt;collectionProp</span> <span class="na">name=</span><span class="s">&#34;Arguments.arguments&#34;</span><span class="nt">/&gt;</span>
</span></span><span class="line"><span class="ln"> 15</span><span class="cl">        <span class="nt">&lt;/elementProp&gt;</span>
</span></span><span class="line"><span class="ln"> 16</span><span class="cl">        <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;HTTPSampler.domain&#34;</span><span class="nt">&gt;</span>${__P(host,target)}<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 17</span><span class="cl">        <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;HTTPSampler.port&#34;</span><span class="nt">&gt;</span>${__P(port,8080)}<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 18</span><span class="cl">        <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;HTTPSampler.protocol&#34;</span><span class="nt">&gt;</span>http<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 19</span><span class="cl">      <span class="nt">&lt;/ConfigTestElement&gt;</span>
</span></span><span class="line"><span class="ln"> 20</span><span class="cl">      <span class="nt">&lt;hashTree/&gt;</span>
</span></span><span class="line"><span class="ln"> 21</span><span class="cl">      <span class="nt">&lt;CSVDataSet</span> <span class="na">guiclass=</span><span class="s">&#34;TestBeanGUI&#34;</span> <span class="na">testclass=</span><span class="s">&#34;CSVDataSet&#34;</span> <span class="na">testname=</span><span class="s">&#34;items.csv&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 22</span><span class="cl">        <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;filename&#34;</span><span class="nt">&gt;</span>items.csv<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 23</span><span class="cl">        <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;fileEncoding&#34;</span><span class="nt">&gt;</span>UTF-8<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 24</span><span class="cl">        <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;variableNames&#34;</span><span class="nt">&gt;&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 25</span><span class="cl">        <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;ignoreFirstLine&#34;</span><span class="nt">&gt;</span>false<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln"> 26</span><span class="cl">        <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;delimiter&#34;</span><span class="nt">&gt;</span>,<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 27</span><span class="cl">        <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;quotedData&#34;</span><span class="nt">&gt;</span>false<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln"> 28</span><span class="cl">        <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;recycle&#34;</span><span class="nt">&gt;</span>true<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln"> 29</span><span class="cl">        <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;stopThread&#34;</span><span class="nt">&gt;</span>false<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln"> 30</span><span class="cl">        <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;shareMode&#34;</span><span class="nt">&gt;</span>shareMode.all<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 31</span><span class="cl">      <span class="nt">&lt;/CSVDataSet&gt;</span>
</span></span><span class="line"><span class="ln"> 32</span><span class="cl">      <span class="nt">&lt;hashTree/&gt;</span>
</span></span><span class="line"><span class="ln"> 33</span><span class="cl">      <span class="nt">&lt;ThreadGroup</span> <span class="na">guiclass=</span><span class="s">&#34;ThreadGroupGui&#34;</span> <span class="na">testclass=</span><span class="s">&#34;ThreadGroup&#34;</span> <span class="na">testname=</span><span class="s">&#34;shoppers&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 34</span><span class="cl">        <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;ThreadGroup.on_sample_error&#34;</span><span class="nt">&gt;</span>continue<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 35</span><span class="cl">        <span class="nt">&lt;elementProp</span> <span class="na">name=</span><span class="s">&#34;ThreadGroup.main_controller&#34;</span> <span class="na">elementType=</span><span class="s">&#34;LoopController&#34;</span> <span class="na">guiclass=</span><span class="s">&#34;LoopControlPanel&#34;</span> <span class="na">testclass=</span><span class="s">&#34;LoopController&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 36</span><span class="cl">          <span class="nt">&lt;intProp</span> <span class="na">name=</span><span class="s">&#34;LoopController.loops&#34;</span><span class="nt">&gt;</span>-1<span class="nt">&lt;/intProp&gt;</span>
</span></span><span class="line"><span class="ln"> 37</span><span class="cl">          <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;LoopController.continue_forever&#34;</span><span class="nt">&gt;</span>false<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln"> 38</span><span class="cl">        <span class="nt">&lt;/elementProp&gt;</span>
</span></span><span class="line"><span class="ln"> 39</span><span class="cl">        <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;ThreadGroup.num_threads&#34;</span><span class="nt">&gt;</span>${__P(threads,10)}<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 40</span><span class="cl">        <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;ThreadGroup.ramp_time&#34;</span><span class="nt">&gt;</span>${__P(rampup,5)}<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 41</span><span class="cl">        <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;ThreadGroup.scheduler&#34;</span><span class="nt">&gt;</span>true<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln"> 42</span><span class="cl">        <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;ThreadGroup.duration&#34;</span><span class="nt">&gt;</span>${__P(duration,30)}<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 43</span><span class="cl">        <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;ThreadGroup.delay&#34;</span><span class="nt">&gt;&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 44</span><span class="cl">        <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;ThreadGroup.same_user_on_next_iteration&#34;</span><span class="nt">&gt;</span>true<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln"> 45</span><span class="cl">      <span class="nt">&lt;/ThreadGroup&gt;</span>
</span></span><span class="line"><span class="ln"> 46</span><span class="cl">      <span class="nt">&lt;hashTree&gt;</span>
</span></span><span class="line"><span class="ln"> 47</span><span class="cl">        <span class="nt">&lt;RandomVariableConfig</span> <span class="na">guiclass=</span><span class="s">&#34;TestBeanGUI&#34;</span> <span class="na">testclass=</span><span class="s">&#34;RandomVariableConfig&#34;</span> <span class="na">testname=</span><span class="s">&#34;roll 1-5&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 48</span><span class="cl">          <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;variableName&#34;</span><span class="nt">&gt;</span>roll<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 49</span><span class="cl">          <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;outputFormat&#34;</span><span class="nt">&gt;&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 50</span><span class="cl">          <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;minimumValue&#34;</span><span class="nt">&gt;</span>1<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 51</span><span class="cl">          <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;maximumValue&#34;</span><span class="nt">&gt;</span>5<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 52</span><span class="cl">          <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;randomSeed&#34;</span><span class="nt">&gt;&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 53</span><span class="cl">          <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;perThread&#34;</span><span class="nt">&gt;</span>false<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln"> 54</span><span class="cl">        <span class="nt">&lt;/RandomVariableConfig&gt;</span>
</span></span><span class="line"><span class="ln"> 55</span><span class="cl">        <span class="nt">&lt;hashTree/&gt;</span>
</span></span><span class="line"><span class="ln"> 56</span><span class="cl">        <span class="nt">&lt;OnceOnlyController</span> <span class="na">guiclass=</span><span class="s">&#34;OnceOnlyControllerGui&#34;</span> <span class="na">testclass=</span><span class="s">&#34;OnceOnlyController&#34;</span> <span class="na">testname=</span><span class="s">&#34;login once&#34;</span><span class="nt">/&gt;</span>
</span></span><span class="line"><span class="ln"> 57</span><span class="cl">        <span class="nt">&lt;hashTree&gt;</span>
</span></span><span class="line"><span class="ln"> 58</span><span class="cl">          <span class="nt">&lt;HTTPSamplerProxy</span> <span class="na">guiclass=</span><span class="s">&#34;HttpTestSampleGui&#34;</span> <span class="na">testclass=</span><span class="s">&#34;HTTPSamplerProxy&#34;</span> <span class="na">testname=</span><span class="s">&#34;POST /login&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 59</span><span class="cl">            <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;HTTPSampler.postBodyRaw&#34;</span><span class="nt">&gt;</span>true<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln"> 60</span><span class="cl">            <span class="nt">&lt;elementProp</span> <span class="na">name=</span><span class="s">&#34;HTTPsampler.Arguments&#34;</span> <span class="na">elementType=</span><span class="s">&#34;Arguments&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 61</span><span class="cl">              <span class="nt">&lt;collectionProp</span> <span class="na">name=</span><span class="s">&#34;Arguments.arguments&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 62</span><span class="cl">                <span class="nt">&lt;elementProp</span> <span class="na">name=</span><span class="s">&#34;&#34;</span> <span class="na">elementType=</span><span class="s">&#34;HTTPArgument&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 63</span><span class="cl">                  <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;HTTPArgument.always_encode&#34;</span><span class="nt">&gt;</span>false<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln"> 64</span><span class="cl">                  <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;Argument.value&#34;</span><span class="nt">&gt;</span>{&#34;user&#34;:&#34;load-test&#34;}<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 65</span><span class="cl">                  <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;Argument.metadata&#34;</span><span class="nt">&gt;</span>=<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 66</span><span class="cl">                <span class="nt">&lt;/elementProp&gt;</span>
</span></span><span class="line"><span class="ln"> 67</span><span class="cl">              <span class="nt">&lt;/collectionProp&gt;</span>
</span></span><span class="line"><span class="ln"> 68</span><span class="cl">            <span class="nt">&lt;/elementProp&gt;</span>
</span></span><span class="line"><span class="ln"> 69</span><span class="cl">            <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;HTTPSampler.path&#34;</span><span class="nt">&gt;</span>/login<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 70</span><span class="cl">            <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;HTTPSampler.method&#34;</span><span class="nt">&gt;</span>POST<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 71</span><span class="cl">            <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;HTTPSampler.use_keepalive&#34;</span><span class="nt">&gt;</span>true<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln"> 72</span><span class="cl">          <span class="nt">&lt;/HTTPSamplerProxy&gt;</span>
</span></span><span class="line"><span class="ln"> 73</span><span class="cl">          <span class="nt">&lt;hashTree&gt;</span>
</span></span><span class="line"><span class="ln"> 74</span><span class="cl">            <span class="nt">&lt;JSONPostProcessor</span> <span class="na">guiclass=</span><span class="s">&#34;JSONPostProcessorGui&#34;</span> <span class="na">testclass=</span><span class="s">&#34;JSONPostProcessor&#34;</span> <span class="na">testname=</span><span class="s">&#34;token&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 75</span><span class="cl">              <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;JSONPostProcessor.referenceNames&#34;</span><span class="nt">&gt;</span>token<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 76</span><span class="cl">              <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;JSONPostProcessor.jsonPathExprs&#34;</span><span class="nt">&gt;</span>$.token<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 77</span><span class="cl">              <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;JSONPostProcessor.match_numbers&#34;</span><span class="nt">&gt;</span>1<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 78</span><span class="cl">              <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;JSONPostProcessor.defaultValues&#34;</span><span class="nt">&gt;</span>NOT_FOUND<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 79</span><span class="cl">            <span class="nt">&lt;/JSONPostProcessor&gt;</span>
</span></span><span class="line"><span class="ln"> 80</span><span class="cl">            <span class="nt">&lt;hashTree/&gt;</span>
</span></span><span class="line"><span class="ln"> 81</span><span class="cl">          <span class="nt">&lt;/hashTree&gt;</span>
</span></span><span class="line"><span class="ln"> 82</span><span class="cl">        <span class="nt">&lt;/hashTree&gt;</span>
</span></span><span class="line"><span class="ln"> 83</span><span class="cl">        <span class="nt">&lt;IfController</span> <span class="na">guiclass=</span><span class="s">&#34;IfControllerPanel&#34;</span> <span class="na">testclass=</span><span class="s">&#34;IfController&#34;</span> <span class="na">testname=</span><span class="s">&#34;items 4/5&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 84</span><span class="cl">          <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;IfController.condition&#34;</span><span class="nt">&gt;</span>${__jexl3(${roll} <span class="ni">&amp;lt;</span>= 4)}<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 85</span><span class="cl">          <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;IfController.evaluateAll&#34;</span><span class="nt">&gt;</span>false<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln"> 86</span><span class="cl">          <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;IfController.useExpression&#34;</span><span class="nt">&gt;</span>true<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln"> 87</span><span class="cl">        <span class="nt">&lt;/IfController&gt;</span>
</span></span><span class="line"><span class="ln"> 88</span><span class="cl">        <span class="nt">&lt;hashTree&gt;</span>
</span></span><span class="line"><span class="ln"> 89</span><span class="cl">          <span class="nt">&lt;HTTPSamplerProxy</span> <span class="na">guiclass=</span><span class="s">&#34;HttpTestSampleGui&#34;</span> <span class="na">testclass=</span><span class="s">&#34;HTTPSamplerProxy&#34;</span> <span class="na">testname=</span><span class="s">&#34;GET /items/[id]&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 90</span><span class="cl">            <span class="nt">&lt;elementProp</span> <span class="na">name=</span><span class="s">&#34;HTTPsampler.Arguments&#34;</span> <span class="na">elementType=</span><span class="s">&#34;Arguments&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 91</span><span class="cl">              <span class="nt">&lt;collectionProp</span> <span class="na">name=</span><span class="s">&#34;Arguments.arguments&#34;</span><span class="nt">/&gt;</span>
</span></span><span class="line"><span class="ln"> 92</span><span class="cl">            <span class="nt">&lt;/elementProp&gt;</span>
</span></span><span class="line"><span class="ln"> 93</span><span class="cl">            <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;HTTPSampler.path&#34;</span><span class="nt">&gt;</span>/items/${item_id}<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 94</span><span class="cl">            <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;HTTPSampler.method&#34;</span><span class="nt">&gt;</span>GET<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln"> 95</span><span class="cl">            <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;HTTPSampler.use_keepalive&#34;</span><span class="nt">&gt;</span>true<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln"> 96</span><span class="cl">          <span class="nt">&lt;/HTTPSamplerProxy&gt;</span>
</span></span><span class="line"><span class="ln"> 97</span><span class="cl">          <span class="nt">&lt;hashTree&gt;</span>
</span></span><span class="line"><span class="ln"> 98</span><span class="cl">            <span class="nt">&lt;HeaderManager</span> <span class="na">guiclass=</span><span class="s">&#34;HeaderPanel&#34;</span> <span class="na">testclass=</span><span class="s">&#34;HeaderManager&#34;</span> <span class="na">testname=</span><span class="s">&#34;auth&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 99</span><span class="cl">              <span class="nt">&lt;collectionProp</span> <span class="na">name=</span><span class="s">&#34;HeaderManager.headers&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln">100</span><span class="cl">                <span class="nt">&lt;elementProp</span> <span class="na">name=</span><span class="s">&#34;&#34;</span> <span class="na">elementType=</span><span class="s">&#34;Header&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln">101</span><span class="cl">                  <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;Header.name&#34;</span><span class="nt">&gt;</span>Authorization<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln">102</span><span class="cl">                  <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;Header.value&#34;</span><span class="nt">&gt;</span>Bearer ${token}<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln">103</span><span class="cl">                <span class="nt">&lt;/elementProp&gt;</span>
</span></span><span class="line"><span class="ln">104</span><span class="cl">              <span class="nt">&lt;/collectionProp&gt;</span>
</span></span><span class="line"><span class="ln">105</span><span class="cl">            <span class="nt">&lt;/HeaderManager&gt;</span>
</span></span><span class="line"><span class="ln">106</span><span class="cl">            <span class="nt">&lt;hashTree/&gt;</span>
</span></span><span class="line"><span class="ln">107</span><span class="cl">          <span class="nt">&lt;/hashTree&gt;</span>
</span></span><span class="line"><span class="ln">108</span><span class="cl">        <span class="nt">&lt;/hashTree&gt;</span>
</span></span><span class="line"><span class="ln">109</span><span class="cl">        <span class="nt">&lt;IfController</span> <span class="na">guiclass=</span><span class="s">&#34;IfControllerPanel&#34;</span> <span class="na">testclass=</span><span class="s">&#34;IfController&#34;</span> <span class="na">testname=</span><span class="s">&#34;flaky 1/5&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln">110</span><span class="cl">          <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;IfController.condition&#34;</span><span class="nt">&gt;</span>${__jexl3(${roll} == 5)}<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln">111</span><span class="cl">          <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;IfController.evaluateAll&#34;</span><span class="nt">&gt;</span>false<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln">112</span><span class="cl">          <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;IfController.useExpression&#34;</span><span class="nt">&gt;</span>true<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln">113</span><span class="cl">        <span class="nt">&lt;/IfController&gt;</span>
</span></span><span class="line"><span class="ln">114</span><span class="cl">        <span class="nt">&lt;hashTree&gt;</span>
</span></span><span class="line"><span class="ln">115</span><span class="cl">          <span class="nt">&lt;HTTPSamplerProxy</span> <span class="na">guiclass=</span><span class="s">&#34;HttpTestSampleGui&#34;</span> <span class="na">testclass=</span><span class="s">&#34;HTTPSamplerProxy&#34;</span> <span class="na">testname=</span><span class="s">&#34;GET /flaky&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln">116</span><span class="cl">            <span class="nt">&lt;elementProp</span> <span class="na">name=</span><span class="s">&#34;HTTPsampler.Arguments&#34;</span> <span class="na">elementType=</span><span class="s">&#34;Arguments&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln">117</span><span class="cl">              <span class="nt">&lt;collectionProp</span> <span class="na">name=</span><span class="s">&#34;Arguments.arguments&#34;</span><span class="nt">/&gt;</span>
</span></span><span class="line"><span class="ln">118</span><span class="cl">            <span class="nt">&lt;/elementProp&gt;</span>
</span></span><span class="line"><span class="ln">119</span><span class="cl">            <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;HTTPSampler.path&#34;</span><span class="nt">&gt;</span>/flaky<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln">120</span><span class="cl">            <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;HTTPSampler.method&#34;</span><span class="nt">&gt;</span>GET<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln">121</span><span class="cl">            <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;HTTPSampler.use_keepalive&#34;</span><span class="nt">&gt;</span>true<span class="nt">&lt;/boolProp&gt;</span>
</span></span><span class="line"><span class="ln">122</span><span class="cl">          <span class="nt">&lt;/HTTPSamplerProxy&gt;</span>
</span></span><span class="line"><span class="ln">123</span><span class="cl">          <span class="nt">&lt;hashTree/&gt;</span>
</span></span><span class="line"><span class="ln">124</span><span class="cl">        <span class="nt">&lt;/hashTree&gt;</span>
</span></span><span class="line"><span class="ln">125</span><span class="cl">        <span class="nt">&lt;TestAction</span> <span class="na">guiclass=</span><span class="s">&#34;TestActionGui&#34;</span> <span class="na">testclass=</span><span class="s">&#34;TestAction&#34;</span> <span class="na">testname=</span><span class="s">&#34;think&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln">126</span><span class="cl">          <span class="nt">&lt;intProp</span> <span class="na">name=</span><span class="s">&#34;ActionProcessor.action&#34;</span><span class="nt">&gt;</span>1<span class="nt">&lt;/intProp&gt;</span>
</span></span><span class="line"><span class="ln">127</span><span class="cl">          <span class="nt">&lt;intProp</span> <span class="na">name=</span><span class="s">&#34;ActionProcessor.target&#34;</span><span class="nt">&gt;</span>0<span class="nt">&lt;/intProp&gt;</span>
</span></span><span class="line"><span class="ln">128</span><span class="cl">          <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;ActionProcessor.duration&#34;</span><span class="nt">&gt;</span>0<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln">129</span><span class="cl">        <span class="nt">&lt;/TestAction&gt;</span>
</span></span><span class="line"><span class="ln">130</span><span class="cl">        <span class="nt">&lt;hashTree&gt;</span>
</span></span><span class="line"><span class="ln">131</span><span class="cl">          <span class="nt">&lt;UniformRandomTimer</span> <span class="na">guiclass=</span><span class="s">&#34;UniformRandomTimerGui&#34;</span> <span class="na">testclass=</span><span class="s">&#34;UniformRandomTimer&#34;</span> <span class="na">testname=</span><span class="s">&#34;think 1-3s&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln">132</span><span class="cl">            <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;ConstantTimer.delay&#34;</span><span class="nt">&gt;</span>1000<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln">133</span><span class="cl">            <span class="nt">&lt;stringProp</span> <span class="na">name=</span><span class="s">&#34;RandomTimer.range&#34;</span><span class="nt">&gt;</span>2000<span class="nt">&lt;/stringProp&gt;</span>
</span></span><span class="line"><span class="ln">134</span><span class="cl">          <span class="nt">&lt;/UniformRandomTimer&gt;</span>
</span></span><span class="line"><span class="ln">135</span><span class="cl">          <span class="nt">&lt;hashTree/&gt;</span>
</span></span><span class="line"><span class="ln">136</span><span class="cl">        <span class="nt">&lt;/hashTree&gt;</span>
</span></span><span class="line"><span class="ln">137</span><span class="cl">      <span class="nt">&lt;/hashTree&gt;</span>
</span></span><span class="line"><span class="ln">138</span><span class="cl">    <span class="nt">&lt;/hashTree&gt;</span>
</span></span><span class="line"><span class="ln">139</span><span class="cl">  <span class="nt">&lt;/hashTree&gt;</span>
</span></span><span class="line"><span class="ln">140</span><span class="cl"><span class="nt">&lt;/jmeterTestPlan&gt;</span></span></span></code></pre></div><p>各元件在這份 test plan 裡負責的事：</p>
<ul>
<li><strong>HTTP Request Defaults</strong>：所有 HTTP sampler 沒填的主機與 port 從這裡取，值寫成 <code>${__P(host,target)}</code>，執行時用 <code>-Jhost=...</code> 換掉，不必改 <code>.jmx</code>；<code>target</code> 是沒給屬性時的預設值。</li>
<li><strong>CSV Data Set</strong>：<code>items.csv</code> 第一行是表頭 <code>item_id</code>，下面五行是 <code>101</code> 到 <code>105</code>。<code>variableNames</code> 留空時，JMeter 把第一行當成變數名，所以 sampler 裡寫 <code>${item_id}</code>。<code>shareMode.all</code> 讓所有 thread 共用同一個讀取位置，每個 thread 每一輪讀下一行，讀到檔尾時 <code>recycle</code> 讓它回到開頭。這三個欄位接受別的值，這次都沒有實跑，依官方文件〈Component Reference〉的 CSV Data Set Config：<code>shareMode</code> 改成每個 thread 各自一份時，每個 thread 從第一行讀起，同一時間的請求集中在前幾行；<code>recycle</code> 改成 false、<code>stopThread</code> 改成 true 時，讀到檔尾的那個 thread 就停止，測試會在資料用完時提早結束。</li>
<li><strong>Thread Group</strong>：<code>num_threads</code>、<code>ramp_time</code>、<code>duration</code> 都從屬性讀；<code>loops</code> 是 <code>-1</code>（不限次數），由 <code>scheduler</code> 加 <code>duration</code> 決定什麼時候結束。這是 closed model：固定的 thread 數各自送出請求、等回應、停完再開始下一輪（<a href="/blog/backend/knowledge-cards/open-closed-workload-model/" data-link-title="Open and Closed Workload Model（開放與封閉負載模型）" data-link-desc="設計壓測的注入方式、要在固定到達率與固定同時在線人數之間選一種，或壓測量到的延遲低於 production、懷疑負載工具少送了請求時讀">Open and Closed Workload Model</a>）。<code>same_user_on_next_iteration</code> 是 GUI 上的 Same user on each iteration，勾選時連線在每一輪之間保留，這次的受測服務用 HTTP，沒有 TLS 握手的成本；換成 HTTPS 的服務時，這個選項決定每一輪要不要重做握手（<a href="/blog/backend/knowledge-cards/tls-handshake-cost/" data-link-title="TLS Handshake（TLS 握手）的成本與連線重用" data-link-desc="壓測量到的上限落在負載平衡器或反向代理的 CPU、Connect Time 隨負載上升，或要判斷負載工具該不該在每一輪重建連線時讀">TLS Handshake（TLS 握手）的成本與連線重用</a>）。不勾選時，<code>httpclient.reset_state_on_thread_group_iteration</code> 的預設值 true 讓每一輪開頭關掉連線，每一輪的第一個請求都重新建立連線，〈CLI mode 執行與 JTL〉數的 <code>Connect</code> 大於 0 的筆數會接近總輪數、不是 thread 數；sampler 的 keep-alive 關掉時，每個請求都重新建立連線（依官方文件與那張卡推出，這次沒有實跑）。Thread Group 本身也可以換：換成 5.5 起內建的 Open Model Thread Group 時，每次抵達建立一個新 thread、跑完一輪就結束，是 open model，每次抵達都重新建立連線，Once Only Controller 的登入也變成每次抵達一次；timer 換成 Constant Throughput Timer 或 Precise Throughput Timer 時是 pacing，仍然是 closed model（<a href="/blog/backend/knowledge-cards/open-closed-workload-model/" data-link-title="Open and Closed Workload Model（開放與封閉負載模型）" data-link-desc="設計壓測的注入方式、要在固定到達率與固定同時在線人數之間選一種，或壓測量到的延遲低於 production、懷疑負載工具少送了請求時讀">Open and Closed Workload Model</a> 的 pacing 那一段）。這兩種這次都沒有實跑。<code>on_sample_error</code> 是 <code>continue</code>：一個 sample 失敗時，這一輪接著跑後面的元件；改成 Stop Test 時，第一個 <code>/flaky</code> 回 500 就結束整次測試，改成 Start Next Thread Loop 時跳過這一輪剩下的元件（官方文件〈Component Reference〉的 Thread Group，這次沒有實跑）。</li>
<li><strong>Random Variable 與兩個 If Controller</strong>：每一輪開頭產生一個 1 到 5 的整數 <code>roll</code>，<code>roll</code> 小於等於 4 時送 <code>GET /items/[id]</code>、等於 5 時送 <code>GET /flaky</code>，兩者互斥，比例平均是四比一。為什麼不用 Throughput Controller，見〈Throughput Controller 與 CSV Data Set 的週期對齊〉。</li>
<li><strong>Once Only Controller 與 JSON Extractor</strong>：每個 thread 只在第一輪送 <code>POST /login</code>，JSON Extractor 從回應取出 <code>$.token</code> 存進這個 thread 的變數 <code>token</code>，之後每一輪的 Header Manager 用它。登入寫在每一輪裡的話，每一輪都多一次登入，登入的 endpoint 變成主要的負載來源。</li>
<li><strong>Flow Control Action 加 Uniform Random Timer</strong>：timer 讓 thread 在它範圍內的每一個 sampler 執行之前停一次，範圍是 timer 所在那一層與底下的元件；掛在 thread group 底下時，登入、商品頁、會失敗的 endpoint 前面都會停。這次把 timer 掛在一個 Flow Control Action 底下，放在每一輪的最後，所以每一輪只停一次，停 1000 毫秒加上 0 到 2000 毫秒的隨機值。Flow Control Action 不產生 sample，JTL 裡沒有它的列。</li>
<li><strong>sampler 的名稱</strong>：<code>GET /items/[id]</code> 是這個 sampler 在 JTL 與報告裡的 label，<code>[id]</code> 只是給人讀的名稱；不同商品編號的請求都算進同一個 label，因為 JMeter 依 sampler 的名稱分列統計，不依實際送出的網址。</li>
</ul>
<h2 id="throughput-controller-與-csv-data-set-的週期對齊某個商品編號沒被請求到的成因與改法">Throughput Controller 與 CSV Data Set 的週期對齊：某個商品編號沒被請求到的成因與改法</h2>
<p>第一版的 test plan 用兩個 Throughput Controller 控制比例：一個設成 80% 包住商品頁，一個設成 20% 包住會失敗的 endpoint。要重現這一段時，把上一節的 <code>.jmx</code> 另存一份 <code>jmeter/shop1.jmx</code>，刪掉 <code>roll 1-5</code> 那個 <code>RandomVariableConfig</code>（連同它後面的 <code>&lt;hashTree/&gt;</code>），再把兩個 <code>IfController</code> 元素各換成一個 Throughput Controller，各自後面那段包著 sampler 的 <code>&lt;hashTree&gt;…&lt;/hashTree&gt;</code> 保留不動。商品頁那一個是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">&lt;ThroughputController</span> <span class="na">guiclass=</span><span class="s">&#34;ThroughputControllerGui&#34;</span> <span class="na">testclass=</span><span class="s">&#34;ThroughputController&#34;</span> <span class="na">testname=</span><span class="s">&#34;items 80%&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nt">&lt;intProp</span> <span class="na">name=</span><span class="s">&#34;ThroughputController.style&#34;</span><span class="nt">&gt;</span>1<span class="nt">&lt;/intProp&gt;</span>              <span class="c">&lt;!-- 1 是百分比模式 --&gt;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nt">&lt;boolProp</span> <span class="na">name=</span><span class="s">&#34;ThroughputController.perThread&#34;</span><span class="nt">&gt;</span>false<span class="nt">&lt;/boolProp&gt;</span>    <span class="c">&lt;!-- 計數由所有 thread 共用 --&gt;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nt">&lt;intProp</span> <span class="na">name=</span><span class="s">&#34;ThroughputController.maxThroughput&#34;</span><span class="nt">&gt;</span>1<span class="nt">&lt;/intProp&gt;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nt">&lt;FloatProperty&gt;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nt">&lt;name&gt;</span>ThroughputController.percentThroughput<span class="nt">&lt;/name&gt;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nt">&lt;value&gt;</span>80.0<span class="nt">&lt;/value&gt;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nt">&lt;savedValue&gt;</span>0.0<span class="nt">&lt;/savedValue&gt;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nt">&lt;/FloatProperty&gt;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nt">&lt;/ThroughputController&gt;</span></span></span></code></pre></div><p>會失敗的 endpoint 那一個只差 <code>testname</code> 與 <code>&lt;value&gt;</code>（<code>flaky 20%</code>、<code>20.0</code>）。兩個 Throughput Controller 各自用全域的計數決定這一輪要不要執行，下文的算式代入之後，20% 那一個只在 80% 那一個跳過的那幾輪執行，兩者剛好互補，每一輪送出其中一個請求，每 5 輪剛好 4 輪商品頁、1 輪會失敗的 endpoint（這次 JTL 裡有請求的 141 輪中商品頁 113 筆、會失敗的 endpoint 28 筆；duration 到時已經開始、還沒送出請求的那幾輪不在 JTL 裡）。跑完之後依 URL 數 JTL 裡商品頁的請求：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker run --rm --network loadlab -v <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">/jmeter:/mnt/jmeter&#34;</span> lab-jmeter <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  jmeter -n -t shop1.jmx -l out/run1.jtl -e -o out/report1 -f -Jthreads<span class="o">=</span><span class="m">10</span> -Jduration<span class="o">=</span><span class="m">30</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">awk -F, <span class="s1">&#39;NR&gt;1 &amp;&amp; $3 ~ /items/ {print $14}&#39;</span> jmeter/out/run1.jtl <span class="p">|</span> sort <span class="p">|</span> uniq -c
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">#   28 http://target:8080/items/101</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">#   28 http://target:8080/items/102</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">#   29 http://target:8080/items/104</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">#   28 http://target:8080/items/105</span></span></span></code></pre></div><p>這條 <code>jmeter</code> 指令的參數在〈CLI mode 執行與 JTL〉逐一說明；<code>-f</code> 讓重跑時先刪掉上一次的 <code>out/run1.jtl</code> 與 <code>out/report1</code>。<code>$3</code> 是 JTL 的 <code>label</code> 欄，<code>$14</code> 是 <code>URL</code> 欄；<code>awk -F,</code> 只在欄位裡沒有逗號時對得上欄號，錯誤訊息這類欄位帶逗號時 JMeter 會加引號，後面的欄號跟著位移，這時改用懂 CSV 引號的工具（例如 Python 的 <code>csv</code> 模組）。這次的錯誤訊息沒有逗號，所以對得上。<code>103</code> 一次都沒有出現，報告上的錯誤率、延遲看不出任何異樣。成因是兩個各自正確的元件週期對齊了。</p>
<p>Throughput Controller 的百分比模式是確定性的計數。5.6.3 的原始碼 <code>ThroughputController</code> 在測試開始時把「輪數」設成 -1，每一輪開頭先加 1（所以第 1 輪的輪數是 0），再拿「這一輪之前已執行的次數」代入 <code>(100 × 已執行次數 + 50) ÷ (輪數 + 1)</code>，小於設定的百分比時才執行；<code>perThread</code> 是 false 時，兩個計數都是所有 thread 合計的值。以 80% 代入，第 1、2 輪的值是 50、75，都執行；第 3 輪是 250 ÷ 3 = 83.3，跳過；之後第 8、13、18……輪被跳過，也就是 Throughput Controller 每 5 輪固定跳過 1 輪。20% 那一個代入同一個算式，正好只在第 3、8、13……輪執行。</p>
<p>CSV Data Set 在 <code>shareMode.all</code> 下同樣由所有 thread 共用，每一輪往下讀一行，5 行一個循環。兩個全域的序列週期都是 5，商品頁被跳過的第 3、8、13……輪，每次都讀到 CSV 的第 3 行 <code>103</code>；改了百分比或 CSV 的行數之後，被跳過的輪次可能固定對到另一行，也可能分散到每一行：Throughput Controller 的週期與 CSV 的行數有公因數時，固定對到某幾行；兩者互質時，分散到每一行。thread 同時開始新的一輪時，讀 CSV 與 Throughput Controller 的判定之間可能被別的 thread 插進來而錯開一格，所以沒有停頓、thread 很多的負載下，那一行仍可能有少量請求，筆數明顯偏少（這一點從機制推出，沒有實跑）。</p>
<p>這次只是少了一個商品編號；資料多、比例不同時，同樣的對齊會讓 CSV 裡固定對到被跳過那一輪的那幾行資料永遠不被請求，受測服務的快取命中率跟著偏離 production，量到的延遲偏低（測試資料怎麼取得接近 production 的分佈，見 <a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a>）。改法是讓「這一輪送哪一個請求」跟 CSV 的位置無關，也就是〈test plan 的元件〉那一版的 Random Variable 加兩個 If Controller。改完之後照〈CLI mode 執行與 JTL〉的指令跑出 <code>out/run2.jtl</code>，把上面那條 <code>awk</code> 的檔名換成 <code>jmeter/out/run2.jtl</code> 再數，五個商品編號各有 19 到 23 筆；隨機數每次不同，這個筆數每次重跑都會變。這個改法的代價是比例變成隨機的：這次 125 輪裡會失敗的 endpoint 有 19 筆，佔 15%，不是 20%，兩次執行的比例不同時，<code>Total</code> 的延遲與錯誤率跟著變，樣本少的 PR 執行受影響最大。要同時保住精確的四比一，可以保留 Throughput Controller、改用 Random Variable 產生商品編號（不從 CSV 依序讀）；另一種做法是把 CSV 的行數改成跟 Throughput Controller 的週期互質，代價是改了比例或資料量之後要重新檢查一次。這兩種這次都沒有實跑。在 <code>roll 1-5</code> 填固定的 <code>randomSeed</code> 保不住四比一：固定的 seed 讓每次重跑抽到同一個序列，兩次執行的比例比較接近，而比例仍然由抽樣決定，加上所有 thread 共用同一個產生器，duration 結束時跑到第幾輪每次不同（依 <code>RandomVariableConfig</code> 的原始碼推出，這次沒有實跑）。</p>
<h2 id="cli-mode-執行與-jtl">CLI mode 執行與 JTL</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker run --rm --network loadlab -v <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">/jmeter:/mnt/jmeter&#34;</span> lab-jmeter <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  jmeter -n -t shop2.jmx -l out/run2.jtl -e -o out/report2 -f <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  -Jthreads<span class="o">=</span><span class="m">10</span> -Jduration<span class="o">=</span><span class="m">30</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;exit=</span><span class="nv">$?</span><span class="s2">&#34;</span></span></span></code></pre></div><p>參數的意思：<code>-n</code> 是 CLI mode（不開 GUI）；<code>-t</code> 是 test plan；<code>-l</code> 是 JTL 的路徑，每一個 sample 寫一行；<code>-e -o out/report2</code> 在執行結束後從 JTL 產生 HTML dashboard 到 <code>out/report2</code>；<code>-f</code> 先刪掉上一次留下的 JTL 與報告資料夾，沒有它時 <code>-o</code> 指向的資料夾不是空的就失敗（〈dashboard、statistics.json 與從 JTL 重新產生報告〉有這個錯誤的訊息）；不給 <code>-e -o</code> 時只寫 JTL、不產生 dashboard 與 <code>statistics.json</code>，後面的門檻判定就要改讀 JTL；<code>-Jthreads=10 -Jduration=30</code> 設定 JMeter 屬性，test plan 裡的 <code>${__P(threads,10)}</code> 讀到的就是它。<code>shop2.jmx</code> 是改用 If Controller 的那一版。10 個 thread、30 秒、ramp-up 5 秒是示範用的值，沒有比較其他值。</p>
<p>執行中每 30 秒（以時鐘對齊）印一行 summary，結束時印合計：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">summary =    135 in 00:00:32 =    4.3/s Avg:    39 Min:     1 Max:   644 Err:     2 (1.48%)</span></span></code></pre></div><p><code>135 in 00:00:32</code> 是 32 秒內 135 筆 sample，<code>4.3/s</code> 是平均每秒的 sample 數，<code>Avg</code>、<code>Min</code>、<code>Max</code> 是毫秒，<code>Err</code> 是失敗的筆數與比例。這次有 2 個 sample 失敗（<code>/flaky</code> 回 500），<code>echo &quot;exit=$?&quot;</code> 印出的是 <code>exit=0</code>。JMeter 的 CLI mode 正常跑完（thread group 照排程跑到 <code>duration</code>、沒有因為 test plan 讀不到或 <code>-o</code> 寫不進去這類錯誤而中止）時 exit code 是 0，失敗的 sample 不改變它（這次實測）；這份 test plan 沒有 assertion，assertion 判成失敗時同樣不改變 exit code，依據是官方文件的推論，見 <a href="/blog/backend/06-reliability/vendors/jmeter/jmeter-performance-regression-gate/" data-link-title="JMeter 在效能 regression gate 的用法：JMeter 之外的門檻判定、逐筆 assertion 與統計門檻的分工、thread group 的負載模型、施壓端設定與基準的可比性、PR 與 nightly 的分工、證據保存與 gate 失敗的排查順序" data-link-desc="用 JMeter 實作效能 regression gate 時的判斷：門檻判定放在 JMeter 之外的哪一段、逐筆 assertion 與分佈門檻的分工、負載模型與施壓端設定怎麼影響前後兩版的比較、要保存的執行條件，以及 gate 失敗時怎麼把誤判和真正的退化分開">JMeter 在效能 regression gate 的用法：JMeter 之外的門檻判定、逐筆 assertion 與統計門檻的分工、thread group 的負載模型、施壓端設定與基準的可比性、PR 與 nightly 的分工、證據保存與 gate 失敗的排查順序</a> 的〈JMeter 在 regression gate 裡負責的環節與 exit code 涵蓋的範圍〉。Locust 的約定相反，同一個受測服務、同樣有失敗的請求時回 1（<a href="/blog/backend/06-reliability/vendors/locust/hands-on/locust-local-lab/" data-link-title="Locust 在本機跑負載測試的實測紀錄：受測服務、locustfile、headless 執行與 CSV、內建的 exit code、quitting 事件裡的門檻判定、--reset-stats 與 on_start、master 與 worker 的分散執行" data-link-desc="要在本機用 Docker 照一次實測跑 Locust：寫 locustfile、headless 執行、讀終端機的統計表與 CSV、在 quitting 事件裡判定延遲門檻、開 master 與多個 worker 分散執行時讀；也用在受測服務變慢而 exit code 還是 0、加了 --reset-stats 之後登入請求還在統計裡、master 回報 worker failed to send heartbeat 的時候">Locust 在本機跑負載測試的實測紀錄</a> 的〈內建的 exit code：有失敗、沒有失敗但變慢〉）。所以 CI 只看 JMeter 的 exit code 的話，有失敗 sample 的那次執行照樣讓 job 通過；門檻要在 JMeter 之外讀結果判定，寫法在〈JMeter 之外的門檻判定〉。</p>
<p>JTL 的第一行是欄位名，這次的設定寫出的是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">timeStamp,elapsed,label,responseCode,responseMessage,threadName,dataType,success,failureMessage,bytes,sentBytes,grpThreads,allThreads,URL,Latency,IdleTime,Connect
</span></span><span class="line"><span class="ln">2</span><span class="cl">1793156978830,624,POST /login,200,OK,shoppers 1-4,text,true,,179,203,5,5,http://target:8080/login,624,0,371</span></span></code></pre></div><p><code>timeStamp</code> 是毫秒的 epoch，<code>elapsed</code> 是這個 sample 從送出到收完回應的毫秒數，<code>success</code> 是 <code>true</code> 或 <code>false</code>，<code>Latency</code> 是從送出請求到收到第一個回應位元組的毫秒數，<code>Connect</code> 是建立連線花的毫秒數（含 TLS 握手，官方〈Glossary〉；<code>Latency</code> 沒有扣掉它），<code>allThreads</code> 是當下所有 thread group 的活躍 thread 數。<code>threadName</code> 的格式是 thread group 的名稱、一個空格、第幾個 thread group、<code>-</code>、第幾個 thread（5.6.3 的原始碼 <code>AbstractThreadGroup</code>），所以這一筆是第 1 個 thread group <code>shoppers</code> 裡第 4 個 thread 的第一個請求，624 毫秒裡有 371 毫秒花在建立連線。<code>awk -F, 'NR&gt;1 &amp;&amp; $17&gt;0' jmeter/out/run2.jtl | wc -l</code>（<code>$17</code> 是 <code>Connect</code> 欄）數得出 <code>Connect</code> 大於 0 的剛好 10 筆，全是 <code>POST /login</code>，也就是每個 thread 的第一個請求。之後的請求重用同一條連線，<code>Connect</code> 是 0，這是 Same user on each iteration 勾選、sampler 開了 keep-alive 的結果。哪些欄位會寫出來由 <code>jmeter.properties</code> 的 <code>jmeter.save.saveservice.*</code> 決定，這次用預設值；兩次執行的這組屬性不同時，JTL 的欄位也不同，後處理要先核對欄位名。</p>
<h2 id="dashboardstatisticsjson-與從-jtl-重新產生報告">dashboard、statistics.json 與從 JTL 重新產生報告</h2>
<p><code>-e -o</code> 產生的 <code>out/report2/</code> 裡有 <code>index.html</code>（瀏覽器打開就是 dashboard）與 <code>statistics.json</code>。<code>statistics.json</code> 每個 label 一個物件，加上 <code>Total</code>；這次的值（只列幾個欄位）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">label            sampleCount  errorPct  pct2ResTime
</span></span><span class="line"><span class="ln">2</span><span class="cl">GET /items/[id]          106      0.00           76
</span></span><span class="line"><span class="ln">3</span><span class="cl">GET /flaky                19     10.53          203
</span></span><span class="line"><span class="ln">4</span><span class="cl">POST /login               10      0.00          644
</span></span><span class="line"><span class="ln">5</span><span class="cl">Total                    135      1.48          217</span></span></code></pre></div><p><code>pct1ResTime</code>、<code>pct2ResTime</code>、<code>pct3ResTime</code> 是三個百分位數的延遲，對應 <code>jmeter.properties</code> 裡的 <code>aggregate_rpt_pct1</code>、<code>aggregate_rpt_pct2</code>、<code>aggregate_rpt_pct3</code>，預設是第 90、95、99 百分位（這次的 image 裡這三行是註解掉的預設值）。有人在 <code>user.properties</code> 改了這三個值，同一個欄位名就代表另一個百分位數，所以門檻判定的程式讀 <code>pct2ResTime</code> 時，執行條件要一起記下這組屬性。<code>POST /login</code> 的 p95 是 644 毫秒，遠高於其他 label：每個 thread 的第一個請求要建立連線，登入又只有 10 筆，<code>Total</code> 被它拉高到 217 毫秒。這些延遲是這台筆電、這一次的值，受測服務與 JMeter 共用同一台機器的 CPU，同一份 test plan 重跑會變動，不能拿來當門檻。</p>
<p>報告可以不重跑測試、從保存的 JTL 重新產生：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker run --rm -v <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">/jmeter:/mnt/jmeter&#34;</span> lab-jmeter <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  jmeter -g out/run2.jtl -o out/report2b
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 再跑一次同一條：</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># An error occurred: Cannot write to &#39;/mnt/jmeter/out/report2b&#39; as folder is not empty</span></span></span></code></pre></div><p>第一次產生的 <code>statistics.json</code> 與執行時產生的那一份 sample 數相同。同一條指令再跑一次時，<code>-o</code> 指向的資料夾已經有內容，JMeter 回 exit 1；<code>-g</code> 這條指令要換一個空的資料夾，或先刪掉舊的。從 JTL 重新產生報告的用途是換一組設定重算：百分位數的設定改了、或要排除 ramp-up 的那一段時，拿同一份 JTL 加上不同的屬性產生報告，排除區間的屬性與寫法見 <a href="/blog/backend/06-reliability/vendors/jmeter/jmeter-performance-regression-gate/" data-link-title="JMeter 在效能 regression gate 的用法：JMeter 之外的門檻判定、逐筆 assertion 與統計門檻的分工、thread group 的負載模型、施壓端設定與基準的可比性、PR 與 nightly 的分工、證據保存與 gate 失敗的排查順序" data-link-desc="用 JMeter 實作效能 regression gate 時的判斷：門檻判定放在 JMeter 之外的哪一段、逐筆 assertion 與分佈門檻的分工、負載模型與施壓端設定怎麼影響前後兩版的比較、要保存的執行條件，以及 gate 失敗時怎麼把誤判和真正的退化分開">JMeter 在效能 regression gate 的用法：JMeter 之外的門檻判定、逐筆 assertion 與統計門檻的分工、thread group 的負載模型、施壓端設定與基準的可比性、PR 與 nightly 的分工、證據保存與 gate 失敗的排查順序</a> 的〈ramp-up 期間的樣本與統計區間〉。</p>
<h2 id="jmeter-之外的門檻判定">JMeter 之外的門檻判定</h2>
<p>下面這支程式存成 <code>jmeter/gate.py</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 讀 JMeter dashboard 產生的 statistics.json，依門檻決定 exit code。</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">#   用法：python gate.py &lt;statistics.json&gt; [label ...]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">#   不給 label 時判 Total；門檻從環境變數讀</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kn">import</span> <span class="nn">json</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">MAX_ERROR_PCT</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;GATE_MAX_ERROR_PCT&#34;</span><span class="p">,</span> <span class="s2">&#34;5&#34;</span><span class="p">))</span>   <span class="c1"># 百分比</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">MAX_P95_MS</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;GATE_MAX_P95_MS&#34;</span><span class="p">,</span> <span class="s2">&#34;300&#34;</span><span class="p">))</span>       <span class="c1"># pct2ResTime，預設是第 95 百分位</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">path</span><span class="p">,</span> <span class="n">labels</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">:]</span> <span class="ow">or</span> <span class="p">[</span><span class="s2">&#34;Total&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="n">stats</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">load</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="k">except</span> <span class="p">(</span><span class="ne">OSError</span><span class="p">,</span> <span class="ne">ValueError</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>      <span class="c1"># 報告沒有產生、JTL 是空的、檔案寫壞</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;GATE ERROR: cannot read </span><span class="si">{</span><span class="n">path</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="n">failed</span> <span class="o">=</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">missing</span> <span class="o">=</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="k">for</span> <span class="n">label</span> <span class="ow">in</span> <span class="n">labels</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="n">s</span> <span class="o">=</span> <span class="n">stats</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">label</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="c1"># 沒有這個 label、沒有樣本，或缺判定要用的欄位，都算缺資料</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="k">if</span> <span class="n">s</span> <span class="ow">is</span> <span class="kc">None</span> <span class="ow">or</span> <span class="n">s</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;sampleCount&#34;</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span> <span class="ow">or</span> <span class="s2">&#34;errorPct&#34;</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">s</span> <span class="ow">or</span> <span class="s2">&#34;pct2ResTime&#34;</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">s</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;GATE ERROR: no samples for </span><span class="si">{</span><span class="n">label</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="n">missing</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">        <span class="k">continue</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="n">problems</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="k">if</span> <span class="n">s</span><span class="p">[</span><span class="s2">&#34;errorPct&#34;</span><span class="p">]</span> <span class="o">&gt;</span> <span class="n">MAX_ERROR_PCT</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="n">problems</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;error </span><span class="si">{</span><span class="n">s</span><span class="p">[</span><span class="s1">&#39;errorPct&#39;</span><span class="p">]</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2">% &gt; </span><span class="si">{</span><span class="n">MAX_ERROR_PCT</span><span class="si">}</span><span class="s2">%&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="k">if</span> <span class="n">s</span><span class="p">[</span><span class="s2">&#34;pct2ResTime&#34;</span><span class="p">]</span> <span class="o">&gt;</span> <span class="n">MAX_P95_MS</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="n">problems</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;p95 </span><span class="si">{</span><span class="n">s</span><span class="p">[</span><span class="s1">&#39;pct2ResTime&#39;</span><span class="p">]</span><span class="si">:</span><span class="s2">.0f</span><span class="si">}</span><span class="s2"> ms &gt; </span><span class="si">{</span><span class="n">MAX_P95_MS</span><span class="si">}</span><span class="s2"> ms&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">    <span class="k">if</span> <span class="n">problems</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;GATE FAIL: </span><span class="si">{</span><span class="n">label</span><span class="si">}</span><span class="s2"> &#34;</span> <span class="o">+</span> <span class="s2">&#34;; &#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">problems</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">        <span class="n">failed</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;GATE PASS: </span><span class="si">{</span><span class="n">label</span><span class="si">}</span><span class="s2"> error </span><span class="si">{</span><span class="n">s</span><span class="p">[</span><span class="s1">&#39;errorPct&#39;</span><span class="p">]</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2">%, p95 </span><span class="si">{</span><span class="n">s</span><span class="p">[</span><span class="s1">&#39;pct2ResTime&#39;</span><span class="p">]</span><span class="si">:</span><span class="s2">.0f</span><span class="si">}</span><span class="s2"> ms&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="c1"># 門檻不成立排在缺資料之前：有資料的流程已經量到退化，這件事不因別的流程缺資料而消失</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">1</span> <span class="k">if</span> <span class="n">failed</span> <span class="k">else</span> <span class="mi">4</span> <span class="k">if</span> <span class="n">missing</span> <span class="k">else</span> <span class="mi">0</span><span class="p">)</span></span></span></code></pre></div><p>下面用一個 shell 函式包住 <code>docker run</code>：<code>docker run</code> 依 <code>-v &quot;$PWD/jmeter:/w&quot; -w /w</code> 把 <code>jmeter/</code> 掛進 container 的 <code>/w</code> 並設成工作目錄，<code>gate.py</code> 與 <code>out/</code> 都從那裡讀。寫成函式，而不是把 <code>docker run …</code> 那一串存進一個變數再用 <code>$變數 gate.py …</code> 呼叫，是因為 zsh 不會把變數的內容切成幾個詞，那種寫法在 zsh 底下會把整串當成一個指令名，回報找不到指令。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">gate<span class="o">()</span> <span class="o">{</span> docker run --rm -v <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">/jmeter:/w&#34;</span> -w /w python:3.12-slim python gate.py <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 不給 label，判 Total</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">gate out/report2/statistics.json
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># GATE PASS: Total error 1.48%, p95 217 ms               → exit 0</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">gate out/report2/statistics.json <span class="s2">&#34;GET /flaky&#34;</span> <span class="s2">&#34;GET /items/[id]&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># GATE FAIL: GET /flaky error 10.53% &gt; 5.0%</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># GATE PASS: GET /items/[id] error 0.00%, p95 76 ms      → exit 1</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">gate out/report2/statistics.json <span class="s2">&#34;GET /checkout&#34;</span> <span class="s2">&#34;GET /flaky&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># GATE ERROR: no samples for GET /checkout</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># GATE FAIL: GET /flaky error 10.53% &gt; 5.0%              → exit 1</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">gate out/report2/statistics.json <span class="s2">&#34;GET /checkout&#34;</span> <span class="s2">&#34;GET /items/[id]&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># GATE ERROR: no samples for GET /checkout</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># GATE PASS: GET /items/[id] error 0.00%, p95 76 ms      → exit 4</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">gate out/missing/statistics.json
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># GATE ERROR: cannot read out/missing/statistics.json: ... → exit 4</span></span></span></code></pre></div><p><code>GET /checkout</code> 是這份 test plan 裡沒有的 label，用來看缺資料時的結果。上面五條 <code>gate</code> 指令的結果顯示兩件事。第一件是判定範圍的影響：只判 <code>Total</code> 時，<code>/flaky</code> 10.53% 的錯誤率被大量成功的商品頁攤成 1.48%，門檻通過；逐個 label 判定時，<code>/flaky</code> 那一條失敗。逐個列 label 時只列每一輪都會送的請求：<code>POST /login</code> 每個 thread 只送一次，延遲包含建立連線的時間（〈dashboard、statistics.json 與從 JTL 重新產生報告〉那一節），審查時把它列進 label 實跑過，p95 575 毫秒，300 的固定門檻每一次都判它失敗。</p>
<p>第二件是缺資料的處理：exit code 4 是這支程式自己的約定，代表報告沒有產生或缺少某個 label 的資料，跟「門檻不成立」的 1 分開，CI 才分得出這次是退化還是測試本身沒跑成；同一次判定裡兩種都有時回 1，因為有資料的那個流程已經量到退化，這件事不因別的流程缺資料而消失。這個優先序與 <a href="/blog/backend/06-reliability/vendors/jmeter/jmeter-performance-regression-gate/" data-link-title="JMeter 在效能 regression gate 的用法：JMeter 之外的門檻判定、逐筆 assertion 與統計門檻的分工、thread group 的負載模型、施壓端設定與基準的可比性、PR 與 nightly 的分工、證據保存與 gate 失敗的排查順序" data-link-desc="用 JMeter 實作效能 regression gate 時的判斷：門檻判定放在 JMeter 之外的哪一段、逐筆 assertion 與分佈門檻的分工、負載模型與施壓端設定怎麼影響前後兩版的比較、要保存的執行條件，以及 gate 失敗時怎麼把誤判和真正的退化分開">JMeter 在效能 regression gate 的用法：JMeter 之外的門檻判定、逐筆 assertion 與統計門檻的分工、thread group 的負載模型、施壓端設定與基準的可比性、PR 與 nightly 的分工、證據保存與 gate 失敗的排查順序</a> 〈門檻判定的落點：自寫的後處理與外層工具〉那一支比較基準的程式相同；那一篇的程式也叫 <code>gate.py</code>，介面不同：那一支比較這一次與基準兩份 <code>statistics.json</code>，這一支只讀一份、拿固定門檻比；關鍵流程各自設門檻的理由也寫在那一篇。</p>
<p>exit code 4 涵蓋不到最常見的一種沒跑成：受測服務整個連不上時，每一筆 sample 都失敗，報告照樣產生，錯誤率是 100%，這支程式回的是 1，跟退化分不開；要把這種情形也歸到 4，另外判錯誤率是不是 100%，或在測試之前先打一次健康檢查（審查時另外實跑過：<code>-Jhost</code> 指向不存在的 container，summary 是 <code>Err: 7 (100.00%)</code>，<code>-e -o</code> 照樣產生 <code>statistics.json</code>，這支程式印 <code>GATE FAIL: Total error 100.00% &gt; 5.0%</code>、回 1）。受測環境只有一部分故障時（一台 replica 掛掉、連線被重設），健康檢查會通過，錯誤率落在 0 與 100% 之間，這兩種做法都把它判成退化；分辨的依據是失敗的類別：JTL 的 <code>responseCode</code> 以 <code>Non HTTP response code</code> 開頭的是連線層的錯誤，指向受測環境，HTTP 的錯誤狀態碼才可能是這一版程式的退化，錯誤率本身分不出來（把錯誤類別寫進判定這次沒有實跑）。門檻的環境變數不是數字時，<code>float()</code> 拋出例外，Python 同樣以 exit 1 結束；逐個 label 判定時，樣本少的 label（這次 <code>GET /flaky</code> 只有 19 筆）的 p95 與錯誤率每次重跑差很多，實際用時要另外設最少樣本數，不足時歸到 4：條件加在程式裡判缺資料的那一行，<code>sampleCount</code> 小於最少樣本數時跟 0 筆一樣記成缺資料（這兩點這次都沒有實跑）。最少樣本數要多少這篇沒有量，取法是讓基準版本在同一個環境重跑幾次，看那個 label 的 p95 波動降到可以接受時的樣本數（<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a>〈Baseline 設定〉）。</p>
<p><code>Total</code> 是英文語系下 dashboard 寫出的鍵名，JMeter 用別的語系產生報告時要先核對這個鍵。5 與 300 是佔位的門檻，實際的值從基準版本在同一個環境量到的結果取（<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">6.13 Performance Regression Gate</a>〈Baseline 設定〉）。這段程式也可以換成 shell 加 <code>jq</code>，或用 Taurus、jmeter-maven-plugin 這類工具內建的判定，這次都沒有實跑，各自的取捨見同一篇的〈門檻判定的落點：自寫的後處理與外層工具〉。</p>
<h2 id="remote-testing一台-client-與兩台-server">remote testing：一台 client 與兩台 server</h2>
<p>一台施壓機送不出需要的負載時，JMeter 的 remote testing 讓一台 client 把 test plan 送到幾台 server 上執行、收回結果（官方用語是 client 與 server，舊資料寫成 master 與 slave，也有資料把 server 稱為節點）。這次在同一個 Docker network 裡開兩台 server，每一台都掛載同一個 <code>jmeter/</code>：test plan 送到 server 上執行時，<code>items.csv</code> 是在 server 的工作目錄裡找，client 不會把資料檔送過去。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">for</span> i in <span class="m">1</span> 2<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  docker run -d --name js<span class="nv">$i</span> --network loadlab -v <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">/jmeter:/mnt/jmeter&#34;</span> lab-jmeter <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>    jmeter-server -Dserver.rmi.ssl.disable<span class="o">=</span><span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>    -Djava.rmi.server.hostname<span class="o">=</span>js<span class="nv">$i</span> -Dserver.rmi.localport<span class="o">=</span><span class="m">50000</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># server 的 JVM 要幾秒才起來，剛啟動就看 log 可能還是空的；跑過測試之後 log 最後幾行會變成測試的開始與結束</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">docker logs js1 <span class="p">|</span> grep -E <span class="s1">&#39;local port|Created remote object&#39;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># Using local port: 50000</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"># Created remote object: UnicastServerRef2 [liveRef: [endpoint:[js1:50000](local), ...</span></span></span></code></pre></div><p>client 指定這兩台 server 執行：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker run --rm --name jclient --network loadlab -v <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">/jmeter:/mnt/jmeter&#34;</span> lab-jmeter <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  jmeter -n -t shop2.jmx -R js1,js2 <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  -Jserver.rmi.ssl.disable<span class="o">=</span><span class="nb">true</span> -Djava.rmi.server.hostname<span class="o">=</span>jclient -Jclient.rmi.localport<span class="o">=</span><span class="m">60000</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  -Gthreads<span class="o">=</span><span class="m">5</span> -Gduration<span class="o">=</span><span class="m">20</span> -l out/remote.jtl -f
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># Configuring remote engine: js1</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># Configuring remote engine: js2</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># Starting distributed test with remote engines: [js2, js1] ...</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># summary =     86 in 00:00:22 =    3.8/s Avg:    87 Min:     2 Max:  1535 Err:     2 (2.33%)</span></span></span></code></pre></div><p>各參數的作用：</p>
<ul>
<li><strong><code>server.rmi.ssl.disable=true</code></strong>：client 與 server 之間用 Java RMI 溝通，JMeter 預設要求 RMI 走 SSL，要先用 <code>create-rmi-keystore.sh</code> 產生 keystore 並放到每一台（官方文件〈Remote Testing〉）。這次在一台機器的 Docker network 裡實驗，關掉 SSL；client 與 server 跨過不受控制的網路時，關掉 SSL 等於讓網路上的任何人都能對 server 下指令，要照官方文件建 keystore，這次沒有實跑那一條。client 與 server 兩邊都要設成同一個值。</li>
<li><strong><code>java.rmi.server.hostname</code></strong>：RMI 會把這台機器的位址告訴對方，對方再連回來；不設時 Java 取 container 自己解析到的位址，常常是對方連不到的位址，例如 loopback 位址（官方文件〈Remote Testing〉寫了 loopback 的情形；在 Docker 或 NAT 後面取到對方連不到的位址，是從 RMI 回報位址的機制推出，這次沒有實跑不設的情形）。每一台設成對方連得到它的名稱，這次用 container 名稱。</li>
<li><strong><code>server.rmi.localport</code> 與 <code>client.rmi.localport</code></strong>：把 RMI 用的 port 固定下來（server 的 50000、client 收結果的 60000）。官方文件〈Remote Testing〉的〈Tips〉寫明 client 連 server 時另外用 RMI registry 的 port（預設 1099，server 用 <code>server_port</code> 屬性改），server 把結果回傳給 client 時，client 從 <code>client.rmi.localport</code> 起最多開三個 port；所以跨主機時，server 那邊開 1099 與 50000，client 那邊開 60000 起的三個 port。這次同一個 network 裡沒有防火牆，不設這兩個屬性也連得上，沒有比較不設的情形。</li>
<li><strong><code>-R js1,js2</code></strong>：要執行的 server 名單。</li>
<li><strong><code>-G</code> 與 <code>-J</code></strong>：<code>-G</code> 設定的屬性送到每一台 server，<code>-J</code> 只設在 client 本身。</li>
<li><strong><code>-D</code> 與 <code>-J</code></strong>：<code>-D</code> 設定 Java 的系統屬性，<code>java.rmi.server.hostname</code> 是 Java 的 RMI 自己讀的系統屬性，所以用 <code>-D</code>；<code>server.rmi.ssl.disable</code> 是 JMeter 的屬性，這次 server 那邊用 <code>-D</code> 設、client 這邊用 <code>-J</code> 設，兩邊的 log 都印出 <code>Disabling SSL for RMI as server.rmi.ssl.disable is set to 'true'</code>：JMeter 的屬性以 Java 的系統屬性為預設值（5.6.3 的原始碼 <code>JMeterUtils.loadJMeterProperties</code>），所以 <code>-D</code> 設的值 JMeter 也讀得到；反過來，<code>-J</code> 不會設 Java 的系統屬性，<code>java.rmi.server.hostname</code> 只能用 <code>-D</code>。</li>
</ul>
<p><code>-G</code> 與 <code>-J</code> 的差別用登入的筆數就看得出來。每個 thread 只登入一次，所以登入筆數等於 thread 的總數：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">awk -F, <span class="s1">&#39;NR&gt;1 &amp;&amp; $3==&#34;POST /login&#34;&#39;</span> jmeter/out/remote.jtl <span class="p">|</span> wc -l
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># -Gthreads=5 的那一次印出 10</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 同一條 client 指令，把 -Gthreads=5 換成 -Jthreads=5、結果寫到另一個檔</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># （duration 縮短成 15 秒只是省時間；每個 thread 只登入一次，登入筆數只看 thread 數）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">docker run --rm --name jclient --network loadlab -v <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">/jmeter:/mnt/jmeter&#34;</span> lab-jmeter <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  jmeter -n -t shop2.jmx -R js1,js2 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  -Jserver.rmi.ssl.disable<span class="o">=</span><span class="nb">true</span> -Djava.rmi.server.hostname<span class="o">=</span>jclient -Jclient.rmi.localport<span class="o">=</span><span class="m">60000</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  -Jthreads<span class="o">=</span><span class="m">5</span> -Gduration<span class="o">=</span><span class="m">15</span> -l out/remote-j.jtl -f
</span></span><span class="line"><span class="ln">10</span><span class="cl">awk -F, <span class="s1">&#39;NR&gt;1 &amp;&amp; $3==&#34;POST /login&#34;&#39;</span> jmeter/out/remote-j.jtl <span class="p">|</span> wc -l
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 印出 20</span></span></span></code></pre></div><p><code>-Gthreads=5</code> 時每台 server 跑 5 個 thread，合計 10 個：每台 server 執行的是完整的 test plan，thread group 裡設定的 thread 數是每一台各自的量。Locust 的分散執行是反過來的：master 把 <code>-u</code> 個使用者分給各個 worker，合計不隨 worker 數增加（<a href="/blog/backend/06-reliability/vendors/locust/hands-on/locust-local-lab/" data-link-title="Locust 在本機跑負載測試的實測紀錄：受測服務、locustfile、headless 執行與 CSV、內建的 exit code、quitting 事件裡的門檻判定、--reset-stats 與 on_start、master 與 worker 的分散執行" data-link-desc="要在本機用 Docker 照一次實測跑 Locust：寫 locustfile、headless 執行、讀終端機的統計表與 CSV、在 quitting 事件裡判定延遲門檻、開 master 與多個 worker 分散執行時讀；也用在受測服務變慢而 exit code 還是 0、加了 --reset-stats 之後登入請求還在統計裡、master 回報 worker failed to send heartbeat 的時候">Locust 在本機跑負載測試的實測紀錄</a> 的〈master 與兩個 worker〉），從其中一個工具換到另一個時，同樣的數字代表的施壓量不同。改成 <code>-Jthreads=5</code> 時，server 讀不到這個屬性，各自用 test plan 裡的預設值 10，合計 20 個；指令照樣跑完、exit code 是 0，不會有任何跟屬性有關的警告（輸出開頭那幾行 <code>WARN StatusConsoleListener The use of package scanning ...</code> 是 JMeter 用的 log4j 自己的訊息，<code>-G</code> 那一次也有），所以 remote testing 時每個要傳給 test plan 的屬性都用 <code>-G</code>。</p>
<p>JTL 由 client 寫出，分得出每一筆來自哪一台 server：remote testing 時 <code>threadName</code> 帶上 server 名稱的前綴（<code>js1-shoppers 1-1</code>、<code>js2-shoppers 1-1</code>）。<code>allThreads</code> 欄這次最大是 5，它是每一台 server 自己的活躍 thread 數，不是全部 server 的合計。要在 JTL 多一欄 Hostname 分節點計算時，在寫 JTL 的 client 上設 <code>jmeter.save.saveservice.hostname=true</code>（預設 false，這次沒有實跑）。summary 那一行的筆數與延遲是這一次的值，受測服務與兩台 server 共用同一台筆電的 CPU，量到的延遲包含施壓端與受測服務爭用 CPU 的時間（<a href="/blog/backend/knowledge-cards/load-generator-saturation/" data-link-title="Load Generator Saturation（施壓端飽和）" data-link-desc="壓測的吞吐量停在某個值、延遲上升，要確認瓶頸在受測服務還是在執行負載工具的那幾台機器時讀">Load Generator Saturation</a>），重跑會變。施壓量大到要分散時，client 收結果本身會成為新的瓶頸、結果改用 Backend Listener 送到外部的時序資料庫、資料檔怎麼分到每一台 server，這些判斷寫在 <a href="/blog/backend/09-performance-capacity/vendors/jmeter/jmeter-capacity-probing/" data-link-title="JMeter 在容量探測的用法：thread group 固定的負載變數、逐級加壓、knee 的判讀、施壓端與分散施壓的檢查、多 protocol 的涵蓋範圍與排查順序" data-link-desc="用 JMeter 找飽和點時的判斷：thread group 與 timer 量的是哪一種負載模型、Open Model Thread Group 的逐級加壓、從 dashboard 判讀 knee、施壓端與 TLS 連線的失真、分散施壓，以及容量結論要附的條件">JMeter 在容量探測的用法：thread group 固定的負載變數、逐級加壓、knee 的判讀、施壓端與分散施壓的檢查、多 protocol 的涵蓋範圍與排查順序</a> 的〈分散施壓：remote mode 的結果回傳、Backend Listener 與資料檔〉。</p>
<p>server 中途結束時，client 的行為依結束的時間點而不同，審查時殺掉 <code>js2</code> 各實跑過一次：</p>
<ul>
<li><strong>測試已經開始之後</strong>（被 OOM 殺掉、主機重開）：client 不會自己結束，停在 <code>Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445</code> 那一行；那一次 <code>-Gduration=40</code>，354 秒之後仍在等，另一台 server 的 log 已經印 <code>Finished the test</code>，JTL 只寫了一部分，報告沒有產生。CI 要等到 job 的逾時才結束。</li>
<li><strong>啟動的過程中</strong>：client 印 <code>The following remote engines have not started:[js2]</code>，exit code 是 0，沒有寫出 JTL。</li>
<li><strong>還在設定 server 時</strong>：client 印 <code>Following remote engines could not be configured</code>，exit code 是 1。</li>
</ul>
<p>所以 CI 跑 remote testing 時，job 要設一個比 duration 長一點的逾時，並從 client 的輸出找 <code>have not started</code> 與 <code>could not be configured</code>；JMeter 的 exit code 分不出這幾種情形，前兩種都不會產生報告，<code>gate.py</code> 讀不到 <code>statistics.json</code> 時回 4。</p>
<h2 id="清理-container-與-docker-network">清理 container 與 Docker network</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker rm -f js1 js2 target
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker network rm loadlab</span></span></code></pre></div><p>兩台 server 沒有加 <code>--rm</code>，要自己刪；client 帶著 <code>--rm</code>，跑完就刪了。還有執行中的 container 連著 network 時，<code>docker network rm</code> 失敗，訊息是 <code>network loadlab has active endpoints</code>，後面列出 <code>target</code>、<code>js1</code>、<code>js2</code> 這幾個還在跑的 container，所以先刪 container 再刪 network。<code>jmeter/out/</code> 底下的 JTL 與報告，以及 <code>jmeter/</code> 底下的 <code>jmeter.log</code>、<code>jmeter-server.log</code>（兩台 server 掛載同一個目錄，寫的是同一份）都留在本機的目錄。</p>
]]></content:encoded></item></channel></rss>