// loadtest/upload-burst.js —— L1/L2 工作人员并发上传压测场景(P5-prep Task 3)。 // // M 名工作人员(VU)并发执行小图上传全链:presign → 对象存储 PUT → complete, // 并可选轮询「公开可见」(传输完成→发布,worker 变体流水线 + 发布事务异步) // 与「ov 侧可见」(cn→ov 同步端到端)。数据面与 scripts/prod-smoke.sh ③④ 同径。 // // 兼作 read-feed.js 的数据种子:ALBUM_ID 缺省时 setup 建时间戳相册并打印 // albumId/publicToken——用打印出的 ALBUM_TOKEN 跑 read-feed.js 即得固定测试相册。 // // 用法(README.md 有完整参数表): // k6 run --env BASE_CN=https://photo.example.cn --env ACCOUNT=a --env PASSWORD=p \ // --env VUS=3 --env ITERATIONS=20 --env WAIT_PUBLISH=1 upload-burst.js // // 注意:fixtures 用内嵌 800x600 小图(3118 字节)——只压「链路与并发正确性」; // 真实尺寸图片的吞吐/处理时延另用真实样本按计划 §6 注记单独验证。 import { Counter, Trend } from 'k6/metrics'; import { check } from 'k6'; import { createAlbum, envInt, envStr, login, pollUntilVisible, uniqId, uploadPhoto, } from './lib/helpers.js'; // ── 参数 ── const BASE_CN = envStr('BASE_CN', envStr('BASE', '')); const BASE_OV = envStr('BASE_OV', ''); // 留空 = 跳过 ov 同步观测 const ACCOUNT = envStr('ACCOUNT', ''); const PASSWORD = envStr('PASSWORD', ''); const ALBUM_ID = envStr('ALBUM_ID', ''); // 空 = setup 建新相册(种子模式) const VUS = envInt('VUS', 3); // L1=3 / L2=10(同时上传人数) const ITERATIONS = envInt('ITERATIONS', 20); // 每人上传张数 const WAIT_PUBLISH = envStr('WAIT_PUBLISH', '0') === '1'; // 轮询 cn 公开可见 const WAIT_SYNC = envStr('WAIT_SYNC', '0') === '1'; // 轮询 ov 公开可见(需 BASE_OV) const POLL_TIMEOUT = envInt('POLL_TIMEOUT', 60); // 轮询上限秒 const POLL_INTERVAL = envInt('POLL_INTERVAL', 2); // 轮询间隔秒 // ── 指标 ── const presignLatency = new Trend('upload_presign_duration'); const putLatency = new Trend('upload_oss_put_duration'); // 对象存储外部依赖 const completeLatency = new Trend('upload_complete_duration'); const chainLatency = new Trend('upload_chain_duration'); // 三步端到端 const publishWait = new Trend('publish_wait_duration'); // complete → cn 公开可见 const syncWait = new Trend('ov_sync_wait_duration'); // complete → ov 公开可见 const publishTimeout = new Counter('publish_timeout_total'); const syncTimeout = new Counter('ov_sync_timeout_total'); const uploadFailed = new Counter('upload_failed_total'); export const options = { scenarios: { uploaders: { executor: 'per-vu-iterations', vus: VUS, iterations: ITERATIONS, maxDuration: '30m', }, }, // thresholds 占位 —— D05 确认后取消注释(候选口径 = 项目计划 §6: // 传输完成到发布 P95 < 10s,上传耗时单独统计不计入处理时延): // thresholds: { // upload_presign_duration: ['p(95)<500'], // upload_complete_duration: ['p(95)<2000'], // publish_wait_duration: ['p(95)<10000'], // http_req_failed: ['rate<0.01'], // }, }; export function setup() { if (!BASE_CN || !ACCOUNT || !PASSWORD) { throw new Error('缺少 --env BASE_CN= --env ACCOUNT=… --env PASSWORD=…'); } if (WAIT_SYNC && !BASE_OV) { throw new Error('WAIT_SYNC=1 需要 --env BASE_OV='); } const token = login(BASE_CN, ACCOUNT, PASSWORD); let albumId = ALBUM_ID; let publicToken = ''; if (!albumId) { const album = createAlbum(BASE_CN, token, `k6-upload-${Date.now()}`); albumId = album.albumId; publicToken = album.publicToken; console.log( `[seed] 已建相册 albumId=${albumId} ALBUM_TOKEN=${publicToken}` + '(read-feed.js 用此 token;照片不会自动清理,软删见 README)', ); } else { // 复用既有相册:publicToken 由调用方提供(种子模式首次运行会打印); // 未提供则 WAIT_PUBLISH/WAIT_SYNC 轮询自动跳过(默认函数内按空值守护)。 publicToken = envStr('ALBUM_TOKEN', ''); console.log( `[seed] 复用相册 albumId=${albumId}` + (publicToken ? ` ALBUM_TOKEN=${publicToken}` : '(未传 ALBUM_TOKEN,跳过公开可见轮询)'), ); } return { token, albumId, publicToken }; } export default function (data) { try { const result = uploadPhoto(BASE_CN, data.token, data.albumId, uniqId('k6up')); presignLatency.add(result.timings.presign); putLatency.add(result.timings.put); completeLatency.add(result.timings.complete); chainLatency.add(result.timings.chainMs); check(result, { 'upload chain ok': (r) => Boolean(r.photoId) }); if (WAIT_PUBLISH && data.publicToken && result.photoId) { const poll = pollUntilVisible( BASE_CN, data.publicToken, result.photoId, POLL_TIMEOUT, POLL_INTERVAL, 'publish_poll_cn', ); if (poll.found) { publishWait.add(poll.waitedMs); } else { publishTimeout.add(1); } } if (WAIT_SYNC && BASE_OV && data.publicToken && result.photoId) { const poll = pollUntilVisible( BASE_OV, data.publicToken, result.photoId, POLL_TIMEOUT, POLL_INTERVAL, 'publish_poll_ov', ); if (poll.found) { syncWait.add(poll.waitedMs); } else { syncTimeout.add(1); } } } catch (err) { uploadFailed.add(1); console.error(`upload chain failed: ${err.message}`); } }