一站式动态多环境建设案例
作者: 李思源、武良军
问题背景
致景科技成立于 2013 年 12 月,是领先的纺织产业互联网企业,国家高新技术企业。旗下拥有“百布”、“全布”、“天工”、“致景金条”、“致景纺织智造园”、“致景智慧仓物流园”等业务板块,致力于通过大数据、云计算、物联网等新一代信息技术,全面打通纺织服装行业的信息流、物流和资金流,帮助行业实现协同化、柔性化、智能化的升级,构建纺织服装纵向一体化的数智化综合服务平台。
我们作为集团公司已经成立 2 年多的一个业务团队,项目并行开发上线的情况越来越多。值得一提的是,我们目前处于微服务化拆分刚开始的阶段,目前 35 个微服务,拆完之后大概会去到 60 个左右。在这样的背景下,原先大家都使用一套开发/测试/生产环境串行跑研发流程,随着项目数量、开发测试需求的变多,微服务拆分的进行,原先的方式已经不太适合我们。下面简单罗列一下,我们在这过程中所遇到的三个问题。
项目测试环境被抢占
最典型的问题就是一个项目测试环境经常性被缺陷修复的测试流程抢占,导致项目测试时断时续,对测试而言缺乏沉浸式体验,同时测试环节成为项目并行度的主要瓶颈,验证影响项目迭代的进度。
开发联调环境不稳定
为了保证开发的体验,开发环境是允许开发同学自由发布。由于使用一套环境,不同的同学进行开发环境发布,经常性地导致联调中断。不少开发同学转而寻求端到端的线下联调,在个人机器上部署上下游应用,这种模式在微服务化推广之后,特别是面对众多的微服务应用基本上寸步难行。如何解决开发阶段代码调试的便携性,成为了我们遇到的第二个问题。
线上灰度环境的缺乏
第三个问题也是最重要的,之前我们缺少专门提供给产品经理进行功能验证的预发环境。新功能完成测试之后直接上线到线上环境,研发团队为了避免避免对客户产生不良影响,经常性地将发布计划安排在晚上。抛开研发团队的发布幸福度不谈,线上环境缺乏灰度发布能力意味着新功能上线以后就会对全量用户放开,一旦发生了产品设计缺陷或者代码漏洞的情况,那么影响面将会是全网的,风险巨大且不可控。
综上所述,我们需要解决线下缺乏隔离的多套环境来支持多项目的开发和测试,同时在线上需要具备灵活的流量路由策略支持灰度发布需求。
方案调研与探索
结合我们公司实际情况,我们的目标是开发团队不依赖运维团队,即 DEV = OPS 。我们可以一键拉起逻辑隔离的开发/项目环境,同时可以支持预发环境隔离,对于生产环境可以通过配置灰度规则流量、自然流量来进行全链路灰度的验证。
根据我们对当前问题的分析,参考目前互联网上的解决方案,都指向了项目环境治理和服务流量治理的方案,我们稍微罗列下常用的几种方案,我们最终选择的是阿里云微服务引擎 MSE 全链路灰度 + 云效应用交付平台 APPSTACK 的集成方案。
自研 Ribbon 实现
我们使用的是 Spring Cloud 框架,在平时的业务开发过程中,后端服务与服务之间的调用往往通过 Fegin 或者 RestTemplate 两种调用方式。这其中是通过 Ribbon 这个组件帮我们做了负载均衡的功能。灰度的核心就是路由,我们可以通过重写 Ribbon 默认的负载均衡算法,在负载均衡调用之前,增加流量路由的逻辑,那么就意味着我们能够控制服务流量的转发。
这个方案要实施下去,对于大厂来说确实可以从 0 到 1 再到 100 进化出来,对我们来说,如果仅仅是实现一个路由的功能,做一个只支持核心场景的简陋版确实不是非常困难的事情。但如果要达到成熟可应用的阶段,需要投入专门的技术资源对其进行管理与维护,同时由于 Spring Cloud 微服务框架本身的复杂性,随着微服务数量逐步增多,链路越来越长,相关的微服务治理问题的定位与解决,也会耗费不菲的时间成本。
物理隔离(蓝绿发布)
这种方案需要为要灰度的服务搭建一套网络隔离、资源独立的环境,在其中部署服务的灰度版本。由于与基础环境隔离,基础环境中的其他服务无法访问到需要灰度的服务,所以需要在灰度环境中冗余部署这些服务,以便整个调用链路正常进行流量转发。此外,注册中心等一些其他依赖的中间件组件也需要冗余部署在灰度环境中,保证微服务之间的可⻅性问题,确保获取的节点 IP 地址只属于当前的网络环境。这个方案需要为这些业务场景采用堆机器的方式来维护多套灰度环境,会造成运维、机器成本过大,成本和代价远超收益;当然如果应用数目很小,就两三个应用,这个方式还是很方便的,可以接受的。
MSE 标签路由+APPSTACK 应用编排(我们的选择)
这两款产品的说明文档见链接:
云效应用交付平台 AppStack :
https://help.aliyun.com/document_detail/321856.html
阿里云微服务引擎 MSE 全链路灰度 :
我们假定通过上面的两篇文章,读者已经对这两个产品已经有了简单的了解,一句话介绍就是:APPSTACK 负责应用的环境管理和流水线发布,MSE 负责流量的全链路灰度。
- MSE 全链路灰度的重要概念
对照下面的 MSE 标签路由的图,我们重点介绍下 MSE 标签路由的几个重要概念如应用的打标、流量染色/自动染色、标识链路传递等,同时下图也是我们采用方案的核心原理,使用域名来标识不同的逻辑隔离环境。
核心示意图
1. 应用(服务)打标
对照核心示意图,我们发现每个应用都有个(base/gray)的 tag,有了这个 tag,我们才可以根据 tag 定义流量规则。
我们创建 MSE 应用的时候是通过特定 annotation 和环境变量给 MSE 应用打标的。
特定 annotation:
alicloud.service.tag=dev1
环境变量:
spring.cloud.nacos.discovery.metadata.version
增加特定的 annotation 跟环境变量
比如通过 annotation 打标之后我们就可以在 MSE 的标签路由中进行流量规则定义,同时我们也可以看到 Nacos 里面的服务有了一个标签相关的元数据(micro.service.env);
MSE 流量规则配置
Nacos 里面元数据信息
也有额外增加一个容器环境变量的做法:
spring.cloud.nacos.discovery.metadata.version
这种做法会在 Nacos 的服务元数据中增加一个 version 属性,像 MSE 云原生网关就会借助这个 version 属性进行流量管理,而 MSE 全链路灰度是借助 alicloud.service.tag 定义的标签进行流量管理。
容器中增加 gray 相关的环境变量
MSE 流量规则配置出现 gray 节点
Nacos 里面有gray环境的元数据信息
MSE 云原生网关可以选择 gray 版本
2. 流量染色/自动染色
简单讲,流量染色就是流量带上了特别的标识,对于 HTTP 请求来说,是请求头里带了一些标识信息,对于 Message 来说,是消息头里带了标识信息;我们这里主要讲下 HTTP 流量染色问题;一种是人工的往 HTTP 请求里面增加标识信息,比如前端请求后端 API 的时候,增加一个 xx:111 的标识信息,那我们就说这个流量被染色了。而自动染色,则说的是一个没有标识的 HTTP 请求,经由某个打了标的 nacos 服务之后,往后调用下一个服务的时候,自动会带上这个 nacos 服务的标签信息在请求头里;最简单的举例就是 a 应用调用 b(gray)应用,那b应用调用后面的 c 应用的时候,会自动带上 x-mse-tag:gray 的请求头,这就是自动染色。
这里特别提到 x-mse-tag:xxx 这个标识,他是 MSE 系统保留的标识,不仅仅代表了染色,同时也代表了链路传递(这个请求链路上的各个节点都会依次传递这个标签下去)和默认的路由规则(优先选择 xxx 标识的服务,没有找到的情况下,再选择 base 服务-没有打标的),这个默认路由规则是不需要显式定义的。
我们的解决方案也是特别地利用了这一点,对照核心示意图,我们在域名名字中添加了流量标识,然后在 Ingress-Nginx 中将流量标识解析出来然后通过 x-mse-tag:xxx 的方式一路传递下去,这样就实现了在整个链路上优先选择 xxx 标识的服务,使用没有标识的 base 服务进行兜底。
3. 标识链路传递
流量被染色,也就是请求头中有特定标识之后,这个标识在调用链路中如何可以传递下去,比如一个 HTTP 请求,带了 user-id:100 的头,陆续需要经过 A->B->C。即调用 A 的时候带了 user-id:100,A 调用 B 的时候也希望可以带上 user-id:100 的请求头,同样 B 调用 C 的时候也要带上 user-id:100。这个就是标识的链路传递,有了这个标识链路传递,我们才可以在为 A/B/C 应用定义按 user-id 的值进行路由的策略。MSE 的标识链路传递的方法是定义环境变量 alicloud.service.header=x-user-id,在入口应用 A(所有版本,gray+base)增加该环境变量以后,往后的调用 B 和 C 的过程中,都会自动添加请求头 x-user-id 进行传递,这样就方便我们在 A,B,C 节点按照特有规则进行路由定义。当然 x-mse-tag 这个特殊的请求头默认就是链路传递的,MSE 会把这个标识层层传递下去并进行默认的路由规则(tag 优先,base 兜底);MSE 标识链路传递的原理如下,借助分布式链路追踪的框架的实现方式,每个应用的探针拦截请求并解析标识,然后暂存到线程空间,在往后调用的时候再通过探针把标识塞到下个请求。通过分布式链路追踪的框架完成标识传递。
- 阿里云效应用交付 APPSTACK 简述
我们把云效 APPSTACK 引入进来,主要目的是方便开发同学通过白屏的管理方式自助完成 MSE 所需要的配置工作,同时在微服务架构下,我们希望应用进行拆分之后,每个应用都有自己的 owner。
通过 APPSTACK 我们可以屏蔽 K8s 的 deployment,service,ingress 等细节,研发同学面向的就是应用+环境+流水线。这样最终开发人员在 APPSTACK 通过流水线完成应用的环境部署,每个环境都会按照 MSE 标签路由的要求打上不同的标识。
应用的多套环境部署
每个环境都会按照 MSE 标签路由的要求打上不同的标识
在这里我们不展开讲述 APPSTACK 的核心功能,我们这里主要的就是借助应用编排,让每个应用的每个环境部署的时候,可以设置好 MSE 标签路由所需要的各种环境变量和 annotation。
我们的解决方案
我们在调研了以上能力之后,根据自己公司的实际场景与业务需求,根据不同环境的特性,定义了多种环境的抽象,基于此构建了一站式动态多环境的能力,并针对主要场景设计了不同的实施方案。
环境定义
通过对阿里云微服务引擎 MSE 标签路由和云效应用编排 APPSTACK 的调研,结合我们前面提到所面临的问题,我们最终定义了我们整个研发体系所需要的环境体系即:多套的开发环境(含基础环境)+多套项目环境(含基础环境)+(集成)测试环境+预发环境+(支持灰度)生产环境,如下图
多套开发环境:目标是支持多个项目的在开发阶段的开发联调,核心要求是各项目动态隔离并且支持端云互联,项目动态隔离是每个项目都有自己的开发联调环境且只需部署有变动应用,端云互联是开发可以将自己本地跑的应用注册到这个 MSE 这个体系里面来,实现可以本地调试的目的,两个研发可以点对点地进行本地 debug 来跟踪问题。开发基础环境是负责兜底服务调用的,每个应用生产部署之后都需要同步更新开发基础环境,保障基础环境是最新的生产版本。
多套项目环境:目标是支持耗时较长的大型项目,比如重大技改,重大业务项目,需要长时间占用测试环境跟内外部关联方进行稳定测试的。核心要求是各个项目动态隔离。关于项目动态隔离的定义同上。
测试环境:目标是支持短平快的项目测试和集成测试,比如日常的缺陷修复,或者多个小项目需要集成到一起发布,同时也是我们日常自动化测试的环境。项目环境中的特性分支也需要经过测试环境的自动化测试才可以上线。
预发环境:目标是支持产品经理在真实环境中验证产品功能,进行验收,预发环境使用的基础建设如数据库等同生产环境是一致的,当然这里对系统设计也会提出更高的要求,比如需要保持向前兼容,就像数据库,只能增列不能减列,不能在 sql 中使用 select * 等等,这些我们通过 DMS 进行数据库结构变更的约束和通过代码检查保障,此处不赘述。
生产环境:目标是支持规则流量+自然流量的全链路灰度,这里的规则流量指的是带有明显特征的流量,通过 MSE 的流量规则能够清晰定义的请求,比如请求头,参数,cookie,body 中数据符合规则的。自然流量则相反,我们不指定任何特征,比如全部流量的 1%导入到灰度环境,这个我们就理解成自然流量。
综合来看,目前的环境体系里,开发环境和项目环境涉及到动态隔离,所以需要部署基础环境来完成服务兜底的能力,这个基础环境也就是 MSE 标签路由中无标签(base)应用的提供者。
这一套环境体系的流转流程主要有:
-
拉取特性分支进入开发环境进行本地开发和前后端联调,然后提测到项目环境
-
项目环境由测试团队完成功能测试之后,将应用部署到(集成)测试环境
-
在(集成)测试环境同其他特性分支一起完成集成,并通过自动化测试和简单验证,就可以部署至预发环境
-
产品经理在预发环境进行功能验收测试,通过之后可以发布到生产环境进行灰度验证
-
在生产环境可以按照规则流量+自然流量进行灰度验证,通过之后就可以导入全部流量
-
最后将特性分支合并至主干后用最新的生产版本更新开发/项目基础环境。
主要场景实施
- 场景一:项目隔离的动态多环境
按照我们的解决方案,项目环境要实现的是逻辑隔离的动态多环境,相当于每个应用我们要通过 APPSTACK 部署基础环境(负责兜底的无标签 base 应用)和动态项目环境(有变更的)同时我们需要保障前端调用后端的域名可以转换成 x-mse-tag 的请求头。
- 通过 APPSTACK 部署好有标签的应用(项目环境)和没标签的应用(基础环境),下列截图仅做示范
- 在 ingress-nginx 中解析域名中的 tag 属性转换成 x-mse-tag 请求头链路传递,通过 ingress 配置携带 header 方式到 api 网关。
通过注解
nginx.ingress.kubernetes.io/configuration-snippet 实现,具体如下:
metadata:
annotations:
nginx.ingress.kubernetes.io/configuration-snippet:
proxy_set_header x-mse-tag dev1
通过这样简单配置之后,前端调用后端服务的时候,只要通过特定的域名请求,那 ingres-nginx 就可以把这个域名对应的请求自动添加一个 x-mse-tag 的请求传递到 api 网关应用,然后借助这个 x-mse-tag 的特殊请求头,在调用下游服务的时候,就会一路优先选择 dev1 标签的服务,没有 dev1 标签的服务就会去找兜底的 base 服务。
- 场景二:规则流量全链路灰度的生产环境
-
通过 APPSTACK 在生产环境部署好有灰度标识的应用。
-
定义流量路由规则,在 MSE 控制台为本次灰度链路中的入口应用设置流量路由规则,比如本次发布更新了 A-B-C 三个应用,A 就是入口应用。
通过这种方式定义之后,我们可以设置符合某些特征的流量进入到 A 应用的 gray 版本,并且向后层层传递过去,不用每个应用重复设置路由规则。这就会满足了规则流量的全链路灰度的要求。
- 场景三:自然流量全链路灰度的生产环境
-
通过APPSTACK在生产环境部署好有灰度标识的应用,略图。这里至少需要为本次项目的入口应用 A(也可以是全部应用)增加一个自动染色的变量 profiler.micro.service.tag.trace.enable=true,这个变量会把经过这个入口应用 A 的流量自动染色,往后传递的时候自动增加 x-mse-tag 的请求头,从而实现全链路灰度
-
定义流量规则,即自然流量的多少比例进入 gray 环境
这样通过入口应用的自动染色,以及入口应用的自然流量分批,我们就可以让进入到入口应用 gray 节点的自然流量进入到全链路的灰度环境(灰度优先,基础应用兜底)。
目前为止,我们实现了默认按照域名进行项目/开发多环境逻辑动态隔离的效果;同时提供给研发团队便捷的白屏管理的工具,可以由项目组独立拉起整个环境,通过三个场景化的实施方案,完美解决了开篇提到的三个问题。
相关技术原理概要介绍
MSE + APPSTACK 的解决方案,核心的地方在于流量规则的定义以及流量标识的传递,其核心的解决方案在于流量中标识的解析和传递,我们再看一次这个标识传递的图:
怎么样才能实现这里面最核心的解析 Extract 和注入 Inject 的功能呢?
答案是探针,为每个 MSE 管理的应用运行时候增加一个 java agent 的探针,完成一个类似 JVM AOP 的能力,在 ACK(K8s)的容器中,MSE 通过如下的方式自动为应用安装 java agent;
-
配置 Webhook,然后根据 Pod 或者 Namespac中的 Labels,来判断是否要挂载 Java Agent。如果需要挂载,则就对 Pod 的声明⽂件做出后续修改
-
获取并添加环境变量 JAVA_TOOL_OPTIONS,⽤于加载 Java Agent。
-
给业务容器添加 Volume,⽤于存储 Java Agent 的⽂件内容。
-
给 Pod 添加 Init container,⽤于在业务容器启动前下载 Java Agent
最终我们每个被 MSE 管理的应用在 POD 层面可以看到如下信息:
有了这个探针,我们可以拦截所有 HTTP REQUEST 的处理类,将我们关心的标识信息暂存起来,然后在应用内部消费 Nacos 服务的时候增加 MSE 支持的路由逻辑,选择合适的服务 provider(比如是打了 gray 标的),同时在调用这个 provider 的服务的时候,可以将暂存的流量标识带在请求头里继续传递下去,大体上跟我们最开始提到的自研 Ribbon 的方式相似。不过 MSE 在这块做得比较成熟,同时不单单支持了 HTTP REST 这种服务调用方式,还包括了 Dubbo、RocketMQ 的消息灰度、数据库灰度等。当然全链路灰度仅仅是微服务引擎 MSE 一个很小的功能,微服务引擎 MSE(Microservices Engine)是一个面向业界主流开源微服务生态的一站式微服务平台,提供注册配置中心(原生支持 Nacos/ZooKeeper/Eureka)、云原生网关(原生支持 Ingress/Envoy)、微服务治理(原生 支持Spring Cloud/Dubbo/Sentinel,遵循 OpenSergo 服务治理规范)的能力,有兴趣的同学可以看看官方文档。
借事修人
作为创业团队来讲,能够快速具备一站式解决服务治理问题,是一件非常酷的事。这整个方案的讨论实施过程中,研发团队对于 K8s、Nginx Ingress、MSE 都有较深入的理解。“像我们部门研发团队,没有专门的运维团队,每个开发人员都可以深入了解每个产品的来龙去脉,想想就很有意义。”