// 协作存储 U7 压测(方案 §3 负载合同 / §5 U7):k6 覆盖非模型普通 API // 的吞吐与延迟阶梯;SSE 可见延迟 P95 由 Go 探针(../sse_probe)承担—— // k6 核心不支持 SSE 流式读取,两者分工记录在同一份结果目录。 // // 目标实例:run_load.sh 起的隔离栈(mock 模式 backend + 隔离 PG),绝不打 // 生产服务。预期限流/冲突(409/429)单列计数,不计入失败。 // // 阶梯与环境变量(由 run_load.sh 注入): // BASE_URL 目标基址(默认 http://127.0.0.1:18980) // ADMIN_* 引导管理员(run_load.sh 预建;setup 里登录后建压测用户) // USERS 压测用户数(默认 30;每 VU 绑定一个用户) // PROFILE smoke | ladder | sustained60m | sustained2h(默认 smoke) // // 阈值(负载合同,待实测后校准): // 读路径 p(95)<300ms、全量 p(99)<1s、非预期失败率<0.1%。 import http from 'k6/http'; import { check, group, sleep } from 'k6'; import { Counter, Trend } from 'k6/metrics'; import exec from 'k6/execution'; const BASE = __ENV.BASE_URL || 'http://127.0.0.1:18980'; const ADMIN_OWNER = __ENV.ADMIN_OWNER || 'load_admin'; const ADMIN_PASS = __ENV.ADMIN_PASS || 'load-admin-pass'; const USERS = parseInt(__ENV.USERS || '30', 10); const PROFILE = __ENV.PROFILE || 'smoke'; // 预期拥塞(负载合同要求单列,不算失败):409 会话忙 / 429 队列满。 const expectedContention = new Counter('expected_contention'); const msgLatency = new Trend('message_roundtrip', true); const stages = { smoke: [{ duration: '30s', target: 10 }], ladder: [ { duration: '2m', target: 10 }, { duration: '3m', target: 30 }, { duration: '5m', target: 100 }, { duration: '2m', target: 0 }, ], sustained60m: [ { duration: '2m', target: 30 }, { duration: '56m', target: 30 }, { duration: '2m', target: 0 }, ], sustained2h: [ { duration: '3m', target: 30 }, { duration: '114m', target: 30 }, { duration: '3m', target: 0 }, ], }; export const options = { stages: stages[PROFILE] || stages.smoke, thresholds: { // 非预期失败:2xx 之外的(409/429 已在 expected_contention 单列)。 http_req_failed: ['rate<0.001'], 'http_req_duration{group:::read}': ['p(95)<300', 'p(99)<1000'], http_req_duration: ['p(99)<1000'], }, summaryTrendStats: ['avg', 'p(50)', 'p(95)', 'p(99)', 'max'], }; // ---- setup:管理员建压测用户,各自建项目与会话(数据集随用户数扩展) ---- export function setup() { // setup 上下文的 cookie jar 自动附着(k6 语义:jar 不作为请求器使用)。 const login = http.post(`${BASE}/api/auth/login`, JSON.stringify({ owner: ADMIN_OWNER, password: ADMIN_PASS }), { headers: { 'Content-Type': 'application/json' } }); if (login.status !== 200) throw new Error(`admin login: ${login.status} ${login.body}`); // 首登强制改密由 run_load.sh 预处理(CLI 建号即带终态口令),此处直接可用。 const users = []; for (let i = 0; i < USERS; i++) { const owner = `load_u${i}`; const password = `load-user-pass-${i}-Aa9`; const created = http.post(`${BASE}/api/admin/users`, JSON.stringify({ owner, name: `压测用户${i}`, password, isAdmin: false }), { headers: { 'Content-Type': 'application/json' } }); // 409 = 已存在(重复压测同一栈),接受。 if (created.status !== 200 && created.status !== 409) throw new Error(`create user ${owner}: ${created.status} ${created.body}`); users.push({ owner, password }); } return { users }; } // ---- default:每 VU 一个用户身份,混合读写 ---- export default function (data) { const u = data.users[exec.scenario.iterationInTest % data.users.length]; // VU 上下文的 cookie jar 自动附着;每 VU 绑定一个用户身份。 const login = http.post(`${BASE}/api/auth/login`, JSON.stringify({ owner: u.owner, password: u.password }), { headers: { 'Content-Type': 'application/json' } }); if (login.status !== 200) { check(null, { 'user login': () => false }); return; } // 会话/项目绑定 VU(VU 内多次迭代复用同一身份的会话,形成热点会话)。 if (!u.__ctx) { const proj = http.post(`${BASE}/api/projects`, JSON.stringify({ name: `压测-${u.owner}` }), { headers: { 'Content-Type': 'application/json' } }); const conv = http.post(`${BASE}/api/projects/${proj.json().id}/conversations`, JSON.stringify({ title: '压测会话' }), { headers: { 'Content-Type': 'application/json' } }); u.__ctx = { projectID: proj.json().id, convID: conv.json().id }; } const { projectID, convID } = u.__ctx; group('read', () => { const projs = http.get(`${BASE}/api/projects`); check(projs, { 'list projects 2xx': (r) => r.status === 200 }); const detail = http.get(`${BASE}/api/conversations/${convID}`); check(detail, { 'conversation detail 2xx': (r) => r.status === 200 }); const cards = http.get(`${BASE}/api/conversations/${convID}/cards`); check(cards, { 'cards 2xx': (r) => r.status === 200 }); const settings = http.get(`${BASE}/api/settings`); check(settings, { 'settings 2xx': (r) => r.status === 200 }); }); group('write', () => { const requestId = `req-${u.owner}-${exec.scenario.iterationInTest}`; const body = JSON.stringify({ text: `压测消息 ${Date.now()}`, requestId }); const t0 = Date.now(); const sent = http.post(`${BASE}/api/conversations/${convID}/messages`, body, { headers: { 'Content-Type': 'application/json' } }); if (sent.status === 200) { msgLatency.add(Date.now() - t0); const id = sent.json().message && sent.json().message.id; check(sent, { 'message has id': () => !!id }); // 幂等重放(矩阵行:同 requestId 返回同一逻辑结果):抽样 10%。 if (exec.scenario.iterationInTest % 10 === 0) { const replay = http.post(`${BASE}/api/conversations/${convID}/messages`, body, { headers: { 'Content-Type': 'application/json' } }); check(replay, { 'requestId replay same message': (r) => r.status === 200 && r.json().message && r.json().message.id === id, }); } } else if (sent.status === 409 || sent.status === 429) { // 热点会话忙/队列满:预期拥塞,单列。 expectedContention.add(1); } else { check(sent, { 'message send 2xx': () => false }); } }); sleep(1); }