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 参数化架构总览

下图展示从参数源到压测引擎的完整数据流:

搜索压测动态参数化架构 搜索压测动态参数化数据流 参数源 关键词列表 (CSV/JSON) User-Agent 池 Token/认证信息 分页/时间戳生成器 参数化引擎 轮询 / 随机 / 加权 模板填充 (URL/Body) 关联上下文注入 压测引擎 vegeta k6 wrk (备选) 自定义 Go 脚本

3.5 参数化质量检查清单

  • 唯一性: 每个请求的 (VU, Iteration, Timestamp) 组合全局唯一,避免重复请求污染尾延迟分布。
  • 分布真实性: 热门参数出现频率符合二八定律(20% 参数占 80% 流量)。
  • 边界覆盖: 包含空字符串、超长字符串、特殊字符(如 SQL 注入尝试)等边界参数。
  • 无状态泄漏: 参数化逻辑不依赖上一个请求的响应(除非测试有状态场景)。
  • 预热对齐: 参数分布与生产环境最近 1 小时的流量分布一致(可从日志采样)。
实战检查: 在压测启动后 10 秒,从服务端日志随机抽取 100 条请求,验证参数分布是否符合预期。若发现某个关键词占比超过 30%,说明随机算法有偏,需改用加权轮询。

3.6 本章小结