???
???
???????????????(生活有时会迫使我们弯曲,但在弯曲的轨迹上,我们也能找到属于自己的旅程。 即便离开了我钟爱的技术领域,我仍然在新的旋律中发现着人生的节奏。- 史蒂夫·乔布斯)
???
???
?????????
因为前段时间有些迷茫,再加上入职了新公司,所以三个月的时间没有写博客,以下是当时整个技术团队灰度架构设计的背景,我也尽可能完整详细的进行记录,让大家更了解灰度架构设计的前后因果
设计和开发灰度系统其实已经是21年的事情了,以下是大概的时间线
作为一个SAAS服务提供的产品,除了产品的功能层面以外,客户最关心的是产品的稳定性。 而由于SAAS软件的快速迭代的特性,线上系统在不停的进行升级,要完全防止每次迭代bug引起的故障本身是一件比较难的事情。那我们能做的事情就是降低出现问题的影响:一个是降低线上系统问题影响的客户群体,一个是降低线上系统问题的故障时间。 灰度发布就是达成这个目标的手段之一。
从效果层面上看到的灰度,就是不同客户可以使用我们不同的软件版本。 这样系统升级的时候,可以从不同的客户群体开始升级,等该用户群体运行一段时间以后(也就是新的系统经过线上的某些客户验证后没有问题,理论上后续出问题的概率就比较小,如果这部分客户出问题,也只影响这部分客户,不影响其它客户),再逐步升级到后续其它的客户。例如升级的客户群分别是 内部客户(3天)→ 小客户(7天) → VIP客户(7天) → VVIP客户(7天)这样一个步骤。
为了隐私问题,我没有直接把原公司的设计图拿出来,而是以呼叫中心架构为背景重新设计了一张
该设计图涵盖了21年上线时的所有内容,部分细节虽然被忽略,但整体设计就是如此的,其中还有一些缺陷因为资源等问题就没有完善,不过对于当时的架构来说已经能够满足了。
灰度环境的控制有一个灰度环境管理系统,前端使用vuejs,后端使用java开发,它只有一个正式环境,它主要有以下作用
以下对"非用户使用的系统"称为"inner_system"
核心业务能灰度的全部灰度,包括各业务服务,服务的k8s配置文件,服务的cicd配置文件,mongodb数据库,redis数据库,elasticsearch缓存数据库,kafka/nsq消息队列,文件存储,apollo配置中心,kibana日志系统等,inner_system可以不灰度,比如租户系统,日志系统,内部使用系统等,直接和正式环境通信,或者同时兼容两个环境,但需要保证inner_system不能影响到主业务,否则也需要进行灰度设计
整个产品的服务集群都是托管在k8s集群中的,涉及到前端部门一个,后端部门三个,运维部门一个,因为采用了微服务架构设计,最终涉及到的业务服务达到40-50个,有些是需要灰度的,有些是不需要灰度的,最终统计后,需要灰度的业务服务仍然接近40个
为了支持灰度环境,需要先评估现有正式环境集群的资源配置,比如多少带宽,内存,硬盘,cpu等,然后因为灰度环境的服务比正式环境要少一些,所以额外申请了正式环境百分之70的机器资源, 假如正式环境占用资源100g,那么灰度环境就需要70g的资源。然后添加gray命名空间用来部署灰度服务,再把需要灰度的服务整理出来,将k8s相关的的helm,chart,deployment,hpa,ingress,service,环境变量等配置文件再添加一份灰度的配置,用于支持灰度服务的k8s配置
CICD持续部署,目前用的是gitlab的runner,通过本地服务的.gitlab-ci.yml配置规则,提交的分支匹配指定的规则,或提交指定的标签来触发部署流程
ci部署的流程以打标签为例,开发环境的是dev.开头,测试环境的是release.开头,正式环境是v.开头,这里以正式环境为例
至此ci阶段已经完成,因为cd阶段配置的是manual,手动部署,所以还需要人工点击deploy完成部署
cd阶段的配置流程同样以正式环境为例
整个cicd的流程就是如此,对于灰度来说,我们单独添加了一个.gitlab-ci-gray.yml文件配置灰度服务的cicd规则,和.gitlab-ci.yml不同的是,添加了.gray的标签触发规则,使用了新的命名空间-gray和pod配置文件
apollo配置中心管理了产品下所有服务的环境配置,包括服务间通信地址,数据库,消息队列,业务常量,环境变量等
对于灰度来说,所有服务都需要将正式环境服务配置复制一份,变量名都不需要更改,在正式环境相同应用的下,创建新的集群配置,比如命名为v-gray,然后直接在启动时根据环境变量读取不同的配置即可
包括一些公共配置也是一样的,都需要复制一份到灰度集群配置中
如果是用户上传的文件,对于文件自身来说是不区分环境的,且系统中的文件命名都采用类似uuid的唯一性id,所以不存在环境隔离相关的问题
但对于系统内的文件,一些公共配置文件,对环境隔离有要求的,就需要复制一份出来作为灰度文件处理,后续的同步也需要人工处理
有些旧的业务采用的是本地文件存储的,后来更新后为了减少业务入侵就采用了k8s的nfs文件挂载系统架构
旧业务本地文件存储命名也都是唯一的id,并且都是用户导入导出留下的临时文件,对环境隔离没有要求,所以nfs就不需要做灰度的处理了
日志系统使用的是kibana,内部使用elasticsearch存储和分析日志,因为添加了灰度环境,所以灰度服务的日志也要同步到elastisearch中,日志空间别名直接使用服务的名称进行区分,比如xx-gray就可以,没有其他额外的灰度操作
方案一
只对数据有灰度要求的表,进行灰度数据结构设计,数据结构设计无法满足的,视情况找最佳解决方案。比如定时器任务表,添加env字段标记所属环境,各自环境的定时器只读取属于自己环境的任务即可,比如数据初始化类操作的,做好存在即更新的操作即,等等。其他情况在后续业务发展中,需要做好数据结构的向前兼容等灰度处理,避免出现因环境不一致导致的异常问题。
方案二
对mongodb整个集群或库,或表进行灰度,但最终该方案被弃选
不对mongodb整个表,整个库或者整个集群做灰度的原因
最终,我们内部经过多次讨论,针对现有的业务和架构情况,对数据库灰度,我们使用了方案一
和mongodb灰度方案类似,也是基于成本,维护和便捷性方面的考虑,最终采用了方案一。
es索引缓存在目前的架构中不需要灰度,因为存储的数据结构都比较通用,单一,比如呼叫中心系统中的通话记录,客户信息,工单信息等,并且涉及到es缓存管理的业务并不多,所以没有做灰度。即便需要灰度,也可以参考数据库的灰度方案
因为消息队列的异步特性,导致它无法区分事件来源的所属环境,以通话账单事件为例,灰度环境发送了一个账单事件,因为没有做环境隔离,所以这个事件可能被灰度,也可能被正式环境所消费,如果这个事件被灰度或正式环境都兼容,那么一切正常。但可惜的是,消息队列是我们日常业务中最常使用的数据传递组件之一,假如某天灰度环境发布了新功能,账单事件的数据结构没有做向前兼容,如果该事件被灰度环境消费,那么没有问题,但如果被旧的正式环境消费,那么可能会出现数据不一致,业务异常等问题,将直接影响到用户的使用,造成企业损失
综上所述,消息队列需要进行灰度环境隔离,可以使用集群,topic或事件级别的方式进行隔离。
参考数据库的灰度方案,对灰度和正式环境的消费主题做灰度设计。消息队列的灰度分为两种级别,一个是消费事件,但涉及更改的消费事件多到上百种,对原业务代码侵入性太高,并且还涉及到多部门协作,考虑到成本和维护问题,最终采用消费者主题灰度级别,只需要再复制一套消费者主题,添加后缀标识,就能够完成环境隔离
假如有通话账单消费者主题topic_bill_call,设计为灰度就是的topic_bill_call_gray和topic_bill_call,这里直接将旧topic_bill_call作为正式环境的topic,就可以避免对正式环境做更改,降低了影响范围,然后灰度环境的使用topic_bill_call_gray
首先需要将消息队列的topic进行统一管理,用于环境隔离
先将代码中topic的常量,字符串,环境变量都迁移到配置中心apollo集群配置中
在apollo配置中心,消息队列所在的集群配置都需要复制一份后缀为_gray的topic的变量,用于灰度环境获取
再添加服务的启动环境变量,根据不同环境,启动时从apollo获取不同的集群配置并转换为业务识别的topic
然后在实际生产或消费消息时,各服务原代码不需要做更改,只需要在启动时做一些topic变量的转换即可
但还有一个问题,每一个产品下都有自己的研发部门,并且产品之间有些业务场景需要做数据通信,但并不是每一个产品都需要灰度环境等环境隔离架构,如何在其他部门没有灰度环境的情况下做好架构兼容?
假如有以下场景,多部门间通过同一个消息队列topic进行通信,比如通话账单,租户状态等,如何做到其他部门的业务无感知,不受任何影响呢?
那么这个问题其实就是如何让其他部门和以前一样,使用一个topic来执行业务,也就是同时分发和接收灰度和正式环境的消息,下面以两个场景进行举例,并列出解决方案,一个是租户系统A,一个是呼叫系统B,A系统只有一个正式环境,呼叫系统有灰度和正式两个环境
每当有租户在A系统注册成功时,都会向B系统同步该租户的注册状态,进行一系列初始化操作。
在之前,因为双方都只有一个系统,那么是一对一的关系,但现在B有两个环境,变成了一对多,可是对A来说只能对接一个环境。
为了减少B系统对A系统造成的影响,也为了让A系统对接一个更为稳定的环境,所以决定让A系统继续对接原来B系统的正式环境,包括原环境的消息队列,http地址等,这样无论B系统怎么改造,A系统也不会受到影响。如果A系统有更改,比如topic事件,接口数据结构等,B系统也要和之前一样同步升级。最终就是A系统一个topic或服务的事件分别由B系统的两个环境来消费处理。
每当有坐席在B系统完成一通电话时,都需要给A系统发送一个账单事件,用于A系统计算账单。
因为现在B有两个环境,在之前,B系统和A系统是一对一关系,但现在B系统多了一个灰度环境,为了保证灰度环境的账单事件能同步,B系统两个环境账单业务的topic或http地址保持不变,也就是B系统两个环境生产的账单事件由A系统一个topic或服务来消费处理。
虽然方案一能解决环境隔离问题,但对旧代码存在较大的入侵,需要将原代码中的所有topic迁移至apollo配置中心统一管理,并且因为对topic做了别名,所以为保证灰度环境代码能够正常运行,需要在启动时对topic别名进行配置和转换。对这些的更改,涉及到众多的服务,先不说研发,就测试而言,需要从0对整个系统进行测试,链路过长,成本过高
针对方案一的问题,主要是对现有代码的入侵过大,导致额外的研发和测试成本,那么针对该问题,出现了消息队列灰度集群方案
消费队列灰度集群配置和原正式集群相同,因为在灰度环境的主要是中小型用户,资源占用方面为原集群的百分之70甚至更低
和方案一不同的是,灰度集群服务使用新的地址和端口连接消息队列集群,所以需要先在apollo配置中心添加灰度集群配置,里面包含了消息队列,数据库等相关连接配置
配置好apollo后,还需要配置各服务的启动环境变量,在灰度部署的文件中,将消息队列变量替换为灰度的,那么在启动时,就可以读取灰度环境的连接配置了,因为连接的是新的灰度集群,原topic等命名都不用更改,那么业务代码不需要更改,开发和测试的成本就更低了
方案二虽然额外部署了一套消息队列灰度集群,增加了硬件消耗,但极大的减少了开发和测试成本,降低了代码侵入性,保障了系统的稳定性。再加上研发资源吃紧,时间比较紧,所以我们最终选择了方案二,以增加硬件成本的方式来提高整个系统的高可用性
经过统计,需要灰度的服务一共高达40个,百分之70是nodejs服务,百分之30是java和python服务,在做好以上组件的各项灰度配置,并和需要灰度的40个服务的进行配置集成后,更多的是需要对灰度和正式环境进行全量测试,整个系统的0-1测试(但后来因测试测试资源有限,加上时间不够,只对核心功能做了全量测试)
以下对"非用户使用的系统"称为"inner_system"
inner_system虽然不需要灰度,但它们都与现有核心业务服务有交互,比如http或消息队列通信,或者共用了某个数据库表,或者共用了某个路径的文件存储,又或者共用了某个apollo集群配置等,为了保持现有的系统正常运行,也为了不影响核心业务,所以需要将inner_system中各服务和核心业务的交互罗列出来,逐一排查和确认
以下分别用三个系统进行举例
租户系统
导出系统
灰度环境管理系统
呼叫系统(核心业务)
只有一个正式环境的时候,只有一个客户端,且客户端和服务端通信依赖的是nginx配置文件,里面配置了各个服务的路由转发,包括业务,在线,售后等系统。
但现在有两个环境,两个客户端,所以就还需要一个灰度网关作为请求入口,根据用户不同的环境,渲染不同环境的静态资源,请求不同环境的后端接口。
灰度网关的设计,可以参照上面的灰度架构设计图。
因为系统架构采用的是k8s,可以使用命名空间进行环境隔离和分发,当然也可以直接在业务中对环境进行过滤,自己指定转发路径
用户的账号根据体量分为三个等级,小用户,中型用户,大型用户,并且三类用户分别对应三个分组,但还有一个默认分组,指向正式环境,存放了新开户或没有被分组的用户
产品部会安排人员定期更新用户和分组的关系,比如每周,每个月。
用户和分组的关系是固定的,升级前,先将小用户组切到灰度环境,升级后的三天内观察小用户组的使用情况,如果没有问题,则陆续将中大型用户组迁移到灰度环境,三个组在灰度环境都没问题后,再升级最新代码到正式环境
以上是整个灰度架构设计的全部流程,但还有诸多细节方面没有提到,从设计,开发,联调,测试,上线整个阶段遇到了许许多多的问题,但也都一一迎刃而解了
上面只提到了一些相对比较核心和关键的问题,其背后还有特别多的业务细节问题,但因为和业务绑定程度较高,所以这里就不阐述了,但归根结底都属于"环境隔离"问题,解决的思路都和上述的场景十分类似