一、为什么要让k6的数据“活”起来

很多朋友刚开始用k6做性能测试时,习惯直接看终端里那一行行实时数字:每秒请求数、响应时间、错误率……看完就关了。这当然很方便,但问题也很明显——测试一结束,数据就没了。你没法回答“上周那版接口为什么比现在快”“半夜那波流量到底压垮了哪个服务”,更别提画趋势图了。

把数据存下来,再放到一个能查询、能画图的工具里,这事儿说白了就是“持久化与分析”。而k6本身不提供存储,它只负责把数字吐出来。那我们就得给它找一个“数据归宿”,Prometheus就是一个特别合适的搭档。它不是最复杂的方案,但绝对是最常见、最容易上手、社区资料最丰富的一套组合。

这套组合的思路很简单:k6在跑测试的时候,一边往自己终端上打结果,一边顺便把指标推给Prometheus,Prometheus把数据存成时间序列,然后你用Grafana或者其他查询工具,想看什么时候的数据都行。整个过程一旦配好,后面就全是自动化的事,你只管发测试、看报表。

二、这套方案适合用在哪儿

我先说说我实际工作中遇到的应用场景,大家感受一下:

第一个场景:日常回归测试。 每次发版前,你在预发环境跑一遍k6脚本,压一下核心接口。以前是截图保存终端输出,但截图没法搜索,也没法对比。现在k6每次测试的完整指标都进Prometheus了,你想对比“上一个版本”和“这个版本”的P95响应时间,一条SQL的事儿。

第二个场景:夜间定时压测。 用crontab或者Jenkins在凌晨两点触发一次k6压测,压半小时,人不用盯着。早上来了打开Grafana,看昨天夜里峰值期间各项指标是否异常,比如内存有没有漏、连接数有没有打满。没有持久化,这个流程根本走不通。

第三个场景:多轮测试趋势分析。 你调了线程池参数,跑一轮测试,调一下,再跑一轮,如此反复。光看单次结果你很难感受到变化趋势,但把几十次测试的响应时间曲线叠在一起看,你就知道每次优化到底起了多大作用,是不是有抖动。

第四个场景:团队共享数据。 你测完了,同事也想看看他负责的模块在这轮测试里的表现。如果数据在你本地,他没法看。数据进了Prometheus,大家只要连上同一个面板,就都能看,权限还能分开配置。

这套方案最舒服的一点是,它不要求你改造业务代码,也不要求你用特定编程语言写k6脚本。不管你是测Java写的后端接口,还是Node.js写的网关,或者直接测静态页面,都是一样的套路。

三、技术上的优缺点,我得跟你交个底

任何一套方案都不是万能的,我用了这么久,把体会写下来,方便你决定要不要入坑。

优点方面:

第一,k6自带Prometheus远端写入支持,官方维护的adapter(也就是k6的某个输出器)已经非常成熟,你不用自己造轮子。第二,Prometheus本身是时序数据库,存这种带着时间戳的指标数据简直是天作之合,查询性能好,而且支持标签过滤,比如你想只看某个测试脚本的数据,或者只看某个实例的数据,用标签一筛就有。第三,Grafana对Prometheus的支持是原生级的,你到模板市场里搜k6,能找到一堆现成的看板,不要白不要。第四,数据保留周期可以自己调,你想保留一个月还是一年都行,磁盘够大就留着,不像某些SaaS平台按量收费。

缺点方面:

第一,如果你是单机部署,Prometheus本身就成了一个单点,要是它挂了,k6的推送可能会受影响,不过大多数性能测试环境没那么苛刻,这个风险可以接受。第二,k6默认推送的指标数量非常多,如果不做裁剪,Prometheus的存储会增长得很快,你得学会用标签过滤或者只保留关键指标。第三,查历史数据时,Prometheus的查询语言PromQL有一定学习成本,第一次接触可能觉得绕,但习惯之后会发现它其实很逻辑。第四,这套组合存的是聚合后的指标数据,不像当时抓包一样能看到每个请求的详细日志,你如果需要定位具体的某一次慢请求,还得靠k6自己的报告或者其他日志系统。

所以我的建议是:如果你是做宏观趋势分析、容量规划、稳定性验证,这个组合非常合适;如果你需要逐请求排障,那还得搭配日志工具,而不是靠Prometheus。

四、动手实践:从零搭建一套完整链路

