Files
photo-live/loadtest/read-feed.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

173 lines
6.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/read-feed.js —— L1/L2 观众读路径压测场景(P5-prep Task 3)。
//
// 模拟 N 名观众(VU=观众,ramp 阶段爬坡-稳态-退坡)在 ov 域公开相册上的四类
// 行为混合:持续分页读最新(latest)/ 热门(hot)切换 / 大图请求(presigned
// 直链打到对象存储,单独 tag 区分)/ 点赞批次 / 浏览上报(每 VU 一次,
// 30min 去重窗口内重复上报 counted=false 属正常语义)。
//
// 用法(README.md 有完整参数表):
// k6 run --env BASE=https://photo.example.com --env ALBUM_TOKEN=xxx \
// --env VUS=100 --env STEADY=5m read-feed.js
//
// 阈值(thresholds)为占位注释:D05(数量并发与网络目标)未定,候选口径
// 对齐项目计划 §6 候选指标,确认后在 options.thresholds 取消注释启用。
import http from 'k6/http';
import exec from 'k6/execution';
import { check, sleep } from 'k6';
import { Counter, Trend } from 'k6/metrics';
import {
envFloat,
envInt,
envStr,
fetchFeedPage,
likeBatch,
reportVisit,
uniqId,
} from './lib/helpers.js';
// ── 参数(全部可 --env 覆盖;缺省 = L1 基础档)──
const BASE = envStr('BASE', '');
const ALBUM_TOKEN = envStr('ALBUM_TOKEN', '');
const VUS = envInt('VUS', 100); // L1=100 / L2=1000(在线观众)
const RAMP_UP = envStr('RAMP_UP', '1m');
const STEADY = envStr('STEADY', '5m');
const RAMP_DOWN = envStr('RAMP_DOWN', '1m');
const PAGES = envInt('PAGES', 3); // 每迭代翻页深度
const PAGE_LIMIT = envInt('PAGE_LIMIT', 30); // 契约上限 60
const HOT_SHARE = envFloat('HOT_SHARE', 0.5); // 迭代内切热门排序概率
const BIGIMG_SHARE = envFloat('BIGIMG_SHARE', 0.3); // 迭代内取大图概率
const LIKE_SHARE = envFloat('LIKE_SHARE', 0.4); // 迭代内点赞批次概率
const THINK_MIN = envFloat('THINK_MIN', 0.5); // 观众思考时间(秒)
const THINK_MAX = envFloat('THINK_MAX', 3);
// ── 指标(按操作分组,输出解读见 README)──
const listLatency = new Trend('feed_list_duration'); // latest 分页请求
const hotLatency = new Trend('feed_hot_duration'); // hot 排序页
const imageLatency = new Trend('image_get_duration'); // 对象存储直链大图(外部依赖)
const likeLatency = new Trend('like_confirm_duration'); // 点赞批次确认
const visitLatency = new Trend('visit_report_duration'); // 浏览上报
const likeAccepted = new Counter('like_accepted_total');
const likeRejected = new Counter('like_rejected_total'); // 429 限速 / batchAccepted=false
const listBroken = new Counter('feed_list_error_total'); // 非 200 / 解析失败
export const options = {
scenarios: {
viewers: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: RAMP_UP, target: VUS },
{ duration: STEADY, target: VUS },
{ duration: RAMP_DOWN, target: 0 },
],
gracefulRampDown: '30s',
},
},
// thresholds 占位 —— D05 确认后取消注释(候选口径 = 项目计划 §6):
// 普通列表 API P95 < 500ms;点赞确认 P95 < 2s(广播链路另由前端观测)。
// thresholds: {
// feed_list_duration: ['p(95)<500'],
// feed_hot_duration: ['p(95)<500'],
// like_confirm_duration: ['p(95)<2000'],
// visit_report_duration: ['p(95)<500'],
// 'checks': ['rate>0.99'],
// http_req_failed: ['rate<0.01'],
// },
};
// k6 每个 VU 独立初始化模块:以下状态天然按 VU 隔离(观众身份)。
const visitorId = uniqId(`visitor-${exec.vu.idInTest}`);
const likeSessionId = uniqId(`sess-${exec.vu.idInTest}`);
let visitReported = false;
export function setup() {
if (!BASE || !ALBUM_TOKEN) {
throw new Error('缺少 --env BASE=<ov 基址> 与 --env ALBUM_TOKEN=<公开相册 token>');
}
const res = http.get(`${BASE}/v1/public/albums/${ALBUM_TOKEN}`);
if (res.status !== 200) {
throw new Error(`相册不可访问(HTTP ${res.status}):核对 BASE/ALBUM_TOKEN`);
}
return { albumTitle: res.json('title') };
}
export default function () {
// ① 浏览上报(每观众会话一次;窗口语义见文件头)
if (!visitReported) {
visitReported = true;
const res = reportVisit(BASE, ALBUM_TOKEN, visitorId, uniqId('vr'));
visitLatency.add(res.timings.duration);
check(res, { 'visit report 200': (r) => r.status === 200 });
}
// ② 最新分页(游标推进至 nextCursor=null 或 PAGES 页)
const items = [];
let cursor;
for (let page = 0; page < PAGES && cursor !== null; page += 1) {
const res = fetchFeedPage(BASE, ALBUM_TOKEN, {
cursor,
limit: PAGE_LIMIT,
tag: 'list_public_photos',
});
listLatency.add(res.timings.duration);
if (
!check(res, {
'list 200': (r) => r.status === 200,
'list has items': (r) => Array.isArray(r.json('items')),
})
) {
listBroken.add(1);
break;
}
const body = res.json();
for (const item of body.items) {
items.push(item);
}
cursor = body.nextCursor; // null = 末页,循环条件止
}
// ③ 热门排序一页(sort=hot 仅 ov 实现)
if (Math.random() < HOT_SHARE) {
const res = fetchFeedPage(BASE, ALBUM_TOKEN, {
limit: PAGE_LIMIT,
sort: 'hot',
tag: 'list_public_photos_hot',
});
hotLatency.add(res.timings.duration);
check(res, { 'hot list 200': (r) => r.status === 200 });
}
// ④ 大图请求(presigned 直链 → 对象存储/CDN,非被测 API;tag 区分解读)
if (items.length > 0 && Math.random() < BIGIMG_SHARE) {
const item = items[Math.floor(Math.random() * items.length)];
const url = item.browse && item.browse.url;
if (url) {
const res = http.get(url, { tags: { name: 'oss_image_get' } });
imageLatency.add(res.timings.duration);
check(res, { 'image 200': (r) => r.status === 200 });
}
}
// ⑤ 点赞批次(匿名端点;限速口径见 README——压测会话请注意 LIKE_IP_WINDOW_DELTA)
if (items.length > 0 && Math.random() < LIKE_SHARE) {
const item = items[Math.floor(Math.random() * items.length)];
const delta = 1 + Math.floor(Math.random() * 3); // 契约 1..20,取保守值
const res = likeBatch(
BASE,
ALBUM_TOKEN,
item.photoId,
likeSessionId,
uniqId('batch'),
delta,
);
likeLatency.add(res.timings.duration);
if (res.status === 200 && res.json('batchAccepted') === true) {
likeAccepted.add(1);
} else {
likeRejected.add(1); // 429=限速触发(压测口径问题,见 README);非 200=链路异常
}
}
sleep(THINK_MIN + Math.random() * (THINK_MAX - THINK_MIN));
}