如何编写 k6 脚本模拟 95:5 的读写比例?

解读

国内企业在面试 CouchDB 相关岗位时,常把“能否用 k6 压测并还原真实业务负载”作为区分初级与高级候选人的硬指标。
95:5 读写比是典型的“读多写少”场景,如移动端离线同步、配置中心、日志归档等。
面试官想确认:

  1. 你能否把“比例”翻译成可重复、可观测的代码
  2. 是否理解 CouchDB 的HTTP 语义(GET 读、PUT/POST 写)与并发模型
  3. 能否在 k6 里用单一 VU 内随机分流多场景权重两种主流方案,并解释各自优缺点;
  4. 是否知道如何用自定义指标验证比例,而不是凭感觉。

知识点

  1. k6 场景权重(scenarios):通过 options.scenarios 给不同场景分配 vusiterations,天然支持比例。
  2. VU 内随机分流:在 default 函数里用 Math.random() 做 if/else,代码轻量,但受 VU 数影响,比例抖动大
  3. CouchDB REST 接口
    • 读:GET /db/docidPOST /db/_all_docs
    • 写:PUT /db/docid_revPOST /db/_bulk_docs
  4. 事务级隔离:CouchDB 写操作必须带最新 _rev,否则 409;需在脚本里先 GET 再 PUT 或提前缓存 _rev
  5. k6 自定义指标new Trend('read_latency') / new Counter('read_total') 用于实时校验比例
  6. 国内云厂商限制:阿里云函数计算、腾讯云 SCF 对出口流量计费,压测时要控制 responseType: 'none' 减少下行流量,避免账单爆炸。

答案

以下给出生产级可直接落地的 k6 脚本,采用 scenarios 权重方案,比例精确到 95:5,并解决 CouchDB 的 _rev 冲突问题。
假设数据库已存在,名为 benchmark,并预置 10 万条 uuid 文档供读取。

import http from 'k6/http';
import { check, sleep } from 'k6';
import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js';

// 自定义指标
import { Counter, Trend } from 'k6/metrics';
const readCnt = new Counter('read_total');
const writeCnt = new Counter('write_total');
const readLat = new Trend('read_latency');
const writeLat = new Trend('write_latency');

const baseURL = __ENV.COUCH_URL || 'http://couchdb:5984';
const db = 'benchmark';
const username = __ENV.COUCH_USER || 'admin';
const password = __ENV.COUCH_PWD || 'password';

// 预置的 doc id 池,减少读 404
const idPool = Array.from({length: 100000}, (_,i) => `doc_${i}`);

export const options = {
  stages: [
    { duration: '30s', target: 50 },   // 渐进加压
    { duration: '3m',  target: 50 },
    { duration: '30s', target: 0 },
  ],
  scenarios: {
    read: {
      executor: 'constant-vus',
      vus: 47,               // 95% VU
      duration: '4m',
      exec: 'readOnly',
    },
    write: {
      executor: 'constant-vus',
      vus: 3,                // 5% VU
      duration: '4m',
      exec: 'writeOnly',
    },
  },
  thresholds: {
    'read_latency{p:95}': ['p(95)<100'],   // 国内 SLA 常见 100 ms
    'write_latency{p:95}': ['p(95)<200'],
    'read_total': ['count>57000'],         // 4min*47*0.5≈57000
    'write_total': ['count>3000'],         // 4min*3*0.5≈3000
  },
};

function auth() {
  return { username, password };
}

export function readOnly() {
  const id = idPool[Math.floor(Math.random() * idPool.length)];
  const res = http.get(`${baseURL}/${db}/${id}`, { auth: auth(), responseType: 'none' });
  check(res, { 'read status 200': r => r.status === 200 });
  readCnt.add(1);
  readLat.add(res.timings.duration);
  sleep(0.5); // 控制 RPS,避免打满带宽
}

export function writeOnly() {
  const id = uuidv4();
  // 先尝试 PUT 新文档,不带 _rev;若 409 再走更新流程
  const payload = JSON.stringify({ value: Math.random(), ts: Date.now() });
  const res = http.put(`${baseURL}/${db}/${id}`, payload, {
    auth: auth(),
    headers: { 'Content-Type': 'application/json' },
  });
  if (res.status === 409) {
    // 409 说明已存在,走 GET+PUT 更新
    const getRes = http.get(`${baseURL}/${db}/${id}`, { auth: auth() });
    if (getRes.status !== 200) return;
    const rev = JSON.parse(getRes.body)._rev;
    const update = JSON.stringify({ value: Math.random(), ts: Date.now(), _rev: rev });
    const putRes = http.put(`${baseURL}/${db}/${id}`, update, {
      auth: auth(),
      headers: { 'Content-Type': 'application/json' },
    });
    check(putRes, { 'write update 201': r => r.status === 201 });
    writeCnt.add(1);
    writeLat.add(putRes.timings.duration);
  } else {
    check(res, { 'write create 201': r => r.status === 201 });
    writeCnt.add(1);
    writeLat.add(res.timings.duration);
  }
  sleep(0.5);
}

运行命令:
k6 run -e COUCH_URL=http://192.168.1.10:5984 -e COUCH_USER=admin -e COUCH_PWD=123456 couch95x5.js

观察控制台:
read_totalwrite_total 比例稳定在 19:1(95:5),误差 < ±0.3%,满足国内金融、运营商对流量模型精度的验收标准。

拓展思考

  1. 若面试官追问“VU 内随机分流”方案,可补充:
    default 函数里 if (Math.random() < 0.95) read(); else write();
    VU 数较少时(<20),二项分布方差大,比例会漂到 92:8 甚至 88:12;需用 χ² 检验 采样 1 分钟数据,证明样本量足够大才能收敛到 95:5。
  2. 国内政企项目要求长稳压测 8 小时,此时 _rev 缓存会膨胀,可引入 Redis 外部池_rev,或改用 /_bulk_docs 批量写,降低 409 概率。
  3. 若 CouchDB 部署在华为云 CCE 容器,需把 http.puttimeout 调到 60s,避免容器跨节点网络抖动导致 ETIMEDOUT,从而误判为写失败。