微服务 + CI/CD + Kubernetes 实践
公司的应用架构一开始就选定了微服务+Kubernetes+Istio,整个开发环境都在内网,使用 Jenkins 做半自动化的 CI/CD.
前后端架构
前后端都遵从同一套设计思想:分层架构,提升复用能力,以后端为例,它的层次结构如下:
- 基础层:封装了开源的各种数据库连接器(mysql/redis/es/oss/message_queue)、系统配置(apollo-config/log/tracing)、业务API(短信邮件API、视频处理API、收付款API、发票API)等通用的组件,向上提供公司定义好的统一 API。
- 基础层屏蔽了第三方库的差异,开源库可以随时换,而上层代码不需要做任何修改(当然前提是 API 设计得足够合理)。
- 脚手架层(BaseXxx):基于基础层,定义了如下几类应用的框架结构(BaseClass):
- 测试器(BaseTester):灌假数据,设定测试参数,仅在测试环境使用。
- 微服务(BaseService):负责具体的业务处理,大部分微服务都只提供 gRPC 接口,供内部 Gateway/Service 调用。只有少数业务关联性很强的微服务才同时充当 Gateway,对外提供 HTTP API。
- 网关(BaseGateway):API 聚合层,用户的请求入口。网关不承担任何业务,只是单纯地将用户的 HTTP 请求转换为 gRPC,转发给具体的微服务进行处理。
- 通用工具(Tools): SQL 实用工具类,安全工具类(证书密码等)、缓存工具类、文件处理工具类、RPC工具类等等。
- 一次性任务(Worker):每个 Worker 都会自动定时执行一大堆 Job,这类 Job 主要负责帮微服务清理一些数据,比如每五分钟检测一下某类数据是否过期?过期就清理掉关联的所有数据。
- 中台:继承了脚手架提供的基类(BaseClass),按功能实现了一些比较通用的中台服务。比如权限认证服务、用户服务、订单服务、广告服务等。
- 每个微服务,都包含有:微服务自身、测试器,部分需要异步处理的微服务还有 Worker。
- 因为 Service/Tester/Worker 都还包含一些通用的逻辑,不方便拆分,因此它们都保存在同一个 Git 仓库中。
- 在 CI 流程中,每微服务就需要构建出 Service/Tester 这两个镜像,部分微服务还需要构建 Worker.
- 契约层:中台的所有 RPC 接口定义,单纯抽离成一层,保证 RPC 接口的稳定性。
- 业务层:这一层的东西,才涉及到产品的核心业务逻辑。代码结构和中台是一样的,但是这一层的微服务,同时也对外提供 HTTP API,因此它是「微服务+网关」。
- 网关层:单纯的 API 聚合层,它聚合了中台的部分 API,提供给外部访问。
这种结构的目的,就是提升代码的复用能力,把应用层能复用的东西,都抽离到底层去了。
好处是非常敏捷,开发新业务时,因为底层的东西(中台、脚手架)都可以直接拿来用,开发速度就非常快。
但这要求我们的基础层API一开始就设计得足够好,因为越到后期,API 的影响面就越大,几乎无法修改。
而且为了将各微服务之间的接口稳定下来,还将所有微服务的接口定义给抽离出了单独的一层:Contract 契约层,里面只包含所有微服务自己的接口定义。
总结:这样一个后端架构,我不是架构师也不写后端,见识目前也不够,不知道该怎么评价它的好坏。
但是通过和朋友交流、网上搜索资料,我了解到目前类似的高复用能力的架构确实是非常少见。
一、CI/CD
前面介绍的这么一个后端架构体系,总共有上百个 Git 仓库,而且公司内部的 dotnet 依赖库链路,深的有五六层。。
这导致一个问题,就是底层的任何代码库的更新,都需要(以基础层的更新为例):
- 运行该代码库的单元测试,构建出 nuget 包,推送到内网包仓库
- 运行「脚手架层」所有代码库的单元测试,构建出 nuget 包,推送到内网包仓库
- 运行「中台层」+「业务层」所有代码库的单元测试、集成测试,构建出容器镜像,推送到内网镜像仓库。
总而言之,必须按照依赖链的顺序,从底层开始层层向上进行依赖包更新、单元测试集成测试,构建出 artifact 保存。
最终完成对上百个 Git 仓库的依赖更新与测试,才算完成一次更新。
这样一个体量和复杂度,长期人工更新是不可能的,手动按顺序一个个去调用各 Git 仓库的流水线,显然也不太现实。。。
必须使用复杂的调度策略,按依赖顺序去按层次启用各 Git 仓库的 CI 流水线。
我调研了目前流行的 Jenkins/Gitlab-CI/Drone/Tekton 等工具,基本都实现不了这个需求。
1. Continuous Integration 方案
目前我们是使用 Jenkins 作为 CI/CD 工具,通过 Python 调用 Jenkins API,实现了上述复杂的任务批量调度功能。
但是 Jenkins 任务数量过多,为了回溯构建记录,构建记录也会越来越多,结果用一段时间,Jenkins API 的超时概率就开始不断增长,导致这个批量调度任务的功能经常因为 API 超时而失败。。
这有 Jenkins 自身性能的缺陷,也有我们磁盘 IO 太垃圾的原因。但是总的来说我现在不太喜欢现在这套基于 Jenkins 的系统,一直想换掉它。至少后端构建流水线想换个新方案。
最近找到个 Argo Workflow 可以基于 DAG 有向无环图来定义工作流,感觉可以很完美地解决这个问题。正在深入调研。
2. Continuous Deployment 方案(测试环境)
构建完成后,需要通过一个「后端版本快照」的功能,将所有容器镜像的最新版本号扫描出来,然后生成它们的 k8s yaml 配置文件,保存到 git 仓库中,并打上 tag(时间戳)。
这样能实现随时部署任意时刻的后端版本。
k8s 配置生成方面,我们目前是使用的自定义 yaml 模板,通过字符串替换的方式进行填充。以后可能会考虑使用 kustomize/helm。
最后通过一个部署的任务将 yaml 配置应用到测试用的 k8s 集群中,这样就完成了一整套完整的后端测试环境的部署。
测试开发人员目前一共有近三十台测试服务器(k8s 节点),每台都可以通过上述任务,一键部署一套完整的后端测试环境(数据库和一整套微服务)。
这三十多台测试节点的测试环境都是互相隔离的,互不影响。这保证开发人员、测试人员都能互不干扰的使用各自的后端来测试。
包括自动化测试时,也是通过这个一键部署的功能,保证每天测试的都是最新的后端微服务的。
2.1 Kubernetes 配置生成
k8s 配置生成,目前这是完全使用 Python 实现的,代码结构比较乱。我一直在找更好的方案。
最近觉得 helm+kustomize 可能是个比较好的方案,待深入调研。
3. 自动化测试方案
前面提到我们一套微服务系统,现在有一百多个 Git 仓库,非常深的依赖层级。
而部署一套完整的测试环境,Service+Tester+Worker+数据库,一共有 120+ 个容器。
如此大的体量,每次更新,尤其是底层依赖的更新,一定是需要做更高层次的测试才能放心的。单纯的有单元测试+集成测试远远不够。
公司的测试团队一直有 4-7 人,专门负责:
- 使用 airtest 编写前端 APP 的 UI 测试用例。
- 使用 Python 编写后端的 API 测试用例。
然后运维这边编写调度脚本,在每天晚上:
- 自动地运行所有的 UI 测试用例。
- 目前估计已经有上千个 UI 测试用例了,两套后端系统+两到四台手机(部分测试需要多台手机,如聊天),需要连续跑 13-15 个小时。
- 自动地运行所有的 API 测试用例:
- API 测试用例比 UI 测试更多,目前估计已经好几千个用例了,现在也是分了两台测试服务器跑,大概也要 7-9 小时。
除了这些自动化用例,上线前部分重点功能,还需要手动测试,比如:
- 充值支付:需要调用微信和支付宝,目前是手工测试。
- 其他重点更新,或者不方便自动化测试的功能。
4. GitOps
GitOps 的 CI/CD 貌似只适合一些扁平的应用,而对公司这种分层结构的代码就有点无所适从。
单纯 GitOps 的 CD 我们在生产环境倒是用得很舒服,现在使用的是 fluxcd,考虑到有个好看的 UI,后面也可能换成 Argo CD.
但是向 Jenkins-X 的这种全生命周期管理,一个工具管理所有环境,我们公司目前还没这么大的魄力。
云上生产环境部署
生产环境,目前使用的部署方案是 flux+flagger+istio。
在阿里云上开了个专门的 yaml 版本快照仓库,通过 fluxcd 将该仓库的 yaml 定时同步到生产环境的 k8s 集群中。
按依赖顺序进行升级
在升级后端微服务时,后端要求我们按微服务的调用顺序进行反向升级,也就是先升级服务端,再升级客户端。理由是这样的:
- 服务端的新版本会同时提供旧 API 和 新 API,因此是可以无缝升级的。
- 但是客户端的新版本会直接使用新 API 发起请求,如果先升级客户端,那新 API 不存在就会报错。
我们在本地依据后端提供的调用链,定义好了升级层级,通过 Python 实现了自动化地,按依赖顺序分批升级到新版本。
说实话类似这样的「微服务编排」功能,目前也完全没有在网上找到相关案例。
貌似各种开源的产品如 kubevela,都假设各应用之间,是完全互相独立的,不存在依赖关系。或者说它们是手工管理这个依赖关系。
日志、监控、报警
日志和监控都直接使用的阿里云服务。
日志报警是自己写了些报警规则,让阿里云日志服务把关键错误日志抓到,报警到钉钉。(可以报警出详细日志,记得 SELECT content, COUNT(*) as total GROUP BY content 就行)
监控是使用了阿里云的云监控,以及 prometheus 监控及告警。
云监控和日志报警的规则,都是通过 pulumi 进行自动化配置。