小团队的 微服务 + CI/CD + Kubernetes 实践


公司的应用架构一开始就选定了微服务+Kubernetes,整个开发环境都在内网,使用 Jenkins 做半自动化的 CI/CD.

整个前后端都拆分得很细,分了很多层次:

  1. 脚手架层:封装了开源的各种库(mysql/redis/es 的 CRUD 库,log/tracing/config 库等)。
  2. 基础层(BaseXxx):基于脚手架层,实现了如下几类应用的基础层:测试器、数据库升级器、微服务、网关、通用工具、一次性任务等。
  3. 中台:在基础服务上,按功能实现了一些比较通用的中台服务。比如权限认证服务、用户服务、订单服务等。
  4. 应用层:每个仓库对应一个具体的微服务,也可能包含测试器、数据库升级器等。可能会在 CI 中被打包成多个镜像。

最终只对外暴露出几个对外网关。

这种结构的目的,就是提升代码的复用能力,把应用层能复用的东西,都抽到下面两层去了。
但这要求我们的基础层API一开始就设计得足够好,因为越到后期,API 的影响面就越大,几乎无法修改。

一、CI/CD

1. Continuous Integration 方案

目前我们是使用 Jenkins 作为 CI/CD 工具,层次结构也完全对应前面讲到的代码层次结构。

通过一套 BatchJob (批量构建任务,串行或并行地调用相关的子任务)来按依赖顺序,自下向上地层层更新 csharp/python/golang 依赖,构建 nuget 包,最后打包成 docker 镜像。

更新过程中会通过如下几个检测项来判断是否需要构建 nuget 包/docker 镜像:

  1. 更新私有依赖,返回值:是否更新了依赖
  2. 更新第三方依赖的小版本,返回值:是否更新了依赖、是否有主版本变更(主版本需要手动升级)
  3. 与上次构建相比,代码仓库是否存在变更(目的是加速构建)

每一个仓库的依赖更新与版本自增都对应一个 jenkins 任务,由每一层的 BatchJob 按预先定义好的顺序启动这些小任务。(相互独立的任务会被并行调用,以加速构建)

而在应用层,是通过 batchjob 并行构建所有的 docker 镜像。

有一个专门的 job_config 仓库(Python 模块),存储着:

  1. Git 仓库、Jenkinsfile 与 Jenkins 任务的对应关系,以 yaml 格式保存
    • 通过 python 代码提供 api,动态地从上述配置中查询出各层的 Git 仓库、Jenkins 任务的各种信息。
    • 提供命令从上述 yaml 配置中生成出所有的 jenkins jobs 配置(xml文档),这样就不需要通过 UI 一个个添加 Jenkins 任务。
  2. 上述的任务分层、任务之间的优先级(相同优先级的任务是相互独立的,可并行构建)

2. Continuous Deployment 方案

构建完成后,需要通过一个“镜像快照”的功能,将所有镜像的版本号、扫描出来,然后生成它们的 k8s yaml 配置文件,保存到 git 仓库中,并打上 tag(时间戳)。方便随时回退。

k8s 配置生成方面,我们目前是使用的自定义模板,通过字符串替换的方式进行填充。以后可能会考虑使用 kustomize。

最后通过一个部署的任务将指定的版本的 yaml 应用到集群中。

另外现在正在考察 jenkins-x,以后可能会将应用层的镜像构建到 k8s 部署,从 jenkins 移出来。

3. CI/CD 目前存在的问题

3.1 Kubernetes 配置生成

先说说 k8s 配置生成,试用了 kustomize,发现它功能还是比较弱,匹配不上我们现在的 yaml 配置生成的需求。也可能是我们目前的使用姿势不对吧。

3.2 GitOps

另外也查了很多 Jenkins-X 的资料,它遵从 GitOps 开发流程,能检测 Git 仓库变更/Pull Request,直接生成 Docker 镜像并部署到 Preview - Stageing - Production 环境。

