Skip to content

使用 阿里云ECS 和 github actions 为 SQLFlow 构建高效 CI 系统

weiguoz edited this page Oct 22, 2020 · 3 revisions

背景

SQLFlow 是一个把 SQL 程序编译成端到端机器学习workflow的编译器。SQLFlow 扩展了 SQL 语法,使用简练的 SQL 语句即可描述常见的机器学习从数据预处理到模型训练预测等各个环节的任务。为了确保 SQLFlow 可以稳定高速的持续开发迭代,就需要一个稳定高效的 CI 环境。SQLFlow 在早期开发阶段一直使用 Travis CI 作为 CI 系统,每个 Github Pull Request 的每个 commit 都会触发 CI 的执行。但随着项目逐步发展,功能完善,支持更多的DB系统,更多的机器学习框架,更多场景案例的积累,导致使用 travis-ci 的 CI 检查时间很长,如果同时有大量的 Pull Request,会需要等待非常长的时间才能确定 CI 检查的结果是否正确,极大的降低了迭代的效率。

为什么从 travis-ci 迁移到 github actions

Travis-ci 是一个非常流行的免费 CI 系统,非常多的在 github 开源的项目都在使用。它支持大部分主流编程语言的 CI 模版环境,用户使用 github 账号可以直接登录,创建自己的 CI 任务,并使用 travis-ci 免费提供的虚拟机资源运行 CI 任务。Github Actions 是 Github 最新提供的 CI/CD 系统,可以和 Github 项目无缝结合,最近也有不少项目在使用。SQLFlow 考虑切换到使用 Github Actions 的原因包括:

  1. Travis-ci 对每个项目都有并发任务数的限制,如果同时有多个 pull request 提交,只能等待前面的 CI 任务执行完,才能开始执行,如果需要有更多的并发执行资源,就需要购买 travis-ci 的 一些 plan,通常会比较昂贵。
  2. Travis-ci 每个 CI 任务都是启动全新的虚拟机,SQLFlow CI 需要能覆盖 SQL 程序编译成 workflow 然后在 Kubernetes 上执行的单测。这样每个任务,都需要先安装一次 minikube,pull Docker image,build Docker image,花去了大量的时间,并且 SQLFlow Docker 镜像体积较大,build和pull都会占用很多时间。
  3. 如果可以把 SQLFlow 的编译测试环境直接作为虚拟机的镜像启动虚拟机,则可以省去 pull Docker image, build Docker image的大量时间,每次 CI 任务只需要安装运行时依赖即可。然而 travis-ci 要支持自定义的主机,则还需要付费升级成为 "Enterprise" 版本。

SQLFlow 使用 Travis-ci 的 CI 流程简介

我们可以通过 SQLFlow v0.4.0 版本中的 .travis.yml 配置文件来了解 SQLFlow 使用 travis-ci 的方式: https://github.com/sql-machine-learning/sqlflow/blob/v0.4.0/.travis.yml

CI 任务被分成了多个并行的 "Job" 包括 "mysql", "hive", "maxcompute"等,这里以 "mysql" Job 为例,执行步骤是:

  1. 构建 sqlflow:mysql 镜像,用于启动 MySQL 服务并预置了测试用例中的公开数据集,如iris。参考目录 docker/mysql
  2. 执行 scripts/travis/build.sh,这个脚本会构建 sqlflow:devsqlflow:ci 等镜像,后续的单元测试就会使用 sqlflow:ci 这个镜像执行。其中 sqlflow:dev 包含了 SQLFlow 基础的编译环境,镜像中安装了 Go, Java, Python的相应版本。sqlflow:ci 镜像则包含了执行但愿测试所需要的运行时依赖,比如 Tensorflow.
  3. 使用 sqlflow:mysql 启动一个容器,将 MySQL 服务端口转发到主机,供单测使用。
  4. 使用 sqlflow:ci 启动一个容器,在容器中执行单元测试的脚本 scripts/test/mysql.sh 执行单测。

其他的几个 job 也有类似的步骤,而且 "workflow" 这个 job 还需要先安装 minikube 会消耗更多的时间。这样,由于 travis-ci 对每个 job 都会重新启动一个新的虚拟机实例,每次执行实际的单元测试之前,有一半甚至以上的时间都是在构建 Docker 镜像,这个步骤,可以通过使用自定义主机省去。

使用自定义主机减少不必要的 Docker Build 环节

Github actions 可以支持用户使用自己的云端主机作为 CI 任务执行的 worker,并且这个功能是可以免费使用的。这样,我们可以通过在阿里云ECS上创建一个虚拟机,预先安装好运行 SQLFlow 单测需要的编译,运行环境,这样每次运行单元测试就不需要每次都执行 docker build,只在发布最新 Docker 镜像的时候 build 并 push 镜像到 DockerHub。

首先我们登录阿里云控制台,创建一台ECS,选择地域为香港,因为我们在使用CI发布 Docker 镜像到 DockerHub 的时候,香港节点可以获得最快的速度。然后根据 CI 任务的负载选择合适的机器配置,这里我们选择了 4vCPU 8GiB的机型。后续也可以根据机器实际负载升级或降低配置。ECS 创建完成后,我们登录到机器上开始配置 SQLFlow 的编译测试环境,安装对应版本的Docker, Go, Python, Java, protobuf, minikube等。机器配置的方法可以参考 docker/dev/install.shscripts/travis/install_minikube.sh。为了验证当前机器的环境可以正确的执行 SQLFlow 的所有单元测试任务,我们先在当前机器上手动运行单测,以便补充安装需要的工具。

