Files
photo-live/loadtest/upload-burst.js
T
浅念 f71a887f17 test(loadtest),docs: monitoring, cdn checklist and k6 framework
- monitoring.md: 指标→实现映射(1Panel+云监控+结构化日志,不引入额外监控
  系统);pino 字段表如实列自代码(访问日志 req.id/res.statusCode/
  responseTime + sync/worker 自定义 msg 字段);六类告警 jq 检索语句与
  阈值建议总表;日志保留(compose json-file 10m×3 已设 + 每日归档建议)
- cdn-checklist.md: 全步骤待凭证执行;图片加速三选一架构核对(双层签名
  冲突分析,推荐 c 起步=CDN 仅加速 viewer 静态、presigned 直链不变,
  图片收益后评估 b=CDN 鉴权+私有桶回源授权);缓存规则对齐源站 immutable;
  失效/预热 CLI 样例与多地域冷缓存测试点模板
- loadtest/: k6 框架——read-feed.js(N 观众四 mix:latest/hot 分页+大图+
  点赞+浏览,ramp 阶段)、upload-burst.js(M 并发上传全链含 complete 与
  发布/同步可选轮询,兼作数据种子)、lib/helpers.js(登录/建册/上传全链
  +内嵌 JPEG fixture 与 prod-smoke 同源)、README(运行方式/参数表/
  P95 输出解读对齐计划 §6 候选指标;D05 未定→阈值占位注释)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-21 01:52:47 +08:00

145 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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}`);
}
}