f71a887f17
- 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>
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}`);
|
||
}
|
||
}
|