但是我 GitOps 和公司目前的这套构建体系不太契合:这种以 Git 仓库为中心的方式,好像只面向能生成最终的 Docker 镜像/K8s Pod 的 Git 代码,而不适合用于构建底层依赖包。

比如说我更新了一个基础层的依赖包 A,现在需要让所有的应用层项目都引用这个新版本依赖。应用层可能有几十上百个仓库,手动更新几乎不可能。
通过 GitOps 做不到让应用层的这上百个仓库自己更新一下底层依赖。只能借助一个 Jenkins 的 BatchJob,调用一下所有应用层依赖更新的子任务。

P.S. dotnet-nuget/java-maven 目前没有很多现代现代语言都有的 动态依赖(如 pyproject.toml/package.json/pubspec.yml/go.mod/Cargo.toml,可以通过指定范围之类的方法灵活配置依赖) + 依赖快照(如 pyproject.lock/yarn.lock/pubspec.lock/go.sum/Cargo.lock,所有依赖的完整快照,保证当前依赖环境可复现) 这类的依赖管理方法,以及 poetry update/yarn upgrade/flutter packages upgrade/go get -u/cargo update 之类的依赖升级命令。
只有一个 xxx.csproj/xxx.xml 记录所有直接依赖的精确版本。要做自动化依赖管理,只能自己写脚本去访问 nuget api,读取并更新 csproj 文件。
查询资料 nuget versions 发现 nuget/maven 确实也支持基于范围的版本依管理,但问题是它们没有 xxx.lock 文件作为环境快照!这意味着使用灵活的依赖管理,可能导致历史环境无法复现。

另外 GitOps 自动化部署的流程也和公司目前的部署方法不切合。我们每个开发/测试人员都有一套自己的开发/测试环境,有的测试会需要使用特定版本的一套后端微服务。也就是说测试人员需要能够控制自己测试环境整套微服务的版本,比如回退到某个时间点、将版本固定在某个时间点。而且不能在工作时间自动更新测试环境的后端微服务。(否则测试到一半,后端滚动更新了微服务,那大半天的测试就作废了。)

GitOps 只适合一些扁平的应用,而对公司这种分层结构的代码就有点无所适从。

解决方法
  1. 已有的 Jenkins 分层更新构建流程不变,只在应用层进行 GitOps 方式的 CI/CD。因为应用层仓库相互之间是独立的。
  2. 给 GitOps 的构建部署提供专用环境:Preview - Stageing - Production,个人服务器的部署仍然使用现有流程。

旧的分层更新任务会修改应用层的 csproj 文件,这样就会触发 GitOps。

以这种方式进行结合是比较好的,GitOps 和现有的分层结构不会冲突。

3.3 网络问题/缓存问题

每次构建 dotnet 程序时,都需要从公网拉取依赖,使用静态 Jenkins Slave 时,拉了第一遍后本地就有缓存了,不需要再拉第二第三遍。
可用 Serverless Jenkins (Jenkins-X)的话,每次都是启动新容器,岂不是每次都要拉依赖?这个挺费时间的。
暂时想到的解决方案是使用 Baget 的缓存功能,让私有 nuget 仓库来缓存这些依赖。

云上生产环境部署

开发人员需要一个直观的 UI 界面,进行生产环境的灰度部署、监控分析。我们调研了很多管理平台:Rancher/Rainbond/KubeSphere,以及这些工具与 OAM/Istio 的契合度。

目前想到的比较好的一个方案,就是 flux+flagger+istio(istio 可换成 linkerd2),
此方案使用 github.com/gitee.com/coding.net 私有仓库保存生产环境的 k8s 配置,在内网通过 jenkins 生成 k8s 配置,在生产环境中通过 flux 监控该仓库的更新,然后 flux 使用 flagger 对其中的 k8s deployment 变更进行自动的灰度发布。

另外 Jenkins-X 有 Preview-Staging-Production 的一套 GitOps 部署流程,也可以一试。只是 jx 目前只提供了 CLI,没有 UI。上手可能有点难。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM