K6压测报告生成全指南:从终端摘要到HTML与Grafana可视化
做压测这些年我越来越确信一件事团队里能熟练敲出k6 run script.js的人不少但能把跑完的结果变成一份有说服力报告的人少得可怜。K6压测工具本身的能力不需要我多吹真正拉开差距的是压测收尾的那一步——报告生成。终端里蹦出来的一屏英文指标自己看着挺爽扔给项目经理、扔给下游团队对方往往是懵的。一份合格的K6压测报告至少得说清楚三件事系统扛住了没、瓶颈到底在哪、下个版本改完怎么和这次对比。这篇文章我就把K6报告这块从头到尾盘一遍从最基础的终端摘要到CI/CD里自动产出HTML报告再到Grafana长期趋势面板全部基于我真实跑过的项目经验来写希望能帮你在“压测最后一公里”上少走弯路。1. 压测收尾为什么难在报告而不是难在造压1.1 终端摘要能看懂不等于这份报告“能用”先看一份典型的K6终端输出。不同版本排版会有点差异但核心字段基本是长这个样子的scenarios: (100.00%) 1 scenario, 50 max VUs, 1m30s max duration (incl. graceful stop): * default: Up to 50 looping VUs for 1m0s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s) ✓ check status is 200 checks.........................: 100.00% ✓ 1200 ✗ 0 data_received..................: 18 MB 300 KB/s data_sent......................: 7.2 MB 120 KB/s http_req_blocked...............: avg1.04ms min0s med1.02ms max9.8ms p(90)1.8ms p(95)2.1ms http_req_connecting............: avg0.12ms min0s med0.1ms max3.2ms p(90)0.2ms p(95)0.3ms http_req_duration..............: avg38.2ms min4.2ms med36.1ms max118.4ms p(90)61.2ms p(95)68.5ms http_req_failed................: 0.00% ✓ 0 ✗ 1200 http_req_receiving.............: avg0.35ms min0.02ms med0.3ms ... http_req_sending...............: avg0.18ms min0.01ms med0.17ms ... http_req_tls_handshaking.......: avg0.32ms min0s med0.31ms ... http_req_waiting...............: avg37.4ms ... http_reqs......................: 1200 1200/s iteration_duration.............: avg38.5ms ... iterations.....................: 1200 1200/s vus...........................: 1 min1 max50 vus_max.......................: 50 min50 max50这段输出的信息密度其实很高50个虚拟用户跑了一分钟1200个请求平均耗时38.2msp95是68.5mscheck全过。但问题恰恰出在这——这串数字是给机器看的或者说是给“已经知道自己在看什么”的压测工程师看的。你把它截图发群里运维要问连接耗时多少开发要问哪个接口慢领导只想知道能不能上线。同样一份终端摘要十个人能读出十种结论。我见过不止一个团队把终端摘要当成“压测报告”直接交差。结果就是压测做了报告出了但谁都没法基于这堆平均数和百分位数做任何决策。这是报告生成第一道坎——不是K6没给数据而是数据没有被组织成“能回答问题”的形式。1.2 从裸数字到汇报结论还缺的关键一环那缺的是哪一环我自己的理解是报告要完成三层翻译。第一层把指标翻译成状态。系统到底扛住了没这不是看哪个数字而是要同时看请求成功率、p95耗时、迭代是否全部完成、资源有没有被打满。K6本身提供了thresholds机制来做这件事但很多人不配导致报告里只有一堆数字没有“通过/失败”的结论。第二层把平均值翻译成瓶颈点。一份只有http_req_duration的报告是没有灵魂的因为一个混合脚本里可能同时有/login、/order/submit、/product/list三个请求它们的耗时差异可能是一个天上一个地下。我做过一个电商项目终端摘要上p95只有80ms看着非常健康但把tags按接口分组之后才发现/order/submit这一个接口的p95已经跑到2.3秒。平均数永远会掩盖最慢的那个点报告不做拆分就永远找不到真凶。第三层把本次结果翻译成可对比的基线。这次压测和上次比是变好了还是变坏了接口耗时是稳定还是抖动没有归档的数据没有长期趋势每次压测都是“一次性结论”团队自然就不重视报告。这也是为什么我坚持把报告接入CI/CD和Grafana后面专门讲。所以报告生成的本质不是“导出个文件”而是把机器语言翻译成业务语言。K6报告功能的设计逻辑也正是沿着这个思路展开的。2. 生成K6报告的几条主流路线各自解决什么问题2.1 终端输出与summary导出最省事但只适合自测K6默认跑完测试后会在终端打印一份摘要这就是最原始的“报告”。它的好处是零配置适合你在本地调试脚本时快速看一眼。但要说它有什么不足那就是压根没法留存终端一关数据就没了而且纯文本形态对非技术人员极不友好。如果你只是想给这份终端摘要留个底K6提供了一个非常轻量的导出方式k6 run --summary-exportsummary.json script.js这条命令会把刚才终端里那一堆聚合统计值导成一个summary.json。它包含几乎所有终端里展示的指标字段http_req_duration的avg、min、med、max、p(90)、p(95)http_reqs的总量checks的通过失败数vus的min和max等等。这个文件很小几十KB顶天了适合作为每次压测的“结论快照”留存下来。需要注意的是--summary-export导出的只是聚合结果不是原始时间序列。什么意思就是你只知道p95是68.5ms但不知道这1200个请求里哪一个时刻、哪一个VU发出的请求耗时最高。一旦要做深度分析或者要拿数据画趋势图这个文件就不够用了。旧版本的K6依赖这个参数新版本官方更推荐用handleSummary来自定义汇总输出这个我们留到第三章细说。2.2 完整JSON/CSV导出给二次加工留足原料如果要拿到每个HTTP请求的完整明细就需要用--out参数导出原始时间序列数据。最常用的是JSON格式k6 run --out jsonresult.json script.js跑完之后result.json里会按时间顺序记录每一个数据点包括每个请求的发起时间、耗时、状态码、标签、VU编号以及每次check的结果。这份文件才是真正的“原油”后面讲到的HTML报告生成、Grafana长期趋势面板原料都来自这里。除了JSONK6也支持直接导出CSVk6 run --out csvresult.csv script.jsCSV的好处是能用Excel直接打开对不熟悉命令行的同事很友好。但CSV没有JSON那样保留了完整的嵌套结构标签信息的呈现会比较扁平我自己的习惯是优先用JSON只有需要丢给非技术同事做手工分析时才会顺手导一份CSV。这里要提醒一个坑JSON导出文件会非常大。一次100并发跑10分钟请求量轻松破十万result.json可能轻松上GB。所以我在正式压测里一般不会全程导出全量JSON而是要么只导聚合的summary.json要么在CI里把原始JSON当成构建产物留存设置好保留天数别让磁盘被撑爆。2.3 Web Dashboard实时界面调试脚本比压测本身更常用从K6比较新的版本开始官方提供了一个实验性的Web Dashboard功能可以在压测过程中实时打开一个浏览器面板看到请求速率、耗时分布、VU数量这些图表不断刷新。启用方式因版本而异新版本可以直接k6 run --web-dashboard script.js如果命令参数里没有--web-dashboard可以试试通过环境变量开启K6_WEB_DASHBOARDtrue k6 run script.js具体的以你本地k6 run --help输出的参数为准。这个Web Dashboard的实时可视化能力在正式压测中当然很有用但我用得最多的场景其实是调试脚本。怎么说呢跑压测脚本时如果写了个死循环或者某个接口响应异常终端只能看到滚动日志很不直观。有了Web Dashboard你可以实时看到每分钟请求数的变化曲线、错误率的跳变哪个环节出问题一眼就能定位。它相当于给了压测过程一个“心电图”。不过要说明的是这个功能目前还挂着“实验性”的标签同一个版本之间的行为和API都可能调整我不建议把它作为团队标准报告方案。它可以作为辅助工具但正式的汇报材料还是得走下面这两种路线。2.4 HTML报告生成器汇报场景的“标准答案”K6官方其实没有内置一个“一键生成HTML报告”的命令这一点和JMeter不太一样。所以在实际工作中我们通常借助社区工具来完成比较常见的有k6-reporter、k6-to-html还有其他一些开源脚本。它们的原理大同小异读取--out json导出的完整时间序列数据渲染成一个单文件的HTML页面里面包含请求速率趋势图、响应时间分布、百分位数统计、check结果表格可以直接扔给任何人查看不需要装任何工具。拿其中一种举个例子大致流程是这样的npm install -g k6-reporter k6 run --out jsonresult.json script.js k6-reporter result.json执行完会在当前目录生成一份report.html浏览器打开就能看到完整的可视化报告。这类工具生成的HTML通常自带CSS和JavaScript单文件就能发群里很适合作为压测交付物。这里有个高频踩坑点我必须提醒一下HTML生成工具读的应该是完整时间序列的result.json而不是--summary-export导出的那个summary.json。两种JSON的字段结构完全不一样你把summary.json喂给HTML生成器常常会得到一个缺胳膊少腿的报告或者干脆报错。我自己第一次用的时候就是没分清这两个文件白折腾了半小时。2.5 Prometheus Grafana长期趋势与团队共享压测报告如果只出现一次那它的价值就打了折扣。更理想的状态是每次压测的数据都沉淀下来形成一条性能趋势线这样团队可以随时回答“这个接口的p95从两个月前到现在是变好了还是恶化了”。要做到这一点最成熟的方案就是配合Prometheus加Grafana。K6可以直接把指标推到Prometheus方式是k6 run --out prometheus script.js执行前需要配置好Prometheus的remote write地址通过环境变量K6_PROMETHEUS_RW_SERVER_URL指定。另一种更常见的传统做法是走InfluxDB中转k6 run --out influxdbhttp://influxdb:8086/k6 script.js然后Grafana接入InfluxDB数据源导入社区现成的K6 dashboard模板几十个指标图表就都有了。我在团队里用的是Grafana方案每次压测跑完打开面板就能看到这次和上次的曲线叠在一起性能是回归了还是提升了一目了然。有人可能会问直接用Grafana做压测报告是不是更直观确实如此但它有一个门槛你得有InfluxDB或Prometheus这套监控基建还得维护dashboard模板。如果团队之前没有这套东西一上来就搭全套成本并不低。我建议先走HTML报告的轻量路线等压测真正变成了周期性活动再考虑往Grafana上迁。报告方案数据形态适用场景主要缺点终端摘要聚合统计本地自测、快速查看无法留存、纯文本summary JSON聚合统计快速归档结论缺少时序细节完整JSON/CSV全量时间序列二次分析、转HTML文件大、增长快Web Dashboard实时图表脚本调试、排错实验性功能HTML报告可视化页面正式汇报、跨团队分享依赖第三方工具Prometheus/Grafana长期时序指标趋势对比、团队共享基建成本高3. 报告质量是“测”出来的脚本里就该埋好的三个因子3.1 thresholds阈值让报告直接回答“过没过”很多人跑压测的时候脚本里就一个http.get()跑完盯着终端看半天也不清楚到底算过还是没过。如果你也有这个困扰那说明还没用上thresholds。阈值是K6里“自动判断”的核心机制它在压测过程中实时比对指标跑完直接在终端标红标绿干不干净直接看结果。一个典型的阈值配置长这样export const options { thresholds: { http_req_duration: [p(95)500, p(99)1000], http_req_failed: [rate0.01], }, };这个配置的意思是95%的请求耗时必须在500ms以内99%的请求耗时必须在1000ms以内请求失败率必须低于1%。跑完之后K6会逐条告诉你哪些阈值命中了哪些没命中。更关键的是如果有阈值没达到K6进程会以非零状态码退出——这意味着你可以直接在CI里用它做质量门禁压测不达标构建就失败完全不需要人肉判断。我自己的经验是阈值一定要在压测开始前就和团队对齐而不是跑完再反过来找数字。提前定义“什么叫过”报告才有判断标准也省得每次压测完开会争论。3.2 check断言把业务语义写进报告thresholds管的是性能指标check管的是业务正确性。两者经常被混为一谈但其实分工很明确。check在你的脚本里断言“这次请求返回的结果是否符合预期”比如状态码是不是200、响应体里有没有包含某个关键字段、某个JSON字段的值对不对import { check } from k6; import http from k6/http; export default function () { const res http.get(http://test.k6.io); check(res, { status is 200: (r) r.status 200, body contains hello: (r) r.body r.body.includes(hello), }); }check的结果会汇总到报告里终端摘要中那行checks.........................: 100.00% ✓ 1200 ✗ 0就是check的通过率。这里有一个非常典型的坑http_req_failed这个指标K6默认是把返回4xx/5xx的请求算作失败但还有一种情况是接口返回了HTTP 200业务上却是个错误比如登录失败、订单创建失败。这时候http_req_failed显示0%看起来一切正常但check会诚实地告诉你业务失败率有多高。所以我做压测的时候几乎每个关键请求都会写check否则报告里的“成功率”很可能是假象。3.3 自定义指标与标签打破“平均数字骗人”的困局前面提到平均数会掩盖慢接口解决这个问题的办法就是给请求打标签然后在报告里按标签分组查看。K6里的标签机制非常灵活你可以在HTTP请求上挂任意自定义标签也可以在更高维度给脚本打标签。比如我压测一个下单流程时会这样埋点import http from k6/http; import { Trend } from k6/metrics; import { check } from k6; const orderDuration new Trend(order_submit_duration); export default function () { const payload JSON.stringify({ sku: 1001, num: 1 }); const res http.post(http://api.example.com/order/submit, payload, { tags: { api: order_submit }, }); orderDuration.add(res.timings.duration, { api: order_submit }); check(res, { order submit is 200: (r) r.status 200, }); }这样在终端摘要或者后续的HTML报告里order_submit_duration就会作为一个独立的自定义指标出现。更重要的是K6会把http_req_duration按标签分组展示你能直接看到{ api: order_submit }这个标签下的请求耗时分布而不是被其他快接口拉低平均值。标签同样可以用在阈值里。比如希望下单接口单独达标其他接口可以稍微放宽就可以写export const options { thresholds: { http_req_duration{api:order_submit}: [p(95)300], }, };这种按标签拆分的报告逻辑才是压测报告“能找到瓶颈在哪”的关键。没有标签的压测报告基本只能看个整体健康度属于“只知道人生病了但不知道哪个器官出了问题”。3.4 handleSummary用代码精确控制报告出口如果你对默认的终端摘要格式不满意或者想同时导出多种格式的报告K6提供了一个相当强大的自定义能力——handleSummary。这个函数会在测试结束时被调用接收全部聚合数据你可以对数据进行任意加工然后返回一个“文件名到内容”的映射K6会帮你把内容写入对应文件。一个最基础的用法是这样import { textSummary } from https://jslib.k6.io/k6-summary/0.0.1/index.js; export function handleSummary(data) { return { stdout: textSummary(data, { indent: , enableColors: true }), summary.json: JSON.stringify(data, null, 2), result-processed.json: JSON.stringify({ p95: data.metrics.http_req_duration.values[p(95)], p99: data.metrics.http_req_duration.values[p(99)], totalRequests: data.metrics.http_reqs.values.count, }, null, 2), }; }在这个例子里我不仅把完整的summary导出到文件还顺手抽取了几个关键指标生成一个精简的result-processed.json这个文件可以直接被团队内部的其他报表系统消费。你甚至可以在handleSummary里写逻辑根据阈值是否通过生成一份带结论的Markdown或HTML文件。handleSummary是K6报告功能里最被低估的能力之一。它意味着报告生成不再是“K6给我什么我就看什么”而是“我想要什么报告就写代码生成什么报告”。对需要对接内部流程、定制汇报模板的团队来说这个功能省去了大量手工整理数据的苦力活。4. 现场解读一份K6报告每个数字背后的判断逻辑4.1 从请求耗时分层定位慢在哪一环很多人看K6终端摘要只看http_req_duration这一个数字这就浪费了报告里最有价值的排查信息。http_req_duration实际上是一个总耗时它由三个子阶段组成而报告里恰恰把这些子阶段都单独列出来了http_req_sending客户端发送请求数据的时间。http_req_waiting发送完后等待服务器响应的时间也叫TTFB这是最关键的一段。http_req_receiving接收到响应体数据的时间受响应体大小和网速影响。另外K6还会列出http_req_blocked、http_req_connecting、http_req_tls_handshaking它们发生在连接建立阶段不计入http_req_duration但也是排查网络问题的重要线索。我一般拿到报告会先看http_req_waiting因为它是服务端处理能力的直接体现。如果waiting占总耗时的90%以上说明网络传输没瓶颈问题几乎可以肯定出在服务端处理上。如果receiving突然变高那很可能是响应体变大、带宽被打满或者服务端一次性返回了太多数据。如果是blocked高常见原因是DNS解析慢或者本地连接池不够用导致排队。这套分层判断逻辑比单纯盯着一个平均耗时要有效得多。4.2 p(95)/p(99)的正确读法和一个常见误区K6报告里每个耗时指标都会给出avg、min、med、max、p(90)、p(95)这一串值。p(95)的含义是把所有请求的耗时从小到大排序95%的请求都低于这个值。它比平均值更能反映“大多数用户的真实体验”。读报告时有个常见误区就是只看平均数和p95忽略p99。实际经验是如果p95很漂亮但p99突然飙高说明系统在大约1%的请求上出现了明显的抖动。这个抖动比例放在大流量场景下一点不小——10万请求里就是1000个请求体验很差这里面可能藏着连接池打满、GC停顿或者限流策略触发的问题。我自己的判断习惯是p95和p99一起看两者差距越大说明系统尾部延迟越严重越需要排查。还有一个容易混淆的口径问题。浏览器性能面板里的p95和K6压测报告里的p95不是同一个东西。浏览器的p95通常包含页面渲染、脚本执行等客户端耗时K6的http_req_duration只统计到网络请求完成不包含页面渲染。做对比时一定要用同口径的数据否则会得出完全错误的结论。4.3 checks与成功率的关系两个口径别搞混在报告里checks和http_req_failed是两套独立的成功率。http_req_failed默认只看HTTP状态码4xx和5xx算失败。checks则看的是你在脚本里写的业务断言。实际压测中我经常看到这两种数据打架。最典型的是接口返回HTTP 200但响应体里的code字段是5001表示业务逻辑失败。这时候http_req_failed是0%checks却可能只有80%。如果你只盯着http_req_failed就会以为系统一切正常实际上业务失败率已经高到没法上线了。反过来也有情况比如服务端返回了503但你的check写的比较宽松没有检查状态码导致checks是100%而http_req_failed是100%。所以解读报告时一定要搞清楚这两个口径各自主张什么。我强烈建议在写脚本时就对每个核心接口加上响应体内部的业务码校验把“真实的业务成功率”刻进报告里。4.4 报告数字“对不上”的三种真实原因团队里经常会有这样的争论同一次压测A说p95是70msB说p95是90ms到底谁对排除有人看错行最常见的原因有三个。第一数据口径不同。--summary-export导出的summary.json是聚合统计值而--out json导出的完整时间序列再自行计算时可能会因为数据点的选取范围、采样边界稍有差异算出来的p95不完全一样。两边都有道理但必须统一口径再对比。第二压测时间太短。K6在压测开始时需要创建VU、建立连接池前几十秒的数据往往不稳定。如果整个压测只跑30秒其中10秒是“预热期”报告里的p95就会被预热阶段的慢请求拉高。我一般建议单场景压测至少跑1分钟取稳态区间做判断必要的时候可以通过标签区分预热阶段和正式阶段。第三没算清楚gracefulStop。K6的scenario默认有一个gracefulStop收尾阶段VU不会瞬间中断而是把手头正在执行的迭代跑完再退出。所以报告里的总时长会比你设置的duration要长一点末尾那段时间的请求也会计入统计。如果你的服务在收尾阶段出现了异常波动报告数字就会和“理想中的压力模型”对不上。这种情况可以通过查看vus曲线来判断末尾VU数逐渐掉到0的过程就是gracefulStop在起作用。5. 把报告接入CI/CD自动化压测、产物留存的完整实践5.1 在GitLab CI中用容器镜像跑K6相比于本地手动跑压测把K6压测任务放进CI/CD里最大的价值是“可重复”和“可留存”。每次代码变更、每次依赖升级都可以触发同一套压测脚本跑出来的结果自动归档团队随时随地可以对比。K6官方提供了Docker镜像grafana/k6这让CI接入变得非常简单。以GitLab CI为例一个最基础的压测任务是长这样的performance-test: stage: test image: grafana/k6:latest script: - k6 run --vus 50 --duration 30s --out jsonresult.json tests/load.js artifacts: paths: - result.json expire_in: 30 days when: always rules: - if: $CI_PIPELINE_SOURCE schedule - if: $CI_COMMIT_BRANCH main这里有几个细节值得展开。镜像直接用grafana/k6进容器就能执行k6 run省去在CI机器上装环境的麻烦。--out jsonresult.json导出的原始数据通过artifacts配置保留30天这样即使CI日志被清理报告数据也还在。rules里限制了触发条件只有定时计划和推送到主干时才跑避免每次push都触发一次压测导致资源浪费。如果你用的是GitHub Actions方式也类似官方就有现成的grafana/k6-action可以直接调用。无论是哪种平台核心思路就一条让压测结果像构建产物一样被自动留档而不是每次压测完就消失。5.2 JSON产物转化成HTML报告的具体配置CI里跑完K6之后最常见的需求是把原始JSON转成一份人人能看的HTML报告。这一步我通常会在CI的下一个job里完成。具体做法可以是先安装HTML报告工具再读取上一步的JSON产物generate-html-report: stage: report image: node:20 script: - npm install -g k6-reporter - k6-reporter result.json artifacts: paths: - report.html expire_in: 90 days needs: - performance-test用needs关键字把两个job串起来压测job产出result.json报告job消费它并生成report.html。这样团队在Merge Request页面就能直接下载到一份可视化压测报告也可以把report.html上传到对象存储或者内部Wiki上长期留存。再次强调一个关键点HTML报告工具消费的是完整时间序列JSON也就是--out json导出的那个文件。如果你在压测命令里只用了--summary-export拿到的是聚合汇总JSON字段结构完全不同很多图表会渲染不出来或者显示为空。我在实际项目里就见过同事把这两个搞混生成的HTML报告里趋势图全部空白排查了半天才发现是原料给错了。5.3 Grafana长期趋势与告警联动如果压测已经成为团队的习惯动作那Grafana长期趋势面板绝对值得投入。有了历史数据之后性能好不好不再靠拍脑袋而是看曲线。K6接入Grafana有两条常用路径。一条是走InfluxDB压测时指定--out influxdbhttp://你的地址:8086/k6K6会把指标实时写入InfluxDB然后Grafana用InfluxDB作为数据源导入社区现成的K6 dashboard模板请求速率、响应时间、VU数、错误率这些图表就都有了。另一条是走Prometheus remote write配置K6_PROMETHEUS_RW_SERVER_URL环境变量后用--out prometheus直接推送然后在Grafana里用Prometheus数据源查询指标。Grafana的告警规则可以用来做自动提醒。比如设置“http_req_duration的p95超过500ms持续3分钟”就触发告警通知到钉钉、飞书或者邮件。这里要注意的是K6不是7×24小时常驻的监控agent它只在压测期间产生数据所以告警规则更适合做成“每次压测跑完自动检查”而不是像基础设施监控那样持续盯防。发布前的性能回归压测配合这套自动告警基本上能把明显的性能劣化挡在上线前。5.4 报告之外团队真正需要的结论格式工具层面的事情做完了最后还有一个同样重要的问题报告里到底该写什么。我见过不少团队HTML报告生成了Grafana面板也搭了但大家还是不看。原因很简单——报告里全是图表和数据没有结论。压测报告不是数据展示而是一次性能评审。我给团队定的报告结构和内容大概是这样的压测基本信息环境地址、脚本版本、压测时间、VU规模和总请求数。核心结论系统是否达到预期哪些接口/场景达标哪些不达标。关键证据用p95、p99、错误率、check通过率这几个数字支撑结论。瓶颈定位根据耗时分层和标签分组指出慢在服务端处理、网络还是数据量。优化建议根据定位结果给出可执行的下一步比如加缓存、扩副本、优化慢SQL、调整连接池参数。每次压测报告里这五部分缺一不可。如果只是把K6生成的图表一股脑贴上去那充其量是数据堆砌不是报告。K6生成报告的能力再强最终还是要落到有人能根据报告做决策上来。最后分享一个我自己的小习惯。以前我也喜欢直接把终端摘要截图丢群里直到被运维同事追着问“waiting和sending到底啥意思”我才意识到这份“报告”根本没有传达任何信息。现在我每次压测至少做两件事一是把完整JSON时间序列归档到CI的artifacts里保留至少一个月方便事后翻旧账二是用handleSummary把关键结论写成固定的精简JSON再补齐一份带结论的摘要发给团队。K6压测工具本身只是个开始真正值钱的是报告能不能让你的团队在五分钟内搞清楚“系统行不行、哪里不行”。这件事值得花时间打磨。