前言
随着春节大促即将到来,为了确保线上业务高效稳定地运行,电商企业大多会对旗下关键业务应用进行多轮测试。通过模拟线上较高流量的请求,来观察服务性能的实际表现。以某企业的业务测试报告举例:
图 1 压测报告显示,成功率非常低,且全局接口成功率都很低
通过报告可以看到:当应用所承受的流量增加至特定临界点时,请求成功率大幅下降,导致整个测试周期内平均成功率相当惨淡,仅有 9.89%,并伴随着较高的响应时间(RT)。经过深入分析,发现这种高失败率现象普遍存在于所有接口上,并且在整个压测过程中未显示出任何回归稳定状态的迹象。
面对这类压测结果的直观推断是应用可能已达到其性能瓶颈。Prometheus 监控所采集的 CPU 使用情况进一步印证了这一假设:应用实例的 CPU 使用率几乎达到饱和状态,应用在当前情景下无法处理如此高 TPS(每秒事务数)。该企业采用的优化手段为:一方面,通过链路追踪(Tracing)数据和 CPU 火焰图,逐步定位到代码层面性能瓶颈,并开展针对性优化;另一方面,为应用集成开源的流量控制防护和熔断降级组件来应对线上流量的不确定性,并对关键业务应用进行扩容处理,进一步增强应用可用性。
图 2 压测过程中应用实例 pod 的 CPU 指标
在实际业务场景中类似问题并不少见,并可以总结为以下两个挑战:
- 如何定位复杂业务系统中的性能瓶颈?
- 如何应对流量的不确定性,保护好我们的服务?
示例中的公司对于以上问题给出了自己的解答:模拟线上流量开展性能测试,以 Tracing 数据 + CPU 火焰图作为问题定位依据,通过流控和熔断降级来保护服务。上述方案在开源领域拥有比较成熟的落地实践:
- 使用 OpenTelemetry 的 SDK/Agent 采集 Tracing 数据[1]
- 使用 Async Profiler 工具生成 CPU 火焰图[2]
- 使用 Sentinel 实现流量治理[3]
但实际上,无论是对业务代码进行改造,还是搭建 OpenTelemetry 服务端用于数据接收,都会带来一定的成本投入,研发不再能专注于业务代码开发和维护。那么,是否有无侵入、自动化方式来解决问题呢?阿里云推出的全新 3.x 版本 Java 探针为这些问题带来了全新回答。
Java 探针(也称为 JavaAgent)可以在应用运行态增强应用本身的字节码,业务应用本身不需要做任何代码改动即可实现额外能力的扩展[4]。部署在 kubernetes 中的应用,还可以基于 init-container 实现探针自动注入,进一步降低接入成本。
如何定位业务系统性能瓶颈?
正如前文所说,在定位和优化慢调用问题时,除了观测应用的各个关键指标外,还有“两大法宝”:调用链数据(Tracing)和 CPU 火焰图,可以帮助定位业务调用中耗时较长、CPU 较高的代码片段然后做对应修复。然而,这“两大法宝”却各有自己的“硬伤”:
Tracing 数据常常存在观测盲区
对 Tracing 而言,其数据一般依赖 Agent/SDK 提供的自动或手动的埋点实现数据采集,然后上报到 Tracing 数据收集端(Collector)存储,再由展示端的 Dashboard 关联并展示出来。显然,一条链路的精细程度取决于埋点的颗粒度。但实际上,Tracing 数据的采集也会带来一定的开销,并不会无限制地细分粒度,只会对通用框架中的关键方法进行埋点。那么对于某些业务中的高开销逻辑,往往就可能缺失埋点,从而无法对业务逻辑的耗时进行准确判断,类似下图中的埋点缺失问题在调用链中经常存在。
图 3 常见的 Tracing 数据中容易存在未覆盖埋点导致的观测盲区
CPU 火焰图难以帮助定位线上问题
对 CPU 火焰图而言,可以直观展现业务应用执行过程中 CPU 密集点。火焰图中方法栈宽度代表方法执行时长,通过对“宽底座”、“大平头”的方法开展优化,缩减其调用耗时以达到性能提升效果。然而,许多隐藏的性能问题在测试阶段往往不容易暴露出来,但在线上环境却能显露无余。而火焰图的生成需要花一些时间来采集,当集中于对线上业务进行止血的时候,现场的保存往往有所缺失,毕竟不可能在线上问题发生后先跑个 5 分钟的火焰图。这可能会让整个问题的定位复盘过程变得更加复杂。