1. 项目迁移背景

1.1 为什么要在“太岁”上动土?

目前公司的测试环境、UAT环境、生产环境均已经使用k8s进行维护管理,大部分项目均已完成容器化,并且已经在线上平稳运行许久。在我们将大大小小的项目完成容器化以后,测试、UAT、生产环境的发版工具以及CICD流程慢慢的实现统一化管理,并且基于k8s开发了内部的发版审核平台,同时接入了Jira等项目管理工具。

在自研平台进行发版时,能够自动关联项目的开发进度以及Release版本,最重要的是其可以控制发版权限、统一发版工具及发版模式,并且支持一键式发版多个项目的多个模块,同时也包括了发版失败应用的统一回滚及单个应用的回滚。

因为该项目从始至今一直在使用GitRunner进行发版,并且基于虚机部署,所以一直没有集成到发版审核平台,但是由于项目比较重要,并且涉及的服务和机器较多,所以必须要把这个项目进行容器化并且统一发版工具才能更好的适应公司的环境,以及更好的应对下一代云计算的发展。

1.2 为什么要弃用Git Runner?

首先我们看一下Git Runner发版的页面,虽然看起来很简洁清爽,但是也难免不了会遇到一些问题。
在这里插入图片描述

1.2.1 多分支并行开发问题

当多分支并行开发或者能够发版到生产环境的分支较多时,很容易在手动部署的阶段点错,或者看串行,当然这种概率很小。而且在一个项目的微服务拆分成多个Git的Project时,发版时需要先找到对应的Project,然后在找到对应的发版分支,然后再点击发版,如果有二十个微服务需要发版,那么你需要重复二十次这个流程。

我们还可以看到另外一个问题,每次提交或者合并,都会触发构建,当我们使用Git Flow分支流时,可能同时有很多分支都在并行开发、并行测试、并行构建,如果Git Runner是基于虚机创建的,很有可能会出现构建排队的情况,当然这个排队的问题,也是能解决的。

1.2.2 多微服务配置维护问题

其次,如果一个项目稍微大一些,维护起来也不是很方便。比如这个准备要迁移的项目,一个前端和二十多个业务应用,在加上Zuul、ConfigServer、Eureka将近三十个服务,每个服务对应一个Git仓库,然后每个服务同时在开发的分支又有很多,如果想要升级GitLab CI脚本或者微服务的机器想要添加节点,这将是一个枯燥乏味的工作。

1.2.3 安全问题

最后,还有一个安全的问题,GitLab的CI脚本一般都是内置在代码仓库里面的,这就意味着任何有Push或者Merge权限的人都可以随意的修改CI脚本,这会导致意想不到的结果,同时也会威胁到服务器和业务安全,
针对发版而言,可能任何的开发者都可以点击发版按钮,这些可能一直都是一个安全隐患。

但是这些并不意味着Git Runner是一个不被推荐的工具,新版的GitLab内置的Auto DevOps和集成Kubernetes依旧很香。但是可能对于我们而言,使用Git Runner进行发版的项目并不多,所以我们想要统一发版工具、统一管理CI脚本,所以可能其它的CI工具更为合适。

1.3 为什么要容器化?

1.3.1 端口冲突问题

容器化之前这个项目采用虚机部署的,每个虚拟机交叉的启动了两个或者三个微服务,这会遇到一个问题,就是端口冲突的问题,在项目加入新应用时,需要考虑服务器之间端口冲突问题的,还要考虑每个微服务的端口不能一样,因为使用虚拟机部署应用时,可能会有机器节点故障需要手动迁移应用的情况,如果部分微服务端口一样,迁移的过程可能会受阻。

另外,当一个项目只有几个应用时,端口维护起来可能没有什么问题,像本项目,涉及三十多个微服务,这就会成为一件很痛苦的事情。而使用容器部署时,每个容器相互隔离,所有应用可以采用同样的端口,就无需再去关心端口的问题。

1.3.2 程序健康问题

