背景介绍
随着滴滴国际化业务的发展和扩张,当前已在全球多个国家提供面向当地的出行服务,为了给用户提供更好的体验和更低的响应延迟,异地多机房灵活部署及云上弹性部署的需求日益强烈。出于体验、成本、合规和稳定性的考量,2020至今,国际化业务进行了多次不同规模的机房部署。早期部署过程非常低效,除去SRE的参与,需要部署全量模块的业务RD参与资源梳理、资源申请、适配新机房代码改动、上线部署、联调测试一系列工作。部署范围通常几百个模块,部署周期在2-3个月,几百个人2个多月的协作,期间难免发生错误,沟通协调成本可想而知非常巨大。
随着云原生技术的发展和运用,微服务数量越来越多是不可逆转的趋势,为了减少部署过程中参与的业务RD人数,首先要找到问题根因是什么?
根因分析
批量服务交付时,业务RD的工作主要集中在3点:
1.资源梳理和申请
2.代码改造适配新环境
3.部署联调
其中第一点的效率问题由应用中心解决(见结尾延伸阅读),后两点——适配新环境时大量服务的代码改动效率和部署效率低下,是本文要解决的重点问题。RD主要的改造部署工作步骤和分析如下:
步骤1:梳理出零散的环境差异内容
业务现状:根据差异内容存在的位置不同,可分为以下四种类型:按照环境区分的配置文件中的机房差异、代码中硬编码的机房差异、用环境标识判断的业务逻辑差异
问题分析:单个服务中环境差异内容的位置分散,部署周期远远大于开发上线周期的特点,决定了RD的梳理只能是一次性工作,下次交付又要重新进行复杂的梳理,因此从单服务角度,不同环境使用同一套基准代码,将环境差异全部集中在一起配置,能够有效框定梳理范围。现实中大家也多是这么倡导的,但是环境差异配置与代码存储在同一个git仓库中,无法强制保证每个开发者遵守该倡议,难免腐化。
步骤2:针对各种类型繁多的差异内容适配新环境
业务现状:各个服务的下游依赖种类繁多,环境差异五花八门,每个负责RD都要独自处理所有下游依赖的适配工作;对下游来讲,因为被多个上游同时依赖着,机房交付时,多个上游都会分别找过来沟通,以寻求支持适配方案,不得不重复沟通。早期的多次机房部署均是依靠堆人的方式解决,几乎是八仙过海各显神通。
问题分析:微服务架构是滴滴主流开发架构,各个微服务遵循单一职责原则,高内聚低耦合,基于接口契约进行服务间通信,独立进行开发、测试和部署。微服务架构下的职责划分,促进了系统的可维护性、可扩展性和灵活性,日常开发好处多多。与此同时,这样的职责划分意味着机房部署时服务间单点沟通,各个服务RD日常对各自服务环境差异内容自定义命名。
步骤3:内容适配改造完成后,各自进行线上部署
业务现状:每个RD完成各自负责服务的新环境适配改造后,进行上线部署。当组织者和所有相关服务全部确认上线完成后,测试团队介入,构造测试数据进行回归测试。
问题分析:前2步所需时间都由服务本身的复杂度和RD的个人节奏决定,所有人都改造部署完毕再进行联调,注定存在漫长的等待。组织者要与所有人拉齐部署进度,服务复杂度低的RD往往要等待几周甚至一个多月才能开始联调,如若代码修改出现纰漏,又要重新拉齐部署进度。而现实中,由于新机房部署联调时没有线上流量不承担风险,基础数据缺乏导致测试数据构造存在一定成本,部署RD往往不会对部署的新环境服务进行完备的测试,发现纰漏进行返工,屡见不鲜。
总结一下,当前业务RD适配新机房的部署工作低效,主要存在以下三个问题:
1.服务中环境差异内容位置分散,且耦合在代码逻辑中,梳理工作复杂且每次部署需要重复进行;
2.服务环境差异种类繁多,集团缺乏相应标准与支撑工具,各个服务中环境差异内容进行自定义命名,形成信息孤岛,每个RD独自负责适配工作,导致上下游重复沟通;
3.各个RD人工部署,人员数量庞大导致信息拉齐困难;新交付集群无线上流量不承担风险,改造内容自测不充分,导致频繁返工,拖慢整体联调测试回归进度。
解决思路
想要根本解决问题一,必须将配置与代码彻底分离,不再存储于同一个git仓库,同时禁止代码感知当前环境标识,所有环境一套代码,从而杜绝增量的环境差异内容被添加到代码中。这在描述云原生应用最佳实践的十二要素配置管理原则中有所提及,云原生领域Heroku创始人AdamWiggins提出的"TheTwelve-Factor App"是业界广泛认可的云原生微服务开发原则,其中提到的配置管理的方法论:a.微服务应该只有一套基准代码,但可以存在多份部署,不同环境的差异应该在部署时动态确定; b.发布可回溯,配置和代码共同组成一份release,每个release可管理可复用; c.配置可分离,任何配置变更不需要改动代码。基于此,更加清晰明确了代码与环境配置之间的关系。
图片来源:《The Twelve-Factor App》
配置分离容易,从代码中抽出来换个地方存储即可,难点在于解决配置分离带来的3个新问题——如何与原有的开发集成流程尽可能的保持一致、如何弥补解耦带来的使用方式割裂、代码如何读取解耦的配置内容。
1.开发流程一致性方面,需要支持对配置的权限管理、增删改查、版本管理、协作加锁等功能;
2.弥补解耦带来的使用方式割裂体验,提供新功能,CR时支持用户查看读取到配置后的diff, 而非分别查看代码改动的diff和配置改动的diff.
3.代码读取解耦配置内容方面,关键在于满足以下三点1.侵入性低 2.迭代成本低 3.改造成本低
侵入性过高,就偏离了分离的初衷,同时带来不可接受的迭代成本,比如使用常规的sdk方式进行读取,一旦预定义模板发生扩展变动,就需要所有接入服务升级sdk。迭代成本过高的典型代表还是sdk,滴滴内部使用了多种主流语言进行开发,如果提供sdk的方式,需要支持多种语言,配置托管平台想要提供更多新功能时,也需要对所有语言进行支持,迭代成本高。改造成本过大,会对推广带来极大的阻力。绝大部分代码使用结构体加载原有的配置,解耦带来的新的配置读取方式如果破坏掉原有的结构体,导致所有线上服务改造成本巨大,测试回归范围更是几乎覆盖线上所有核心逻辑,这也是不可接受的。基于以上分析,我们采用了占位符替换的方式,这种轻量的方式允许用户完全保留原有的配置格式,仅在需要托管的集群差异配置取值处替换成CaC的占位符。改造完毕后,只需要diff下替换过占位符的配置文件是否存在差异即可。且占位符具有语言无关的特点,无须进行多语言适配。
面对问题二,从差异内容的功能出发,总结共性内容,对环境差异进行标准化定义,便可以打破信息孤岛,这一适配改造工作便可以由任何一个熟悉这套标准的人胜任,从而避免各个服务RD独自负责适配工作带来的巨大沟通协调成本。
建立一个好的标准规范是一个系统化的过程,参考以下优秀范式
【标准初创期】为了看清现状最先想到的是遍历所有国际化代码中的环境差异内容,其次总结共性,枚举,最后设置校验规则。这里的关键点在于遍历和枚举的不全面,以及校验规则的明确性。
遍历和枚举不全的原因,一方面是由于静态代码扫描无法遍历未来的动态演进的,另一方面由于国际化代码量庞大,通过几个人工走读的方式不现实,只好借助代码扫描工具,字符串比对定位到代码中的环境标识的位置,再进行人工分析。国际化大部分服务都存在多个集群,但对于少数服务仅存在一个集群环境,代码中未做环境区分,根据环境标识扫描时无法发现其真实潜在的环境差异。而枚举不全,则是因为代码历史遗留的环境标识判断逻辑五花八门,随着人员变动,许多逻辑已经丢失引入时的背景信息和资料。
对以上的遍历结果,权衡之下,最终对出现频率高影响范围大的依赖进行枚举,redis/kafka/mq,联系客服咨询官方推荐的集群差异使用方式,提出一套环境差异配置模板。
对于解决遍历和枚举不全的问题,需要建立通用的扩展机制进行兜底,同时防兜底的滥用和无序扩散。为此提出了服务粒度的通用扩展标准,和完全自定义的扩展标准。
经过对扫描结果进行总结,共发现5类最广泛的环境差异内容:
资源中间件及下游微服务调用链接路由信息(eg ip,port/domain/url)账号密码与鉴权超时重传与连接池资源标识(eg kafka topic/es index)环境元信息上述总结出的5类环境差异内容中,前4类通用于微服务的API调用,基于此可提出以USN(弹性云唯一服务名)为维度的微服务调用通用标准,这样随着用户的接入,微服务间调用带来的环境差异被逐渐收敛到同一服务维度。随着数据积累,可针对某一服务再次总结提炼,最终纳入枚举范围。完全自定义标准部分,采用加白的手段防范自定义扩展的滥用。
【标准建立早期】对于初版标准,以少数边缘服务为试点,快速进行验证。
【标准发展期】发展期面临推广,为了保障用户严格遵守标准,需要设计校验规则。校验规则直接影响着用户的每次使用体验,其关键点在于明确性,当用户触发了某条校验规则时,能够清晰看懂,违反了标准的什么规则导致被拦截。为此,我们设计了一套动态适配标准的校验规则,能够具体到标准配置模板中的每个字段。
问题三,在于人员庞大的协调成本,和人工操作的不可靠性,我们可以在标准化的基础上更进一步,采用集中化管理的方式,同时打通应用中心的资源交付能力,根据标准化配置结构,自动化生成新集群的配置,再自动完成打包部署上线工作,这样看起来就是一个较为理想的交付流程了。其中自动化的关键在于,建立标准化配置对应的交付规则。
综上所述,围绕 配置分离、标准化、自动化三个目标,我们搭建了CaC(Configuration as Code, 配置即代码)集群差异配置管理平台。该平台托管了代码中分离出的环境差异配置,并且为托管的配置提供了与原有开发流程一致的版本管理、加锁、冲突处理、增删改查等核心功能,同时弥补了解耦带来的使用方式割裂的体验问题;支持了环境差异标准的使用和校验;打通了应用中心的资源交付能力,共同完成服务新集群的自动化交付。
解法
用户端:主要用来对分离出来的环境差异配置进行托管,是用户日常开发中管理和使用配置的窗口。
管理端:用于对预定义配置模板、模板自定义扩展功能白名单、配置校验规则、交付规则进行动态管理,采用热下发的方式,是标准修订与更新的管理工具。
预定义模板:环境差异标准化定义的落地,用户依照此标准进行配置托管。具体包括配置模板、校验规则、交付规则3部分内容。
配置连通:提供标准化校验、配置注入读取、新环境交付、防腐等一系列的功能,这些功能作用于分离的配置上。
代码适配:配置分离带来的代码改造,主要是CI/CD流程中配置的注入方案。
稳定性保障:CI/CD流程影响了所有服务的发布上线,是非常重要的流程,CaC作为新引入的外部依赖,必须提前进行相应的稳定性保障工作。
防腐建设:提供代码扫描、资源探测等能力,防止用户后续开发中,将环境差异再次硬编码至代码中。
用户端
用户端框图中的所有功能,是为了提供和原有开发流程保持一致的功能,以及尽可能的降低配置分离带来的割裂感。
分离的配置如何进行保存和版本管理?代码开发普遍是分支开发主干上线模式,配置分离后,就要支持与代码分支一一对应的配置版本管理,为了与代码的分支版本管理保持一致,采取了配置和业务代码仓库一一对应的独立git仓库存储方式。如下所示,USN(Unique Service Name)是弹性云系统中的全局唯一服务名,对于共用代码仓库的服务(即一个git仓库下存在多个子目录),配置仓库也共用一个,其对应配置仓库由USN目录进行区分。
业务代码和配置,2个git仓库,两套版本,分别进行加锁冲突避免,再进行版本映射,理解起来会十分复杂。为此设定配置分支与代码分支保持相同的名称,复用同一条CI/CD流水线,当配置或者代码存在更新时,自动触发流水线从第一步重新执行。配置分支名与代码分支名保持一致,当同名代码分支合入主干加锁,其他分支自然无法合入主干不存在协作冲突,在代码生成稳定版解锁主干时,配置同步合入主干形成稳定版本。用户只改代码不改配置时,不再创建同名分支配置,默认使用master的配置即可。
配置的增删改查早期提供了webide的形式进行编辑,用户参考预定义配置模板(yaml形式)的维护文档,在webide上直接编辑yaml文件,每个环境存在一份yaml文件。近期升级了白屏化页面,表单的形式自动化渲染配置模板,前端支持更好的交互,规则校验更加前置。
管理端
管理端是用来管理CaC标准的平台,标准涵盖了预定义配置模板、配置校验规则、交付规则、模板自定义扩展功能白名单,管理端提供了上述模板和规则的版本管理和发布功能。模板涉及的类型是随着枚举不断新增的,校验规则与交付规则需要适配模板变更同步变化,自定义扩展白名单随着业务的不断接入需要持续新增,因此要求管理端对标准相应内容修改后能立即生效,滴滴集团内部的Apollo配置同步平台即可满足需求。
预定义模板
预定义模板是指用户托管环境差异配置时遵守的标准配置模板,加上由模板衍生的校验规则和交付规则,共同构成CaC标准。
标准配置模板需要包含用户开发中遇到的所有可能的集群间差异,正如第三节解决思路中问题二的分析,模板是通过代码扫描历史代码仓库中环境标识总结而来的,主要包含了高频使用的资源中间件、不可或缺的环境变量标识、收敛的通用下游微服务和兜底的自定义扩展4个部分。
【资源中间件】模板的主要内容,包含了redis、mysql、ddmq、fusion等等多个高频使用的下游中间件。其中并非只包含了某类型中间件存在环境差异的字段,考虑到与应用中心打通从而进行自动化交付的目标,应用中心需要一些资源关联信息,这些资源信息可能不存在集群差异但是作为交付源信息,也是配置模板中必要存在的。
【环境变量标识】模板提供了环境变量标识字段,用于用户传递数据采集监控报警,同时通过代码扫描字符串对比禁止用户使用环境标识进行代码逻辑判断。这样当用户遇到新的集群间差异时,只能将其托管至CaC平台。
【通用下游微服务】除了资源类型中间件,代码中还存在一些历史遗留的环境差异,针对各种微服务调用时的环境差异配置(eg token, url, 四层lvs-vip, domain), 对于历史包袱和长尾效应,我们采用弹性云提供的唯一服务名USN进行服务级别的组织,为将来的收敛保留可能。
【自定义扩展】针对其他的未能覆盖的场景,暂时提供extend自定义扩展支持。为了防止extend滥用,提供了服务级别(即USN为维度)的加白方式,开放给用户使用。迁移机房时需要提前与RD收集新机房目标值,内容由RD自行维护。
#1.资源中间件mysql: defaultTag: username: '' password: '' database: '' //无集群差异,交付源信息需要 ip: '' port: 0 disfname: '' readTimeout: 0 writeTimeout: 0 connTimeout: 0 totalTimeout: 0 maxIdelConns: 0 maxOpenConns: 0 maxConnLifeTime: 0 maxQps: 0 maxLimit: 0 redis: defaultTag: #tagName ip: '' port: 0 #必须为数字#......省略其他类型资源中间件......#2.环境变量标识env: physical: '' #字典sim/pre/small/online group: '' #字典simOnline/preview/productinrouter: ''#3.通用下游微服务microService: ibt-xxx-usn: #microService 下游服务的tagName为该服务的usn ip: '' port: 0 domain: '' #域名 appid: '' appsecret: '' disfname: '' readTimeout: 0 writeTimeout: 0 connTimeout: 0 totalTimeout: 0 retry: 0 interfaceConfig: interfaceNameA: #microService tagName,接口纬度扩展配置,由RD确定 url: '' readTimeout: 0 writeTimeout: 0#4.自定义扩展extend: key: 'value'校验规则,是为了确保用户能够正确的按照预期将配置托管至CaC,主要分为以下6种类型:
模板字段key值校验:早期webide阶段用户自行编辑yaml文件,防止用户自行随意输入,对配置key值进行校验。模板字段取值类型校验:防止webide编辑形式下,用户随意输入其他取值类型。接口校验:通用下游微服务中,规定tag名为usn, 调用弹性云系统进行真实性校验,防止乱写错写。配置化必填校验(mustAll、atLeastONe):对于各个中间件下游,连接地址信息必然是必填项,为了防止用户遗漏,进行必填校验。配置化字典校验(dict):部分字段取值官方文档中规定的字典,进行字典校验,防止用户书写错误。配置化正则校验(pattern、exclude-pattern):配置模板区分string和number两种类型,对于string类型,字段有明显含义和范式的,比如ip和http域名,添加了对应的正则校验,防止乱用错用。配置化校验规则的实现方式如下图所示,通过将校验规则明确到配置模板的叶子节点上,使得触发校验时,可以进行清晰明确的提示,且支持随着配置模板的修改动态适配。
交付规则,是服务于应用中心的自动化交付时的新环境CaC配置回填功能,即与应用中心约定了CaC配置模板中哪些字段由应用中心自动生成,哪些直接Copy源集群,哪些需要由用户介入手动填写。
配置连通
配置联通模块是整个CaC集群差异配置管理平台的中枢枢纽,贯通了诸多模块,使之协同运作。其核心功能有以下3个:
1. 配置校验——连通了用户端、预定义模板和防腐建设
配置校验部分,一方面通过动态渲染预定义模板,动态进行配置化规则校验,直接在前端页面进行拦截,提交时也会在后端进行二次校验拦截,防止用户不遵守CaC标准。另一方面,通过打通应用中心的资源探测能力,根据用户线上的实际资源流量,发现用户代码中未接入CaC的资源,以及应用中心已经销毁了的作废资源,进行拦截,防止用户后续开发中的配置腐化。
2. 配置注入脚本——连通了用户端托管的分离配置和用户的业务代码
在为代码注入分离的配置的方式上,一般存在热加载和CI/CD注入两种方式,下面是方案对比
分析集群差异配置的内容,绝大多数为下游依赖连接信息、账密信息、超时重传连接池信息。这些信息不需要频繁调整,但是更加重视部署的一致性、可审计性和安全性,对比热加载和CI/CD的注入优缺点,后者更加适应当前的场景。结合滴滴内部的CI/CD流程,将配置的变更融入代码CI/CD同一条流水线中,编译打包、CodeReview、发单部署、回滚的全流程,与原有流程完全保持一致,能够最大限度降低用户的学习和适应成本。
CI/CD注入配置的具体实现方式为,在编译打包过程中拉取并执行保存在配置联通模块中的配置注入脚本cac_build.sh,执行cac_build.sh脚本时一共做了三件事:1.拉取对应分支的分离配置,2.执行占位符替换逻辑,3.下载启停时环境选择脚本cac_control.sh。然后在服务启动时执行cac_control.sh来选择对应的配置文件覆盖原始文件。
其中占位符替换时,无论是占位符路径填写错误,还是配置忘记托管,当代码中的CaC占位符在分离的配置中找不到,立即报错,编译打包任务整体失败,防止错误引入线上。针对接入CaC的服务编译打包时,若代码中一个CaC占位符也没有发现,极有可能是所有的占位符格式错误,立即报错,编译打包任务整体失败,防止错误引入线上。
3. 配置拉取与回填——连通了用户端托管的集群差异配置、预定义模板中交付规则和应用中心
通过为应用中心提供分离的集群差异配置拉取接口,和master分支新增集群配置接口,以及交付规则拉取接口,打通整个自动化新集群交付全流程。
代码适配
1.占位符改造
将代码中多个集群化差异的配置文件,合并成一个文件,命名为以CacRegion_ 开头,此文件即为线上读取的配置文件。将合并文件中,集群间差异的值,用占位符替换,占位符格式为 ^cac{{key1.key2}},其中key1.key2为CaC配置中yaml的tag层级,层级需取到叶子节点(基本类型)
2.编译打包build脚本与启停control脚本改造
滴滴内部统一的编译打包和部署时服务启停,分别依赖build.sh和control.sh两个脚本文件,CaC涉及到的改动点分别是:
在build.sh打包结束后的结尾拉取并执行cac_build.sh,即curl -s --connect-timeout 1 -m 3 --output "cac_build.sh" http://aaa.bbb.com/cacConfig/api/service/getCacBuildSh 、 sh cac_build.sh在control.sh中服务启动之前添加sh cac_control.sh,并指定CacRegion_开头的配置文件新路径。实现原理为build时对CacRegion_文件内占位符进行替换生成所有机房的配置文件,然后在服务启动时选择对应的配置文件覆盖原始文件。
稳定性保障
CI/CD流程作为一个非常稳定的流程,应该尽可能少的引入外部依赖,CaC作为新引入的外部依赖,一旦出现问题,将导致用户CI/CD流程阻塞,无法及时上线,为了应对潜在风险发生CaC在使用流程和新引入的功能点上做了以下保障:
1.降级兜底
CaC引入的新流程,主要是在编译打包过程中,拉取并执行3份CaC提供的shell脚本cac_build.sh,这份脚本平时保存在CaC的后端服务中,若服务出现不可用情况,用户可以主动引入离线保存的脚本,即可继续执行编译部署能力。
2.监控报警
服务部署时,若CaC配置不符合预期,导致部署失败,会立即上报metric触发一级报警电话短信通知CaC小组成员关注跟进。
防腐建设
当单次改造完成后,用户将所有的集群差异配置按照CaC标准托管至平台,若立即进行新集群的交付往往能够立即享受到自动化带来的收益,但是随着后续的开发迭代,如果不熟悉CaC的RD将新引入的资源依赖再次保存在代码仓库中时,等到很久之后的下一次交付任务到来时,必然会导致交付的失败或者错误。为了避免这种场景的发生,防腐建设必不可少,我们分别从动态的流量探测和静态的代码扫描,进行了3方面的防腐建设:
1.提供基于应用中心流量探测的资源发现和校验能力,防止用户提交无效和错误资源,发现代码中未接入CaC的资源信息。
2.建设CaC代码扫描规则,识别代码中的环境标识、ip等链接信息
3.存量代码,引入代码扫描触发能力,作为改造时的辅助工具;增量代码,CR阶段自动触发代码扫描
落地与应用
最终建站过程中代码内的环境差异信息的管理能力,落地为CaC环境差异信息管理平台,通过用户端和管理端,与应用中心打通,连通了业务代码架构与环境交付能力。最终我们在2023年与2024年国际化出行新机房真实建站过程中,全面应用了CaC环境差异信息管理能力,大幅提升了交付的效率,达成了一次完整建站过程中人力投入减少80%的效果,初步解决了建站交付协同困难与低效的问题。
建站交付的内容在不断变化,环境差异内容就会随之变化,环境差异信息的标准是打通交付的灵魂,为了进一步拓展交付能力适配多云环境交付场景,架构中的环境差异标准需要覆盖更多的资源组件,同时管理好自定义环境差异能力的尺度与边界,赋予用户一定的自定义交付能力。需要注意的是,要防止自定义的滥用,以及标准的腐化,这会导致交付效率再次逐渐减低,因此衍生的管控能力,会降低现有开发流程的自由度,需要做好平衡与取舍。
延伸阅读
《The Twelve-Factor App》https://12factor.net/config作者:刘渭桢
来源-微信公众号:滴滴技术
出处:https://mp.weixin.qq.com/s/z3u7PVEJZ2FoxXIo-4fjJQ