下面我带大家一步步来,假设你机器上已经装好了Docker,我们直接用容器来启动Prometheus和Grafana,这样最省事,不用操心生不兼容的问题。k6则用官方二进制或者npm装都行,我推荐用brew或者直接去GitHub下载release包。

4.1 先启动Prometheus

我们先把Prometheus的配置文件写出来,放在当前目录下,文件名就叫prometheus.yml。

# prometheus.yml
# 这是Prometheus的全局配置,里面声明了它自己多久抓一次自己的指标,以及告警规则的位置
global:
  scrape_interval: 5s      # 每5秒抓取一次指标,性能测试场景下5秒足够了
  evaluation_interval: 5s  # 每条规则每5秒评估一次,这里我们没配告警规则,但留着无害

# 远程写入的配置,k6会把数据推送到这里
# 注意:这里的url是k6的adapter暴露的端点,我们后面会在docker里一起启动它
remote_write:
  - url: http://localhost:9090/api/v1/write
    # 如果你不想接收所有标签,可以在这里配置write_relabel_configs
    # 比如去掉一些不常用的标签,不过我们刚入门,先全量接收

# 采集配置,我们会让Prometheus自己监控自己,顺便看看远端写入了哪些数据
scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

这里解释一下,k6不是直接跟Prometheus接口对话的,它中间有一个叫k6-prometheus-runtime的adapter,不过在新版k6里,官方把remote write的能力直接集成到了输出器里。我们用的命令里会指明输出到Prometheus,同时要指定remote write的地址。

然后我们用Docker把Prometheus跑起来:

# 用docker启动Prometheus容器
# -v 把当前目录下的prometheus.yml挂载到容器里
# -p 把容器的9090端口映射到本机9090
# --name prometheus 给容器起个名字方便管理
# prom/prometheus 是官方镜像,latest标签表示最新版
docker run -d --name prometheus \
  -p 9090:9090 \
  -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \
  prom/prometheus:latest

启动之后,你可以打开浏览器访问http://localhost:9090,如果能看到Prometheus的界面,就说明它活着了。这个页面里个有个“Status”菜单,点进去可以看到配置和targets状态,我们后面用来验证数据是否写入。

4.2 准备一个简单的k6脚本

我们不要一上来就写特别复杂的场景,先用一个最基础的脚本,模拟几个用户访问一个叫test-api的接口。这里我会用一个假地址,你换成你自己的服务地址就行。

// 这是k6的测试脚本,用JavaScript编写,文件名保存为test.js
// 不用安装任何依赖,k6自带运行环境

import http from 'k6/http';      // 引入HTTP请求模块,用来发送请求
import { check } from 'k6';      // 引入check函数,用来断言响应是否符合预期
import { sleep } from 'k6';      // 引入sleep函数,用来模拟用户思考时间

// 配置项,这里定义测试的负载模型
export const options = {
  vus: 10,              // 同时模拟10个虚拟用户
  duration: '30s',      // 持续压测30秒
};

// 这是默认执行的函数,每个虚拟用户会不断重复执行它
export default function () {
  // 发起一个GET请求,返回响应对象
  const res = http.get('http://test-api.example.com/api/users');

  // 检查响应状态码是不是200,如果不是,k6会标记为失败
  check(res, {
    '状态码是200': (r) => r.status === 200,
  });

  // 模拟用户浏览了1秒后再进行下一次操作,避免请求太快导致不像真实用户
  sleep(1);
}

这个脚本虽然简单,但已经包含了虚拟用户数、持续时长、请求、断言、思考时间这几个核心元素。你后面扩展的时候,无非就是从单个请求变成多个请求,从固定并发变成阶梯并发,思路是一样的。

4.3 启动k6并推送数据到Prometheus

k6有一个专门的输出器叫experimental-prometheus-rw,它是官方支持的实验性功能,但在大多数版本里已经稳定了。我们用下面的命令跑测试:

# 运行k6脚本,同时把指标推送到Prometheus
# K6_PROMETHEUS_RW_SERVER_URL 环境变量指定了remote write的服务器地址
# 注意这个地址必须是Prometheus的/api/v1/write端点
# --vus 和 --duration 可以在命令行覆盖脚本里options的配置
k6 run --out experimental-prometheus-rw \
  -e K6_PROMETHEUS_RW_SERVER_URL=http://localhost:9090/api/v1/write \
  test.js

