iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
Software Development

不會寫程式,我照樣做效能測試 - Claude Code × k6 的 30 天系列 第 13

Day 13|模擬真實使用者:Think Time、Pacing 與 Scenarios 入門

  • 分享至 

  • xImage
  •  

一、開場實驗:量一量機器人的火力

先動手,再講理。把 first-test.js 複製成 no-sleep.js,刪掉 sleep(1) 那行,然後兩支各跑一次同樣的設定:

k6 run --vus 1 --duration 10s first-test.js   # 有 sleep
k6 run --vus 1 --duration 10s no-sleep.js     # 沒 sleep

比較兩次的 http_reqs:有 sleep 的大約 9 個請求;沒 sleep 的,依你的網路狀況可能是幾十到上百個。同樣號稱「1 個使用者」,火力差了一個數量級。

https://ithelp.ithome.com.tw/upload/images/20260911/20161809DfMEYhgcYc.png
圖 1:同樣是 1 個 VU——沒有停頓的腳本,測的是一種不存在的使用者

真人瀏覽網站的節奏是「請求、看一下、再請求」,大部分時間花在閱讀和思考,這段停頓就叫 think time。沒有 think time 的腳本不是「比較有效率的測試」,是在測一種不存在的使用者:它算出的「系統每秒能處理幾個請求」也許沒錯,但「50 個 VU」對應到幾個真人,完全失去換算基礎——Day 12 的估算公式裡,一輪耗時包含 think time,正是這個原因。從此看到別人的腳本,第一眼先找 sleep:沒有它,數字先打個問號。

二、Think Time 的設計:隨機,而且看動作

固定 sleep(1) 比沒有好,但仍不真實——真人的停頓不會整齊劃一,而且不同動作的停頓天差地遠:掃一眼列表兩三秒,填一張表單可能三十秒。兩個設計原則:

import { sleep } from 'k6';
 
// 原則一:隨機化——在合理範圍內取亂數
sleep(Math.random() * 3 + 1);   // 1 到 4 秒之間
 
// 原則二:依動作訂範圍——寫成具名函式,語意清楚
const glance  = () => sleep(Math.random() * 2 + 1);   // 掃一眼:1-3 秒
const ponder  = () => sleep(Math.random() * 5 + 3);   // 挑選猶豫:3-8 秒
const fillIn  = () => sleep(Math.random() * 10 + 8);  // 填寫內容:8-18 秒

範圍哪裡來?最好的來源是行為數據(分析工具裡的頁面停留時間);沒有的話,自己操作一次碼錶計時,或依常識估——和 Day 12 的土法估算同一個精神:講得出依據,之後用數據修正。

三、多步驟旅程:用 group 分段說故事

真實使用者做的是「一趟旅程」而非「一個請求」。QuickPizza 上一趟典型旅程:逛首頁、要一份披薩推薦、登入後送出評分。這正好把前幾天的所有技能串起來——登入是 Day 10、唯一帳號是 Day 9、think time 是今天。用 group 把旅程分段,每段獨立統計:

Prompt 1|生成完整旅程腳本

請建立 journey.js,模擬一趟 QuickPizza 使用者旅程,5 個 VU 跑 1 分鐘:
setup():沿用 Day 10 的做法——註冊唯一帳號並登入取得 token。
default(data) 依序三段,每段用 k6 的 group 包起來:
1. group「瀏覽首頁」:GET https://quickpizza.grafana.com/,停 1 到 3 秒
2. group「取得推薦」:POST /api/pizza(示範 token 即可),停 3 到 8 秒
3. group「送出評分」:POST /api/ratings,內容 {stars: 5, pizza_id: 1},
   帶登入的 token,停 2 到 5 秒
每段都要 check 狀態碼。請解釋 group 的作用,以及為什麼停頓時間各段不同。

旅程骨架長這樣(節錄):

import { group, check, sleep } from 'k6';
 
export default function (data) {
  group('瀏覽首頁', function () {
    const res = http.get(`${BASE}/`);
    check(res, { '首頁 200': (r) => r.status === 200 });
    sleep(Math.random() * 2 + 1);          // 掃一眼:1-3 秒
  });
 
  group('取得推薦', function () {
    /* POST /api/pizza ... */
    sleep(Math.random() * 5 + 3);          // 挑選猶豫:3-8 秒
  });
 
  group('送出評分', function () {
    /* POST /api/ratings 帶 data.token ... */
    sleep(Math.random() * 3 + 2);          // 給分前想想:2-5 秒
  });
}

跑完看輸出:http_req_duration 會多出依 group 拆開的統計,哪一段慢、慢多少,一目瞭然。這對之後的判讀與回報價值極大——「送出評分那一段的 p(95) 是首頁的三倍」比「整體有點慢」有用得多。

四、Scenarios:讓不同角色同時上演

到目前為止,所有 VU 都在演同一個劇本。真實流量從來不是這樣:大多數人只逛不買,少數人走完整流程。scenarios 讓多個劇本並行,各自有自己的人數與行為:

https://ithelp.ithome.com.tw/upload/images/20260911/20161809AG3wmGbwMw.png
圖 2:兩個劇本同時上演——瀏覽者七成、評分者三成,貼近真實流量的組成