使用过Java程序的人大部分都遇到过程序假死的情况,比如端口明明是通的,但是请求就是不处理,这就是一种程序假死的现象。而我们在使用虚机部署时,往往不能把健康检查做的很好,或许在虚机上面并没有做接口级的健康检查,这就会造成程序假死无法自动处理的问题,并且在虚机上面做一些接口级的健康检查及处理操作并不是一件简单的事情,同样也是一件枯燥乏味的事情,尤其是当一个项目微服务过多,健康检查接口不一致时更为痛苦。

但在k8s上面,自带的Read和Live探针用以处理上面的问题就极其简单,如图所示,我们可以看到目前支持三种方式的健康检查:
healthCheck

  • tcpSocket: 端口健康检查
  • exec: 根据指定命令的返回值
  • httpGet: 接口级健康检查

同时这些健康检查的灵活性也很高,可以自定义检查间隔、错误次数、成功次数、检查Host等参数,而且上面提到的接口级健康检查httpGet也支持自定义主机名、请求头、检查路径以及HTTP或者HTTPS等配置,可以看到用k8s自带的健康检查可以省去我们很大一部分工作,不用再去维护非常多令人讨厌的脚本。

1.3.3 故障恢复问题

在使用虚机部署应用时,有时可能会碰到宿主机故障,单节点的应用无法使用,或者多节点部署的应用由于其他副本不可用,导致自身压力大出现服务延迟的情况。而恰恰宿主机无法很快恢复,这时可能就需要手动添加节点或者需要新加服务器才能解决这类问题,这个过程可能会很漫长,或许也很痛苦。因为需要去准备依赖环境,然后才能去部署自己的应用,并且有时候你可能还需要更改CI脚本。。。

而使用k8s编排时,我们无需关心这类问题,一切的故障恢复、容灾机制都由强大的k8s负责,你可以去喝杯咖啡,或者你刚打开电脑去处理这个问题时,一切都已经恢复如初。

1.3.4 其他小问题

当然k8s给我们带来的便利性和解决的问题远不止上面所说的,容器镜像帮我们解决了依赖环境的问题,服务编排帮我们解决了故障容灾的问题,我们可以使用k8s的包管理工具一键创建一套新的环境,我们可以使用k8s的服务发现让开发人员无需再关注网络部分的开发,我们可以使用k8s的权限控制让运维人员无需再去管理每台服务器的权限,我们可以使用k8s强大的应用程序发布策略让我们无需过多的考虑如何实现零宕机发布应用及应用回滚,等等,这一切的便利性正在悄悄的改变着我们的行为。

2. 迁移计划

2.1 蓝绿迁移

首先来看一下迁移之前的架构
vmStart
和大多数SpringCloud架构一样,使用NodeJS作为前端,Eureka用作服务发现,Zuul进行路由分发,ConfigServer作为配置中心。这种架构也是SpringCloud在企业中最普遍的架构,没有使用更多额外的组件,所以我们在第一次迁移时,也没有考虑太多,还是按照迁移其他项目时用的方案,即在k8s上新建一套环境(本次迁移没有涉及到中间件),也就是容器化环境,配置一个同样的域名,然后添加hosts解析进行测试,没有问题的话直接进行域名切换即可完成迁移。这种方式是最简单也是最常用的方式,类似于程序发版的蓝绿部署,此时在k8s新建一套环境对应的架构图如下:
blueGreen
在进行测试时,此项目同时并行了两套环境,一套虚机环境,一套容器环境,容器环境只接收测试人员的流量,两套环境连接的是同一套中间件服务,因为其他项目大部分也是按照这种方式迁移的,并且该项目在测试环境也进行过同样的流程,没有出现什么问题,所以也同样认为这种方式在本项目也不会出现什么问题。但往往现实总会与预期有所差异,在测试过程中由于两套环境并存,导致了部分生产数据出现问题,由于容器环境没有经过完整性测试,也没有强制切换域名,后来紧急关停了所有的容器问题才得以恢复。由于时间比较紧迫,我们并没有仔细排查问题所在,只是修复了部分数据,后来我们认为可能是迁移过程中部分微服务master分支和生产代码不一致造成的,当然也可能并不是这么简单。为了规避这类问题再次发生只能去修改迁移方案。