你可能会问,为什么这里没有用adapter容器,而是直接指定URL?因为新版的k6内置了这个能力,只要在环境变量里指定了目标地址就行。如果你用的是旧版k6,那就需要先启动一个adapter容器,但新版本不用了,省力很多。

跑起来后,你会在终端看到k6的实时统计,同时每隔几秒Prometheus就会收到一批数据。测试结束后,终端会显示汇总报告,但这些数据已经悄悄存在Prometheus里了。

4.4 用Grafana展示数据

光有Prometheus还不够,因为查数据要用PromQL,对普通人来说不友好。我们加一个Grafana来画图。

# 启动Grafana容器
# -p 3000:3000 把Grafana的默认端口映射到本机
# 使用官方镜像grafana/grafana
# 挂载一个数据目录,避免容器删除后配置丢失,这里我们先用匿名卷,简单起见
docker run -d --name grafana \
  -p 3000:3000 \
  grafana/grafana:latest

启动后访问http://localhost:3000,默认账号密码都是admin,登录后会提示你改密码,可以先不改直接跳过。

接下来把Prometheus添加为数据源,步骤如下:

  • 点击左侧齿轮图标,进入“Data Sources”
  • 点击“Add data source”
  • 选择“Prometheus”
  • 在HTTP URL那里填上http://localhost:9090(因为Grafana在容器里,访问宿主机要用host.docker.internal,但我们这里用简单方式,直接把Prometheus端口映射到了localhost,所以填localhost即可)
  • 拉到页面底部,点击“Save & Test”,看到绿色提示就成功了

然后创建一个仪表板,添加一个面板,查询语句写一个最简单的:

# 这里展示的是k6的http请求速率指标
# 全名叫http_reqs,rate函数用于计算每秒速率,[1m]表示取最近1分钟的平均值
rate(http_reqs_total[1m])

这样你就能看到一条曲线,这就是你刚才那次压测的请求吞吐量。同样,你可以添加更多面板,比如:

# 请求耗时分布,k6会给出直方图桶,我们可以用histogram_quantile来算95分位
histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
# 虚拟用户数
k6_vus

把这些面板排好,保存一下,你就拥有一个自己专属的k6监控看板了。之后每次跑测试,数据自动追加,曲线自动延长。

五、进阶技巧:把多个测试场景分开存

你可能会遇到这样的情况:一天跑好几轮测试,有登录测试、有下单测试、有搜索测试,这些数据全混在一起,你怎么区分?Prometheus有标签机制,k6也会附加上一些标签,但你也可以自己主动加。

修改k6脚本,在options里增加一个自定义标签:

// test-with-tags.js
// 在脚本里通过tags字段给整个测试打标签
import http from 'k6/http';
import { check } from 'k6';

export const options = {
  vus: 5,
  duration: '10s',
  tags: {
    // 自定义标签,标记这次测试属于哪个业务线,哪个环境
    service: 'order-service',
    env: 'staging',
    test_scenario: 'search',
  },
};

export default function () {
  const res = http.get('http://test-api.example.com/api/products?keyword=phone');
  check(res, { '200': (r) => r.status === 200 });
}

然后在Prometheus里查询时,你可以这样过滤:

# 只看env为staging的数据,job名称由k6自动设置
rate(http_reqs_total{env='staging'}[5m])
# 只看order-service这一个服务的数据
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{service='order-service'}[5m])) by (le))

这样做的好处是,即便你跑了十几次测试,在Grafana里切换标签就能看到不同场景的曲线,互不干扰。

六、数据保留与清理

Prometheus默认把数据保留15天,如果你想把性能测试数据存更久,可以在启动命令里加参数:

# 通过--storage.tsdb.retention.time参数设置保留时间
# 这里设置成180天,也就是半年
# 注意:这个参数要在docker run的命令里传给Prometheus容器
docker run -d --name prometheus \
  -p 9090:9090 \
  -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \
  prom/prometheus:latest \
  --storage.tsdb.retention.time=180d

如果你发现数据增长太快,可以只保留关键指标,在prometheus.yml的remote_write里面加一个过滤配置,只写入名字是http_reqs_total或者k6_vus的指标。但一般来说,压测数据量远小于业务监控数据量,这点存储开销微不足道。

七、注意事项,我踩过的几个坑

我把自己当初折腾时遇到的一些坑分享给你,免得你重走弯路。

