K6负载测试与OWASP ZAP安全扫描集成实战
发布时间:2026/9/28 23:43:52 作者:尧图编辑部 阅读量:1,286

我们团队最近频繁收到线上反馈说某个核心接口在活动大促时响应特别慢甚至出现 5xx 错误。排查到最后发现既不是代码逻辑问题也不是数据库慢查询而是接口在真实并发压力下暴露出的资源竞争。与此同时安全团队又接二连三地扫描出几个中危漏洞——这让我意识到性能和安全这两件事如果分开做效率真的太低了。后来我们把 K6 负载测试和 OWASP ZAP 安全扫描集成到同一条流水线里一次压测既能看抗压能力又能顺带暴露安全风险。这套方案跑了一段时间效果非常稳定。这篇就把我们的实践过程、踩过的坑和优化思路完整分享一下。先交代一下背景。K6 是 Grafana Labs 旗下的一款开源负载测试工具用 Go 编写脚本却走 JavaScript 语法轻量、高并发、资源占用低非常适合嵌入 CI/CD 流水线。OWASP 则是一个专门关注 Web 应用安全的开源社区它维护的 OWASP Top 10 基本是 Web 安全领域的必修课而 OWASP ZAPZed Attack Proxy是一款免费的 Web 应用安全扫描器支持主动扫描、被动扫描和 API 调用非常适合自动化安全测试。而“负载测试”和“安全集成”这两件事放在一起本质上是在回答同一个问题系统在真实压力下是否仍然安全可靠性能测试验证的是“能不能扛住”安全测试验证的是“扛住的时候会不会被打穿”。传统做法是把两者拆开性能团队压测安全团队扫描结果互相不通气。集成之后一次完整的压测流程能够同时产出容量评估和安全风险评估效率直接翻倍。这篇文章适合谁看正在搭建 CI/CD 流水线、准备把性能测试和安全测试自动化的测试开发工程师、后端工程师、DevOps 工程师都可以从中找到可以直接抄作业的方案。下面我会从设计思路、K6 实操、ZAP 集成、流水线优化、问题排查五个方面逐步展开。1. 为什么要把负载测试和安全扫描绑定在一条流水线里1.1 拆开做的问题时间差和盲区先聊一个真实案例。我们之前某次版本上线前压测团队单独跑了一轮 K6 测试结论是 QPS 峰值能到 800P95 延迟 280ms看起来一切正常。但上线后的第二天安全团队用 ZAP 对同一个接口做主动扫描发现存在一个会导致资源耗尽的高危风险——未登录状态下某个接口可以批量拉取数据且没有任何限流。这两个问题单独看都不致命但如果放到一起大促流量一来攻击者用低成本的并发请求就能把服务打挂。这就是拆开做的最大问题性能团队没有安全上下文安全团队没有容量上下文。压测通过不代表安全可行安全通过也不代表能扛住真实流量。两者之间有一个盲区而集成测试就是为了填平这个盲区。1.2 集成后的协作方式集成后的典型工作流长这样启动 ZAP 代理开启被动扫描模式监听特定端口K6 压测脚本通过该代理发送请求压测流量自动被 ZAP 捕获压测结束后ZAP 基于捕获的流量生成一份被动扫描报告对关键接口触发一轮 ZAP 主动扫描确认压力状态下的漏洞综合 K6 的容量指标和 ZAP 的安全告警生成一条流水线门禁结果。这个流程的核心逻辑是先让系统承担真实负载再在相同负载下做安全诊断。如果系统在 500 并发下已经被打得很吃力了这时候 ZAP 扫描出任何高危漏洞优先级都要往上提一级因为攻击者可能不需要很大的成本就能触发它。1.3 人员与工具链的协同还有一个容易被忽略的好处集成之后开发、测试、运维对“系统是否可上线”的评估口径统一了。以前性能测试用 JMeter 的报告安全测试用 ZAP 的报告两个报告格式不同、维度不同、关注点不同评审会上经常争论“到底听谁的”。现在我们把 K6 的 thresholds 和 ZAP 的风险等级统一映射到一个“质量门禁”里P95 延迟超过 500ms 卡发布高危漏洞数量不为 0 卡发布其他情况放行。规则清晰自动化去执行争议自然就少了。2. K6 负载测试的核心实操与参数设计2.1 安装与基础配置K6 的安装方式非常轻量。macOS 上一条命令就能搞定brew install k6Linux 或者 CI 环境里官方推荐直接用二进制包解压即用不依赖 JVM也不依赖 Python 环境。我们内部构建机上就是这么干的直接下载二进制放到 /opt/k6 下然后软链到 /usr/local/bin整个过程不到一分钟。# 安装 k6 的 Linux 命令示例 wget https://github.com/grafana/k6/releases/download/v0.49.0/k6-v0.49.0-linux-amd64.tar.gz tar -xzf k6-v0.49.0-linux-amd64.tar.gz sudo cp k6-v0.49.0-linux-amd64/k6 /usr/local/bin/Docker 方式也推荐docker run --rm -i grafana/k6 run - script.js实测下来K6 单机就能轻松模拟上万虚拟用户资源占用远低于 JMeter 的同等并发这也是它在云原生环境下越来越流行的原因。2.2 一个可以直接用的 K6 脚本模板这里给出一份我们在项目中实际使用的脚本框架覆盖了常规 HTTP 接口压测、阈值设置和外部数据源引入import http from k6/http; import { check, sleep } from k6; import { Trend, Rate } from k6/metrics; // 自定义指标 const requestDuration new Trend(request_duration, true); const errorRate new Rate(request_errors); // 压测配置 export const options { stages: [ // 逐步增加并发观察系统预热和失败临界点 { duration: 30s, target: 50 }, { duration: 1m, target: 100 }, { duration: 1m, target: 200 }, { duration: 2m, target: 300 }, { duration: 30s, target: 0 }, // 收尾观察系统恢复 ], thresholds: { // 质量门禁P95延迟不超过500ms错误率不超过1% http_req_duration{type:normal}: [p(95)500], request_errors: [rate0.01], }, }; const BASE_URL __ENV.BASE_URL || https://staging.example.com; export default function () { const payload JSON.stringify({ username: loadtest_user, keyword: performance_test, }); const params { headers: { Content-Type: application/json, X-Client-Version: 2.3.1, // 添加一个压测标记方便后端链路追踪和日志过滤 X-Load-Test: k6, }, // 如果后续要接 ZAP这里就指向 ZAP 代理地址 // proxy: http://localhost:8080, }; const res http.post(${BASE_URL}/api/v1/search, payload, params); // 结果校验 check(res, { status is 200: (r) r.status 200, transaction time 500ms: (r) r.timings.duration 500, }); // 写入自定义指标 requestDuration.add(res.timings.duration); errorRate.add(res.status ! 200); // 模拟真实用户的思考时间 sleep(1); }关于 stages 的设计我说一下我的习惯不要一开始就上高并发。先跑一小段时间观察错误率再逐步加压。因为很多系统在低并发时表现正常一旦超过某个临界点会出现雪崩式的错误。阶梯式压测能帮你找到这个临界点。阈值thresholds是 K6 里非常实用的功能。它不只是“统计”数据而是直接决定压测是否“通过”。在流水线里我们会把 thresholds 的结果作为是否继续构建的判定条件。特别注意这个{type:normal}标签语法可以在请求级别打标签把正常请求和探活请求分开统计避免监测请求拖低整体指标。2.3 执行压测的命令与报告输出脚本写好后执行命令也很简单k6 run --vus 200 --duration 2m --out jsonresults.json load_test.js如果是在流水线里我推荐加上--summary-trend-stats参数输出更详细的百分位数据k6 run --summary-trend-statsavg,min,med,p(90),p(95),p(99),max --out jsonresults.json load_test.jsK6 的标准输出里自带一份简洁的人类可读报告包含 http_req_duration、http_reqs、iterations 等核心指标。要拿到更精细的数据可以落一份 JSON 报告后续让脚本去解析。我们在流水线里会做一步用 Python 脚本读取 results.json把 P95 延迟、错误率、最大并发数提取出来然后推送到我们内部的质量看板。这样每次压测的现实数据都被沉淀下来能纵向对比趋势而不只是看当次一锤子买卖的结果。2.4 K6 性能参数的关键解读K6 压测报告里我们平时最看重的几个指标是http_req_duration单次请求的完整耗时包含 DNS、连接、TLS、首字节、下载。这是衡量用户体验最直接的指标。http_req_failed请求失败率。只要不是 0就说明有请求没成功必须去排查是并发太高导致连接被拒绝还是后端 5xx。iterations脚本迭代次数代表了实际完成的业务操作数量比单纯看 QPS 更能反映系统吞吐。vus虚拟用户数。在阶梯压测里vus 上升到哪个阶段开始出现错误这就是容量拐点。有个常见的理解误区P95 延迟不等于平均延迟。平均延迟是 200msP95 可能是 2s这意味着 5% 的请求体验已经接近失败边缘。我们团队现在只看 P95、P99平均延迟只作为参考。另外一个容易被忽视的地方是http_req_blocked如果这个值长期大于 300ms说明大概率遇到了 TCP 连接队列阻塞已经在向系统资源瓶颈逼近了。3. OWASP ZAP 安全扫描集成从被动扫描到主动扫描3.1 ZAP 的两种工作模式OWASP ZAP 是 Web 应用安全测试领域使用率极高的工具它支持两种核心模式被动扫描Passive ScanZAP 处于代理模式不主动发送任何请求只分析经过它的流量。这种方式不会对被扫描系统造成额外影响而且可以在 K6 压测过程中实时抓包分析。主动扫描Active ScanZAP 会对目标和参数主动发起攻击测试比如 SQL 注入、XSS、命令注入等。这种方式能发现更深层的问题但会显著增加系统负载不建议在压测高并发阶段同时跑。我们集成的思路是先用被动扫描伴随 K6 压测再用主动扫描补充攻击面覆盖。被动扫描能发现实际流量里暴露的问题主动扫描能覆盖到更多参数组合和攻击向量。3.2 用 Docker 快速部署 ZAPZAP 在流水线环境里的部署最推荐 Docker 方式docker pull ghcr.io/zaproxy/zaproxy:stable以代理模式启动docker run -u zap -p 8090:8090 -i ghcr.io/zaproxy/zaproxy:stable zap.sh -daemon -port 8090 -host 0.0.0.0 -config api.disablekeytrue这里有几个关键参数要解释-daemon后台运行模式不启动图形界面适合自动化场景。-config api.disablekeytrue关闭 API 访问密钥。注意这个参数只在可信内网环境里使用如果把 ZAP 暴露到了公网绝对要保留 API key否则任何人都能调用你的扫描器发起请求。我们内部流水线阶段之间都是内网通信所以关闭了密钥省去在代码里管理密钥的麻烦。-port 8090代理端口。K6 的脚本会通过这个端口发请求。启动之后可以验证 ZAP 是否正常工作curl http://localhost:8090/JSON/coresee/返回一段 JSON 说明 ZAP 已经就绪。3.3 让 K6 压测流量经过 ZAP 代理启动 ZAP 后只需要在 K6 脚本里加上代理配置即可上面脚本模板中已注释。K6 支持在每个请求里指定代理或者在脚本顶部全局配置import http from k6/http; const params { proxy: http://localhost:8090, }; export default function () { const res http.get(https://stage-api.example.com/api/v1/products, params); }如果你想把所有请求都统一走代理更推荐在命令行里传参这样不需要改脚本k6 run -e http_proxyhttp://localhost:8090 load_test.js执行之后所有 K6 发到目标服务器的流量都会先经过 ZAPZAP 的被动扫描引擎会同步分析每个请求和响应。压测结束之后ZAP 中已经积累了压测期间的完整流量数据可以基于这些数据生成风险报告。3.4 调取 ZAP 的扫描结果ZAP 支持通过 API 获取扫描结果。被动扫描的结果可以用下面这个接口拉取curl http://localhost:8090/JSON/alert/view/alerts返回的 JSON 里包含风险等级Risk、告警描述、URL、参数、解决方案等字段。我们在流水线里会写一个小脚本去解析这些字段只提取 High 和 Medium 级别的告警来卡门禁Low 级别和 Informational 级别只记录不阻塞。这一不其实很关键。ZAP 的告警里还有很多误报比如响应头缺失、Cookie 缺少 HttpOnly 标志——这些信息是有价值的但如果每次都因为一个 Low 级告警卡发布开发和测试都会被烦死。合理的做法是High 级别告警直接卡门禁Medium 级别告警需要开发确认是否修复Low 级别和 Informational 级别只记录。这是我们在上线初期反复调整后才定下的策略按这个口径团队既不会放过真正的安全风险又不会被误报淹没。3.5 主动扫描与 ZAP 的上下文配置主动扫描是 ZAP 的重型武器。在 K6 压测释放流量之后、系统恢复平稳阶段我们对关键接口发起主动扫描。ZAP 的主动扫描默认会把整个网站都跑一遍但建议只针对关键接口否则耗时很长而且可能触发系统的风控策略产生大量脏数据。我们一般这么做先通过 ZAP API 设置扫描范围再发起主动扫描curl -X POST http://localhost:8090/JSON/ascan/action/scan/? \ -d urlhttps://stage-api.example.com/api/v1/upload \ -d recursefalse \ -d scanPolicyNameDefault Policy主动扫描的耗时取决于目标接口的复杂度。一个简单的接口可能几秒钟就扫完而一个包含大量参数和文件上传的接口可能要几分钟甚至十几分钟。注意主动扫描的请求量巨大扫描期间目标系统的负载会明显升高。所以在流水线里主动扫描一定要放在 K6 压测完成之后避免两个高负载操作叠加导致系统被压垮。扫描完成后主动扫描的告警数据会以同样的格式被/JSON/alert/view/alerts接口返回统一解析即可。4. 集成流水线的编排与优化策略4.1 按阶段编排压测与扫描在实际的 CI/CD 流水线里我们的执行顺序是这样的构建部署阶段把最新代码部署到 staging 环境。基础冒烟测试阶段先跑一轮非常轻量的 K6 脚本50 并发1 分钟验证核心链路是否正常。ZAP 环境准备阶段启动 ZAP 容器开启代理和被动扫描。负载压测阶段K6 阶梯式加压流量全部经过 ZAP 代理。这个阶段根据测试目标不同可能跑 5 到 15 分钟。被动扫描评估阶段压测结束后拉取 ZAP 被动扫描告警分析结果。主动扫描阶段对关键接口发起 ZAP 主动扫描。结果汇总与门禁判定阶段合并 K6 threshold 结果和 ZAP 告警结果判定是否允许进入下一道工序。把这个顺序展开最大的好处是性能压测期间ZAP 被动扫描数据的价值是最高的。因为压测流量模拟的是真实用户行为而主动扫描的请求很多是随机生成的特殊 payload跟真实用户的关联度弱一些。被动扫描抓到的告警更能反映真实风险。4.2 质量门禁的判定规则我们内部的质量门禁规则如下表工序硬性门槛软性门槛K6 压测P95 500ms错误率 1%P99 1200msZAP 被动扫描High 告警 0Medium 告警数 5ZAP 主动扫描High 告警 0Medium 告警数 10综合评估无阻断级缺陷已知风险有缓解方案这条规则的形成并不是一开始就固定的。最早我们严格卡 Medium 告警必须为 0结果团队连续两周都在处理各种安全配置项的误报比如“Web 浏览器漏洞”“CSP 缺失”等等——这些问题确实值得修但要求发布前全修完开发节奏就被打乱了。后来我们把 Medium 级别改成软性门槛要求开发团队给出修复时间点而不是发布前必须清零整体效率立刻改善了很多。K6 的 thresholds 同样承担着硬性门禁的职责。注意thresholds 的判定不是“平均值只要达标”就行而是每一个数据点都要满足条件。比如p(95)500的意思是每 60 秒窗口内第 95 百分位延迟必须全部小于 500ms。只要有一个窗口超标整个测试就会被判定为失败。4.3 报告整合与存档流水线的最后一步我们会把 K6 的 JSON 报告和 ZAP 的告警报告合并生成一份统一的 HTML 报告。这里推荐一个思路不需要自己写复杂的图表库直接用现成的模板。K6 自带k6 convert命令可以把 JSON 转成 HTMLZAP 的报告可以用它的-report参数直接生成 HTML 格式。两个报告合并到一个文件夹下用流水线的 artifact 功能统一归档。这个环节我要特别提一个坑报告归档路径里不要带中文和空格否则 Jenkins 拉取 artifact 的时候容易出问题。我们早期一直用测试报告-$(date %s)这种方式命名后来发现 Jenkins 侧偶发下载失败去掉中文路径之后问题就消失了。4.4 减少误报和噪音的三条经验配置 ZAP 排除静态资源后缀图片、CSS、JS 文件不需要安全扫描。可以在 ZAP API 里配置全局排除规则或者在 ZAP 界面的“Session Properties”里设置。我们内部把.js .css .png .jpg .gif .ico .woff .woff2全部加入排除列表告警数量减少了大概四分之一。统一认证上下文的处理压测和扫描都会涉及到登录态。如果系统有独立的测试账号体系尽量在 K6 脚本里预置 Token 或 Cookie在 ZAP 里配置对应的认证上下文。注意压测过程中如果大量请求因为未认证而返回 302ZAP 的被动扫描会把这当成“重定向异常”甚至“开放重定向风险”上报造成大量无效告警。这个我们真实遇到过一开始甚至排查了很久。把 K6 压测标记写入请求头上一节脚本里的X-Load-Test: k6头不仅方便后端排查问题ZAP 里也能通过设置告警过滤规则只关注这类请求。在 ZAP 的 Alert Filter 中可以配置优先处理带特定请求头的告警有效降低噪音。5. 常见问题与排查技巧实录集成过程中我们踩过的坑基本集中在下面这几个方面每个问题都附上排查思路和解决方案。5.1 ZAP 代理导致 K6 请求响应变慢现象加了 ZAP 代理之后K6 压测的响应时间显著上升从 200ms 变成 600ms 甚至更高。排查一开始以为 ZAP 拖垮了链路后来通过对比发现ZAP 代理本身处理流量有延迟但更主要的原因是ZAP 默认对自己的被动扫描也有并发限制请求一多就排队了。解决调大 ZAP 的被动扫描线程数或者调整 K6 与 ZAP 之间的超时时间。ZAP 可以通过 API 调整curl -X POST http://localhost:8090/JSON/pscan/action/setMaxAlertsPerRule/ -d maxAlertsPerRule999这个方法能让被动扫描处理更多告警而不丢弃。实际操作中我们把 K6 压测的并发控制在 300 以下、ZAP 代理解析正常的情况下额外延迟控制在 100ms 以内对结果影响可以接受。5.2 HTTPS 证书校验失败现象K6 请求经过 ZAP 代理时目标站是 HTTPSK6 提示证书校验失败。排查ZAP 对 HTTPS 流量会做 TLS 拦截默认身份是一张自签名证书。K6 使用标准 Node.js 的 TLS 校验逻辑当然不认。解决K6 脚本里设置insecureSkipTLSVerify: trueexport const options { insecureSkipTLSVerify: true, };注意这个配置只应该在测试环境使用生产环境压测不应该关闭 TLS 校验。如果你确实需要在生产环境做压测建议把 ZAP 的 CA 证书导入压测机器的信任链而不是直接跳过校验。具体做法先导出 ZAP 的 CA 证书curl http://localhost:8090/OTHER/core/other/rootcert/然后安装到系统信任链即可。5.3 主动扫描导致目标系统 500现象ZAP 主动扫描某个文件上传接口时系统开始频繁 500。排查主动扫描会向接口发送大量畸形参数比如超长字符串、SQL 注入 payload、XSS payload。文件上传接口尤其脆弱因为灰度上传文件往往有大小限制、类型白名单等逻辑畸形参数很容易触发未捕获的异常。解决不是所有接口都适合直接主动扫描。我们后来把主动扫描接口列表做了分级核心交易接口登录、支付只做被动扫描和人工安全测试不做主动扫描非核心且具备良好容错能力的接口才允许主动扫描。同时在扫描前对目标系统做一次数据库快照扫描后能快速恢复脏数据。这个操作在 staging 环境是必须的否则生产数据被安全测试污染后续排查问题时会非常痛苦。5.4 K6 压测结果不稳定波动巨大现象同一份脚本两次跑出来的 P95 延迟差距超过 50%。排查一是确认压测机器的性能是否稳定K6 所在宿主机的 CPU 和网络是否被其他任务抢占二是确认目标系统是否部署在同一批机器且流量均衡策略一致三是检查是否有定时任务在压测期间跑批任务。解决在流水线里为压测任务预留独占资源或者在压测之前先跑一轮基准baseline测试保证基线环境一致。测出来的数据是用来做趋势对比的如果环境不稳定数据就没有参考价值。我们现在的做法是每次发布前都跑同一组压测脚本把结果记录到同一个平台看环比趋势。单次跑得好可能只是因为运气好连续三次跑得好才是真的没问题。5.5 ZAP 的告警太多团队懒得看现象ZAP 初次报告输出 500 多条告警研发团队看完不想处理。排查真正的高风险告警可能只有 5 条剩下的 495 条基本都是低危配置项、信息泄露、误报。解决在流水线解析 ZAP 报告时把所有告警分成三类需要立即处理High、需要计划处理Medium、仅记录Low Informational。同时定期更新 ZAP 的告警过滤规则把已知的误报模式加入白名单。再一个建议是不要直接抛一堆告警给开发而是把告警按照接口维度、风险等级分组每组附上修复建议和最短的复现路径。同样的告警数量处理效率可以翻倍。6. 优化策略让集成方案可持续演进6.1 用例库的动态更新机制压测脚本和安全扫描策略不应该是写一次就永远不变。我们的团队每两周会集中回顾一次线上问题把新出现的性能瓶颈和漏洞类型沉淀到用例库里。比如上个月发现某接口在大量 POST 请求下出现了连接池泄露我们就在 K6 脚本里增加了一个专门模拟长时间保持连接的场景又比如最近文件上传接口的漏洞变多了我们就在 ZAP 的主动扫描策略里增加了一组专门的测试规则。这个过程其实就是“测试资产持续演进”不追求一次做全而是跟着线上问题一起成长。有一个技巧值得分享在 K6 脚本里用环境变量参数化测试场景这样生产环境、staging 环境、开发环境都能复用同一套脚本只需要切换 BASE_URL、并发量等参数即可。脚本的可维护性提升了团队才愿意持续更新它。6.2 多环境分层策略不同的环境压测和安全测试的强度应该不同。开发环境跑冒烟级 K650 并发1 分钟配合 ZAP 被动扫描只检查是否存在高危新漏洞。Staging 环境跑完整压测阶梯到 500 并发和主动扫描输出完整的质量门禁报告。生产环境不主动压测除非业务方申请并经过风险评估安全扫描以被动扫描为主不跑主动扫描。这个分层策略让我们避免了一个大坑早期我们所有环境都跑同样强度的压测结果开发环境被压崩了反而延误了开发进度。测试是服务于交付的不是制造阻塞的。6.3 与性能基准库联动我们在内部做了一个简单的性能基准库每次发布版本都会拿当前版本的 P95 延迟跟上一个版本对比。如果显著变慢立刻定位是哪个接口引入了性能回退。这个基准库不需要复杂的平台支撑一个表格或者一个 dashboards 服务就够用了。ZAP 的告警结果我们也会做同样的对比统计上周的告警总数、按风险等级拆分看趋势。如果 Medium 告警数量持续增长说明安全测试还不够到位或者代码质量在下降需要及时介入。指标本身不是目的指标背后的趋势才是。最后再分享一点个人体会这套集成方案跑了大半年最直观的变化是发布前的性能和安全性评估从“人工填写两份报告”变成“一条自动化流水线出结果”效率提升非常明显。更重要的是开发团队第一次在压测报告中看到 ZAP 的安全告警时会主动问“这个接口在 500 并发下还有这个漏洞那攻击者是不是很容易利用”——性能和安全的联动思考比单独任何一份报告都更有说服力。如果你所在团队也打算尝试这套方案我建议先小范围跑起来挑一个核心且重要的接口搭一个最小的 K6 ZAP 代理环境跑通数据流向。不用一开始就把整个 CI/CD 流水线都改造了先证明这件事有价值再逐步铺开。你一定会遇到证书、代理、误报这类问题但这些都是小坑过了之后你会收获一个真正能反映系统抗压能力和安全水位的高质量质量门禁。