Files
photo-live/loadtest/upload-burst.js
T

145 lines
5.6 KiB
JavaScript
Raw Normal View History

// 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=<cn 基址> --env ACCOUNT=… --env PASSWORD=…');
}
if (WAIT_SYNC && !BASE_OV) {
throw new Error('WAIT_SYNC=1 需要 --env BASE_OV=<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}`);
}
}