iT邦幫忙

2026 iThome 鐵人賽

DAY 28
1

day28_title

前言

看過先前的文章,我相信各位可以把一個 bun server 弄起來,我這裡就不掩飾了,我們著重在其他部分壓測
今天的主題主要以 wrk 這個工具為主,當然我個人也有用過 k6 個人覺得相當不錯,壓測的部分讀者可以根據自己
喜歡的部分去嘗試壓測,仿間壓測的部分相當多,並沒有一定要用 wrk 不過我們這裡以 wrk 為主
我們 try try 看 可以撐到多少 QPS 然後再做簡單的優化,當然,因為篇幅有限,我們能做的也是比較明顯的部分
希望讀者能在這篇能得到一些基本的啟發

為什麼選 wrk 優點是什麼?

仿間有非常多壓測工具,比如 ab, autocannon, k6, 還有老字號的 Jmeter, 我們這次嘗試用 wrk

當然既然選擇 wrk 就要說一下他的優點,以及為何選擇

  • 效能足夠高 : 這為何重要呢? 能在單一多核心 CU 上產生相當可觀的負載,結合多執行緒與 epoll, kqueue 等擴展事件通知系統
    本身不會成為瓶頸

  • 官方推薦 : 詳細可以看 bun server,bun vs Node.js: Runtime Speed & Install Benchmarks 2026
    那大家都用,我們這裡也用.當然人家用一定有他的理由,主要理由是,如果壓測工具本身跑不動,根本量不出 Bun server 的真實極限

  • 支持 Lua script : 可以客製化 request,做更貼近真實流量的測試(POST、JSON body、隨機參數等)

安裝 wrk

macOS 可以直接用 Homebrew:

brew install wrk

Linux 則需要自己編譯:

git clone --depth=1 https://github.com/wg/wrk.git
cd wrk
make -j
# 編譯完成後的執行檔會在當前目錄下
sudo cp wrk /usr/local/bin/

準備一個基本的 Bun Server

先寫一支最陽春的 API,之後我們會針對它做優化對照:

index.ts

interface User {
  id: number,
  name: string
}

Bun.serve({
  port: 3000,
  fetch(req: Request) {
    const url = new URL(req.url);
    if (url.pathname === '/ping') {
      return new Response('pong');
    }

    if (url.pathname === 'users') {
      const users = Array.from<unknown, User>({ length: 50 }, (_, i) => ({
        id: i + 1,
        name: `user-${i + 1}`
      }));
      return Response.json(users, { status: 200 });
    }
    return new Response('Not found', { status: 404 });
  }
});

console.log('run on http://localhost:3000');

啟動:

bun index.ts

start server

開始壓測

基本用法

wrk 最基本的三個參數:

  • -t:threads 數量(執行緒)
  • -c:connections 數量(同時保持開啟的連線數)
  • -d:測試持續時間
wrk -t4 -c100 -d30s --latency http://localhost:3000/ping
  • -t4:使用 4 個執行緒
  • -c100:保持 100 個連線
  • -d30s:跑 30 秒
  • --latency:額外印出延遲分布資訊

一個典型的輸出長這樣(數字會依你的機器不同):

pressure_1

怎麼看這份報告

  • Requests/sec:整體吞吐量,這是最直觀的「這台 server 一秒能處理幾個請求」的數字。
  • Latency 分布(50% / 75% / 90% / 99%):平均值容易被少數極端值誤導,實務上更該看 p99——代表 99% 的請求都在這個時間內完成,這才是使用者實際感受到的「慢」。
  • Stdev(標準差):如果標準差很大,代表回應時間很不穩定,可能有 GC、I/O 阻塞或資源競爭的問題。

這裡不需要把執行緒數和連線數設得太大,只要能讓目標程式的 CPU 使用率打到接近 100% 就足夠了;同時測試時間也不能太短,太短的壓測沒有意義
建議搭配 htoptop,一邊壓測一邊觀察 CPU 使用率,確保真的把 server 壓榨出極限,而不是壓測工具自己先撐不住。

補充說明 : 如果讀者使用的是 macbook arm 架構 htop 可能會有失準的狀況,我這時候建議用 btop 各位可以自行安排,以下為 btop 的介面

btop_sample

對了,我這裡只是展示 btop 不是壓測當下的樣子😅 言歸正傳,以下

