全链路压测的方案以及实践 | IT杂货铺
全链路压测是在真实的业务场景,线上的系统环境,发送海量的请求来测试整个核心链路的过程。这个概念在此之前完全没有听说过,问过一些做开发测试的同学,他们对这块还有所了解。这种方案一般大厂里面实践的比较多,小厂的测试完全没有必要这么做。因为这种方案主要是用来测试即将到来的大促等高并发场景,提前验证线上应用的稳定性,及时发现线上应用的性能瓶颈。
方案架构
全链路压测的方案是领导设计的,自己参与了核心代码的编写工作,凭借着记忆与理解画出了下面的架构图,方便读者理解。
全链路压测主要的代码都在解决流量压测标的透传问题,如果压测标无法从上游传递到下游,下游就无法判断流量类型。涉及到流量标透传的相关组件主要有线程池、日志、HTTP、RPC、数据库、缓存、消息队列、外部服务等。上述相关的组件与公司项目的架构有很大的关联,不同的公司选择的技术栈也不同,下面会详细讲解这些组件是如何解决流量标透传问题的。
方案实践
HTTP 请求
一般对外的接口都是使用 HTTP 的方式暴露的,HTTP 是一个比较通用的协议,一般我们会通过 Header 的方式传递额外的信息。例如 key 是“_xxx_context”,value 是你需要携带的数据,可以是普通的字符串,也可以传输 json,但是要注意控制 value 的长度,因为 web server 有限制 header 的长度,具体规则可以参看该 网页的内容。
线程池
线程池是日常开发中经常会用到的技术,特别是异步处理的任务。对于线程间数据的隔离 JDK 给我们提供的 API 是 ThreadLocal,但 ThreadLocal 并不能在父子线程中传递数据,当然有人会说可以使用 InheritableThreadLocal,没错 InheritableThreadLocal 确实可以实现父子线程上下文的传递,但对于线程复用的场景(线程池),子线程对应的父线程会不断变更,然而 InheritableThreadLocal 在子线程中不会同步进行更新操作。那么该如何解决这个问题呢?对于业界的 Java 大厂阿里,它们肯定也会遇到这个问题,所以可以使用阿里开源的 transmittable-thread-local,这个框架可以解决线程复用场景下上下文传递的问题。
代码织入
代码织入总体来说就是增强你代码的功能,为什么要使用代码织入技术,主要还是减少业务方代码改造的成本。例如上面的线程池问题,总不能让业务方自己去改造线程相关的代码吧,这样成本太高,利用代码织入技术可以有效减少改造量。代码增强总的来说有两种途径,一种是 JavaAgent,另外一种是 Java 的动态代理技术,这两种技术显著的差异就是对业务代码侵入性的程度。对于前者优点是依赖较少、功能强大,缺点是技术比较冷门,需要丰富的经验,最终我们选择的是 JavaAgent 这种方式,具体的技术使用的是 AspectJ。一开始没有接触过 AspectJ,使用过程中也遇到一些坑,例如无法增强 JDK 的源码,所以像线程池这类代码你只能从外部调用处考虑切点,还有 AspectJ 不要增强自身需要使用的一些代码,例如我想增强线程池,但是自身又使用了 log4j2 异步日志的功能,这样就会死循环的状态,所以使用 AspectJ 的时候一定要注意选择好切点,避免出现无效或者死循环的问题。
数据库
数据库是业务应用基本上都会接入的组件,如果处理不好,很容易出现数据混乱的问题。针对全链路压测,业界的选择是构造影子库或者影子表。为了节省资源,我们选择的是影子表的方案,因为这种不用再申请额外的数据库,在同一个数据库中创建所有表的影子表,影子表在原表名的基础上加上“_shadow”的后缀,当然表结构也要做变动,我们的处理方式就是在原表的基础上再添加一个字段“xxx_SHADOW”,这样影子流量的数据无法插入到正常的表中。表结构构造完成后,需要做数据的迁移,因为影子流量操作的都是影子表,需要将一些字典表类的数据从原表迁移到影子表,对于不影响流程的表可以不做迁移,例如订单表,因为这类表的数据量往往非常大,迁移起来很耗时。
数据库涉及到的另一个组件就是 ORM 框架,我们不可能让业务人员自己根据流量类型来修改 SQL,需要在 ORM 框架层做统一的封装。目前国内公司使用的都是 MyBatis 框架,借助 MyBatis 的 Interceptor 可以自行添加 SQL 修改的逻辑。SQL 的修改可以使用阿里开源的 Druid,对于 MySQL 支持的非常好,需要注意的是修改的 SQL 语句要覆盖全业务场景,特别是冷门的 SQL,不要因为使用率低而不考虑进去。
缓存
缓存的种类有很多种,例如本地缓存,分布式缓存。根据使用率我们改造的是 Redis,因为这种缓存最通用。Redis 总共有 256 个 DB,我们可以选择一个 DB 作为影子库,例如最后一个,影子流量的缓存操作都在 DB255 上面进行,对于 Key 也可以做改造,避免出现了问题无法排查的情况,可以在原 Key 的基础上添加前缀“xxx_SHADOW”。因为 Redis 的相关框架没有提供扩展的功能,所以需要使用代码织入技术,将切点选择在 Redis 的调用处。
消息队列和 RPC
对于消息队列和 RPC 框架的处理方式与 HTTP 的方案类似,如果是消息队列,例如 RabbitMQ、RocketMQ,可以在请求的 Header 中附带流量标,对于 RPC,例如 Dubbo 可以利用 Filter 在请求的上下文中附带流量标。
日志
压测一般会发出大量的请求,相应的日志也会非常多,压测流量的日志很容易覆盖正常流量的日志。所以我们选择改写 log4j2 的 appender,根据流量类型将日志写入影子目录中,并且配置默认的删除策略,例如日志只保存一天。
第三方接口
对于第三方接口,例如支付,我们不可能将压测流量的请求打过去,所以需要改写请求的逻辑,将 Mock 的逻辑保存在 Mock 平台上面,业务人员根据流量类型选择走真实的逻辑还是 Mock 的逻辑。至于 Mock 相关的代码处理,目前是让业务人员自行编码处理的,如果 Mock 的接口非常多,这样做肯定不可取,而且不利于维护,可以考虑使用 AOP 技术进行统一封装。至于 Mock 类型,可以根据业务场景决定,比较通用的类型例如成功请求的 Mock,失败请求的 Mock,限流请求的 Mock 等。
监控
做全链路压测,监控是非常重要的,要能追溯到每个请求的调用链,这样除了问题才方便排查。我们选择的监控是 Cat,当然还有自研的 APM,对于 Cat 可以使用它的 API 自行埋点,非常方便,如果没有自研的 APM ,使用 Cat 是一个不错的选择。