
Pyroscope 负载测试实战用 k6 压测连续性能分析平台的读 API【免费下载链接】pyroscopeContinuous Profiling Platform. Debug performance issues down to a single line of code项目地址: https://gitcode.com/GitHub_Trending/py/pyroscopePyroscope 是一个开源的连续性能分析Continuous Profiling平台能够将性能问题定位到单行代码。为了让平台在真实流量下保持稳定官方在仓库中内置了一套基于 k6 目录。本文将以该目录下的 README.md 为骨架结合run.sh、.env.template、lib/与tests/中的真实实现完整讲解如何对 Pyroscope 的读 APIread API发起压力测试、如何配置压测参数、以及每条压测请求背后对应的真实查询端点帮助你快速上手并理解这套压测工具的设计思路。目录结构与整体设计k6 目录 的职责非常清晰README 将其划分为两部分压测脚本load test scripts与辅助函数helper functions。tools/k6/ ├── .env.template # 环境变量模板说明每个压测配置项的含义与默认值 ├── README.md # 使用文档 ├── run.sh # 统一启动脚本支持本地运行与 k6 Cloud 运行 ├── lib/ │ ├── env.js # 读取并校验环境变量 │ ├── request.js # 封装 6 类读 API 请求gRPC 风格与 HTTP 风格 │ └── time.js # 相对时间范围last N 秒/分/时/天计算工具 └── tests/ └── reads.js # 读 API 压测主脚本场景编排、请求组装、查询分布注释从文件结构可以看出这套工具的定位是围绕 Pyroscope 读路径进行持续负载验证目前仓库中提供的压测用例是读路径场景reads.js。快速开始本地运行与云运行README 给出了两种运行方式核心命令只有一条# 1. 对本地 Pyroscope 实例运行压测 ./run.sh reads.js # 2. 从 k6 Cloud 运行压测 ./run.sh -c reads.js注意两点命令必须在仓库根目录下执行run.sh内部固定使用tools/k6/tests/${TEST}拼接测试文件路径因此传入的参数是tests目录下的文件名如reads.js。run.sh的完整逻辑如下源码见 tools/k6/run.shset -euo pipefail usage() { echo Usage: $0 [-c] file echo file File name of the test to run echo -c Run the test in the cloud echo -h Print help message and list all available tests } tests() { echo Available tests: for file in tools/k6/tests/*.js; do echo $(basename ${file}) done } IS_CLOUD while getopts hc opt; do case ${opt} in c) IS_CLOUD1 ;; h) usage; echo; tests; exit 0 ;; *) usage; exit 1 ;; esac done shift $((OPTIND-1)) TEST$1 if [ -z ${TEST} ]; then usage exit 1 fi DIR$(cd $(dirname ${BASH_SOURCE[0]}) ; pwd -P) source ${DIR}/.env if [ -n ${IS_CLOUD} ]; then k6 cloud run tools/k6/tests/${TEST} \ -e K6_BASE_URL${K6_BASE_URL} \ -e K6_READ_TOKEN${K6_READ_TOKEN} \ -e K6_TENANT_ID${K6_TENANT_ID} \ -e K6_QUERY_SERVICE_NAME${K6_QUERY_SERVICE_NAME} \ -e K6_QUERY_DURATIONS${K6_QUERY_DURATIONS} else K6_BASE_URL${K6_BASE_URL} \ K6_READ_TOKEN${K6_READ_TOKEN} \ K6_TENANT_ID${K6_TENANT_ID} \ K6_QUERY_SERVICE_NAME${K6_QUERY_SERVICE_NAME} \ K6_QUERY_DURATIONS${K6_QUERY_DURATIONS} \ k6 run tools/k6/tests/${TEST} fi脚本的关键行为-c切换到 k6 Cloud 运行模式对应k6 cloud run。-h打印用法并列出tools/k6/tests/*.js下所有可用测试方便快速查看当前有哪些用例。执行前会source同目录下的.env文件把其中的变量加载进环境本地运行模式通过命令前置的环境变量赋值把 5 个K6_*变量注入 k6 进程云运行模式则通过-e参数逐个传入。脚本使用set -euo pipefail任一环节失败都会立即终止便于在 CI 中直接使用。云运行的前提README 特别提醒从 k6 Cloud 运行时必须事先配置好.env文件。因为云环境下 k6 的执行节点不在你的机器上它只能通过run.sh传入的-e参数拿到目标地址与凭据而这些值全部来自.env。环境变量配置.env.template 逐项说明README 指出每个压测用例都可以通过环境变量调整行为各变量的含义与默认值记录在.env.template中。使用方式把.env.template复制为.env按需修改后run.sh会自动读取因为脚本会source .env。完整模板内容见 tools/k6/.env.template## Copy this template to .env and replace the values with your own. ## reads.js ## (REQUIRED) The base URL of the application under test. K6_BASE_URLhttp://localhost:4040 ## The read token to authenticate with the server. Only used if the server ## requires authentication. K6_READ_TOKEN ## The tenant ID to use for the test, if in running against a multi-tenant ## environment. K6_TENANT_ID ## (DEFAULT fire-dev-001/ingester) The service name to use in the read ## queries. K6_QUERY_SERVICE_NAME ## (DEFAULT 1h) Comma-separated list of durations to use in the read queries. ## These durations will be used in last X time ranges. K6_QUERY_DURATIONS各配置项的作用如下变量是否必填默认值说明K6_BASE_URL必填http://localhost:4040被测 Pyroscope 服务的 Base URL。本地默认端口 4040K6_READ_TOKEN视情况空读请求的认证 token仅当服务端开启认证时才需要K6_TENANT_ID视情况空多租户环境下的租户 IDK6_QUERY_SERVICE_NAME否fire-dev-001/ingester读查询中使用的 service_name 标签值K6_QUERY_DURATIONS否1h逗号分隔的时间跨度列表用于生成 “last X” 相对时间范围如1h,24h从源码可以印证这些变量的实际用途见 tools/k6/lib/env.js 与 tools/k6/tests/reads.js// lib/env.js缺失 K6_BASE_URL 时直接让 k6 报错失败 export const READ_TOKEN __ENV.K6_READ_TOKEN; export const TENANT_ID __ENV.K6_TENANT_ID; export const BASE_URL __ENV.K6_BASE_URL || fail(K6_BASE_URL environment variable missing);// reads.js时间跨度与 service_name 均可在运行时覆盖 const timeRanges (__ENV.K6_QUERY_DURATIONS || 1h).split(,).map((s) { return [parseInt(s.slice(0, -1)), s.slice(-1)]; }); const serviceName __ENV.K6_QUERY_SERVICE_NAME || fire-dev-001/ingester;例如设置K6_QUERY_DURATIONS1h,24h脚本会依次以“最近 1 小时”和“最近 24 小时”两个时间窗口各执行一轮全部查询设置K6_QUERY_SERVICE_NAMEpayment-service则会把所有查询的service_name标签替换为payment-service。若本地 Pyroscope 未开启认证默认单机部署通常如此K6_READ_TOKEN与K6_TENANT_ID可以留空压测多租户部署时则需要填充这两个值。压测用例reads.js 详解查询分布来自真实生产 7 天流量的统计reads.js 中保留了一段非常有价值的注释——它记录了从 Pyroscope 运营环境中抓取的7 天真实查询分布并标明哪些端点已在压测中实现✅哪些尚未覆盖❌count % endpoint implemented ------ ------ ----------------------------------- ----------- 11997 78.03 /querier.v1.QuerierService/SelectMergeProfile ✅ 2298 14.95 /pyroscope/render ✅ 461 3.00 /querier.v1.QuerierService/SelectMergeStacktraces ✅ 221 1.44 /querier.v1.QuerierService/LabelNames ✅ 130 0.85 /querier.v1.QuerierService/Series ✅ 100 0.65 /pyroscope/render-diff ✅ 59 0.38 /querier.v1.QuerierService/ProfileTypes ❌ 54 0.35 /querier.v1.QuerierService/SelectSeries ❌ 28 0.18 /querier.v1.QuerierService/LabelValues ❌ 26 0.17 /querier.v1.QuerierService/SelectMergeSpanProfile ❌ 1 0.01 /querier.v1.QuerierService/GetProfileStats ❌这份分布表揭示了两个重要信息SelectMergeProfile是绝对主力占比高达 78%它是把匹配的 profile 聚合为 pprof 格式的核心查询/pyroscope/render次之占 15%。当前压测脚本平均分配请求到已实现的 6 个端点作者在注释中明确写到最终目标应是让压测流量贴合上述真实分布Ultimately we should try tune our load tests to match this distribution并进一步识别真实查询参数分布。这说明这套工具仍处于演进状态你可以在自己的环境中按需调整请求比例。场景与阈值配置export const options { ext: { loadimpact: { projectID: 16425, name: reads, }, }, scenarios: { even_reads: { executor: constant-arrival-rate, duration: 5m, rate: 10, timeUnit: 1m, preAllocatedVUs: 3, maxVUs: 10, }, }, thresholds: { checks: [rate0.9], }, };场景名even_reads采用constant-arrival-rate执行器即恒定到达率——在 5 分钟内稳定地以每分钟 10 个迭代的速率发起请求这与读 API 的真实访问模式持续、平稳更为接近而非瞬间爆发。preAllocatedVUs: 3、maxVUs: 10预先分配 3 个虚拟用户峰值最多可扩展到 10 个k6 会在需要时自动扩容以满足恒定到达率。阈值checks: [rate0.9]所有check的通过率必须大于 90%否则压测结束时 k6 会以非零退出码结束——这个阈值可以直接作为 CI 中的门禁条件。压测主体default 函数与请求组装// 启用 Pyroscope 自动埋点auto-labeling pyroscope.instrumentHTTP(); export default function() { const timeRanges (__ENV.K6_QUERY_DURATIONS || 1h).split(,).map((s) { return [parseInt(s.slice(0, -1)), s.slice(-1)]; }); const serviceName __ENV.K6_QUERY_SERVICE_NAME || fire-dev-001/ingester; for (const [scalar, unit] of timeRanges) { group(reads last ${scalar}${unit}, () { const { start, end } newRelativeTimeRange(scalar, unit); doAllQueryRequests(serviceName, start, end); }); } }流程为解析K6_QUERY_DURATIONS得到若干个 (数值, 单位) 组合 → 对每个时间窗口建立一个 k6group→ 通过newRelativeTimeRange计算相对时间范围 → 一次性发出全部 6 类查询。time.js中的newRelativeTimeRangetools/k6/lib/time.js支持s/m/h/d四种单位非法单位会直接抛错export function newRelativeTimeRange(scalar, unit) { const end Date.now(); switch (unit) { case s: return { start: end - scalar * 1000, end }; case m: return { start: end - scalar * 60 * 1000, end }; case h: return { start: end - scalar * 60 * 60 * 1000, end }; case d: return { start: end - scalar * 24 * 60 * 60 * 1000, end }; default: throw new Error(Invalid unit: ${unit}); } }值得注意reads.js从 tools/k6/lib/time.js 导入newRelativeTimeRange但lib/time.js中同时存在同名的函数实现reads.js末尾也内联了一份相同实现这是工具演进过程中留下的冗余理解时以lib/time.js为准即可。6 类读请求的端点与底层实现lib/request.jstools/k6/lib/request.js封装了 6 个请求函数分为两类gRPC 风格通过 Connect 协议走 HTTP POST路径形如/querier.v1.QuerierService/XXXSelectMergeProfile、SelectMergeStacktraces、LabelNames、SeriesHTTP 风格直接 GET Pyroscope 传统查询端点/pyroscope/render、/pyroscope/render-diff。所有请求都会经过withHeaders注入统一头部tools/k6/lib/request.jsfunction withHeaders(headers) { const baseHeaders { User-Agent: k6-load-test, Content-Type: application/json, Authorization: Basic ${encoding.b64encode(${TENANT_ID}:${READ_TOKEN})} }; for (const [k, v] of Object.entries(headers || {})) { baseHeaders[k] v; } return baseHeaders; }可以看到认证方式为Basic Auth用户名为TENANT_ID、密码为READ_TOKEN对应多租户下的读写权限隔离。同时每个请求都设置了tags: { name: ... }便于在 k6 结果报表中按端点维度聚合指标。下面是doAllQueryRequeststools/k6/tests/reads.js中 6 类请求的完整请求体与对应端点说明1. SelectMergeProfilegRPC/ConnectdoSelectMergeProfileRequest({ start, end, profile_typeID: process_cpu:cpu:nanoseconds:cpu:nanoseconds, label_selector: {service_name${serviceName}}, });请求路径POST {BASE_URL}/querier.v1.QuerierService/SelectMergeProfile。该方法在 api/querier/v1/querier.proto 中定义为“返回匹配的 profile并以 pprof 格式聚合”SelectMergeProfile returns matching profiles aggregated in pprof format。它返回的是 google.v1.Profile 类型的 pprof 数据是占比最高的查询也是火焰图等可视化能力的核心数据源。2. /pyroscope/renderHTTPdoRenderRequest({ from: start, until: end, query: process_cpu:cpu:nanoseconds:cpu:nanoseconds{service_name${serviceName}}, aggregation: sum, format: json, max-nodes: 16384, });这是 Pyroscope 的传统渲染端点from/until为毫秒时间戳query使用 Pyroscope Query Languageprofile_type{labelvalue}aggregation: sum指定聚合方式format: json控制返回格式max-nodes限制火焰图最大节点数。从 pkg/querier/http.go 的实现看该端点最终会经由SelectMergeProfile/SelectMergeStacktraces的 gRPC 客户端调用完成数据查询与渲染。3. SelectMergeStacktracesgRPC/ConnectdoSelectMergeStacktracesRequest({ start, end, profile_typeID: process_cpu:cpu:nanoseconds:cpu:nanoseconds, label_selector: {service_name${serviceName}}, max-nodes: 16384, });路径POST {BASE_URL}/querier.v1.QuerierService/SelectMergeStacktracesproto 中描述为“返回匹配的 profile并聚合成 flamegraph 形式”SelectMergeStacktraces returns matching profiles aggregated in a flamegraph。与SelectMergeProfile的区别在于返回格式前者返回 pprof后者返回聚合后的调用栈树flamegraph。4. LabelNamesgRPC/ConnectdoLabelNamesRequest({ start, end, matchers: [ {__profile_type__process_cpu:cpu:nanoseconds:cpu:nanoseconds, service_name${serviceName}}, ], });路径POST {BASE_URL}/querier.v1.QuerierService/LabelNamesproto 定义见 api/querier/v1/querier.protoLabelNames returns a list of the existing label names。请求通过matchers限定 profile 类型与 service 范围用于返回该范围内存在的标签名列表。5. SeriesgRPC/ConnectdoSeriesRequest({ start, end, labelNames: [service_name, __profile_type__], matchers: [], });路径POST {BASE_URL}/querier.v1.QuerierService/Series用于查询指定时间范围内、按labelNames维度展开的序列集合series set常用于图表的时间序列选择与浏览。6. /pyroscope/render-diffHTTPdoRenderDiffRequest({ rightQuery: process_cpu:cpu:nanoseconds:cpu:nanoseconds{service_name${serviceName}}, rightFrom: start, rightUntil: end, leftQuery: process_cpu:cpu:nanoseconds:cpu:nanoseconds{service_name${serviceName}}, leftFrom: start - (end - start), // 左时间窗与右时间窗等长向前平移 leftUntil: start, format: json, max-nodes: 16384, });渲染差分火焰图diff flamegraph右侧right*为最近一个时间窗左侧left*为往前平移等长距离的对照时间窗leftFrom start - (end - start)从而对比“现在 vs 之前”的性能变化这也是 Pyroscope 差分对比功能的负载来源。每个请求函数内部都通过 k6 的check断言status is 200一旦任一请求非 200就会拉低整体通过率并最终触发rate0.9的阈值告警。压测报告的解读与工程化建议运行./run.sh reads.js后k6 会在终端输出汇总报告包括每秒请求数RPS、请求延迟分布P90/P95 等、每个group和tag name维度的耗时明细以及checks通过率。结合tags: { name: ... }你可以直接从报告中对比 6 类读端点的延迟与错误率差异快速定位读路径上的性能瓶颈例如SelectMergeProfile大范围聚合耗时、render-diff双时间窗查询开销等。几点工程化建议基于仓库现有实现推断接入 CIrun.sh在本地运行模式下退出码即为 k6 退出码而 k6 在checks通过率跌破阈值rate0.9时会以非零码退出因此可直接作为 CI 门禁。贴近真实流量当前脚本对 6 个端点平均施压与 7 天运营数据SelectMergeProfile占 78%存在差距如需更真实的压测可在doAllQueryRequests内按上述比例调整各请求的调用权重。多时间窗回归利用K6_QUERY_DURATIONS支持逗号分隔的特性如1h,24h一次运行即可同时覆盖“热数据”与“长跨度聚合”两类典型读路径压力。多租户验证配置K6_TENANT_ID与K6_READ_TOKEN后即可针对启用认证的多租户部署验证读路径的鉴权与租户隔离逻辑。总结这套位于 tools/k6 的负载测试工具是 Pyroscope 官方为验证读路径稳定性而内置的“活文档”run.sh提供了本地/云两种运行方式.env.template完整声明了 5 个可调参数reads.js以恒定到达率场景对 6 类核心读端点SelectMergeProfile、/render、SelectMergeStacktraces、LabelNames、Series、/render-diff持续施压并保留了来自生产环境 7 天真实查询分布的演进路线图。无论你是要为自己的 Pyroscope 部署做容量评估、在发布前验证读路径回归还是想深入了解 Pyroscope 读 API 的查询形态都可以直接基于本目录的脚本二次开发。【免费下载链接】pyroscopeContinuous Profiling Platform. Debug performance issues down to a single line of code项目地址: https://gitcode.com/GitHub_Trending/py/pyroscope创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考