145 lines
5.6 KiB
JavaScript
145 lines
5.6 KiB
JavaScript
|
|
// 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}`);
|
|||
|
|
}
|
|||
|
|
}
|