如何编写 k6 脚本模拟 95:5 的读写比例?
解读
国内企业在面试 CouchDB 相关岗位时,常把“能否用 k6 压测并还原真实业务负载”作为区分初级与高级候选人的硬指标。
95:5 读写比是典型的“读多写少”场景,如移动端离线同步、配置中心、日志归档等。
面试官想确认:
- 你能否把“比例”翻译成可重复、可观测的代码;
- 是否理解 CouchDB 的HTTP 语义(GET 读、PUT/POST 写)与并发模型;
- 能否在 k6 里用单一 VU 内随机分流或多场景权重两种主流方案,并解释各自优缺点;
- 是否知道如何用自定义指标验证比例,而不是凭感觉。
知识点
- k6 场景权重(scenarios):通过
options.scenarios给不同场景分配vus与iterations,天然支持比例。 - VU 内随机分流:在
default函数里用Math.random()做 if/else,代码轻量,但受 VU 数影响,比例抖动大。 - CouchDB REST 接口:
- 读:
GET /db/docid或POST /db/_all_docs - 写:
PUT /db/docid带_rev或POST /db/_bulk_docs
- 读:
- 事务级隔离:CouchDB 写操作必须带最新
_rev,否则 409;需在脚本里先 GET 再 PUT 或提前缓存_rev。 - k6 自定义指标:
new Trend('read_latency')/new Counter('read_total')用于实时校验比例。 - 国内云厂商限制:阿里云函数计算、腾讯云 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_total 与 write_total 比例稳定在 19:1(95:5),误差 < ±0.3%,满足国内金融、运营商对流量模型精度的验收标准。
拓展思考
- 若面试官追问“VU 内随机分流”方案,可补充:
在default函数里if (Math.random() < 0.95) read(); else write();
但VU 数较少时(<20),二项分布方差大,比例会漂到 92:8 甚至 88:12;需用 χ² 检验 采样 1 分钟数据,证明样本量足够大才能收敛到 95:5。 - 国内政企项目要求长稳压测 8 小时,此时
_rev缓存会膨胀,可引入 Redis 外部池 存_rev,或改用/_bulk_docs批量写,降低 409 概率。 - 若 CouchDB 部署在华为云 CCE 容器,需把
http.put的timeout调到 60s,避免容器跨节点网络抖动导致 ETIMEDOUT,从而误判为写失败。