技术

我是如何用 Jenkins + Kubernetes 设计按服务选择部署的

AAnonymous
10分钟阅读

引言

这篇文章承接 我如何在 AI 的帮助下把 Omnilude 后端从单体迁移到 MSA。上一篇主要讲为什么要拆分服务边界,这一篇则整理这些边界是如何继续演变成真正的部署单位的。

关于 MSA 的讨论,很容易停留在到底拆成了几个服务。但真正让运维体感发生变化的时刻,并不是代码目录分开的时候,而是部署单位分开的时候。

这篇文章想整理的是,当前这套结构减少了什么问题、它是怎么运作的,以及还有哪些部分没有补齐。

为什么先感受到的是部署拆分,而不是代码拆分

在还是一个大后端的时候,最难受的地方并不只是功能混在一起,而是一个很小的修改也总会扩散成整个系统的重新部署压力。

哪怕我只是改了一点博客功能,也得把整套应用重新构建、重新打镜像,然后再把同一个大块头重新推上去。在这种结构下,变化频繁的区域最先失去实验速度。更大的问题是,那些需要长期保持连接的服务,也会被拖进同一个部署影响半径里。

比如像 backbone-service 这样靠近 WebSocket、SSE 和事件流的服务,如果也被普通功能修改带来的同样压力反复摇动,线上体感会立刻变差。所以对我来说,服务拆分最终更像是一个「什么可以单独部署」的问题,而不是「谁拥有哪个 API」的问题。

Jenkins 如何决定部署目标

服务运行在 Kubernetes 上,CI/CD 由 Jenkins 负责。

这篇文章不会展开太多实现细节,而是重点说明我在 CI/CD 视角下如何切分部署边界。

在当前流水线里,部署的起点是 BUILD_MODE 和按服务拆开的 boolean 参数。使用 all 时可以全量部署;在 auto 模式下,可以只部署手动勾选的服务,也可以在什么都不选时根据 Git 变更自动检测目标。也就是说,手动触发时既可以全量,也可以部分部署;自动触发时则可以根据改动文件收窄目标范围。

可选服务包括 auth-servicebackbone-servicestorage-serviceai-servicegame-serviceblog-servicelegacy-service。仓库仍然是一个仓库,但部署目标从一开始就是按服务维度选择的。

自动检测逻辑也比较简单。它会查看 git diff,再把 modules/{service} 下的改动映射回具体服务。不过这里有一个很重要的例外。如果公共模块 common(jspring) 发生变化,流水线会把它视为公共基础发生了波动,并重新构建所有服务。只有这样,common 的变化才不会被局部部署掩盖掉。

我认为这一点非常关键。选择性部署的优势在于「只动需要动的部分,而且更快」,但如果低估了公共契约的变化,它反而会变得更危险。

制品也按服务拆开

实际构建流程也遵循同样的思路。先把 common publish 到本地 Maven,然后只对被选中的服务按顺序构建到 bootJar。之所以没有选择并行构建,也是一个很现实的判断。在当前阶段,减少 Q 类生成问题和 Kotlin daemon 冲突会更稳定。

镜像构建在 kaniko 容器里完成,这一步同样采用串行执行,而不是并行。只看速度的话,并行会显得更漂亮,但在 Omnilude 目前这个阶段,减少资源冲突、让失败点更容易读懂反而更重要。

镜像名也按服务分别映射。比如 auth-service 对应 jongmin-authai-service 对应 jongmin-aiblog-service 对应 jongmin-blog。这样一来,在镜像仓库里也能直接看出部署单位,之后追踪某个具体服务也会更容易。

文件夹结构也与运维单位对齐

如果说 Jenkins 决定了要构建什么,那么 Kubernetes 就把这个结果固定成真正的运维单位。当前结构是 kubernetes/services/{service}/{dev|prd}。每个服务都有自己的 DeploymentService,而 ingress 则在 ingress/{env} 下按环境分别管理。

这种结构的好处是解释起来很容易。想看某个服务是怎么部署的,直接看那个服务的文件夹就可以。代码边界和 manifest 边界大体一致,所以从运维角度看,责任归属也没有那么模糊。

路由同样遵循这个思路。在 dev-api.omnilude.comapi.omnilude.com 下面,各服务通过 path prefix 接入。也就是说,服务拆分并不是仓库内部的一个抽象概念,它一直延伸到了真实的 API 所有权和 ingress 路由上。

基本的稳定性装置也是在这里落下来的。每个服务的 Deployment 都有 readinessProbelivenessProbe,部分生产服务还加上了 HPAPDBtopology spread constraint。虽然不是所有服务都已经具备同样等级的运维护栏,但至少现在已经能够按服务分别管理:哪些稳定性装置加到了哪里、每个服务推进到了什么程度。

让部署流程保持简单

当前的部署流程比看上去更直线。

  • Jenkins 先决定这次要部署哪些服务。
  • 只构建被选中的服务,并生成镜像。
  • kubectl apply 应用各服务的 manifest。
  • kubectl rollout restart 推出新版本。
  • kubectl rollout status 等待各服务启动完成。
  • 把开始、成功、失败状态发送到 Slack。

这里更重要的不是炫技,而是可读性。在现在这个阶段,我更愿意让任何人都能清楚看出:部署了什么、失败在什么地方、系统正在等待什么。更复杂的部署策略以后还可以叠加,但如果连当前结构都很难解释,运维反而可能变得更不稳定。

应用之后发生了什么变化

这套选择性部署结构带来的最大变化,是一个小改动不再强迫整个系统重新部署。实验速度更重要的 ai-service、希望快速打磨产品功能的 blog-service,以及相对独立的游戏侧工作,现在都可以按不同节奏推进。

另一个收获是可解释性。现在如果有人问 Omnilude 目前的运维结构,我已经可以把服务列表、镜像名、Kubernetes 文件夹、ingress 路径以及 Slack 通知流程连成一条线讲清楚。即使是个人项目,我也认为这种可解释性很重要。运维结构不能只是能跑就结束,它为什么要这样拆,也应该能够讲得通,这样下一轮改进才接得上。

当然,当前结构并不是完成形。自动回滚部署后 smoke test基于指标的部署闸门 现在都还没有补上。目前真正起作用的核心,还是 rollout 检查和 Slack 汇总。

收尾

现在这套结构还谈不上是什么大型 CI/CD 平台,但至少「仓库只有一个,部署按服务拆开」这个原则,已经从 Jenkins 参数、镜像名、Kubernetes 文件夹、ingress 路径一直一致地延伸到了 Slack 通知。

下一步也很明确。要在这套结构之上,真正补上自动回滚、smoke test、以及更好的运维告警等护栏。这部分目前还在进行中,等实际建设完成后,我会在下一篇文章里继续整理。