一、背景介绍
软件系统中全链路指的是从用户请求发起,到最终返回响应的整个过程中所涉及到的所有环节和组件。在微服务软件架构风格盛行的今天,因为微服务独立部署、松耦合等特性,往往一个业务系统由数目较多的服务组成,较多的服务往往带来一系列操作上的复杂性。
全链路部署指的是将整个软件系统的所有服务一次性部署环境中的一种部署方式,这种部署方式可以简化我们日常发布流程,确保系统的所有服务协同工作。
vivo 的 CICD 发布系统从【构建部署脚本自动化】>【持续集成平台化】>【集成更多 DevOps 功能和组件】,演进到现在更加灵活编辑的多服务编排方式。为了适应最新的微服务软件架构风格,CICD 发布系统通过中间件组件和调用链技术实现的落地,基于容器建立了全链路多版本流水线部署能力。
多版本部署则是基于灰度发布的理念,在同一时间内,将不同版本的服务部署在同一个环境中,使得不同版本的服务可以同时运行和提供服务的一种部署方式。
随着互联网发展增速,软件开发和部署需要快速迭代,软件发布变得越来越频繁和复杂。迭代版本持续交付过程中,多个版本并行基本是所有项目常态化的情况,为此项目团队往往会搭建多套测试环境以备功能验证。多套环境的服务、组件和配置数据维护不仅大量占用研发人员的日常工作时间,环境部署所需资源也占用越来越多的公司硬件成本,即便这样可能也无法完全满足产品研发过程中遇到的紧急修复版本或临时插入的高优需求这样需要占用环境的问题。当前项目的应对措施往往都是让当前常规版本临时“让开”,先让紧急分支版本发布到正在使用的测试环境上,验证通过后再释放环境,让常规版本回归继续测试验证。
我们希望通过全链路多版本部署解决传统测试环境存在的如下几个问题。
二、全链路多版本部署技术方案
2.1 部署架构
上文就全链路多版本部署名称解释和与传统环境区别做了简单介绍,为进一步给大家清晰区分全链路多版本部署的测试环境与传统测试环境区别,下面从部署架构图的角度再次阐述下两者的区别。
传统测试环境就是将业务线上的服务全量部署几套以供使用。传统测试环境使用也比较简单,不同环境通常拥有不同的域名或者同一个域名不同的 hosts 映射,用户通过修改配置直接访问到具体环境上。
在全链路多版本环境中,只有基线环境是将业务线上的服务全量部署,其余特性环境只需拉起需要的个别服务即可。
使用全链路多版本环境时,不同环境访问的域名都是同一个,用户需要通过代理工具添加 Request headers,设置 tc_fd = 环境标识,这样带有标识的请求经过网关时,会根据配置的路由规则转发到指定环境。这里路由规则会由 CICD 平台根据服务编排的组成,自动配置到 HTTP 网关、Dubbo、MQ 等中间件平台上。
如下图黄色箭头所示,带有 tc_fd = 1 标识的请求链路为 service_A_1->service_B_1->service_C->service_D_1。因为特性环境1中不存在 service_C,所以请求流量在 service_C 时回落到基线环境,往下调用时继续路由到 service_D_1 服务,保证了环境的完整性。
想要达成全链路多版本流水线的快速部署,逻辑隔离等特性,需要 CICD 平台把控多服务多版本的统一部署,环境治理和标签管理,容器平台保证业务的弹性伸缩能力,业务的流量灰度由 HTTP 网关和 Dubbo、消息中间件路由策略实现,同时需要配置中心来管理所有服务的配置,以及最重要的底层链路追踪和监控来实现完整的微服务架构。
为了实现全链路多版本部署方案,业务程序遵循微服架构,访问时实现逻辑隔离、将系统的流量划分为不同的通道或环境,每个环境都有其独立的流量,避免它们相互影响是关键的一环。
想要达成全链路多版本流水线的快速部署,逻辑隔离等特性,技术上需要实现如下几点:
流量染色
流量隔离
标签传递
环境管理
2.2 流量染色
接口调用请求时,需要在链路中添加染色标识称作流量染色。针对流量类型不同,服务调用方式不同,可以通过如下几种方式进行染色。
2.2.1 客户端 HTTP 服务调用
浏览器端或者 APP 端发起的 HTTP 请求,用户可以通过本地安装的代理工具拦截 HTTP 请求,再按规则配置注入 tc_fd 流量标识。
推荐的代理工具有Charles 和 Chrome 浏览器插件 ModHeader。
2.2.2 服务端 HTTP 服务调用
如果是对外提供的 REST API 服务,服务调用方请求时不带流量标识,可以在网关层按调用方配置“请求头改写”,实现全局修改。
2.2.3 Dubbo 服务调用
本地服务调试时,Dubbo 消费端可以在上下文中设置标签
RpcContext.getContext().setAttachment("Dubbo.tag","流量标识")。
针对整个消费端服务,也可以通过添加 JVM 参数 -Dvivotag = 流量标识进行全局设置。
2.2.4 分布式任务调用
对应配置在“分布式任务调度平台”基于给定的时间点,给定的时间间隔或者给定执行次数自动执行的任务,平台侧也已支持在调度策略上配置当前策略调度分组,以及是否需要调用时添加多版本流量标识。
2.3 流量隔离
上述介绍了几种流量染色方式,当流量染色后,如何将带有环境标识的流量转发到对应的环境呢。我们目前针对 HTTP、Dubbo、MQ 等几种常见流量类型实现了逻辑隔离方案,实现过程中主要考虑到如下几点要素:
应用侵入性低,减少应用接入成本,由平台自动配置和中间件框架实现隔离逻辑;
支持业务常见流量类型,覆盖大部分业务逻辑;
流量隔离改造需考虑性能问题;
满足特性环境任意扩展的需求,组件支持动态扩缩容。
2.3.1 HTTP 流量隔离
HTTP 流量隔离通过 VUA 网关配置实现,VUA(vivo unity access,公司流量统一接入层)是 vivo 统一接入层,基于 APISIX 的二次开发统一接入平台。通过 VUA 中的 traffic-split 插件可以通过配置 match 和 weighted_upstreams 属性,从而动态地将部分流量引导至各种上游服务。
创建新的流水线后,CICD 发布系统根据新增容器工作负载自动到 VUA 网关上创建 upstream,并且配置按环境标识配置 match 属性,用于引导流量按自定义规则,常见支持的规则有判断 HTTPHeader,pathParam,cookie 参数等。
2.3.2 Dubbo 流量隔离
Dubbo 提供了丰富的流量管控策略,通过基于路由规则的流量管控,可以对每次请求进行条件匹配,并将符合条件的请求路由到特定的地址子集。针对全链路多版本测试环境,我们采取动态配置标签路由规则的方式进行打标,标签主要是指对 Provider 端应用实例的分组,标签路由通过将某一个服务的实例划分到不同的分组,约束具有特定标签的流量只能在指定分组中流转,不同分组为不同的流量场景服务,从而实现流量隔离的目的。
具体做法为由 Dubbo 服务治理平台提供标签新增/删除接口用于动态配置标签路由规则,CICD 发布系统在部署时通过容器 Init Container 特性在实例启动前调用新增 tag 接口打标,完成标签路由规则的自动配置。
2.3.3 MQ 消息隔离
除了应用层 RPC(Remote Procedure Call,远程过程调用)协议的流量隔离,大多数业务场景还会对消息的全链路有一定的诉求。vivo 在线业务侧消息中间件自2022完成了从 RabbitMQ 到 RocketMQ 的平滑升级,目前业务现状仍是使用了 RabbitMQ 的 SDK,由平台侧中间件团队提供 mq-proxy 消息网关组件负责 AMQP 协议与 RocketMQ 协议的相互转换,此为我们公司特殊背景。实现消息隔离的过程分生产者和消费者两部分实现。
生产者在发送消息的时候,通过在 user-property 中加上一些字段将环境标签附带在消息体中,使得消息发送到 RocketMQ server 的时候就包含灰度信息。
消费者客户端 SDK 使用全链路 Agent 将版本标识添加到连接属性当中,启动时根据环境标识,由 mq-proxy 自动创建当前带环境标签的 group,并通过消费订阅的消息属性过滤机制,从 topic 中过滤出来属于自己版本的消息。
2.4 标签传递
以上大概介绍了我们支持的三种组件进行流量、消息隔离的基本实现原理。在多版本环境中,真实的业务链路往往是用户通过 HTTP 请求经过网关访问到 service_A 服务,再由 service_A 服务通过 RPC 接口调用到 service_B 服务,service_B 服务生产消息提供给 service_C 服务消费。整个调用过程中如果用户发起请求时加上了 tc_fd 环境标签,也就是流量被染色,请求头中有特定标识之后,标签需要在调用链路中传递下去叫做标签传递。有了这个标识链路传递,我们再为链路上的所有应用定义流量隔离策略才会生效。
标签传递功能借助分布式链路跟踪系统实现,我司分布式链路跟踪系统简称调用链,主要覆盖开发语言为 Java。调用链的 Agent 模块通过字节码增强技术,使用 Java 探针做到了不侵入业务代码的前提下,对服务的类进行拦截,从而植入一些监控埋点上报或者其他代码。
应用到全链路多版本环境部署功能中来,就是在服务接收到请求时,从报文里获取到标签信息,向下游服务发起新的服务请求时,再将获取到的标签信息设置到指定参数位置。向下游传递时几种调用方式的标签设置方式如下:
HTTP 请求,透传参数以 key-value 形式附加在 HTTPRequest 的 headers 中,支持向上游回传,回传的参数存在于 HTTPResponse 的 headers 中;
Dubbo 调用,透传参数以 key-value 形式附加在 RpcInvocation 的 attachments 中;
RMQ Procuder 发送消息时,透传参数以 key-value 形式附加在消息属性 MessageProperties 的 header 中。
2.5 环境管理
相比之前使用流水线部署传统测试环境,全链路多版本流水线在部署过程中赋予测试环境更多的配置属性,在创建和使用测试环境上更加灵活多变。所以从 CICD 平台建设上,我们需要尽可能的完善平台自动化程度,抹平因为流水线差异导致用户增加使用全链路多版本流水线的操作和理解成本。
2.5.1 基线环境
基线环境作为全链路多版本环境中最基础的环境,是当请求链接不带任何环境标签时默认访问到的环境,基线环境被其他特性环境共享,所以保障基线环境稳定性十分重要。我们在前期推广全链路多版本流水线过程中,为了环境规范化部署,要求业务方接入时需要新建基线环境,且同一服务下基线环境存在唯一性。这样做的好处是环境管理更加规范,坏处却是提高了使用成本,一套服务全都新建基线环境占用大量硬件和人力成本,与推广全链路多版本流水线初衷不符。在吸收用户意见及后续优化后,我们支持了在已有测试环境的基础上进行基线环境改造,以支持其他特性环境的兼容。为了管理基线环境,我们还采取以下措施:
统一环境配置:为了避免不同环境使用不同的基线环境配置,需要统一基线环境配置,以确保不同特性环境使用的基线环境一致。
定期更新和维护基线环境:为保证基线环境稳定,需要减少基线环境发布频率,保证部署代码分支质量稳定。按照项目管理特点,可以配置生产环境部署后触发基线环境部署生产环境代码分支;
监控和报警:对基线环境进行监控,如 CPU、内存、磁盘等资源的使用情况,及时发现问题并进行处理。
2.5.2 特性环境
特性环境是指为了测试和验证某个特性而创建的独立环境,在测试环境场景中与版本属性有关联,每个特性环境有属于自己的环境标签属性,具有快速创建和销毁的特性。特性环境的管理也具备如下几个方面的功能:
标签管理:每个特性环境创建时会自动生成全局唯一的环境标签,或者指定已有的环境标签。
快速创建:快速拉起一套新的特性环境,按既有服务流水线模板编排成多服务环境,实现一键运行构建部署所需的多个服务实例。
环境配置自动化:创建特性环境时,避免创建前申请容器资源,创建后配置路由规则等繁琐操作,具体配置功能尽可能由平台实现自动化。
定时销毁:每个特性环境设置使用生命周期,到期不用后定时清理流水线和容器实例,避免冗余环境长期不用占用资源。
2.5.3 链路监控
现在大规模微服务分布式架构软件模块的背景下,帮助理解系统行为、用于分析性能问题的工具分布式链路跟踪系统应运而生。因为全链路多版本流水线特性环境流量隔离的特性,因为链路问题可能会导致服务调用串环境,链路监控功能就更为重要。目前关于链路监控的功能建设如下:
交互便捷:在流水线页面迁入链路可视化菜单,并按环境标签定位到当前环境数据。
调用拓扑图:通过调用拓扑图展示服务间的调用关系和数据流向,在链路元数据中增加环境标签信息,在链路图形化展示上标记环境信息。
问题排查:HTTP 调用时通过调用链返回当前 traceId 到 ResponseHeader 上,更加方便用户通过 traceId 直接定位到具体日志。
三、未来与展望
CICD 部署平台建设全链路多版本流水线初衷是为了实现降本增效,节约公司的硬件和环境运营成本,提升研发人员日常工作效率。在具体推广全链路多版本流水线的过程中也遇到了一些问题,如重新搭建基线环境增加成本,已改为兼容原有测试环境替代。当前全链路流水线建设还刚刚起步,未来还有更多空间值得优化:
支持更多组件和语言:目前流量隔离已支持了 RPC 层的 HTTP 和 Dubbo 流量,消息中间件的 MQ 组件,对于其他 RPC 框架,消息中间件组件,或涉及到非 Java 语言应用时,由于使用范围不普遍,优先级较低,目前还未支持。这项问题会根据公司业务发展和技术应用流行趋势进行调整。
支持数据逻辑隔离:数据的底层存储通常是 MySQL,Redis,MongoDB 等,因为业务场景复杂,数据隔离实现成本高,暂未实现逻辑隔离的功能。如业务有需求,通常建议准备多套数据库使用物理隔离方案,在配置中心创建多套数据库配置信息方便切换。但是若想业务灰度使用更加丝滑,数据逻辑隔离还需要具备。
更多应用场景:目前全链路多服务流水线只应用在测试环境部署,如果业务使用流量染色的功能更加熟悉和稳定,未来此项特性在线上 A/B 测试等场景也可支持。