1. 背景及需求
1.1 系统架构的发展
很久很久以前,在并发量较低时系统大多采用单体架构,由单个web服务直接连接数据库,由nginx在多个web服务节点间做负载均衡。
单体架构
随着系统并发量的提高,单体架构无法满足性能需求,需要对单体服务进行拆分,于是来到了微服务架构。微服务架构与单体架构显著的不同在于链路更长更复杂了。
微服务架构
1.2 测试环境的需求
测试环境与线上环境的典型不同在于线上环境各节点一般情况下代码是一致的,各节点是对等的,请求到达任意节点业务逻辑都是一致的;而测试环境一般是多分支并行开发,每个节点的逻辑是不一致的,既然要测试本次所开发的功能,那就要求请求能精准地到达所部署的节点。
在单体架构下,该需求是很容易实现的,通过采用修改nginx配置,在upstream中仅包含目标测试节点可以很容易实现对目标节点的精准测试,或者不使用nginx直接通过IP+port的形式进行调用也同样可以实现请求精准到达目标节点。如下图所示的A'及A''属于两个不同的需求,分别通过固定nginx upstream及IP+port的形式实现了请求的精准控制。
单体架构测试
而在微服务架构下,该需求实现起来就没那么简单了。如下图所示,链路上包括服务A、B、C,图中A'、B'、C'属于同一个需求,而A''、B''、C''属于另一个需求。采用在单体架构下的解决方案只能控制请求精准地到达A'或者A'',无法实现请求精准地到达B'、C'及B''、C''。
微服务架构测试
2. 传统的测试环境解决方案-物理隔离
为了实现请求精准地到达被测服务节点,传统的做法是提供多套完全互相隔离的测试环境,每套测试环境包含全量的服务及注册中心、MQ broker等。每个需求分配一套测试环境,只需将被测服务部署至该环境内即可实现请求精准地到达被测节点,这种做法也称作物理隔离。
物理隔离
物理隔离在公司服务数量较少时(20个服务以下)称得上是完美的解决方案,非常简单可靠。但是随着服务数量的增长,物理隔离的缺点逐渐显露——资源浪费太严重。假设整个系统有100个服务,而每次需求仅修改其中的1-3个服务,资源浪费率高达97%-99%。而且服务数量的增长,往往也意味着公司业务的发展壮大,同时进行中的需求数量也在增长,需要提供更多的测试环境,资源消耗巨大。
3. 转转测试环境V1-改进的物理隔离
转转公司传统的测试环境解决方案属于改进型的物理隔离。
3.1 稳定环境
在转转有一套稳定环境包含全量的服务并且与线上代码一致。在测试环境不使用注册中心,而是为每一个服务分配唯一的域名并通过修改机器host映射的方式手动控制服务发现。默认情况下,如果服务A部署在稳定环境的192.168.1.1
上,那么所有测试环境机器的host文件中都存在一项配置192.168.1.1 A.zhuaninc.com
。
3.2 动态环境
每次需求所申请的环境称为动态环境,为一台kvm虚拟机。假设某动态环境的IP为192.168.2.1,在该动态环境部署服务A时,同时将该机器的host文件中192.168.1.1 A.zhuaninc.com(假设稳定环境服务A的IP为192.68.1.1)映射,修改为127.0.0.1 A.zhuaninc.com。
如下图所示的动态环境192.168.4.1,其中A'及E'即为本次需求所修改的服务。
初始化过程如下:
- 申请动态环境192.168.4.1,申请完成后该机器的host文件中包含全量的服务域名映射,默认映射到对应的稳定环境所在的IP,该例中仅展示了F服务的域名映射为192.168.5.1 F.zhuaninc.com (假设稳定环境服务F的IP为192.168.5.1)。
- 部署服务E',同时将127.0.0.1写入host文件。
- 部署服务D,同时将127.0.0.1写入host文件。
- 部署服务C,同时将127.0.0.1写入host文件。
- 部署服务B,同时将127.0.0.1写入host文件。
- 部署服务A',同时将127.0.0.1写入host文件。
- 部署Entry。
- 部署nginx,同时将服务A的upstream修改为仅包含127.0.0.1。
如此以来,就像焊接管道一样,从服务E至nginx倒序焊接出一条没有分叉的管道。测试时只需要通过host映射将被测域名映射到该IP,即可实现请求精准地到达A'及E',E'以后的链路(从F开始的链路)则使用稳定环境。
对于MQ,则通过在动态环境topic添加IP前缀的方式实现与稳定环境的物理隔离,如下图所示不同的topic在broker上存在着不同的队列,稳定环境的消息和动态环境的消息不能互通。
MQ消息物理隔离
如在测试过程中发现需要修改更多的服务,例如需要修改服务F,则在将F服务部署在该动态环境的同时,需要重启服务E',因为E'已经与稳定环境的F建了tcp连接,修改F服务的域名映射并不会导致E'与F之间重新建立tcp连接,所以需要重启E'。如F服务的调用方不仅有E',则都需要重启。
3.3 优缺点
该解决方案近似于物理隔离,但较物理隔离有所改进,改进的地方在于动态环境并没有包含全量的服务,动态环境仅包含了从nginx开始至最后一个被测的服务,而使用稳定环境充当被测链路的尾巴。
3.3.1 优点
- 隔离性强,近似于物理隔离。
- 链路简单,流量封闭在同一台机器上。
3.3.2 缺点
- 需要部署从nginx开始至最后一个被测的服务,存在未修改服务部署在动态环境中的情况,造成资源浪费。
- 由于部署过程依赖服务的调用关系,导致部署效率低下,极端情况下需要数天调试测试环境。
- host管理复杂。
- topic添加IP前缀易出错,代码与环境相关。
- 单台机器内存有限,链路过长无法满足。
4. 转转测试环境V2-基于自动IP标签的流量路由
随着转转公司业务的飞速发展,服务数量迅速增加,每个动态环境部署的服务数量增至30-60个,搭建测试环境的成本越来越高。为了解决该问题,转转架构部、运维部、工程效率部推出了流量路由解决方案。该解决方案在此前的公众号文章转转测试环境的服务治理实践中已有详细讲解,本文仅做简要介绍。
该版本的流量路由技术称为基于自动IP标签的流量路由,之所以选择IP为标签是因为基于现有使用习惯发现所有被测服务部署在同一台虚拟机上,IP地址一样,并且IP易获取,用户无感知;“自动”则体现在无需用户对服务及流量进行打标,打标是完全自动化进行的。
基于自动IP标签的流量路由将测试环境的搭建时间从数小时-数天减少至30分钟-1小时,每环境部署的服务数量从30-60个服务下降至个位数,并且完全兼容没有流量路由时的使用习惯。但是仍然存在申请虚拟机耗时长,kvm内存无法扩容的问题。
5. 转转测试环境V3-基于手动标签的流量路由
基于自动IP标签的流量路由解决了转转测试环境存在的大部分问题,使测试环境的搭建时间从数小时-数天下降至30分钟-1小时,但是仍然存在申请环境耗时长,kvm内存无法扩容的问题,本着精益求精,用户至上的原则,我们开发了基于手动标签的流量路由。
5.1 docker化
为了解决申请环境耗时长,kvm内存无法扩容的问题,我们决定将服务部署在docker容器内,无需提前申请资源,理论上无内存限制。但是docker化以后,基于IP为标签的流量路由显然无法工作了,因为不同的服务部署在不同的docker pod内,IP也是不一样的。
5.2 服务及流量打标
此时就需要手动为服务及流量指定标签,我们称为基于手动标签的流量路由。为服务打标是自动化完成的,在环境平台申请环境时指定标签,在该环境内部署服务时,由环境平台自动添加jvm参数-Dtag=xxx。而为流量打标则通过http header进行,在发起请求时添加header tag=xxx。
申请环境
自动添加jvm参数
请求添加http header
并非所有请求都是通过http发起的,有些由进程内部(如定时任务)直接发起的调用则自动携带当前节点的标签。
5.3 目标形态
基于手动打标的流量路由目标形态如下图所示,标签为yyy
的动态环境,本次需求修改了服务B及服务D,只需要在该动态环境内部署B和D,真正实现修改什么就部署什么。
标签路由使用目标
5.4 RPC调用实现
5.4.1 服务注册、发现及调用
以下图服务A调用服务B为例,服务B有3个节点,稳定环境的B,动态环境的B'(标签为yyy
)及B''(标签为xxx
)。B'和B''在启动时会将标签参数注册到注册中心,服务A在启动时会从注册中心发现B、B'、B'',同时获取它们的标签参数。
RPC标签路由
以红色的链路为例,流量标签为zzz,A在调用时发现并没有动态环境的服务B拥有标签zzz,于是调用稳定环境的B节点。
橙色链路的标签为xxx,A在调用时发现B''的标签为xxx,且为动态环境,则调用B''。
转转使用的RPC为自研RPC框架,在调用时除了传递请求方法及参数等信息之外,还可以通过attachement功能传递额外的参数,而路由标签就是通过该功能在RPC调用时实现传递。
5.5 MQ消息实现
5.5.1 消费原理
若想实现消息可以在动态环境与稳定环境之间路由,通过不同的的topic前缀实现动态环境和稳定环境的物理隔离显然已经行不通。此时的解决方案为动态环境和稳定环境拥有相同的topic,但是不同的消费group,不同的消费group就对应不同的消费offset。动态环境的group添加${tag}前缀,而稳定环境的group添加test_前缀,添加前缀的过程由MQ客户端自动完成,对用户透明。
如下图所示服务B有稳定环境及动态环境(B')节点各一个,B'的标签为xxx。图中通过不同的颜色表示不同的标签,其中绿色表示没有标签。
MQ标签路由
B节点在消费时,首先判断是否有和消息中标签对应的消费组注册到broker上,如果有则过滤掉,否则消费。其中消息1、3、5、7的标签和B'匹配,消息2没有标签,而消息4、6的标签所对应的消费者没有注册到broker上,所以B节点消费了消息2、4、6。
B'节点在消费时,只消费消息中标签和自身标签前缀一致的消息,即消息1、3、5。
5.5.2 存在的问题
假如此时B'下线了,我们发现消息7没有被消费者消费,该消息丢了。这是因为稳定环境消费组和动态环境消费组offset不一致导致的,稳定环境消费组offset大于动态环境消费组offset,解决方案就是B'下线时将其与稳定环境offset之间的消息重新投递。重新投递后假如B'又上线了呢,就带来了消息的重复消费,但是我们认为重复消费是没有问题的,因为mq本身的消费语义就是至少一次,而不是仅仅一次,重复消费幂等性应该由业务逻辑来保证。
另一个问题是批量消费如何解决,mq客户端有批量消费功能,一批消息所携带的标签可能是不一样的。对于这个问题,只需要将消息按标签分组后再执行消费逻辑即可。
5.5.3 标签的传递
转转使用的是RocketMQ,并进行了二次开发,RocketMQ提供了可扩展的header可以用来传递路由标签。
5.6 进程内标签的传递
跨进程的标签传递相对来讲比较容易解决,而进程内标签的传递难度更高。
5.6.1 通过方法参数传递
通过为每一个方法调用添加tag参数可以实现标签的的进程内传递,但是显然这不现实,需要全公司所有的代码配合改动。
5.6.2 通过ThreadLocal传递
jdk内置有ThreadLocal及InheritableThreadLocal可以实现标签的隐式传递,但是ThreadLocal无法实现new Thread及跨线程池传递,而InheritableThreadLocal可以实现new Thread传递却仍然无法实现跨线程池传递。虽然可以通过对Callable和Runnable进行包装实现跨线程池传递,但这仍然要求修改现有的业务代码,成本较高。
5.5.3 通过TransmittableThreadLocal传递
TransmittableThreadLocal[1]是阿里巴巴开源的,通过java agent技术实现的可以跨线程/跨线程池传递的ThreadLocal
,对用户透明,只需要在jvm启动参数中加入对应的java agent参数即可,最终我们采用了该方案。
TransmittableThreadLocal Agent
5.6 上线收益
下图为转转公司动态环境平均部署服务数量曲线,在2022年5月之前平均每环境约部署7-8个服务,在2022年5月之后标签路由开始推广,至2022年7月每环境约部署3-4个服务。虽然从原理上看基于手动标签的流量路由每环境仅比基于自动IP标签的流量路由仅少部署一个服务(Entry),但是由于kvm无法扩容,所以在自动IP路由时代环境初始化时用户总是会预防性地多选择一些服务,以达到申请更大内存虚拟机的目的,而docker化之后,此种担忧不再存在,所以每环境部署的服务数量下降了约4个,动态环境总内存节省了约65%。
测试环境搭建时间从30分钟-1小时下降至2分钟-5分钟,时间的节省主要来自无需提前申请kvm、docker资源隔离服务启动快、无内存扩容担忧初始化服务数量少。
每环境部署服数量曲线
5.7 辅助设施
docker化虽然带来了效率的提升,资源占用的下降,但是也引入了一些问题。每次部署IP地址都会变化,导致远程登录及debug的成本增加。在Http Header中添加路由标签增加了测试同学的工作量。为了解决这些问题,我们又开发了对应的辅助设施来提高工作效率。
5.7.1 泛域名解析
使用http传递标签,仍然需要配置host映射将域名映射至测试环境的nginx,如192.168.1.1 app.zhuanzhuan.com,否则dns解析会将app.zhuanzhuan.com解析至线上nginx。
是否可以免去host配置呢,答案是可以的。通过直接使用域名传递标签的形式来实现免去host配置,如app.zhuanzhuan.com直接使用域名传递标签写作app-${tag}.test.zhuanzhuan.com,该域名会直接解析至测试环境nginx。在开发版app中内置了该功能,在app启动时输入环境标签即可实现域名的切换,无需配置host即可开始测试。
泛域名解析
5.7.2 web shell
docker化以后每次部署都是一个新的docker pod,IP地址也随之变化,如果需要登录查看日志,通过xshell等工具则需要在每次部署后使用新的IP重新登录。引入web shell功能只需要在环境平台页面中点击按钮即可通过web shell的方式直接登录,并且登录后的工作目录默认为该服务的日志目录。
web shell
5.7.3 debug插件
同样因为docker化以后IP地址总是变化,测试环境的远程debug也变得更加不方便,每次需要更换新的IP进行连接。为了解决此问题,我们开发了debug插件,该插件会自动获取项目名作为服务名,需要debug时输入环境标签,插件会自动向环境平台发起请求,而环境平台则通过解析jvm参数的方式获取debug端口并向插件返回IP和debug端口,插件在收到IP和debug端口后自动连接。
debug插件
5.8 优缺点
5.8.1 优点
- 更加节省资源,仅部署X(修改的服务),不需要部署nginx+Entry。
- 申请环境速度快,秒级完成。
- 搭建环境速度快,约2-5分钟。
- 无内存限制。
5.8.2 缺点
- QA有感知,需要在HTTP Header中添加标签。
6. 分布式调用跟踪系统
以下图为例,在D调用E'时出现问题,E'中未打出相应的业务日志,到底是D没有调用呢,还是流量路由存在问题没有路由到E'呢。此时就需要分布式调用跟踪系统的辅助来排查问题。
6.1 原理
分布式调用跟踪系统的原理就是在链路中每个模块的入口和出口处进行埋点,并将埋点采集起来进行可视化展示。如下左图为调用链路,右图为采集到的埋点。
分布式调用跟踪系统原理
每一条链路有唯一的Id称为TraceId,而每一个埋点称之为span,每个span有唯一的Id称为SpanId。
6.2 架构
转转分布式调用跟踪系统采用自研与开源结合的方式,如下图所示。其中Radar为转转自研分布式调用跟踪系统客户端,可与MQ、SCF(转转RPC框架)、Servlet进行整合,异步批量地将埋点上传到Collector服务,Collector服务再将埋点写入kafka。开源部分则使用zipkin实现,zipkin具备自动从kafka消费埋点并存入DB的能力,而且自带UI界面可供查询。
分布式调用跟踪系统架构图
6.3 TraceId的获取
转转使用统一日志门面slf4j,Radar客户端自动将TraceId、SpanId存入MDCContext中,只需在日志配置文件中加入相应的占位符就可以将TraceId、SpanId打印至日志中,如下图所示。
TraceId打印到日志中
在Entry层每次请求结束后将TraceId以Http Header的形式返回至前端,前端收到响应后可立即获取TraceId进行查询。
网关层将TraceId返回
6.4 在路由关键节点采集流量标签和当前节点标签
如下图所示global.route.context.tag为流量标签,而global.route.instance.tag为当前节点标签,通过对比这两个标签是否匹配即可验证流量路由是否正确,本节开头所提到的问题也就迎刃而解。
流量标签及节点标签
7. 总结
转转测试环境治理共经历3个版本,物理隔离、基于自动IP标签的流量路由及基于手动标签的流量路由。
- 物理隔离:随着转转业务的发展,服务数量的增多,搭建测试环境极端情况下需要数天的时间,每环境平均部署服务数量高达30-60个。
- 基于自动IP标签的流量路由:每环境平均部署服务数量下降至7-8个,而环境搭建时间也下降至30分钟-1小时。
- 基于手动标签的流量路由:每环境平均部署服务数量进一步下降至3-4个,搭建时间降至2分钟-5分钟。
流量路由带来效率提升及资源占用下降的同时,也引入了一些问题,如链路复杂性高,ip地址变化等。为了更好地利用流量路由带来的便利,消除负面影响,就需要各种配套设施的辅助,如分布式调用跟踪、泛域名解析、web shell等。
回头来看,流量路由减少了部署时间,降低了资源消耗,得到了业务线的一致好评。架构、运维与工程效率部门的同学排查问题的数量也大大减少。真真正正做到了降本增效,实实在在好项目。两个版本的流量路由分别获得转转公司优秀项目奖。
关于作者
王建新,转转架构部服务治理负责人,主要负责服务治理、RPC框架、分布式调用跟踪、监控系统等。爱技术、爱学习,欢迎联系交流。
参考资料
[1]TransmittableThreadLocal: https://github.com/alibaba/transmittable-thread-local