測試有負載的 API(/users

wrk -t4 -c200 -d30s --latency http://localhost:3000/users

因為 /users 多了一個陣列建構 + JSON 序列化的動作,理論上吞吐量會比純文字的 /ping 低一些。這個對照組很重要,等一下優化完可以直接比較差異。

user pressure

用 Lua Script 測試 POST/JSON

如果要測的是接收 JSON body 的 API,可以寫一個簡單的 Lua script:

-- post.lua
wrk.method = "POST"
wrk.body   = '{"name": "ironman", "day": 28}'
wrk.headers["Content-Type"] = "application/json"

然後這樣執行:

wrk -t4 -c100 -d30s -s post.lua --latency http://localhost:3000/echo

(這裡我只是提及,就不截圖展示了)

觀察到的瓶頸

跑了幾輪之後,常見會發現幾種典型的效能問題:

  1. 每個 request 都重新建立資料結構:像上面 /users 範例,每次請求都重新 Array.from 產生 50 筆假資料,這種靜態或變動不大的資料,完全可以快取起來
  2. 同步阻塞操作:如果 handler 裡面不小心混入了同步的檔案讀寫或 CPU 密集運算,會直接卡住事件迴圈,拖累所有其他連線。
  3. 不必要的 JSON.stringify / parse:字串處理在高頻請求下的成本會被放大。
    k4. Log 太多:每個請求都 console.log,在高並發下 I/O 反而變成瓶頸。

針對瓶頸優化

優化一:預先計算、快取不變的資料

-> 只計算一次,之後直接複用

interface User {
  id: number,
  name: string
}

const cachedUsers = Array.from<unknown, User>({ length: 50 }, (_, i) => ({
  id: i + 1,
  name: `user-${i + 1}`
}));

const cachedUsersResponse = JSON.stringify(cachedUsers);

Bun.serve({
  port: 3000,
  fetch(req: Request) {
    const url = new URL(req.url);
    if (url.pathname === '/ping') {
      return new Response('pong');
    }

    if (url.pathname === 'users') {
      return new Response(cachedUsersResponse, {
        headers: { "Content-Type": "application/json" },
      });
    }
    return new Response('Not found', { status: 404 });
  }
});

console.log('run on http://localhost:3000');

Response.json()(內部要做序列化)換成預先算好的字串 + 直接回傳 Response,可以省下每個 request 重複做的序列化成本。

以下結果

優化二:善用 Bun 的路由(Routes)功能

與其在 fetch 裡面手動 if/else 判斷 pathname,Bun 原生支援路由物件寫法,內部有針對路由比對做優化,同時程式碼也更乾淨:

Bun.serve({
  port: 3000,
  routes: {
    "/ping": () => new Response("pong"),
    "/users": () =>
      new Response(cachedUsersResponse, {
        headers: { "Content-Type": "application/json" },
      }),
  },
  fetch() {
    return new Response("Not Found", { status: 404 });
  },
});

優化三:避免同步阻塞

確認 handler 裡沒有同步的重運算或同步 I/O。
如果真的有 CPU 密集工作(例如圖片處理、加密運算),考慮丟到 Worker 或用 Bun.spawn 開子行程處理,避免卡住主執行緒的事件迴圈。

優化四:拿掉高頻 log,改用取樣或非同步寫入

// ❌ 每個 request 都同步印出
console.log(`${req.method} ${url.pathname}`);

// ✅ 改成取樣紀錄,或用非同步/批次寫入的 logger

優化前後對照

程式碼對比

(順便一提我這裡把 length 改為 1000 因為 50 壓測結果比較相近,
在標準差底下會有點失準,也可能跟我個人的電腦規格較好有關),因此我這裡提供壓測當下的程式碼

before::

interface User {
  id: number,
  name: string
}

Bun.serve({
  port: 3000,
  fetch(req) {
    const url = new URL(req.url)
    if (url.pathname === 'ping') {
      return new Response('pong');
    }
    if (url.pathname === '/users') {
      const users = Array.from<unknown, User>({ length: 1000 }, (_, i) => ({
        id: i + 1,
        name: `user-${i + 1}`
      }));
      return Response.json(users);
    }

    return new Response('Not found', { status: 404 });
  }
});

console.log('run on http://localhost:3000');

after::

interface User {
  id: number,
  name: string
}
const cachedUsers = Array.from<unknown, User>({ length: 1000 }, (_, i) => ({
  id: i + 1,
  name: `user-${i + 1}`
}));

const cachedUsersResponse = JSON.stringify(cachedUsers);

Bun.serve({
  port: 3000,
  routes: {
    "/ping": () => new Response("pong"),
    "/users": () =>
      new Response(cachedUsersResponse, {
        headers: { "Content-Type": "application/json" },
      }),
  },
  fetch() {
    return new Response('Not found', { status: 404 });
  }
});

console.log('run on http://localhost:3000');

用同樣的壓測指令,針對優化前後的 /users 各跑一次,把結果整理成表格會更直觀:

  • before 結果
    before

  • after 結果
    after

版本 Req/Sec p50 延遲 p99 延遲
優化前 14,054.84 14.14ms 14.77ms
優化後 35,224.37 5.65ms 6.7ms

再次提醒 :: 以上數字僅為示意,實際結果請以自己機器實測為準——壓測數字受硬體、作業系統、網路環境影響很大,
重點是「同一台機器、優化前後」的相對比較,而不是拿網路上的數字來對標自己的專案。

小提醒:壓測時的常見誤區

  • 壓測工具和目標 server 別放同一台機器搶資源:理想情況下 wrk 跟 server 應該在不同機器(或至少不同容器),
    否則你量到的可能是壓測工具自己的瓶頸,而不是 server 的瓶頸。

  • 連線數不是越高越好:一開始先從小連線數開始,逐步往上加,找到吞吐量開始下降或延遲開始飆升的臨界點,那才是這台 server 的真實承載上限。

  • 別只看平均值:p99、p999 延遲才是真正決定使用者體驗的關鍵指標。

  • 多測幾輪取穩定值:單次結果容易受到系統當下狀態影響,建議跑 3 次以上取中位數。

小結

今天我們用 wrk 對 Bun server 做了實際的壓力測試,從最基本的用法、如何解讀報告
,到找出瓶頸並實際優化,最後用數字驗證優化是否真的有效。

效能優化最重要的心法就是:先量測,再優化,優化完再量測。沒有數據支撐的優化,往往只是自我感覺良好。


上一篇
Monorepo 管理 —— Bun Workspaces 的最佳實踐
系列文
不只是快 —— Bun 30 天:從底層架構、全套工具鏈到生產部署28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言