可惜了我之前组织打造的性能测试平台
一、全链路压测基础
1、全链路压测基本概念
什么是全链路压测
· 基于实际的生产业务场景、系统环境,基于真实数据模拟海量的用户请求对整个业务链进行压力测试,并持续调优的过程;
· 全链路的核心为:业务场景、数据链路、压力模型和环境拓扑;
· 全链路压测不仅仅是一种测试手段,更确切来说其是一种测试过程,该过程涉及自动化测试/性能测试/高可用测试技术以外,还覆盖性能分析调优以及扩缩容解决方案等等。
为什么做全链路压测
上图是淘宝核心业务应用关系的拓扑图,还不包含了其他的非核心业务应用,所谓的核心业务就是和交易相关的,和钱相关的业务。这张图大家可能看不清楚,看不清楚才是正常的,因为当时的阿里应用数量之多、应用间关系之混乱靠人工确实已经无法理清楚了。
在真实的业务场景种,每个系统的压力都比较大,而系统之间是有相互依赖关系的,单机压测没有考虑到依赖环节压力都比较大的情况,会引入一个不确定的误差。这就好比,我们要生产一个仪表,每一个零件都经过了严密的测试,最终把零件组装成一个仪表,仪表的工作状态会是什么样的并不清楚。
哪些场景适合全链路压测
· 定时定期进行线上运营活动;
· 业务链路以及数据链路调用错综复杂,各子系统之间调用关系密切,均为业务核心调用链路;
· 真实业务流量与历史流量对比预估有量的增长;
· 业务需求频繁迭代,业务链路性能波动较大;
· 测试环境数据、系统版本等无法统一,且资源配置与线上差异较大;
2、几种链路压测方案的对比
2.1 线下压测
顾名思义就是在测试环境进行压测,且是针对一些重点项目这种测试手段,因为测试环境硬件资源以及压测数据与线上差别太大并且服务间依赖关系错综复杂,测试环境很难模拟且不够稳定,压测出来的数据指标参考价值不大,难以用测试环境得出的结果推导生产真实容量。
2.2 预生产环境压测
这个一般是将生成环境的硬件以及软件同步复制到与生产环境一份,然后对服务内部的外部调用接口进行拦截,然后进行压测这样可以评估出来生产环境的真实容量以及达到压测的目的,但是成本非常高,需要将生产环境的硬件完全的复制一份,并未维护成本非常高,部署的时候需要同步的在预生产环境进行部署,以及压测代码的更改。
2.3 引流压测
随着业务量的不断增长,考虑到线下测试结果的准确性,开始尝试生产压测,这种压测手段,我们称之为引流压测。事实上没有真正的模拟放大压力进行测试,而是一种通过缩小在线服务集群数的方式来放大单机处理量。比如一个业务系统的集群有 100 个节点,将其中 90 个节点模拟下线或转发流量到剩余的 10 个节点上实施压测。
引流压测的弊端在于,DB 承受压力不变,上下游系统的压力不变。压测结果仅能代表单个应用的性能,但往往无法识别链路和架构级的隐患,而且在引流过程中倘若出现异常或突如其来的业务高峰,很容易造成生产故障。
2.4 全链路压测
随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。因此,就需要一些可以帮助理解系统行为、用于分析性能问题的工具,以便发生故障的时候,能够快速定位和解决问题,但是他的缺点也很明显就是需要的技术难度很高,需要克服流量染色,数据隔离,日志隔离,风险熔断等技术难题,因位在生产环境压测,所以控制不好风险也是非常高的。
2.5 四种压测方案对比
二、全链路压测平台在美团中的实践
背景
Quake (雷神之锤)作为美团的全链路压测平台,它的目标是提供对整条链路进行全方位、安全、真实的压测,来帮助业务做出更精准的容量评估。因此我们对 Quake 提出了如下的要求:
-
提供模拟线上真实流量的能力
-
压测和 DDoS 攻击不同的是,压测有应用场景,而 DDoS 可能只需要一个请求。为了更真实的还原用户行为,我们需要获取线上的真实流量进行压测。
-
-
具备快速创建压测环境的能力
-
这里的环境指的是线上环境,因为如果压测的是线下环境,即使不考虑“机器配置是否相同”这个因素,像集群规模、数据库体量、网络条件等这些因素,在线下环境下都无法进行模拟,这样得出压测结果,其参考价值并不大。
-
-
支持多种压测类型
-
压测类型除了支持标准的 HTTP 协议,还需要对美团内部的 RPC 和移动端协议进行支持。
-
-
提供压测过程的实时监控与过载保护
-
全链路压测是一个需要实时关注服务状态的过程,尤其在探测极限的时候,需要具备精准调控 QPS 的能力,秒级监控的能力,预设熔断降级的能力,以及快速定位问题的能力。
-
Quake 整体架构设计
Quake 集数据构造、压测隔离、场景管理、动态调控、过程监控、压测报告为一体,压测流量尽量模拟真实,具备分布式压测能力的全链路压测系统,通过模拟海量用户真实的业务操作场景,提前对业务进行高压力测试,全方位探测业务应用的性能瓶颈,确保平稳地应对业务峰值。
架构图:
Quake 整体架构上分为:
-
Quake-Web:压测管理端,负责压测数据构造、压测环境准备、场景管理、压测过程的动态调整以及压测报表展示等。
-
Quake-Brain:调度中心,负责施压资源的调度、任务分发与机器资源管理。
-
Quake-Agent:压测引擎,负责模拟各种压测流量。
-
Quake-Monitor:监控模块,统计压测结果,监控服务各项指标。
1、管理端核心功能
1.1 数据构造
传统的数据构造,一般由测试人员自己维护一批压测数据。但这种方式存在很大的弊端,一方面维护成本相对较高,另一方面,其构造出的数据多样性也不足够。在真实业务场景中,我们需要的是能直接回放业务高峰期产生的流量,只有面对这样的流量冲击,才能真实的反映系统可能会产生的问题。
Quake 主要提供了 HTTP 和 RPC 的两种数据构造方式:
HTTP 服务的访问日志收集
对于 HTTP 服务,在 Nginx 层都会产生请求的访问日志,我们对这些日志进行了统一接入,变成符合压测需要的流量数据。架构图如下:
S3为最终日志存储平台
底层使用了 Hive 作为数仓的工具,使业务在平台上可以通过简单的类 SQL 语言进行数据构造。Quake 会从数仓中筛选出相应的数据,作为压测所需的词表文件,将其存储在 S3 中。
RPC 线上流量实时录制
对于 RPC 服务,服务调用量远超 HTTP 的量级,所以在线上环境不太可能去记录相应的日志。这里我们使用对线上服务进行实时流量录制,结合 RPC 框架提供的录制功能,对集群中的某几台机器开启录制,根据要录制的接口和方法名,将请求数据上报到录制流量的缓冲服务(Broker)中,再由 Broker 生成最终的压测词表,上传到存储平台(S3)。
-
RPC Client:服务的调用方
-
Server:服务提供方
-
Broker:录制后流量缓冲服务器
-
S3:流量最终存储平台
1.2 压测隔离
做线上压测与线下压测最大不同在于,线上压测要保证压测行为安全且可控,不会影响用户的正常使用,并且不会对线上环境造成任何的数据污染。要做到这一点,首要解决的是压测流量的识别与透传问题。有了压测标识后,各服务与中间件就可以依据标识来进行压测服务分组与影子表方案的实施。
测试标识透传
对于单服务来说,识别压测流量很容易,只要在请求头中加个特殊的压测标识即可,HTTP 和 RPC 服务是一样的。但是,要在整条完整的调用链路中要始终保持压测标识,这件事就非常困难。
跨线程间的透传:
对于涉及多线程调用的服务来说,要保证测试标识在跨线程的情况下不丢失。这里以 Java 应用为例,主线程根据压测请求,将测试标识写入当前线程的 ThreadLocal 对象中(ThreadLocal 会为每个线程创建一个副本,用来保存线程自身的副本变量),利用 InheritableThreadLocal 的特性,对于父线程 ThreadLocal 中的变量会传递给子线程,保证了压测标识的传递。而对于采用线程池的情况,同样对线程池进行了封装,在往线程池中添加线程任务时,额外保存了 ThreadLocal 中的变量,执行任务时再进行替换 ThreadLocal 中的变量。
跨服务间的透传:
对于跨服务的调用,架构团队对所有涉及到的中间件进行了一一改造。利用 Mtrace (公司内部统一的分布式会话跟踪系统)的服务间传递上下文特性,在原有传输上下文的基础上,添加了测试标识的属性,以保证传输中始终带着测试标识。下图是 Mtrace 上下游调用的关系图:
1.3 链路诊断
由于链路关系的复杂性,一次压测涉及的链路可能非常复杂。很多时候,我们很难确认间接依赖的服务又依赖了哪些服务,而任何一个环节只要出现问题,比如某个中间件版本不达标,测试标识就不会再往下进行透传。Quake 提供了链路匹配分析的能力,通过平台试探性地发送业务实际需要压测的请求,根据 Mtrace提供的数据,帮助业务快速定位到标记透传失败的服务节点。
1.4 压测服务隔离
一些大型的压测通常选择在深夜低峰时期进行,建议相关的人员要时刻关注各自负责的系统指标,以免影响线上的正常使用。而对于一些日常化的压测,Quake 提供了更加安全便捷的方式进行。在低峰期,机器基本都是处于比较空闲的状态。我们将根据业务的需求在线上对整条链路快速创建一个压测分组,隔出一批空闲的机器用于压测。将正常流量与测试流量在机器级别进行隔离,从而降低压测对服务集群带来的影响。
依赖标识透传的机制,在 Quake 平台上提供了基于 IP、机器数、百分比不同方式的隔离策略,业务只需提供所需隔离的服务名,由 Quake 进行一键化的开启与关闭。
1.5 压测数据隔离
还有一个比较棘手的问题是针对写请求的压测,因为它会向真实的数据库中写入大量的脏数据。我们借鉴了阿里最早提出的“影子表”隔离的方案。“影子表”的核心思想是,使用线上同一个数据库,包括共享数据库中的内存资源,因为这样才能更接近真实场景,只是在写入数据时会写在了另一张“影子表”中。
对于 KV 存储,也是类似的思路。这里讲一下 MQ(消息队列)的实现,MQ 包括生产和消费两端,业务可以根据实际的需要选择在生产端忽略带测试标识的消息,或者在消费端接收消息后再忽略两种选择。
2、调度中心核心设计
调度中心作为整个压测系统的大脑,它管理了所有的压测任务和压测引擎。基于自身的调度算法,调度中心将每个压测任务拆分成若干个可在单台压测引擎上执行的计划,并将计划以指令的方式下发给不同的引擎,从而执行压测任务。
2.1 资源计算
不同的压测场景,需要的机器资源不一样。以 HTTP 服务为例,在请求/响应体都在 1K 以内,响应时间在 50ms 以内和 1s 左右的两个请求,单个施压机能达到的极限值完全不同。影响压测能力的因素有很多,计算中心会依据压测模型的不同参数,进行资源的计算。
主要参考的数据包括:
-
压测期望到达的 QPS。
-
压测请求的平均响应时间和请求/响应体大小。
-
压测的词表大小、分片数。
-
压测类型。
-
所需压测的机房。
2.2 事件注入机制
因为整个压测过程一直处在动态变化之中,业务会根据系统的实际情况对压力进行相应的调整。在整个过程中产生的事件类型比较多,包括调整 QPS 的事件、触发熔断的事件、开启事故注入、开启代码级性能分析的事件等等,同时触发事件的情况也有很多种,包括用户手动触发、由于系统保护机制触等等。所以,我们在架构上也做了相应的优化,其大致架构如下:
在代码设计层面,我们采用了观察者和责任链模式,将会触发事件的具体情况作为观察主题,主题的订阅者会视情况类型产生一连串执行事件。而在执行事件中又引入责任链模式,将各自的处理逻辑进行有效的拆分,以便后期进行维护和能力扩充。
2.3 机器管理
调度中心管理了所有的施压机资源,这些施压机分布在北京、上海的多个机房,施压机采用容器化方式进行部署,为后续的动态扩容、施压机灰度升级以及异常摘除的提供了基础保障。
3、压测引擎优化
在压测引擎的选择上,Quake 选择了自研压测引擎。这也是出于扩展性和性能层面的考虑,特别在扩展性层面,主要是对各种协议的支持,这里不展开进行阐述。性能方面,为了保证引擎每秒能产生足够多的请求,我们对引擎做了很多性能优化的工作。
性能问题
通常的压测引擎,采用的是 BIO 的方式,利用多线程来模拟并发的用户数,每个线程的工作方式是:请求-等待-响应。
通信图:
这种方式主要的问题是,中间的等待过程,线程资源完全被浪费。这种组合模式下,性能问题也会更严重(组合模式:即模拟用户一连串的用户行为,以下单为例,请求组中会包含用户登录、加入购物车、创建订单、支付订单、查看支付状态。这些请求彼此间是存在先后关系的,下一个请求会依赖于上一个请求的结果。),若请求组中有5个串联请求,每个请求的时长是200ms,那完成一组请求就需要 1s 。这样的话,单机的最大 QPS 就是能创建的最大线程数。我们知道机器能创建的线程数有限,同时线程间频繁切换也有成本开销,致使这种通信方式能达到的单机最大 QPS 也很有限。
这种模型第二个问题是,线程数控制的粒度太粗,如果请求响应很快,仅几十毫秒,如果增加一个线程,可能 QPS 就上涨了将近100,通过增加线程数的方式无法精准的控制 QPS,这对探测系统的极限来说,十分危险。
IO 模型优化
我们先看下 NIO 的实现机制,从客户端发起请求的角度看,存在的 IO 事件分别是建立连接就绪事件(OP_CONNECT)、IO 就绪的可读事件 (OP_READ) 和 IO 就绪的可写事件(OP_WRITE),所有 IO 事件会向事件选择器(Selector)进行注册,并由它进行统一的监听和处理,Selector 这里采用的是 IO 多路复用的方式。
在了解 NIO 的处理机制后,我们再考虑看如何进行优化。整个核心思想就是根据预设的 QPS,保证每秒发出指定数量的请求,再以 IO 非阻塞的方式进行后续的读写操作,取消了 BIO 中请求等待的时间。优化后的逻辑如下:
优化一:采用 Reactor 多线程模型
这里主要耗时都在 IO 的读写事件上,为了达到单位时间内尽可能多的发起压测请求,我们将连接事件与读写事件分离。连接事件采用单线程 Selector 的方式来处理,读写事件分别由多个 Worker 线程处理,每个 Worker 线程也是以 NIO 方式进行处理,由各自的 Selector 处理 IO 事件的读写操作。这里每个 Worker 线程都有自己的事件队列,数据彼此隔离,这样做主要是为了避免数据同步带来的性能开销。
优化二:业务逻辑与 IO 读写事件分离
这里说的业务逻辑主要是针对请求结果的处理,包括对请求数据的采样上报,对压测结果的解析校验,对请求转换率的匹配等。如果将这些逻辑放在 Worker 线程中处理,必然会影响 IO 读取的速度。因为 Selector 在监听到 IO 就绪事件后,会进行单线程处理,所以它的处理要尽可能的简单和快速,不然会影响其他就绪事件的处理,甚至造成队列积压和内存问题。为了解决 GC 的问题,主要从应用自身的内存管理和 JVM 参数两个维度来进行优化。
合理分配内存对象:包括请求对象加载机制优化和请求对象的快用快销等方法
JVM 参数调优:适当增大新生代的大小,适当调大新生代向晋升老年代的存活次数,提前触发老年代的 Full GC,设置需要进行内存压缩整理的 GC 次数,内存整理。
4、监控模块
压测肯定会对线上服务产生一定的影响,特别是一些探测系统极限的压测,我们需要具备秒级监控的能力,以及可靠的熔断降级机制。
4.1 客户端监控
压测引擎会将每秒的数据汇总后上报给监控模块,监控模块基于所有上报来的数据进行统计分析。这里的分析需要实时进行处理,这样才能做到客户端的秒级监控。监控的数据包括各 TP 线的响应情况、QPS 曲线波动、错误率情况以及采样日志分析等等。
实时 QPS 曲线
采样日志
4.2 服务端监控
除了通过引擎上报的压测结果来进行相应的监控分析之外,Quake 还集成了公司内部统一的监控组件,有监控机器指标的 Falcon 系统(小米开源),还有监控服务性能的 CAT系统(美团已经开源)。Quake 提供了统一的管理配置服务,让业务能在 Quake 上方便观察整个系统的健康状况。
熔断保护机制
Quake 提供了客户端和服务端两方面的熔断保护措施。
首先是客户端熔断,根据业务自定义的熔断阙值,Quake 会实时分析监控数据,当达到熔断阙值时,任务调度器会向压测引擎发送降低 QPS 或者直接中断压测的指令,防止系统被压挂。
被压服务同样也提供了熔断机制,Quake 集成了公司内部的熔断组件(Rhino),提供了压测过程中的熔断降级和限流能力。与此同时,Quake 还提供了压测故障演练的能力,在压测过程中进行人为的故障注入,来验证整个系统的降级预案。