完成开发环境准备之后,我们就需要把当前的机器注册为 Github Actions 的一个 worker。在 Github SQLFlow项目的 Settings 页面(只有管理员权限才可以查看修改),点击左侧的 Actions 标签,会进入 Github Actions 设置页面,点击 Add runner 按钮,会出现一个解释如何部署 worker 的文档,在这个页面中选择 Operating System: Linux, Architecture: x64,对应 ECS的操作系统和CPU类型。然后复制下面的安装命令即可在把当前 ECS 注册成为 SQLFlow 项目的 worker。注册成功的 worker,可以在 Settings -> Actions 页面下显示在列表中。

完成一台 ECS 的配置之后,我们可以在阿里云控制台中将当前 ECS 保存一个镜像,再启动更多的机器,增加更多的并发 worker。通过镜像启动的新的 worker 只需要简单配置新 worker 的 hostname,/etc/hosts即可,注册为新的 Github Actions worker。

完成 ECS 机器的配置之后,我们就可以开始编写 Github Actions的 CI 任务的 workflow 配置文件,参考:https://docs.github.com/en/free-pro-team@latest/actions/quickstart

SQLFlow 目前的配置文件参考:https://github.com/sql-machine-learning/sqlflow/blob/develop/.github/workflows/main.yml 。Github Actions 可以定义 CI 任务是一个具有依赖关系的 workflow。我们的 CI 任务有执行单测和发布镜像/二进制两大部分,其中发布镜像步骤要依赖单测的执行成功。

  1. 执行单测,包括 job: test-mysql, test-hive-java, test-workflow。可以看到这些 job 都标记了 runs-on: [self-hosted, linux] 表示运行在 "self-hosted" 的机器上,也就是我们自定以的 ECS 的机器。Github Actions 会根据这项配置,在我们注册的 worker 上执行 CI 任务。
  2. 发布镜像,包括 job: push-images, linux-client, macos-client, windows-client,分别发布 Docker 镜像以及sqlflow CLI工具的Linux,MacOS 以及 Windows 版本。这些 job 由于不需要执行单测,就直接使用 Github Actions 的公用 worker 执行,所以标记为 runs-on: ubuntu-latest

为每个 CI 任务构建独立的运行环境

SQLFlow 的 CI 任务除了需要基础的编译环境,如Go,Python,Java等,还需要运行时的许多 Python 包依赖,比如Tensorflow, numpy等。基础编译环境通常不会改变,然而 SQLFlow 的开发迭代必然会不断的更新运行时的依赖,比如升级Tensorflow 的版本等。这样如果我们在 ECS 机器上预先安装这些运行时依赖,随着 SQLFlow 的迭代,这些依赖不能随之更新则会带来不必要的麻烦。所以,我们将这部分依赖单独用一个脚本,在每个 CI job 执行之前执行,并且将这些 Python 依赖安装到 Python virtual env下,保证多个 job 执行不会互相影响。参考 scripts/test/prepare.sh 这里。

设置和使用保密信息

CI任务中会使用一些需要保密的信息,SQLFlow 就会用到阿里云 MaxCompute 的账号密码运行 MaxCompute 的单元测试,还有 DockerHub 的账号密码用于发布最新的 SQLFlow Docker 镜像。我们不希望这些敏感的信息能被公开看到。Github Actions可以非常方便,安全的配置和使用这些信息,只需要:

  1. 在 Github Settings -> Secrets 中添加保密信息,点击 "New secret" 按钮,填写信息的 key (例如:SECRET_KEY) 和 secret value。
  2. 在 Github Actions YAML 配置文件中的 env 设置部分,指定 SECRET_KEY: ${{ secrets.SECRET_KEY }},后续使用环境变量 $SECRET_KEY 就可以得到安全的保密信息。

清理垃圾文件

SQLFlow 的单元测试通常需要一个 DBMS 的实例,比如一个MySQL server 或者 Hive Server。在使用 Github Actions时,我们还是沿用了使用 Docker 镜像的方式启动 MySQL 实例和 Hive 实例,这样,随着容器的销毁,测试用的 DBMS 中的数据也会随之销毁。ECS 的磁盘空间不会随着 CI 任务的不断执行而减小。

另外,SQLFlow 的 test-workflow 的执行也会生成一些 Argo workflow, Kubernetes pods, Docker containers。我们需要定时清理掉这些内容保证 ECS 的磁盘空间不会一直增长导致错误。我们在每台 ECS 机器上配置了 crontab,定时清理过期的 Argo workflow,Kubernetes pods,Docker containers以及Docker images。

总结

Github Actions 提供了免费的自定义 worker 的方式,SQLFlow 使用此方式极大的减少了构建 Docker 镜像作为单元测试执行环境的时间(CI时间从1个小时减少到了30分钟以内)。如果处于预算考虑,您也可以使用 Github Actions 提供的公共资源,免费享有5个并发任务的执行,在资源上也相对 travis-ci 有一定的优势。