2.2 灰度迁移

由于上面的迁移方案出了点问题,就重新定了一个方案,较上次略微麻烦,采用逐个微服务迁移至k8s,类似于应用程序发版的灰度发布。

单个应用迁移时,需要确保容器环境和虚机环境的代码一致,在迁移时微服务采用域名注册的方式。也就是每个微服务都配置一个内部域名,通过域名去注册到Eureka,而不是采用容器的IP和端口去注册(因为k8s内部的IP和虚拟机未打通),此时的环境如下图所示:
canary
此时有一个域名service-c.interservice.k8s指向ServiceC,然后ServiceC注册到Eureka时修改自己的地址为该域名(默认是宿主机IP+端口),之后别的应用通过该地址调用ServiceC,当ServiceC测试无问题后,下线虚拟机里面的ServiceC,最后的架构如图所示:
canaryAll
除了Zuul、前端UI和Eureka,其他服务都使用灰度的方式迁移到k8s,比蓝绿的形式更为复杂,需要为每个微服务单独创建Service、域名,在迁移完成之后还需要删除。到这一步后,除了Eureka其他服务都已经部署在k8s上,而对于Eureka的迁移,涉及的细节更多。

2.3 Eureka迁移

到这一步后,服务访问没有出现其他问题,除了Eureka之外的服务,都已经部署在k8s,而Eureka的过度型迁移设计的问题可能会更多。因为我们不能直接在k8s上部署一套高可用的Eureka集群,然后直接把ConfigServer里面的微服务注册地址改成k8s中的Eureka地址,因为此时两个Eureka集群都是独立的Zone,注册信息并不会共享,这种会在更改配置的过程中丢失注册信息,此时架构图可能会出现如下情况:
eureka
也就是在替换配置的过程中,可能会有ServiceA注册到了之前的Eureka上,ServiceB注册到了k8s中的Eureka,就会导致ServiceA找不到ServiceB,反过来也是同样的问题。

所以在k8s搭建Eureka的集群后,需要给每个Eureka实例配置一个临时域名,然后更改之前的Eureka集群和k8s里面的Eureka集群的zone配置,让k8s里面的Eureka和虚机里面的Eureka组成一个新的集群,这样注册信息就会被同步,无论注册到Eureka都不会造成服务找不到,此时的架构图如下(此时所有的服务还是注册到原来的Eureka集群中):
eurekaVMK8s
接下来需要做的事情,就是更改微服务的配置,此时需要更改地方有三处:

  1. 微服务注册到Eureka的地址更为容器IP和端口,不再使用域名注册,因为此时微服务都已经在k8s中,直接通过内部Pod IP即可连接;
  2. 更改服务注册的Eureka地址为k8s Eureka的service地址,Eureka使用StatefulSet部署,直接通过eureka-0/1/2.eureka-headless-svc就可以连接;
  3. 待所有的微服务都已经迁移完毕后,更改k8s的Eureka集群的zone为:eureka-0/1/2.eureka-headless-svc,并删除其他微服务的Service和域名。

最终的架构图如图:
end

3. 总结

为了保证服务的可用性,我们无奈的采用灰度的方式进行迁移,比蓝绿的方式麻烦了很多,而且需要考虑的问题也有很多。在程序没有任何问题的前提下,还是建议采用蓝绿的方式进行迁移,不仅遇到的问题少,迁移也比较方便快捷。当然采用灰度的方式对于大型的项目或者不能中断服务的项目可能更为稳妥,因为一次性全部切换可能会有遗漏的需要测试的地方。当然无论哪种方式,对应用的容器化、迁移至Kubernetes才是比较重要的事情,毕竟云计算才是未来,而Kubernetes是云计算的未来。

广告时间
有需要学习k8s的可以关注下这个视频教程:https://edu.51cto.com/sd/518e5