第一个坑:remote write地址写错了。 很多人把地址写成了Prometheus的根地址http://localhost:9090,没有加/api/v1/write尾巴。这会导致k6推送失败,但k6可能不报错,只是记录日志,你从Prometheus的Targets页面看不到任何数据。记住,一定是/api/v1/write。

第二个坑:k6版本太老。 如果你的k6是两年前的版本,可能不支持experimental-prometheus-rw这个输出器,需要升级到最新版,或者使用旧方案里的adapter容器。建议直接去GitHub下载最新版本,或者用brew upgrade k6。

第三个坑:Grafana容器里访问Prometheus的地址。 不要在Grafana的URL里写localhost:9090,如果你的Grafana和Prometheus都跑在Docker里,但Grafana的容器网络是独立的,它访问不到宿主机的localhost。我这里因为把Prometheus的端口映射到了宿主机,所以hack一下填localhost能通,但如果你用docker-compose,建议把两个服务放在同一个网络里,并用服务名访问。更稳妥的做法是,在启动Grafana容器时用--add-host=host.docker.internal:host-gateway,然后URL填http://host.docker.internal:9090。这个点非常容易搞混,你要是发现Grafana连不上Prometheus,先检查这个。

第四个坑:时间不同步。 如果k6所在机器和Prometheus所在机器的时间不一致,数据写入后会因为时间戳偏移导致查询时看不到或者曲线错乱。单机部署不会有这个问题,但如果是分布式环境,务必用NTP同步时间。

第五个坑:指标名冲突。 如果你有多套k6同时往同一个Prometheus写,默认指标名是一样的,但不同job的job标签不同,所以查询时最好加上你自定义的标签或者job标签,别把不同实例的数据混在一起算百分位。

八、把数据用起来:一个真实分析案例

我假设你已经配好了整套环境,也跑了一轮测试。现在你想分析“这轮测试下,下单接口为什么变慢了”。

你可以打开Grafana,新建一个面板,查看:

# 下单接口的P95延迟,按30秒粒度计算
histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{endpoint='/api/order'}[1m])) by (le))

接着再对比同一时段的CPU和内存使用情况,如果CPU飙升而P95也升高,说明瓶颈可能在计算密集;如果CPU正常但延迟高,可能卡在了IO或者下游调用。你可以再添加一个面板看k6发起的请求数:

# 请求速率
rate(http_reqs_total{endpoint='/api/order'}[1m])

如果请求速率没有变化,但延迟变高了,那一定是后端处理能力下降。这时候你可以去查慢查询日志、数据库连接池之类的。这个过程本质上跟做应用监控一样,只不过数据源变成了性能测试的负载。

还有一个特别有用的操作,就是把每次测试的版本号作为一个标签。在你的CI/CD流程里,你可以把git commit的短哈希作为k6的标签值,这样在Grafana上就能看到每次构建对应的性能表现,相当于给性能测试做了版本管理。你只需要在CI脚本里加上-e K6_PROMETHEUS_RW_TAGS_GIT_COMMIT=$(git rev-parse --short HEAD),k6会自动把这个标签附加到所有指标上。

九、文章总结

这一套“k6 + Prometheus”的组合方案,本质上解决的问题就俩:一是数据不落地,二是数据不可回溯。k6作为压测工具足够优秀,但它不带持久化;Prometheus作为时序数据库,正好弥补了这个短板,两个人配合以后,你不仅能看到当下的结果,还能复盘过去每一次压测的细节,观察性能的渐变趋势。这套方案的搭建成本非常低,一台机器就能跑完所有组件,学习曲线也平缓,网上资料多,遇到问题基本都能搜到。如果你所在团队还没有一个像样的性能测试数据平台,我建议从这套组合开始,不需要买昂贵的商业工具,不需要写大量复杂的平台代码,只需要把k6脚本写好,然后让数据自动流进Prometheus,再用Grafana画几个面板,基本就够你日常使用了。

当然,方案也不是没有改进空间,比如你可以再叠加一套告警规则,当P99延迟超过阈值时自动通知到钉钉或邮件;你还可以接入Loki记录k6的日志,把日志和指标关联起来;甚至可以通过Kubernetes Operator把整个环境托管在云上。但万变不离其宗,核心的“写指标、存时序、看趋势”这个闭环已经打通了,后面都是锦上添花。希望这篇文章能帮你少踩一些坑,把性能测试数据真正变成你手里有价值的资产。