在美团的价值观中,“以客户为中心”被放在一个很是重要的位置,因此咱们对服务出现故障愈来愈不能容忍。特别是目前公司业务正在高速增加阶段,每一次故障对公司来讲都是一笔很是不小的损失。而整个IT基础设施很是复杂,包括网络、服务器、操做系统以及应用层面均可能出现问题。在这种背景下,咱们必须对服务进行一次全方位的“体检”,从而来保障美团多个业务服务的稳定性,提供优质的用户服务体验。真正经过如下技术手段,来帮助你们吃的更好,生活更好:html
全链路压测是基于线上真实环境和实际业务场景,经过模拟海量的用户请求,来对整个系统进行压力测试。早期,咱们在没有全链路压测的状况下,主要的压测方式有:算法
但以上方式很难全面的对整个服务集群进行压测,若是以局部结果推算整个集群的健康情况,每每会“以偏概全”,没法评估整个系统的真实性能水平,主要的缘由包括:数据库
综合多种因素考虑,全链路压测是咱们准确评估整个系统性能水平的必经之路。目前,公司内全部核心业务线都已接入全链路压测,月平均压测次数达上万次,帮助业务平稳地度过了大大小小若干场高峰流量的冲击。缓存
Quake (雷神之锤)做为公司级的全链路压测平台,它的目标是提供对整条链路进行全方位、安全、真实的压测,来帮助业务作出更精准的容量评估。所以咱们对 Quake 提出了以下的要求:安全
Quake 集数据构造、压测隔离、场景管理、动态调控、过程监控、压测报告为一体,压测流量尽可能模拟真实,具有分布式压测能力的全链路压测系统,经过模拟海量用户真实的业务操做场景,提早对业务进行高压力测试,全方位探测业务应用的性能瓶颈,确保平稳地应对业务峰值。性能优化
架构图服务器
Quake 总体架构上分为:网络
传统的数据构造,通常由测试人员本身维护一批压测数据。但这种方式存在很大的弊端,一方面维护成本相对较高,另外一方面,其构造出的数据多样性也不足够。在真实业务场景中,咱们须要的是能直接回放业务高峰期产生的流量,只有面对这样的流量冲击,才能真实的反映系统可能会产生的问题。多线程
Quake 主要提供了 HTTP 和 RPC 的两种数据构造方式:架构
对于 HTTP 服务,在 Nginx 层都会产生请求的访问日志,咱们对这些日志进行了统一接入,变成符合压测须要的流量数据。架构图以下:
底层使用了 Hive 做为数仓的工具,使业务在平台上能够经过简单的类 SQL 语言进行数据构造。Quake 会从数仓中筛选出相应的数据,做为压测所需的词表文件,将其存储在 S3 中。
对于 RPC 服务,服务调用量远超 HTTP 的量级,因此在线上环境不太可能去记录相应的日志。这里咱们使用对线上服务进行实时流量录制,结合 RPC 框架提供的录制功能,对集群中的某几台机器开启录制,根据要录制的接口和方法名,将请求数据上报到录制流量的缓冲服务(Broker)中,再由 Broker 生成最终的压测词表,上传到存储平台(S3)。
有些场景下,构造出来的流量是不能直接使用的,咱们须要对用户 ID、手机号等信息进行数据偏移。Quake 也是提供了包含四则运算、区间限定、随机数、时间类型等多种替换规则。
数据构造产生的词表文件,咱们须要进行物理上的分片,以保证每一个分片文件大小尽量均匀,而且控制在必定大小以内。这么作的主要缘由是,后续压测确定是由一个分布式的压测集群进行流量的打入,考虑到单机拉取词表的速度和加载词表的大小限制,若是将词表进行分片的话,能够有助于任务调度更合理的进行分配。
作线上压测与线下压测最大不一样在于,线上压测要保证压测行为安全且可控,不会影响用户的正常使用,而且不会对线上环境形成任何的数据污染。要作到这一点,首要解决的是压测流量的识别与透传问题。有了压测标识后,各服务与中间件就能够依据标识来进行压测服务分组与影子表方案的实施。
对于单服务来讲,识别压测流量很容易,只要在请求头中加个特殊的压测标识便可,HTTP 和 RPC 服务是同样的。可是,要在整条完整的调用链路中要始终保持压测标识,这件事就很是困难。
对于涉及多线程调用的服务来讲,要保证测试标识在跨线程的状况下不丢失。这里以 Java 应用为例,主线程根据压测请求,将测试标识写入当前线程的 ThreadLocal 对象中(ThreadLocal 会为每一个线程建立一个副本,用来保存线程自身的副本变量),利用 InheritableThreadLocal 的特性,对于父线程 ThreadLocal 中的变量会传递给子线程,保证了压测标识的传递。而对于采用线程池的状况,一样对线程池进行了封装,在往线程池中添加线程任务时,额外保存了 ThreadLocal 中的变量,执行任务时再进行替换 ThreadLocal 中的变量。
对于跨服务的调用,架构团队对全部涉及到的中间件进行了一一改造。利用 Mtrace (公司内部统一的分布式会话跟踪系统)的服务间传递上下文特性,在原有传输上下文的基础上,添加了测试标识的属性,以保证传输中始终带着测试标识。下图是 Mtrace 上下游调用的关系图:
因为链路关系的复杂性,一次压测涉及的链路可能很是复杂。不少时候,咱们很难确认间接依赖的服务又依赖了哪些服务,而任何一个环节只要出现问题,好比某个中间件版本不达标,测试标识就不会再往下进行透传。Quake 提供了链路匹配分析的能力,经过平台试探性地发送业务实际须要压测的请求,根据 Mtrace提供的数据,帮助业务快速定位到标记透传失败的服务节点。
一些大型的压测一般选择在深夜低峰时期进行,建议相关的人员要时刻关注各自负责的系统指标,以避免影响线上的正常使用。而对于一些平常化的压测,Quake 提供了更加安全便捷的方式进行。在低峰期,机器基本都是处于比较空闲的状态。咱们将根据业务的需求在线上对整条链路快速建立一个压测分组,隔出一批空闲的机器用于压测。将正常流量与测试流量在机器级别进行隔离,从而下降压测对服务集群带来的影响。
依赖标识透传的机制,在 Quake 平台上提供了基于 IP、机器数、百分比不一样方式的隔离策略,业务只需提供所需隔离的服务名,由 Quake 进行一键化的开启与关闭。
还有一个比较棘手的问题是针对写请求的压测,由于它会向真实的数据库中写入大量的脏数据。咱们借鉴了阿里最先提出的“影子表”隔离的方案。“影子表”的核心思想是,使用线上同一个数据库,包括共享数据库中的内存资源,由于这样才能更接近真实场景,只是在写入数据时会写在了另外一张“影子表”中。
对于 KV 存储,也是相似的思路。这里讲一下 MQ(消息队列)的实现,MQ 包括生产和消费两端,业务能够根据实际的须要选择在生产端忽略带测试标识的消息,或者在消费端接收消息后再忽略两种选择。
调度中心做为整个压测系统的大脑,它管理了全部的压测任务和压测引擎。基于自身的调度算法,调度中心将每一个压测任务拆分红若干个可在单台压测引擎上执行的计划,并将计划以指令的方式下发给不一样的引擎,从而执行压测任务。
不一样的压测场景,须要的机器资源不同。以 HTTP 服务为例,在请求/响应体都在 1K 之内,响应时间在 50ms 之内和 1s 左右的两个请求,单个施压机能达到的极限值彻底不一样。影响压测能力的因素有不少,计算中心会依据压测模型的不一样参数,进行资源的计算。
主要参考的数据包括:
由于整个压测过程一直处在动态变化之中,业务会根据系统的实际状况对压力进行相应的调整。在整个过程当中产生的事件类型比较多,包括调整 QPS 的事件、触发熔断的事件、开启事故注入、开启代码级性能分析的事件等等,同时触发事件的状况也有不少种,包括用户手动触发、因为系统保护机制触等等。因此,咱们在架构上也作了相应的优化,其大体架构以下:
在代码设计层面,咱们采用了观察者和责任链模式,将会触发事件的具体状况做为观察主题,主题的订阅者会视状况类型产生一连串执行事件。而在执行事件中又引入责任链模式,将各自的处理逻辑进行有效的拆分,以便后期进行维护和能力扩充。
调度中心管理了全部的施压机资源,这些施压机分布在北京、上海的多个机房,施压机采用容器化方式进行部署,为后续的动态扩容、施压机灰度升级以及异常摘除的提供了基础保障。
业务对压测的需求有高低峰之分,因此平台也须要事先部署一部分机器用于平常的业务压测。当业务申请资源不足时,平台会按需经过容器化方式动态的进行扩容。这样作的好处,一方面是节省机器资源,另外一方面就是便于升级。不难想象,升级50台机器相对升级200台机器,前者付出的代价确定更小一些。
整个机器池维护着几百台机器,若是须要对这些机器进行升级操做,难度系数也比较高。咱们之前的作法是,在没有业务压测的时候,将机器所有下线,而后再批量部署,整个升级过程既耗时又痛苦。为此,咱们引入了灰度升级的概念,对每台施压机提供了版本的概念,机器选择时,优先使用稳定版的机器。根据机器目前使用的状态,分批替换未使用的机器,待新版本的机器跑完基准和回归测试后,将机器选择的策略改成最新版。经过这种方式,咱们可让整个升级过程,相对平顺、稳定,且可以让业务无感知。
调度中心维持了与全部施压机的心跳检测,对于异常节点提供了摘除替换的能力。机器摘除能力在压测过程当中很是有必要,由于压测期间,咱们须要保证全部的机器行为可控。否则在须要下降压力或中止压测时,若是施压机不能正常作出响应,其致使的后果将会很是严重。
在压测引擎的选择上,Quake 选择了自研压测引擎。这也是出于扩展性和性能层面的考虑,特别在扩展性层面,主要是对各类协议的支持,这里不展开进行阐述。性能方面,为了保证引擎每秒能产生足够多的请求,咱们对引擎作了不少性能优化的工做。
一般的压测引擎,采用的是 BIO 的方式,利用多线程来模拟并发的用户数,每一个线程的工做方式是:请求-等待-响应。
通讯图:
这种方式主要的问题是,中间的等待过程,线程资源彻底被浪费。这种组合模式下,性能问题也会更严重(组合模式:即模拟用户一连串的用户行为,如下单为例,请求组中会包含用户登陆、加入购物车、建立订单、支付订单、查看支付状态。这些请求彼此间是存在前后关系的,下一个请求会依赖于上一个请求的结果。),若请求组中有5个串联请求,每一个请求的时长是200ms,那完成一组请求就须要 1s 。这样的话,单机的最大 QPS 就是能建立的最大线程数。咱们知道机器能建立的线程数有限,同时线程间频繁切换也有成本开销,导致这种通讯方式能达到的单机最大 QPS 也颇有限。
这种模型第二个问题是,线程数控制的粒度太粗,若是请求响应很快,仅几十毫秒,若是增长一个线程,可能 QPS 就上涨了将近100,经过增长线程数的方式没法精准的控制 QPS,这对探测系统的极限来讲,十分危险。
咱们先看下 NIO 的实现机制,从客户端发起请求的角度看,存在的 IO 事件分别是创建链接就绪事件(OP_CONNECT)、IO 就绪的可读事件 (OP_READ) 和 IO 就绪的可写事件(OP_WRITE),全部 IO 事件会向事件选择器(Selector)进行注册,并由它进行统一的监听和处理,Selector 这里采用的是 IO 多路复用的方式。
在了解 NIO 的处理机制后,咱们再考虑看如何进行优化。整个核心思想就是根据预设的 QPS,保证每秒发出指定数量的请求,再以 IO 非阻塞的方式进行后续的读写操做,取消了 BIO 中请求等待的时间。优化后的逻辑以下:
这里主要耗时都在 IO 的读写事件上,为了达到单位时间内尽量多的发起压测请求,咱们将链接事件与读写事件分离。链接事件采用单线程 Selector 的方式来处理,读写事件分别由多个 Worker 线程处理,每一个 Worker 线程也是以 NIO 方式进行处理,由各自的 Selector 处理 IO 事件的读写操做。这里每一个 Worker 线程都有本身的事件队列,数据彼此隔离,这样作主要是为了不数据同步带来的性能开销。
这里说的业务逻辑主要是针对请求结果的处理,包括对请求数据的采样上报,对压测结果的解析校验,对请求转换率的匹配等。若是将这些逻辑放在 Worker 线程中处理,必然会影响 IO 读取的速度。由于 Selector 在监听到 IO 就绪事件后,会进行单线程处理,因此它的处理要尽量的简单和快速,否则会影响其余就绪事件的处理,甚至形成队列积压和内存问题。
压测引擎另外一个重要的指标是 Full GC 的时间,由于若是引擎频繁出现 Full GC,那会形成实际压测曲线(QPS)的抖动,这种抖动会放大被压服务真实的响应时间,形成真实 QPS 在预设值的上下波动。严重的状况,若是是长时间出现 Full GC,直接就致使预压的 QPS 压不上去的问题。
下面看一组 Full GC 产生的压测曲线:
为了解决 GC 的问题,主要从应用自身的内存管理和 JVM 参数两个维度来进行优化。
引擎首先加载词表数据到内存中,而后根据词表数据生成请求对象进行发送。对于词表数据的加载,须要设置一个大小上限,这些数据是会进入“老年代”,若是“老年代”占用的比例太高,那就会频发出现 Full GC 的状况。这里对于词表数据过大的状况,能够考虑采用流式加载的方式,在队列中维持必定数量的请求,经过边回放边加载的方式来控制内存大小。
引擎在实际压测过程当中,假设单机是 1W 的 QPS,那它每秒就会建立 1W 个请求对象,这些对象可能在下一秒处理完后就会进行销毁。若是销毁过慢,就会形成大量无效对象晋升老年代,因此在对响应结果的处理中,不要有耗时的操做,保证请求对象的快速释放。
这里放弃对象复用的缘由是,请求的基本信息占用的内存空间比较小。可一旦转换成了待发送对象后,占用的内存空间会比原始数据大不少,在 HTTP 和 RPC 服务中都存在一样的问题。并且以前使用 Apache HttpAsyncClient 做为 HTTP 请求的异步框架时,发现实际请求的 Response 对象挂在请求对象身上。也就是说一个请求对象在接收到结果后,该对象内存增长了响应结果的空间占用,若是采用复用请求对象的方式,很容易形成内存泄露的问题。
这里以 JVM 的 CMS 收集器为例,对于高并发的场景,瞬间产生大量的对象,这些对象的存活时间又很是短,咱们须要:
压测确定会对线上服务产生必定的影响,特别是一些探测系统极限的压测,咱们须要具有秒级监控的能力,以及可靠的熔断降级机制。
压测引擎会将每秒的数据汇总后上报给监控模块,监控模块基于全部上报来的数据进行统计分析。这里的分析须要实时进行处理,这样才能作到客户端的秒级监控。监控的数据包括各 TP 线的响应状况、QPS 曲线波动、错误率状况以及采样日志分析等等。
除了经过引擎上报的压测结果来进行相应的监控分析以外,Quake 还集成了公司内部统一的监控组件,有监控机器指标的 Falcon 系统(小米开源),还有监控服务性能的 CAT系统(美团已经开源)。Quake 提供了统一的管理配置服务,让业务能在 Quake 上方便观察整个系统的健康情况。
Quake 提供了客户端和服务端两方面的熔断保护措施。
首先是客户端熔断,根据业务自定义的熔断阙值,Quake 会实时分析监控数据,当达到熔断阙值时,任务调度器会向压测引擎发送下降 QPS 或者直接中断压测的指令,防止系统被压挂。
被压服务一样也提供了熔断机制,Quake 集成了公司内部的熔断组件(Rhino),提供了压测过程当中的熔断降级和限流能力。与此同时,Quake 还提供了压测故障演练的能力,在压测过程当中进行人为的故障注入,来验证整个系统的降级预案。
最后,总结一下作 Quake 这个项目的一些心得:
其实在 Quake 出来以前,美团公司内部已有一个压测平台(Ptest ),它的定位是针对单服务的性能压测。咱们分析了 Ptest 平台存在的一些问题,其压测引擎能力也很是有限。在美团发展早期,若是有两个大业务线要进行压测的话,机器资源每每会不足,这须要业务方彼此协调。由于准备一次压测,前期投入成本过高,用户须要本身构造词表,尤为是 RPC 服务,用户还须要本身上传 IDL 文件等等,很是繁琐。
Quake 针对业务的这些痛点,整个团队大概花费一个多月的时间开发出了第一个版本,而且快速实现了上线。当时,正面临猫眼十一节前的一次压测,那也是 Quake 的第一次亮相,并且取得了不错的成绩。后续,咱们基本平均两周实现一次迭代,而后逐步加入了机器隔离、影子表隔离、数据偏移规则、熔断保护机制、代码级别的性能分析等功能。
项目刚线上时,客服面临问题很是多,不只有使用层面的问题,系统自身也存在一些 Bug 缺陷。当时,一旦遇到业务线大规模的压测,咱们团队都是全员待命,直接在现场解决问题。后续系统稳定后,咱们组内采用了客服轮班制度,每一个迭代由一位同窗专门负责客服工做,保障当业务遇到的问题可以作到快速响应。尤为是在项目上线初期,这点很是有必要。若是业务部门使用体验欠佳,项目口碑也会变差,就会对后续的推广形成很大的问题。
这应该是全部内部项目都会遇到的问题,不少时候,推广成果决定项目的生死。前期咱们先在一些比较有表明性的业务线进行试点。若是在试点过程当中遇到的问题,或者业务同窗提供的一些好的想法和建议,咱们可以快速地进行迭代与落地。而后再不断地扩大试点范围,包括美团外卖、猫眼、酒旅、金融等几个大的 BG 都在 Quake 上进行了几轮全流程、大规模的全链路压测。
随着 Quake 总体功能趋于完善,同时解决了 Ptest(先前的压测系统)上的多个痛点,咱们逐步在各个业务线进行了全面推广和内部培训。从目前收集的数据看,美团超过 90% 的业务已从 Ptest 迁移到了 Quake 。并且总体的统计数据,也比 Ptest 有了明显的提高。
Quake 目标是打造全链路的压测平台,可是在平台建设这件事上,咱们并无刻意去追求。公司内部也有部分团队走的比较靠前,他们也作一些不少“试水性”的工做。这其实也是一件好事,若是全部事情都依托平台来完成,就会面临作不完的需求,并且不少事情放在平台层面,也可能无解。
同时,Quake 也提供了不少 API 供其余平台进行接入,一些业务高度定制化的工做,就由业务平台独自去完成。平台仅提供基础的能力和数据支持,咱们团队把核心精力聚焦在对平台发展更有价值的事情上。
其实,全链路压测整个项目涉及的团队很是之多,上述提到的不少组件都须要架构团队的支持。在跨团队的合做层面,咱们应该有“共赢”的心态。像 Quake 平台使用的不少监控组件、熔断组件以及性能分析工具,有一些也是兄弟团队刚线上没多久的产品。 Quake 将其集成到平台中,一方面是减小自身重复造轮子;另外一方面也能够帮助兄弟团队推进产品的研发工做。
耿杰,美团点评高级工程师。2017年加入美团点评,前后负责全链路压测项目和 MagicDB 数据库代理项目,目前主要负责这两个项目的总体研发和推广工做,致力于提高公司的总体研发效率与研发质量。
团队长期招聘 Java、Go、算法、AI 等技术方向的工程师,Base 北京、上海,欢迎有兴趣的同窗投递简历到gengjie02@meituan.com。