前言
随着业务的快速发展,我们对私有化部署的流程上也发现不少可以优化改进的地方,今天主要和大家分享一下我们私有化部署方式的演进过程: 第一阶段:以脚本为核心的部署方式(docker文件 + 脚本 + 环境变量) 第二阶段:以 Jenkins 为核心的部署方式(docker文件 + jenkins + 配置文件) 第三阶段:以 Jenkins 和 docker harbor 为核心的部署方式(docker harbor + jenkins + 配置文件)
第一阶段
在我们做第一个私有化部署的系统时,我们的系统架构相对简单,包含的需要部署的组件不超过10个。所以对于部署方案,我们的考虑是:
- 能支持横向扩展。
- 能支持快速部署启动一个开发环境或者 debug 环境。
- 能支持快速部署一个新的客户环境。
所以在第一阶段,我们的做法是用一个很牛逼的脚本,一键启动所有组件。其中组件需要相互关联的部分,都通过变量设置在脚本中,从而避免参数设置不一致导致的部署问题,例如:
DB_ENGINE=mysql
DB_HOST=192.168.0.3,192.168.0.4
DB_PORT=3306
# Start backend server
start_backend_server.sh -e db_host=$DB_HOST -e db_port=$DB_PORT
# Start async worker
start_async_worker.sh -e db_host=$DB_HOST -e db_port=$DB_PORT
...
...
这个方案在我们只有一个产品的时候是非常方便的,操作步骤简单直接,且维护成本不高,也很灵活。
但是随着公司的产品不断增多,需要进行复用的组件和工具也越来越多。例如两个产品的部署过程中,可能都会需要部署 redis、mysql、RabbitMQ,在每个项目中都放一个相同的 start_redis.sh 的脚本显然不是一个很好的方案,这样会导致后续对 redis 的版本升级或者参数调优无法在各个项目中应用。基于 DRY (Don’t Repeat Yourself) 原则,我们开启了第二阶段的部署方案升级。
第二阶段
第二阶段主要是为了解决几个第一阶段的问题:
- 脚本更新不及时。内部测试环境在更新部署的时候使用的是 k8s 集群,于是没有使用脚本进行部署。所以常常出现代码进行了修改,但是只更新了 k8s 的服务配置文件,没更新私有化部署脚本的问题。
- 使用方法和文档更新不及时。当某个组件的部署方式发生变更时,需要对应更新每一个产品的部署文档,常出现一个组件更新了,但是只更新了一个产品的文档,其他的就忘了。
- 脚本复用性差。随着产品的不断增多,每个产品都有一部分重复的基础组件的部署代码,这部分代码无法在产品间进行复用。
- 容错性差。当部署的过程中遇到错误时,在几百行 shell 代码中找到问题的难度大。
- 难以保证幂等性。shell 的编写对大部分人来说是一个过程性的编写方式,难以做到每个步骤都是幂等的,在执行脚本的过程中对执行者要求比较高,需要考虑重复执行脚本带来的副作用。
针对这些问题,我们也考虑了几种不同的解决方案:
备选1: 使用docker编排引擎
使用 k8s 或者 docker swarm 进行容器的编排,用编排文件替代部署脚本。这个方案也是我们在内部测试环境中用的方案,方案的好处很明显:
- 可以利用 k8s 的负载均衡完成集群内部的横向扩容,而不需要额外的负载均衡。
- 可以更充分地利用 k8s 的资源调度,最大程度利用宿主主机的资源。
- 可以使用声明式的配置方式,声明最终希望达成的状态,而由编排引擎来生成、执行相应的操作。同时可以对配置文件进行版本管理,达到 Configuration as Code 和 Infrastructure as Code 的方式。
- 可以保证在测试环境中的部署方式和私有化部署环境中一致,从而通过在测试过程中强制更新 k8s 保证代码和部署流程的一致性。
除开业界已经认同的这些好处之外,也有一部分实际情况的制约:
- 目前 k8s 集群的部署和维护在国内的金融和实体企业仍然是相对空白的,也缺少对该类技术的了解。
- 对于生产环境上使用的 k8s 集群,机构往往希望可以有专业的商业支持服务,包括但不限于证书的配置调试、集群的扩缩容、异构硬件的支持、性能问题的排查、网络问题的排查等。这个支持的成本和其带来的增益比还是不足以支撑这个方案的。
备选2: 基于成熟工具的编排部署(Jenkins + ansible)
为了解决上一个备选方案的制约,我们考虑以一个相对成熟的工具来实现类似的功能。此时我们考虑使用 Jenkins 来进行脚本的封装和管理,同时使用 ansible 和统一的配置文件来实现 k8s 能带给我们的好处。
集群的灵活调度和横向扩容
为了实现灵活的集群内部的横向扩容,我们使用一个集中式的网关进行网络请求的转发,如下图:
所有组件内部的请求访问都经过一个 Nginx 网关进行转发,这样如果我们需要对某个服务进行宿主主机的变更,或者需要增加额外的实例进行横向扩容,都可以在不更改其他服务配置的前提下进行。
声明式的配置方式
为了能达到声明式的配置,我们将配置文件简化成了类似的格式:
main.yaml:
service1:
image: redis
deploy_hosts:
- 192.168.0.2
- 192.168.0.3
- 192.168.0.4
port: 6379
memory_limit: 1G
service2:
image: mysql
deploy_hosts:
- 192.168.0.2
port: 3306
而我们的启动脚本也统一读取这个配置文件来知悉他依赖的组件部署在哪,应该通过哪个端口进行访问。这样以来,我们便实现了 SSOT (Single Source of Truth),不会出现迁移修改组件后,依赖其的服务无法使用的情况。运维同学在看到这个配置文件之后,也可以很直观地看出来每个服务运行在哪、有什么参数配置。
同时我们把启动脚本转换成了一个 Ansible 的脚本。Ansible 的一个特点便是声明式、可复用。我们的 Ansible 脚本从 main.yaml 中读取服务希望达到的状态后,会进行所有相关的操作,包括生成配置文件、拉取最新的 docker、启动 docker、检测是否需要初始化数据、检测服务状态符合预期等。这样一来,我们也实现了 Configuration as Code 和 Infrastructure as Code 的部署方式。
界面化的封装
如果要迁移至 Ansible 和配置文件的这个方案,就必须要解决测试环境的问题,我们希望这个部署方案是和开发、测试的部署方法一致的,这样才能保证部署流程和代码的一致性。在这个基础上,为了简化研发的使用,我们对 Ansible 的脚本和配置文件用 Jenkins 进行了封装。
在一个 docker 包中包含了:
- Jenkins 的所有所需插件和配置,例如每个任务需要调用哪个ansible脚本,每个任务需要触发哪些依赖的任务
- 所有的 Ansible 部署脚本
- 默认的配置文件
- 部署需要用到的工具包,例如 ssh、ansible、openssl 等。
这样一来也大大减少了运维可能出错的操作步骤,同时对于一套标准系统的部署时间也可以减少到小时级别。甚至内部的测试环境部署可以在30分钟内完成,为后续的自动化测试打下基础。
公共组件封装
我们最终选择了方案二(Jenkins + Ansible)作为我们的解决方案,我们把内部所有需要进行私有化部署的系统都迁移到了基于 Jenkins 的部署系统上。
对于常用的公共组件包括储存组件:redis、mysql、oracle、elasticsearch、minio 等,所有产品都可以遵循最佳实践进行部署复用。而对于部分业务中间件,例如文档解析、NLP提取、格式转换等服务,所有产品也可以快速接入使用。
值得一提的是,其他的日志、指标采集、监控告警、看板等功能,也成了整个私有化部署架构中的共享组件,赋能保证了产品的稳定性和可运维性。
不足之处
在这套方案中,仍有几个细节点是待改善的:
- 部署工具本身的版本管理。Jenkins 和部署脚本的内容是会随着产品的迭代而变化的,例如一个月前的产品使用的是 Elasticsearch 6.8,目前的产品使用的是 Elasticsearch 7.6,这时产品的升级过程中,就需要对部署脚本做一定修改,如果使用了错误的部署工具,可能会导致部署的产品出错。
- docker 镜像文件的传输和管理。由于在私有化部署的环境中,有许多环境是完全不能接入外网的,所以我们的 docker 是通过
docker save -o xxx.tar <image>
进行导出,然后将 tar 文件通过 jenkins 下载、拷贝、并加载到目标服务器上的。如果能接入外网,就直接从一个 http 服务器下载并加载镜像文件。- 这个方案的第一个问题是 docker 的版本号不好管理。因为对于 tar 文件的每个版本都归档会造成巨大的储存资源的压力,每个版本都保存下来的话,对于一个集成了 CI/CD 的产品,每天会产生10余个部署文件,无疑是一个浪费。
- 第二个问题是更新时较慢。每次更新代码时,及时只更新一行代码,可能也需要下载1G的 docker 文件,导致在私有化环境中更新速度很慢。
第三阶段
针对第二阶段的一些不足之处,我们也做了更进一步的优化:
- 除了使用 docker 文件进行打包和传输,我们也引入了私有化 docker 镜像仓库进行 docker 包的管理。利用 docker 镜像仓库本身的 TAG 功能进行多版本的归档。由于 docker 包的重复文件层不会重复储存,所以可以支持对所有产品版本的归档储存。
- 对于部署工具本身,也同时加入 docker 的镜像仓库中,用 TAG 进行多版本的归档管理,每次部署时,要求部署工具和代码包的版本作为一个统一整体进行部署。
三阶段的改动相对于第二阶段并不算大,主要工作是:
- 将相关 docker 的打包 CI 任务中,增加推送到 docker 的镜像仓库的配置。
- 在默认配置文件当中,原本的 image: 配置字段设置为基于镜像仓库的地址
- 在 ansible 的启动脚本的 docker_container 模块中,增加 pull 选项的配置,决定是否在重启的时候拉取最新的镜像。这一步是相对 tricky 的,因为我们仍然要兼容不能访问私有化镜像仓库的环境,这些环境中 pull 需要设置为 false,而其他环境需要设置为 true。
总结
我们希望在满足 DRY(Don’t Repeat Yourself)、SSOT (Single Source of Truth)、CaC(Configuration as Code)、IaC(Infrastructure as Code)的原则基础上,尽量简化和自动化部署流程。虽然在实际项目中推广 k8s 的使用遇到了一些困难,但是我们仍然曲线救国,利用老将 Jenkins 和 Ansible 达到了目的。