3. 编写Go压测脚本:使用vegeta或k6实现动态参数化
搜索接口压测的核心挑战在于:请求参数必须动态变化,否则缓存命中率失真、后端无法模拟真实流量分布。本章以 vegeta(Go原生)和 k6(Grafana生态)为例,演示如何构造带动态参数的搜索压测脚本,覆盖 Headers、Body、Query 参数化场景。
选型建议: 团队技术栈以 Go 为主时优先选择 vegeta(二进制部署、Go SDK 二次开发);需要复杂编排、多阶段施压、SLA 断言时选择 k6(JavaScript 脚本、内置 Load Profile)。
3.1 参数化策略对比
| 参数类型 | 参数化方式 | 适用场景 | 性能开销 |
|---|---|---|---|
| Query 参数 | URL 模板 + 随机/轮询填充 | 搜索关键词、分页 offset | 低 |
| Header 参数 | 预定义列表 + 加权随机 | User-Agent、Authorization、Trace-ID | 极低 |
| Body 参数 | JSON 模板 + 动态字段注入 | 复杂搜索 DSL、多条件过滤 | 中(需序列化) |
| 混合参数 | 组合上述三种 + 关联上下文 | 搜索 + 推荐 + 广告联合请求 | 中高 |
3.2 使用 vegeta 实现动态参数化
vegeta 原生支持通过 stdin 传入目标列表,每个目标可独立指定 URL、Header、Body。我们利用 Go 的 text/template 或直接构造 JSON 行来实现动态参数。
3.2.1 基础脚本:Query 参数轮询
# 准备关键词列表 keywords.txt
golang 性能优化
搜索尾延迟
vegeta 压测
动态参数化
缓存穿透
# 使用 vegeta-attack 配合 jq 动态构造目标
cat keywords.txt | while read keyword; do
echo "GET http://search-api.example.com/search?q=$(echo $keyword | jq -sRr @uri)&page=1&size=20"
done | vegeta attack -rate=100 -duration=30s | vegeta report --type=hist[0,10ms,50ms,100ms,200ms,500ms,1s]
关键点: 每行一个完整 URL,vegeta 自动轮询;
jq -sRr @uri 对中文关键词做 URL 编码,避免非法请求。
3.2.2 进阶脚本:Header + Body 动态化(Go SDK)
当需要复杂逻辑(如 Token 过期刷新、请求关联)时,使用 vegeta Go SDK 编写自定义 Targeter:
package main
import (
"encoding/json"
"fmt"
"math/rand"
"time"
"github.com/tsenart/vegeta/v12/lib"
)
type SearchRequest struct {
Query string `json:"query"`
Filters []string `json:"filters,omitempty"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
func main() {
rate := vegeta.Rate{Freq: 100, Per: time.Second}
duration := 30 * time.Second
keywords := []string{"golang 性能", "尾延迟", "vegeta", "k6", "压测"}
userAgents := []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Safari/605",
"curl/8.4.0",
}
targeter := func(t *vegeta.Target) error {
// 动态选择关键词
kw := keywords[rand.Intn(len(keywords))]
// 构造请求体
body := SearchRequest{
Query: kw,
Filters: []string{"type:doc", "lang:zh"},
Page: rand.Intn(50) + 1,
PageSize: 20,
}
bodyBytes, _ := json.Marshal(body)
t.Method = "POST"
t.URL = "http://search-api.example.com/search"
t.Header = map[string][]string{
"Content-Type": {"application/json"},
"User-Agent": {userAgents[rand.Intn(len(userAgents))]},
"X-Trace-ID": {fmt.Sprintf("trace-%d", time.Now().UnixNano())},
}
t.Body = bodyBytes
return nil
}
attacker := vegeta.NewAttacker()
var metrics vegeta.Metrics
for res := range attacker.Attack(targeter, rate, duration, "search-dynamic") {
metrics.Add(res)
}
metrics.Close()
fmt.Println("P99 Latency:", metrics.Latencies.P99)
}
注意: 每次请求都重新
json.Marshal 会增加 GC 压力。生产压测时建议使用 sync.Pool 复用 body 字节切片,或预计算 1000 个不同 body 后轮询。
3.3 使用 k6 实现动态参数化
k6 使用 JavaScript 脚本,内置 __VU(虚拟用户编号)和 __ITER(迭代次数)变量,天然支持参数化。
3.3.1 基础脚本:从文件读取参数
import http from 'k6/http';
import { SharedArray } from 'k6/data';
import { sleep } from 'k6';
// 从文件加载关键词(k6 启动时一次性加载到内存)
const keywords = new SharedArray('keywords', function() {
return JSON.parse(open('./keywords.json'));
});
export const options = {
vus: 50,
duration: '60s',
};
export default function() {
// 使用 VU 编号取模,保证每个 VU 使用不同关键词
const kw = keywords[__VU % keywords.length];
const page = (__ITER % 50) + 1;
const params = {
headers: {
'Content-Type': 'application/json',
'User-Agent': `k6-loader/${__VU}`,
'X-Request-ID': `${__VU}-${__ITER}`,
},
};
const payload = JSON.stringify({
query: kw,
page: page,
page_size: 20,
filters: ['type:doc'],
});
const res = http.post('http://search-api.example.com/search', payload, params);
// 断言:状态码 200 且响应时间 < 500ms
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
3.3.2 进阶:多参数组合与加权分布
真实搜索流量中,热门关键词出现频率远高于长尾词。k6 可通过 weightedArray 实现加权随机:
import { SharedArray } from 'k6/data';
// 关键词 + 权重
const weightedKeywords = new SharedArray('weighted', function() {
return [
{ kw: 'golang', weight: 30 },
{ kw: '尾延迟', weight: 25 },
{ kw: '性能优化', weight: 20 },
{ kw: '分布式追踪', weight: 15 },
{ kw: 'k6 压测', weight: 10 },
];
});
// 根据权重构建累积分布
function pickWeighted(items) {
const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
let random = Math.random() * totalWeight;
for (const item of items) {
random -= item.weight;
if (random <= 0) return item.kw;
}
return items[items.length - 1].kw;
}
export default function() {
const kw = pickWeighted(weightedKeywords);
// ... 后续请求构造同上
}
最佳实践: 将热门关键词权重设为长尾词的 3~5 倍,模拟真实搜索流量分布。同时确保每个 VU 的
__VU 和 __ITER 组合唯一,避免请求完全重复。
3.4 参数化架构总览
下图展示从参数源到压测引擎的完整数据流:
3.5 参数化质量检查清单
- 唯一性: 每个请求的 (VU, Iteration, Timestamp) 组合全局唯一,避免重复请求污染尾延迟分布。
- 分布真实性: 热门参数出现频率符合二八定律(20% 参数占 80% 流量)。
- 边界覆盖: 包含空字符串、超长字符串、特殊字符(如 SQL 注入尝试)等边界参数。
- 无状态泄漏: 参数化逻辑不依赖上一个请求的响应(除非测试有状态场景)。
- 预热对齐: 参数分布与生产环境最近 1 小时的流量分布一致(可从日志采样)。
实战检查: 在压测启动后 10 秒,从服务端日志随机抽取 100 条请求,验证参数分布是否符合预期。若发现某个关键词占比超过 30%,说明随机算法有偏,需改用加权轮询。