:用 wrk 從零搭建性能基線)
很多同學(xué)第一次給 PHP 項目做壓力測試第一反應(yīng)都是找個工具把接口打爆看看會不會掛。我以前也這么想直到有次新功能上線前我用 wrk 隨手壓了一套接口發(fā)現(xiàn) QPS 連 200 都沒到RT 的 P99 卻已經(jīng)飆到 1.5 秒這才意識到一件事沒有一份可靠的 QPS、RT、CPU、內(nèi)存基線數(shù)據(jù)你根本沒法判斷系統(tǒng)是變好了還是變差了。這篇文章就把我從零開始用 wrk 對 PHP 系統(tǒng)做壓測、記錄基線的完整過程寫下來適合完全沒碰過壓測的 PHP 開發(fā)同學(xué)照著操作也能幫想搭一套壓測流程的人省掉不少彎路。1. 壓測前先想明白你測的不是極限是一份可用基線1.1 為什么選 wrk輕量、快、不折騰壓測工具不少常見的有 ab、JMeter、wrk、k6 這幾類。我給 PHP 項目做日?;€測試首選就是 wrk原因很簡單它是一個單文件編譯的 C 語言工具沒有圖形界面沒有插件體系一條命令就能跑完一輪壓測。對比下來大概是這個樣子工具上手成本適合場景主要限制ab很低臨時快速看一眼接口吞吐高并發(fā)下線程模型不如 wrk數(shù)值容易偏低JMeter高復(fù)雜業(yè)務(wù)流程、分布式壓測、報告美化配置麻煩跑少量線程時本地資源占用就不小wrk低單接口/多接口的并發(fā)壓測、基線回歸玩法要靠 Lua 腳本層級報告需要自己整理我說的不折騰是有實際體會的。之前我試過在兩臺測試機上各跑一套壓測一邊用 ab一邊用 wrk同樣打到同一個 PHP 接口ab 的 QPS 明顯低于 wrk。原因是 wrk 基于非阻塞事件模型可以用少量線程維持大量并發(fā)連接壓測機自身的 CPU 消耗低很多。換句話說wrk 更容易讓壓力真正落到被測的 PHP 服務(wù)上而不是先把自己的進(jìn)程拖垮。1.2 壓測要記錄的四個數(shù)字各自代表什么很多人把壓測等同于測 QPS但一份合格的基線報告至少要有四個維度的數(shù)據(jù)QPS每秒請求數(shù)系統(tǒng)在特定并發(fā)下每秒鐘能處理多少個請求這是吞吐量指標(biāo)。RT響應(yīng)時間單個請求從發(fā)出到收到響應(yīng)的時間通常看平均值和高分之位比如 P99。CPU 使用率壓測期間這臺機器到底忙不忙是代碼計算密集還是空等 IO。內(nèi)存占用PHP-FPM 是常駐內(nèi)存的進(jìn)程池隨著并發(fā)上升worker 數(shù)量增加內(nèi)存曲線非常值得盯。這四個數(shù)字必須放在一起看缺一個都容易誤判。比如 QPS 高了但 CPU 已經(jīng) 95% 以上說明系統(tǒng)到上限了QPS 沒漲內(nèi)存卻一直在漲可能是 PHP-FPM worker 在不斷創(chuàng)建或者測試接口本身有泄漏。只有四者對齊才能說這條基線的水位是健康的。1.3 開始之前先確認(rèn)測試環(huán)境是干凈的這也是我第一次壓測踩過的坑。當(dāng)時我在本地開發(fā)環(huán)境上直接壓機器上還跑著編譯器、數(shù)據(jù)庫腳本、一堆開發(fā)工具測出來的 QPS 忽高忽低完全沒法當(dāng)參考?,F(xiàn)在我的習(xí)慣是壓測前必須確認(rèn)幾件事被測代碼是同一個穩(wěn)定分支并記錄 commit 號測試機只跑 nginx PHP-FPM 必要的數(shù)據(jù)庫后臺監(jiān)控進(jìn)程全部關(guān)掉壓測機的網(wǎng)絡(luò)和被測機之間沒有額外限速或代理PHP 的 opcache 保持與服務(wù)一致的開關(guān)狀態(tài)。寧可多花十分鐘清理環(huán)境也不要拿一份臟數(shù)據(jù)當(dāng)基線后面做優(yōu)化對比時你會感謝當(dāng)時的謹(jǐn)慎。2. 從零搭環(huán)境裝好 wrk準(zhǔn)備一個能壓的 PHP 接口2.1 安裝 wrk 的兩種常規(guī)姿勢如果你的測試機是 Linux 系統(tǒng)最簡單的辦法是直接用包管理器裝。以常見的 Debian/Ubuntu 系為例sudo apt install -y wrk wrk --version安裝完直接驗證版本能輸出 wrk 的版本號就說明裝好了。有些發(fā)行版?zhèn)}庫里沒有這個包或者你想用最新版本可以走編譯安裝sudo apt install -y build-essential libssl-dev git clone wrk 官方源碼倉庫地址 cd wrk make sudo cp wrk /usr/local/bin/編譯之前務(wù)必保證有 openssl 的開發(fā)頭文件wrk 默認(rèn)會編出支持 HTTPS 的版本如果缺了 libssl-devmake 階段會報錯報錯信息通常會直接指向 openssl 相關(guān)的頭文件。我第一次編譯時就在這卡過所以單獨提一句。這里多說一句很多 PHP 項目部署時是 Windows 開發(fā)環(huán)境 Linux 生產(chǎn)環(huán)境如果你只在 Windows 上開發(fā)建議直接在 Linux 測試機上跑 wrk不要試圖在 Windows 下折騰編譯。wrk 本身的設(shè)計目標(biāo)就是 Linux/macOSWindows 下的兼容方案既費時間又容易遇到網(wǎng)絡(luò)棧差異。2.2 一個足夠簡單又不失真實的測試接口壓測接口選什么很有講究。我的做法是準(zhǔn)備兩層目標(biāo)第一層是一個最簡單、不查數(shù)據(jù)庫、不調(diào)第三方接口的 JSON 返回接口用來測純 PHP 運行時 PHP-FPM 網(wǎng)絡(luò)棧的底子第二層再測真實業(yè)務(wù)接口比如帶一個簡單的數(shù)據(jù)庫查詢觀察額外損耗。第一層接口可以長這樣?php // test_api.php $data [ code 0, msg ok, data [ user_id 10086, time time(), ], ]; echo json_encode($data);為什么先拿這種接口打底因為壓測是為了定位瓶頸。如果連一個不查庫的接口 QPS 都很低那問題大概率出在 PHP-FPM 配置、opcache、CPU 頻率或中間件上沒必要上來就懷疑 SQL。把這個底子摸清楚之后再去壓真實接口兩者對比就能算出業(yè)務(wù)邏輯本身消耗了多少吞吐量。部署上我建議走 nginx PHP-FPM 的標(biāo)準(zhǔn)組合監(jiān)聽 127.0.0.1:8080 這樣的內(nèi)網(wǎng)地址避免在壓測過程中被防火墻或路由影響。測試機 IP 用 127.0.0.1 還是內(nèi)網(wǎng) IP 都行但如果你想讓壓測機和被測機分離就要用被測機的內(nèi)網(wǎng) IP 訪問。2.3 第一條壓測命令和輸出解讀把接口部署好之后先用 curl 手動確認(rèn)返回正常然后再上 wrk。第一條命令建議放低并發(fā)跑通流程wrk -t2 -c10 -d10s --latency http://127.0.0.1:8080/test_api.php其中-t2表示用 2 個線程-c10表示維持 10 個并發(fā)連接-d10s表示持續(xù) 10 秒--latency會在結(jié)束時輸出更完整的延遲分布。正常跑完會看到類似這樣的輸出Running 10s test http://127.0.0.1:8080/test_api.php 2 threads and 10 connections Thread Stats Avg Stdev Max /- Stdev Latency 4.82ms 2.31ms 45.12ms 86.18% Req/Sec 1.05k 102.34 1.41k 70.31% Latency Distribution 50% 4.50ms 75% 5.20ms 90% 6.80ms 99% 14.20ms 10520 requests in 10.01s, 2.03MB read Requests/sec: 1051.12 Transfer/sec: 207.43KB這里最容易被新手誤讀的地方是 Thread Stats 里的 Req/Sec 和最后的 Requests/sec。Thread Stats 的 Req/Sec 是每個線程自己的平均吞吐如果你的線程數(shù)是 4那么這個數(shù)值大致是總 QPS 分割后的水平而最后的Requests/sec才是整體 QPS也就是我們基線里要記的那個數(shù)字。把這兩行搞混的人不少我第一次看輸出時也愣了半天。3. 正式壓測并發(fā)要加梯度數(shù)據(jù)要取多輪3.1 從低并發(fā)起步找到吞吐量的拐點壓測不是一上來就-c500那樣你只能得到一個打爆了的結(jié)果卻不知道系統(tǒng)是從哪個并發(fā)開始扛不住的。我的標(biāo)準(zhǔn)操作是用一組并發(fā)梯度比如-c10、-c50、-c100、-c200、-c500每個梯度跑 30 秒記錄對應(yīng)的 QPS 和 RT。這個過程本質(zhì)上是尋找拐點一開始并發(fā)上升QPS 會跟著漲因為同時能處理的請求變多了到某個點之后再往上加并發(fā)QPS 不再明顯增長甚至開始回落同時 RT 快速拉高。這個拐點對應(yīng)的并發(fā)量就是系統(tǒng)當(dāng)前配置下的合理服務(wù)水位。舉個實際例子我之前壓一個 PHP 8.2 opcache 的簡單 JSON 接口-c10時 QPS 約 1800-c50時漲到 4200-c100時是 5100-c200的時候只有 4600 了RT 的 P99 從 18ms 漲到 120ms。這就說明 100 左右并發(fā)已經(jīng)接近這臺機器的服務(wù)上限繼續(xù)壓只會讓用戶體驗變差。3.2 平均 RT 會欺騙你P99 才是真實用戶體驗wrk 輸出里的 Latency 一欄Avg是平均響應(yīng)時間但平均值的迷惑性很大如果 90% 的請求都是 5ms剩下 10% 是 500ms平均值會被拉到一個看起來很溫和的數(shù)字。這也是為什么壓測一定要加--latency參數(shù)看延遲分布里的 50%、75%、90%、99% 分位。P99 的含義是99% 的請求都低于這個值剩下的 1% 是最慢的尾部請求。對線上服務(wù)來說用戶體驗往往由尾部延遲決定。我在做基線記錄時RT 這一欄至少記兩個數(shù)平均值和 P99。后續(xù)做優(yōu)化時如果平均值下降了但 P99 在上漲我會立刻警惕——這通常意味著某些慢請求變得更極端了可能存在于鎖競爭或進(jìn)程池打滿的問題。3.3 記錄多輪結(jié)果取穩(wěn)定而不是最高的一輪壓測數(shù)值天然有波動尤其是 PHP-FPM 場景下進(jìn)程復(fù)用、opcache 冷熱、系統(tǒng)定時任務(wù)都可能讓某一輪結(jié)果偏高或偏低。我的做法是同一個并發(fā)梯度至少跑三輪每輪之間停 5 秒把三輪結(jié)果都記下來然后取中位數(shù)或者波動最小的那一輪作為基線值。這里有個反直覺的點最高值往往不值得記錄。比如某輪跑到 5000 QPS下一輪只有 4100我會直接懷疑第一輪是不是正好命中了 opcache 預(yù)熱完成的窗口或者系統(tǒng) GC 還沒觸發(fā)。真正的基線應(yīng)該是可復(fù)現(xiàn)的那一組數(shù)字不是偶爾出現(xiàn)的峰值。如果接口需要 POST 請求wrk 也能通過 Lua 腳本解決。基礎(chǔ)寫法大概是這樣-- post.lua wrk.method POST wrk.headers[Content-Type] application/json wrk.body {user_id:10086,page:1}然后用wrk -t4 -c100 -d30s -s post.lua http://...啟動即可。這個用法不難但注意腳本里的 body 是所有并發(fā)連接共用同一個請求體如果業(yè)務(wù)要求每個請求參數(shù)不同就得用 Lua 腳本在請求時動態(tài)生成復(fù)雜度會高不少一般做基線時不需要一步到位。4. 壓測時盯服務(wù)器CPU 和內(nèi)存基線的采集方法4.1 壓測期間要開的幾個監(jiān)控命令wrk 跑起來只是整個壓測的一半另一半是被測服務(wù)器的資源狀態(tài)。我通常會在壓測開始前先在被測機上開一個后臺記錄腳本把壓測期間的數(shù)據(jù)落盤這樣壓測結(jié)束后可以回放當(dāng)時的資源曲線。最常用的命令有這幾個# 每2秒輸出一次系統(tǒng)概況共記錄30次 vmstat 2 30 # 每2秒輸出一次每個CPU核心的使用率 mpstat -P ALL 2 30 # 查看每個進(jìn)程的CPU和內(nèi)存占用 pidstat -u -r 2 30 # 內(nèi)存概況 free -m如果不想同時開這么多終端可以用一個簡單的循環(huán)腳本把關(guān)鍵數(shù)據(jù)拼進(jìn)同一個日志文件#!/bin/bash # load_monitor.sh interval2 end$((SECONDS60)) while [ $SECONDS -lt $end ]; do echo $(date %F_%T) /tmp/load_test.log top -bn1 | head -15 /tmp/load_test.log free -m /tmp/load_test.log sleep $interval done注意pidstat這個命令來自 sysstat 工具包如果系統(tǒng)里沒有需要先apt install sysstat。mpstat 也一樣。另外這些監(jiān)控命令本身也會消耗少量 CPU在低配機器上可能影響測試結(jié)果所以記錄完后記得把這些進(jìn)程都?xì)⒌粼偃∽罱K數(shù)據(jù)。4.2 從監(jiān)控數(shù)據(jù)反推瓶頸在哪個環(huán)節(jié)拿到資源數(shù)據(jù)后重點看三個地方一是 CPU 的 us/sy 比例。us高說明用戶態(tài)代碼在忙比如 PHP 腳本邏輯復(fù)雜、循環(huán)密集sy高說明系統(tǒng)態(tài)開銷大比如頻繁上下文切換、網(wǎng)絡(luò)包處理、系統(tǒng)調(diào)用太多。如果 sy 高于 us往往不是 PHP 代碼本身的問題而是并發(fā)模型或網(wǎng)絡(luò)配置有瓶頸。二是 CPU idle 和等待 IO 的時間。vmstat里有個 wa 列代表 IO 等待。wa 明顯偏高的時候QPS 上不去多半是磁盤拖后腿比如 nginx 訪問日志瘋狂寫盤或者 PHP slowlog 開著。我之前確實遇到過 CPU 只有 20%、QPS 卻卡在 800 的情況檢查后發(fā)現(xiàn)是訪問日志寫到了普通機械盤上IO 等待直接打滿。三是內(nèi)存里 PHP-FPM 的占用。用ps aux --sort-%mem | head -20看看是否有 worker 進(jìn)程內(nèi)存漲得異常。PHP-FPM 的每個 worker 在默認(rèn)配置下可能占用幾十到一兩百 MB如果并發(fā)開得高幾十個 worker 同時駐留內(nèi)存很容易超預(yù)期。內(nèi)存基線就是用來記錄這個并發(fā)水位下FPM 進(jìn)程池到底吃掉了多少內(nèi)存。4.3 把服務(wù)端數(shù)據(jù)和 wrk 輸出對齊到同一時間線壓測結(jié)束后你會得到兩堆數(shù)據(jù)一堆來自 wrk 的匯總值一堆來自監(jiān)控日志的每秒采樣。很多人直接把這兩堆數(shù)字拼在一起寫報告實際上不對齊時間線的基線是沒法定位問題的。我的做法是壓測開始前在監(jiān)控日志里手動打一條標(biāo)記 start wrk 結(jié)束后再打一條 done 這樣壓測窗口內(nèi)的資源采樣就有明確的起止邊界。后續(xù)做對比時只看窗口內(nèi)的采樣平均值和峰值窗口外的數(shù)據(jù)直接丟棄。然后我習(xí)慣建一張映射表把每次壓測的命令參數(shù)、wrk 結(jié)果、資源監(jiān)控結(jié)果放在同一行這樣一旦某次基線異??梢粤⒖痰雇剖悄囊粚拥淖兞堪l(fā)生了變化。5. 把基線寫成表格一份能長期復(fù)用的記錄模板5.1 基線表該有哪些字段跑完一堆壓測之后如果只是把數(shù)字記在聊天軟件里過兩周就找不到了。我推薦用一張表格管理所有基線字段包括日期代碼版本測試接口并發(fā)QPSRT 均值RT P99CPU%內(nèi)存占用環(huán)境說明2026-06-01某提交號test_api.php100510018ms46ms68%1.8Gopcache開啟FPM動態(tài)10-802026-06-02某提交號test_api.php200460088ms120ms66%2.4G同上超負(fù)荷區(qū)間2026-06-03某提交號user_info.php5062076ms210ms35%2.1G帶一次DB查詢這張表里每一行就是一條基線。環(huán)境說明這欄特別重要因為 opcache 開關(guān)、PHP 版本、FPM 參數(shù)都會直接影響結(jié)果不寫清楚三周后再看數(shù)據(jù)等于瞎猜。5.2 什么時機需要重新測基線基線不是測一次就永久有效的。按我的經(jīng)驗以下變化發(fā)生時必須重新采集PHP 版本升級、框架或核心依賴有大版本變更、php-fpm 池配置調(diào)整pm.max_children、pm.start_servers、nginx 的 keepalive 或 gzip 策略變化、測試接口本身的代碼邏輯改動、測試機硬件規(guī)格變化。如果出現(xiàn)同一接口、同一代碼版本QPS 卻和上周記錄差很多的情況我也會重新測一遍基線而且會特意檢查是不是監(jiān)控腳本在后臺偷跑、是否有人改了系統(tǒng)參數(shù)。這個習(xí)慣幫我排查過好幾次假象——有一次 QPS 突然掉了一半最后發(fā)現(xiàn)是某臺機器上被同事裝了定時任務(wù)壓測窗口正好撞上了大規(guī)模日志清理。5.3 用基線做容量預(yù)估的簡單算法基線最有價值的用法不是看數(shù)字好看而是用來做容量預(yù)估。公式不復(fù)雜需要實例數(shù) (預(yù)估峰值 QPS × (1 冗余系數(shù))) / 單機基線 QPS舉個例子基線表里單機在 100 并發(fā)下 QPS 是 5000如果業(yè)務(wù)方預(yù)測大促峰值請求是 15000 QPS目標(biāo) CPU 水位控制在 70% 左右那么單機預(yù)留 30% 冗余最終需要的實例數(shù)就是15000 × 1.3 / 5000 ≈ 4臺。如果只按 QPS 峰值算而不留冗余一旦流量抖動所有實例都會沖到 90% 以上的 CPU尾部延遲會很難看。需要注意的是這種線性預(yù)估只適用于無狀態(tài)、可水平擴(kuò)展的 PHP 接口。如果接口嚴(yán)重依賴數(shù)據(jù)庫或第三方服務(wù)那么數(shù)據(jù)庫側(cè)的連接數(shù)、負(fù)載也是容量邊界單純按單機 QPS 算會低估風(fēng)險。遇到這種情況我會同時把數(shù)據(jù)庫的 CPU、連接數(shù)也拉進(jìn)基線表。6. 實操中最容易翻車的五個坑6.1 壓測機先崩了測出來的數(shù)就不可信wrk 雖然很輕量但也不是無限跑。如果你用一臺 2 核小機器去壓一個 8 核的服務(wù)端很可能會出現(xiàn)壓測機 CPU 先到 100%而服務(wù)端還很從容的情況。這時的 QPS 數(shù)字反映的是壓測機的上限不是被測系統(tǒng)的上限。判斷方法很簡單壓完看壓測機的top如果 wrk 進(jìn)程吃滿了 CPU 或者系統(tǒng)出現(xiàn)大量 TCP 連接排隊就說明壓力工具先到極限了。解決方式一是減少 wrk 線程數(shù)通常-t設(shè)置為壓測機 CPU 核心數(shù)左右即可沒必要開太多二是把 wrk 放到另一臺閑置機器上跑讓壓測機和被測機分離。6.2 PHP-FPM 配置和 opcache 偷偷影響成績PHP 場景下最容易被忽略的兩個變量是 opcache 和 FPM 進(jìn)程池參數(shù)。opcache 關(guān)閉時每個請求都要重新編譯一遍 PHP 腳本CPU 消耗會明顯上升QPS 直接打折opcache 開啟后前幾個請求還會觸發(fā)編譯緩存所以正式壓測前一定要先打幾發(fā)請求做預(yù)熱。FPM 這邊pm.max_children如果設(shè)置太小并發(fā)一旦超過進(jìn)程池上限請求就開始排隊QPS 停滯、RT 暴漲如果設(shè)置太大內(nèi)存會被 worker 吃滿。我做基線時會把 FPM 配置快照寫進(jìn)環(huán)境說明這樣后續(xù)調(diào)整參數(shù)后對比才公平。另外注意pm.max_requests它是讓 worker 處理多少請求后自動回收的機制設(shè)得太小會導(dǎo)致進(jìn)程頻繁創(chuàng)建銷毀CPU 忽高忽低。6.3 長連接與并發(fā)數(shù)設(shè)置會帶來假數(shù)據(jù)wrk 默認(rèn)使用 HTTP/1.1 keep-alive 長連接這能大幅提高 QPS因為省去了大量 TCP 握手開銷。但真實用戶往往不是這種訪問模式連接會不斷建立和斷開。所以壓測結(jié)果必須注明是否 keep-alive。如果你想知道斷開連接場景下的表現(xiàn)可以在 wrk 里加一個請求頭wrk -t4 -c100 -d30s -H Connection: close http://127.0.0.1:8080/test_api.php你會看到 QPS 明顯下降這是正常的別慌。關(guān)鍵是基線表里要寫清楚當(dāng)時是哪種連接模式否則拿 keep-alive 的基線去對比短連接場景你會以為系統(tǒng)退化了。6.4 日志和磁盤 IO 拖垮整個壓測壓測期間nginx 的 access log 默認(rèn)每接收一個請求就寫一行。QPS 高的時候日志寫入本身就是巨大的磁盤壓力。更隱蔽的是 PHP-FPM 的 slowlog一旦觸發(fā)慢日志就會在請求處理路徑上產(chǎn)生額外 IORT 反而被日志拖得更慢。我的建議是做純性能基線時先把 access_log 臨時關(guān)閉或在配置里指向內(nèi)存盤壓測完再改回來。這個操作不算什么高級技巧但特別實用。我第一次壓測 2000 QPS 時就發(fā)現(xiàn)磁盤 wait 占到 40%所有并發(fā)一上去 CPU 就在等 IO關(guān)掉訪問日志后 QPS 直接翻了一倍?;€測試測的是系統(tǒng)處理能力不應(yīng)該讓日志寫入變成隱形成本。6.5 壓完不收拾現(xiàn)場下一次基線直接失真壓測結(jié)束后的收尾工作同樣重要。需要做的包括停掉壓測期間的監(jiān)控腳本確認(rèn)沒有殘留的壓測進(jìn)程在跑清掉壓測產(chǎn)生的日志文件和臨時腳本如果臨時改過 FPM 參數(shù)或 nginx 日志開關(guān)記得恢復(fù)原狀把壓測時記錄的代碼 commit 號和配置快照歸檔。這些看起來瑣碎但直接影響下一次壓測是否可信。我之前有一回壓完就撤了第二天再測時發(fā)現(xiàn)系統(tǒng)后臺還掛著監(jiān)控腳本測出來的 CPU 基線高了十幾個百分點白白排查了一下午。從那之后我就養(yǎng)成了壓測結(jié)束三分鐘收尾的習(xí)慣寧可慢一點也不能讓基線數(shù)據(jù)帶傷入庫。最后再分享一個經(jīng)驗壓測基線不是給上級看的一張表它是你自己理解系統(tǒng)的一把尺子。同一個接口在不同并發(fā)下的表現(xiàn)、不同配置下的差異、代碼提交前后的吞吐變化都藏在基線的對比里。把這套流程跑順之后你會發(fā)現(xiàn)自己對系統(tǒng)到底能扛多少量這件事終于有了基于數(shù)據(jù)的底而不是靠猜。