Prompt 2|生成雙情境腳本

請建立 mixed.js:用 k6 的 scenarios 同時跑兩個情境,共 1 分鐘:
1. browsers:7 個 VU(constant-vus),只做「瀏覽首頁+取得推薦」,停頓短
2. raters:3 個 VU(constant-vus),做完整旅程(含登入與送出評分),停頓長
兩個情境的函式分開寫(exec 指定)。
請解釋 scenarios 區塊的結構,以及輸出裡怎麼分辨兩個情境的數據。

scenarios 的骨架:

export const options = {
  scenarios: {
    browsers: {                      // 劇本一:瀏覽者
      executor: 'constant-vus',
      vus: 7,
      duration: '1m',
      exec: 'browse',                // 執行下面的 browse 函式
    },
    raters: {                        // 劇本二:評分者
      executor: 'constant-vus',
      vus: 3,
      duration: '1m',
      exec: 'rate',
    },
  },
};
 
export function browse() { /* 逛首頁+要推薦,短停頓 */ }
export function rate()   { /* 完整旅程,長停頓 */ }

70/30 這個比例哪裡來?和 think time 的範圍一樣:最好來自流量數據(幾成的工作階段有走到結帳),沒有就先用有依據的估算並寫明出處。比例錯了,測試測的就是另一個世界。

封閉與開放:固定 VU 和固定到達率是兩種世界

剛才用的 constant-vus 與 Day 12 的 stages(背後是 ramping-vus),都屬於「封閉模型」:固定一群人在系統裡活動。還有另一個家族——constant-arrival-rate,「開放模型」:固定的是每秒新來幾個人,不管系統多慢。差別在系統變慢的時候:

https://ithelp.ithome.com.tw/upload/images/20260911/20161809ozZeYvPFrv.png
圖 3:封閉模型裡客人等餐時不點新的;開放模型裡新客照樣進門——真實世界比較像後者

封閉模型有個隱性的仁慈:系統變慢時,VU 都卡在等回應,發出請求的頻率自動下降——你的測試在系統最需要被考驗的時刻,自己鬆手了。開放模型不仁慈:門口每秒照樣走進兩個新客人,跟真實尖峰一樣。示意設定如下(今天看懂即可,不必跑):

scenarios: {
  open_model: {
    executor: 'constant-arrival-rate',
    rate: 2, timeUnit: '1s',        // 每秒開始 2 輪(不管上一輪多慢)
    duration: '2m',
    preAllocatedVUs: 10,            // 預先準備的人力池
    maxVUs: 20,                     // 人力池上限
  },
},

有一個輸出訊號要先認識:dropped_iterations。它出現代表人力池不夠用——通常意味著系統回應太慢、VU 都被卡住了。這不是測試壞掉,是重要的發現本身:你的到達率,系統跟不上。

五、注意事項:真實性的細節

https://ithelp.ithome.com.tw/upload/images/20260911/20161809clV6SGCKUr.png

給 RD 的一句話:QA 問「使用者在這頁平均停多久」「幾成的人會走到結帳」時,分析平台上的那兩個數字就是答案。開放一份唯讀的行為數據存取,QA 的負載模型會從想像變成證據——這比事後爭論「你的測試不真實」有建設性得多。

六、觀念驗證:三個問題確認你有帶走今天的重點

• 同事說「我把 sleep 都拿掉,10 個 VU 就能測出你 100 個 VU 的量,省資源」——他說得對嗎?錯在哪裡?(第一節)
• 要模擬「開賣尖峰:不管系統多慢,每秒都湧入固定人數」——封閉還是開放模型?哪個 executor?看到 dropped_iterations 代表什麼?(第四節)
• 旅程測試顯示「送出評分」那段的 p(95) 是其他段的三倍——是 group 幫你看見的。如果整支腳本沒有分 group,這個發現會變成什麼樣子?(第三節)

七、小結

今天把測試從「發請求」升級成「演真人」:think time 讓 1 個 VU 回到 1 個真人的火力,隨機化與依動作分級讓停頓有了真實的節奏;group 把旅程分段說故事,慢在哪一步一目瞭然;scenarios 讓瀏覽者與評分者同時上演,比例來自證據。最後是那個專業分水嶺的觀念:封閉模型在系統變慢時會自己鬆手,開放模型才像真實世界的尖峰——固定 VU 和固定到達率,測的是兩種世界,選哪種取決於你要回答的問題。

附錄:常用 executor 速查

https://ithelp.ithome.com.tw/upload/images/20260911/201618099OzsFqQrLM.png

// think time 三級距範本
const glance = () => sleep(Math.random() * 2 + 1);   // 1-3 秒
const ponder = () => sleep(Math.random() * 5 + 3);   // 3-8 秒
const fillIn = () => sleep(Math.random() * 10 + 8);  // 8-18 秒
``` 

上一篇
Day 12|負載模型:Smoke、Load、Stress、Spike、Soak 與 Breakpoint
系列文
不會寫程式,我照樣做效能測試 - Claude Code × k6 的 30 天13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言