您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
京东物流全链路压测技术解密
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
京东物流全链路压测技术解密
自猿其说Tech
2022-04-11
IP归属:未知
19920浏览
测试
### 1 全链路压测背景简介 为了降低京东单位订单资源成本,京东集团启动将持续四年(2020-2023)的泰山项目。高保真压测是泰山项目的项目方向及规划之一,为了承接订单后军演压测流量,物流启动全链路压测项目来配合泰山项目进行压测技术改造。经过多轮全链路压测方案的讨论研究、验证、改进,压测流量的全局准确识别、向下流转透传、安全隔离等难点逐个被攻破。协同泰山项目组,在物流内部推动系统改造,以支持订单后的高保真压测流量触达物流各关键系统,以此来验证系统稳定性、评估系统容量、识别链路薄弱点。颠覆以往只有憋单演练一种途径的不足,组织憋单演练需要协调全国各仓作业调整,演练窗口时间短,难以产生足量订单进行验证,同时生鲜、医药等夜间配送的订单可能受憋单影响时效和用户体验,因此订单后的全链路军演压测势在必行。 ### 2 京东物流全链路压测技术方案 #### 2.1 全链路压测方案设计 全链路压测技术方案的核心思路是压测数据隔离。通过对中间件改造,让中间件具备识别和透传压测流量功能,同时选择适用的影子技术持久化压测流量,以达到数据隔离的目的。方案示意图如下图1: <center>![](//img1.jcloudcs.com/developer.jdcloud.com/6970512d-6289-44b8-92ba-3aac43ccaa9f20220411144249.png) 图1 全链路压测技术方案示意图</center> 实现全链路压测核心步骤: 1. 生成带压测标识的压测流量(流量来源:上游带压测标识的流量或者压测脚本生成带压测标识的流量)。 2. 压测标识处理组件识别并透传压测流量,同时保证压测标识在被压测服务间传递不丢失。 3. 选用合适的影子技术,持久化压测流量(与生产存储介质物理或逻辑隔离,风险可控、易于维护)。 #### 2.2 全链路压测压测方案技术实现 物流侧全链路压测改造是基于压测标识透传组件TraceHolder,TraceHolder原理是在线程的transmittableThreadLocals中保存压测标识。业务系统使用的中间件,如JSF、MySQL、Clover、JMQ、Jimdb等都已引入TraceHolder组件。在压测流量源头添加压测标识,流经的各个中间件支持压测标识的识别互认和向下传递,在生产环境形成一条压测的影子链路。TraceHolder API如下: // 开启forcebot标 TraceHolder.setForcebot(); // 获取forcebot标,true、false TraceHolder.isForcebot(); // 移除forcebot标 TraceHolder.removeForcebot(); #### 2.3 全链路压测压测方案技术落地举例——Mysql影子库方案 业务系统使用的中间件,如JSF、MySQL、Clover、JMQ、Jimdb等都已引入TraceHolder组件,本章节以Mysql中间件为例(Mysql提供的shadow-jar已接入TraceHolder),解析压测标识透传和业务系统改造细节,其他中间件技术落地细节大致相同,此文档不再详细讲解。 ###### 1.Mysql影子库实现原理: - 基于spring-jdbc中AbstractRoutingDataSource实现,AbstractRoutingDataSource维护了一个map,key为DataSourceKey,value为DataSource。 - 当TraceHolder.setForcebot()开启后,shadow-jar通过TraceHolder.isForcebot() 判断是否需要切换至影子库,ShadowDynamicDataSource类重写了determineCurrentLookupKey。 ###### 2.Mysql影子库实现示意图,如下图2。 <center>![](//img1.jcloudcs.com/developer.jdcloud.com/e59e6692-1f55-4447-9aaa-fc128fda820520220411144421.png) 图2 Mysql压测标识透传示意图</center> 判断用户请求中的本地变量的压测标识值是否为true,如果为true,则为压测流量,流量自动路由到对应的影子库。如果为false,则为生产流量,流量自动路由到生产库。 ##### 3.核心源码 shadow-jar中重写determineCurrentLookupKey方法,当压测标识开启时,可以获取对应影子库的dataSourceKey。 ``` public class ShadowDynamicDataSource extends AbstractRoutingDataSource { public ShadowDynamicDataSource() { } public Object determineCurrentLookupKey() { // 未开启影子库 直接返回 if (!ShadowDataSourceUtils.checkShadowEnable()) { return null; } else { // 开启了影子库 DataSourceKey拼接shadow String dataSourceKey = ShadowDataSourceUtils.getShadowDataSourceKey(); return dataSourceKey; } } // 判断是否为压测流量 public static boolean checkShadowEnable() { return TraceHolder.isForcebot(); } } ``` 备注:此处只精简了部分核心代码,想了解其他剩余的源码,请自行查看shadow-jar源码。 ###### 4.Mysql全链路压测改造案例 - 项目引入shadow-jar - 新增影子数据源dataSourceMysqlOrderJproxyshadow ``` <bean id="dataSourceMysqlOrderJproxyshadow" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="shadowUrl" value="#{dburlProps['presort.mysql.order.shadowUrl']}"/> <property name="username" value="${presort.mysql.order.username}" /> <property name="password" value="${presort.mysql.order.password}" /> <property name="defaultAutoCommit" value="true"/> <property name="maxActive" value="#{dburlProps['presort.mysql.order.maxActive']}"/> <property name="maxIdle" value="#{dburlProps['presort.mysql.order.maxIdle']}"/> <property name="maxWait" value="#{dburlProps['presort.mysql.order.maxWait']}"/> <property name="initialSize" value="#{dburlProps['presort.mysql.order.initialSize']}"/> <property name="minIdle" value="#{dburlProps['presort.mysql.order.minIdle']}"/> <property name="minEvictableIdleTimeMillis" value="#{dburlProps['presort.mysql.order.minEvictableIdleTimeMillis']}"/> <property name="timeBetweenEvictionRunsMillis" value="#{dburlProps['presort.mysql.order.timeBetweenEvictionRunsMillis']}"/> <property name="poolPreparedStatements" value="false" /> <property name="removeAbandoned" value="true"/> <property name="removeAbandonedTimeout" value="120"/> <property name="logAbandoned" value="true"/> </bean> ``` - 为targetDataSources注入生产和影子两个数据源 ``` <bean id="dataSource" class="com.jd.jdbc.shadow.routing.ShadowDynamicDataSource"> <!-- 为targetDataSources注入两个数据源 --> <property name="targetDataSources"> <map key-type="java.lang.String"> <entry key="master" value-ref="dataSourceMysqlOrderJproxy"/> <entry key="mastershadow" value-ref="dataSourceMysqlOrderJproxyshadow"/> </map> </property> <!-- 为指定数据源RoutingDataSource注入默认的数据源--> <property name="defaultTargetDataSource" ref="dataSourceMysqlOrderJproxy"/> </bean> ``` - sqlSessionFactory配置 ``` <bean id="mysqlOrderSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="configLocation" value="classpath:mybatis-config-mysql-order.xml" /> <property name="dataSource" ref="dataSource" /> </bean> ``` - 事务配置 ``` <bean id="transactionManagerMysqlOrder" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> ``` - 变量配置获取影子库标识: shadowdb.url=jdbc:mysql://XXXX&statementInterceptors=com.jd.jdbc.shadow.interceptor.ShadowInterceptor - 备注:其中XXXX为原始配置 ### 3 全链路压测技术收益 1.** 从单点容量评估到链路容量评估:**随着系统架构的演变,微服务分布式系统架构的广泛使用。服务间组合依赖的关系日益复杂,单独压测每个系统,或者局部的系统,很难对整体业务系统的性能作出准确的评估。全链路压测可以更好的暴露出链路上的薄弱点,可以通过量化的监控指标,为调节节点间的比例提供数据参考,更准确的进行整体容量评估。 2. **从线下压测到线上高保真压测:**虽然我们以前每次大促前会在线下搭建与线上等比例的压测环境,但是仍会因为中间件或者依赖的外围系统不稳定而产生不同程度的失真。全链路压测具有识别压测流量和线上流量的能力,因此可以在线上系统进行压测,通过模拟线上大促同级别的流量,验证系统的性能,可以很大程度上降低因环境不同而带来的失真。因此线上全链路压测,可以更接近大促的实际场景,为大促稳定提供更好的保障。 3. **从单一到多样的大促军演:**物流侧以往军演备战通过憋单来实现全链路压测,但是憋单有明显的不足,憋单时间窗口短难以产生足量订单模拟大促流量洪峰,并且影响医疗、生鲜等门店商品配送时效。全链路高保真压测通过工具生成与线上相似的压测流量,向生产环境施加大促同等或几倍于大促量级的单量洪峰,弥补了以往只有憋单演练一种途径的不足。 4. **从大量业务代码改造到统一中间件升级:**以往物流侧需要修改大量业务代码才能实现压测标识识别和透传,给技术人员增加大量的开发和验证工作,扩大了项目风险,并且大量的冗余代码为项目后期升级和维护带来很大挑战。针对上述问题,系统基于的各中间件统一升级改造支持压测标识互认,业务代码无需改动或只需少量的改动,就可以完成对全链路压测的支持,极大的降低了系统改造的难度和风险。 ### 4 全链路压测技术解密 #### 4.1 压测标识透传互认解密——JDK自带的ThreadLocal技术 ##### 4.1.1 ThreadLocal存储结构 压测标识需要在整个调用链中进行传递,线程上下文环境成为解决这个问题最合适的技术,所以采用ThreadLocal技术用来存储在整个调用链中都需要访问的数据(压测标识)。ThreadLocal存储结构示意图如下图3: <center>![](//img1.jcloudcs.com/developer.jdcloud.com/7b42b9e9-7d17-48f7-bc59-7c0d834067d020220411145104.png) 图3 ThreadLocal存储结构示意图</center> 1. 当线程调用threadLocal对象的set(Object value)方法时,数据并不是存储在ThreadLocal对象中,而是存储在Thread对象中,这也是ThreadLocal的由来,具体存储在线程对象的threadLocals属性中,其类型为ThreadLocal.ThreadLocalMap。 2. ThreadLocal.ThreadLocalMap为Map结构,即键值对,键为threadLocal对象,值为需要存储到线程上下文的值(threadLocal#set)方法的参数。 ThreadLocal存储结构总结:当前线程Thread-->获取当前线程维护的ThreadLocalMap对象-->这个对象里面有一个Entry []数组,key是ThreadLocal对象,value是值。 ##### 4.1.2 ThreadLocal技术要解决的典型业务场景 1)解决服务内部压测标识透传透传互认(要保证压测标识在服务内部的异步执行的线程和线程池中不丢失,不混乱),如下图4,在服务A的内部创建异步执行的线程或者线程时,压测标识可以在线程间正确透传。 <center>![](//img1.jcloudcs.com/developer.jdcloud.com/39dc4b06-f441-4439-a620-c55554a049c220220411145204.png) 图4 典型业务场景示意图——服务内透传</center> - 上游流量打标压测标识forceBot:1(可以是压测发起的压测流量,也可以是上游服务下发的压测流量)。 - 服务A将forceBot:1存入线程池本地变量。 - 服务A 通过new Thread()创建一个子线程,需要保证压测标识可以透传给子线程。 - 服务A 通过new ThreadpoolExecutor()创建一个线程池,需要保证压测标识可以正确透传给线程池中的线程。 2)解决服务间的压测标识透传透传互认(要保证压测标识在服务间不丢失,不混乱),如下图5,在服务A和服务B之间压测标识可以正确透传。 <center>![](//img1.jcloudcs.com/developer.jdcloud.com/10a1c1d8-2671-49e7-9e5c-893f7d64f9f920220411145252.png) 图5 典型业务场景示意图——服务间透传</center> - 上游流量打标压测标识forceBot:1(可以是压测发起的压测流量,也可以是上游服务下发的压测流量)。 - 服务A讲forceBot:1存入线程池本地变量。 - 服务A 通过new Thread()创建一个子线程,需要保证压测标识可以透传给子线程,服务A 通过new ThreadpoolExecutor()创建一个线程池,需要保证压测标识可以正确透传给线程池中的线程,服务A内部调用了JMQ、Mysql、JiMdb等中间件,需要保证压测标识可以在这些中间件之间透传。 - 服务A通过RPC协议调用服务B,需要保证压测标识可以正常透传给服务B,服务B需要保证压测标识可以在服务B内部所有的组件内正确透传互认。 JDK自带的ThreadLocal局限性:JDK的自带的ThreadLocal和InheritableThreadLocal技术只能解决父子线程间透传压测标识,不能解决线程池内透传压测标识(ThreadLocal局限性:线程本地变量无法在父子线程之间传递。InheritableThreadLocal局限性:子线程可以访问父线程中的线程本地变量,但是如果业务逻辑中使用线程池技术时,子线程访问的本地变量可能都来源于第一个外部线程,造成线程本地变量混乱。)。所以需要解决线程池内透传本地变量,需要更先进的ThreadLocal技术(TransmittableThreadLocal)。 #### 4.2 压测标识透传互认解密——开源的ThreadLocal技术 为了解决InheritableThreadLocal的局限性,实现线程本地变量在线程池的执行过程中,能正常的访问父线程设置的线程本地变量,业内知名公司开源了技术组件TransmittableThreadLocal,可以解决InheritableThreadLocal的局限性,实现了本地变量可以在线程池中正常透传。TransmittableThreadLocal调用时序图如下图6(官方提供)。 <center>![](//img1.jcloudcs.com/developer.jdcloud.com/49372e9d-7264-4c22-bee9-c45f73e600af20220411145350.png) 图6 TransmittableThreadLocal内部调用示意图</center> InheritableThreadLocal不支持线程池的根本原因是InheritableThreadLocal是在父线程创建子线程时复制的,由于线程池的复用机制,“子线程”只会复制一次。要支持线程池中能访问提交任务线程的本地变量,其实只需要在父线程向线程池提交任务时复制父线程的上下文环境,那在子线程中就能够如愿访问到父线程中的本地变量,实现本地环境变量在线程池调用中的透传,从而为实现链路跟踪打下坚实的基础,这也就是TransmittableThreadLocal最本质的实现原理。 #### 4.3 压测标识透传互认解密——京东压测标识透传组件TraceHolder 京东压测标识透传组件TraceHolder,由京东零售-技术与数据中心-技术平台部提供,TraceHolder组件引用了TransmittableThreadLocal技术组件,能保证压测标识在线程池中正常透传,同时京东内部常用的中间件都接入了TraceHolder组件(JSF、JMQ、JIMDB、Mysql、Clover),保证压测标识在中间件之间可以透传互认,TraceHolder组件核心代码如下: ```java package com.jd.traceholder; class Context {//@1 private static final ThreadLocal<Context> tl = new TransmittableThreadLocal<Context>() { protected static Context getContext() {//@2 return (Context)tl.get(); } public void set(String key, Object value) {//@3 this.vars.put(key, value); } } ``` 代码@1:使用TransmittableThreadLocal类实例化ThreadLocal对象,确保本地变量可以在线程池内部正常透传。 代码@2:获取本地变量值,(Context)tl.get()方法调用jdk自带的ThreadLocal源码。 代码@3:存入本地变量值,在需要调用的的代码段,可以调用TraceHolder.isForcebot()获取该值。 京东压测标识透传组件TraceHolder解决了全链路压测的两个核心问题,一是解决了压测标识透传的可行性和可靠性;二是所有中间件都接入同一个SDK(压测标识透传互认SDk),降低了中间件之间压测标识互认的成本,同时降低业务代码的改造难度,让全链路压测变的简单。 备注:此处只精简了部分核心代码,想了解其他剩余的源码,请自行查看TraceHolder源码。 ### 5 自研压测标识透传SDK ###### 1.自研SDK流程图 ![](//img1.jcloudcs.com/developer.jdcloud.com/2e175ab4-519d-47de-9b2a-36dc291549cd20220411150107.png) ###### 2.新增StressContext类。 ```java public class StressContext { boolean isForceBot; public boolean isForceBot(){ return isForceBot; } public void setForceBot(boolean isForceBot){ this.isForceBot = isForceBot; } } ``` ###### 3.新增StressContextUtils类,采用TransmittableThreadLocal技术,实现本地变量设置和获取。 ```java public class StressContextUtils { static final ThreadLocal<StressContext> local = new TransmittableThreadLocal<StressContext>(){ @Override protected StressContext initialValue(){ StressContext stressContext = new StressContext(); return stressContext; } }; public static boolean isForceBot(){ return local.get().isForceBot(); } public static void setForceBot(boolean isForceBot){ local.get().setForceBot(isForceBot); } } ``` 备注:本章节演示了自研压测标识透传的SDK的核心源码,如果需要额外的功能,需自行实现。 ### 6 全链路压测案例 #### 6.1 全链路压测案例——写交易,下游JSF应用服务存在性能瓶颈 1)问题场景描述:全链路压测过程中,某系统的某个接口存在TP99飙升,方法可用率下降问题,如下图7、图8。 <center>![](//img1.jcloudcs.com/developer.jdcloud.com/7a2c7803-d340-4dcb-99ca-07e53220280d20220411150228.png) 图7 上游系统TP99飙升图 ![](//img1.jcloudcs.com/developer.jdcloud.com/6276970e-0139-46c4-a8ad-891a9a2ff93220220411150240.png) 图8 上游系统方法可用率下降图</center> 2)问题分析:通过分析该时段的异常日志,发现某个下游服务的接口抛异常:[JSF-23003]Biz thread pool of provider has bean exhausted。 3)解决方案:下游服务通过优化代码和服务器扩容来满足大促的预期吞吐量。 #### 6.2 全链路压测案例——读交易,下游ES中间件服务存在性能瓶颈 1)问题场景描述:全链路压测过程中,某系统的某个接口存在TP99飙升,吞吐量不达预期,被压测接口的TP99监控如下图9。 <center>![](//img1.jcloudcs.com/developer.jdcloud.com/85e85e2d-b297-4e04-b247-55b10dd77e7720220411150341.png) 图9 上游系统TP99飙升图</center> 2)问题分析:通过对整个调用链路上接口的耗时分析,发现下游某个服务提供的查询的接口存在性能瓶颈,存在同步TP99飙升的现象(飙升的原因为ES存在性能瓶颈),下游系统TP99监控如下图10。 <center>![](//img1.jcloudcs.com/developer.jdcloud.com/d8a9cb38-bb8e-4e78-a235-2a19373b9e3f20220411150418.png) 图10 下游系统TP99飙升图</center> 3)解决方案:替换掉ES集群中存在性能短板的ES服务器(为了应对大促流量,下游服务进行了ES扩容,新申请的ES服务器本身存在性能问题)来满足大促的预期吞吐量。 总结:通过模拟大促流量在生产环境进行全链路压测,可以提前发现整个调用链路上潜在的性能问题。如果只是进行单点压测或者线下压测,链路上潜在的性能问题很难被提前发现。 ### 7 总结 全链路压测技术在不同的公司有不同的落地实践,京东从2015起开始探索和实施全链路压测,直到2021的618才真正意义上实现从商城到物流侧的全链路成功实践。本次能成功的主要原因是技术上采用通用的SDK技术(ThreadLocal技术)解决了压测标识透传互认,同时推动中间件团队接入通用的SDK,在中间件维度统一来做压测流量识别和流量隔离。业务线改造只需升级中间件的SDK就可以解决大部分压测数据隔离问题,避免了业务系统改造时,需要大量修改务业务逻辑,从而使全链路压测改造工作变的简单可行。 ------------ ###### 自猿其说Tech-JDL京东物流技术与数据智能部 ###### 作者:张航舰 田年勇(快递技术少年帮)
原创文章,需联系作者,授权转载
上一篇:基于Spring-AOP的自定义分片工具
下一篇:repromise系统资源收敛与核心服务优化实践
相关文章
安全测试之探索windows游戏扫雷
Jmeter压测实战:Jmeter二次开发之JSF采样器实现
Laputa自动化测试框架介绍
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号