作者 | Shalabh Chaturvedi
译者 | Sambodhi
策划 | 褚杏娟
背   景
我们使用 Serverless Dagster Cloud 来开发和部署 Dagster 代码,无需设置本地开发环境或任何云基础架构。当提交更改到 GitHub 时,GitHub Action 会直接构建和部署代码到 Dagster Cloud,然后可以在界面上查看并与 Dagster 对象进行交互。Dagster Cloud 可以利用一个远程环境来共享部署,并且可以利用自动创建的临时环境与合作者协作,实现了个人本地开发和共享远程环境的结合。
最初,我们在 Dagster Cloud Serverless 中使用了标准的基于 Docker 的构建流程,但很快发现这个流程会使“编辑 - 部署 - 运行”的循环变得非常缓慢。为了将过程加速,我们实现了一个在 Docker 镜像之外运送代码的系统。本文将描述我们分析的问题、选择的解决方案以及在此过程中做出的各种权衡。
Docker 镜像存在的问题
当我们在 GitHub 上构建 Docker 镜像并将其部署到 Dagster Cloud 时,每次提交需要 3~5 分钟才会在 Dagster UI 中显示。在每次迭代中,无服务器开发人员通常会对代码进行微小更改,但是必须等待 3 分钟以上才能看到该更改的效果,这很快就会变得非常烦人。我们分析了“当你更改一行代码并提交时会发生什么”,并发现以下问题:
你可以看到,有两样东西花了最多的时间:
  • 构建 Docker 镜像(60 ~ 90 多秒)
  • 部署 Docker 容器(90 秒)
让我们分别看一下这两个问题。
构建 Docker 镜像
有一些需要注意的关于构建 Docker 镜像的事情:
  1. Docker 镜像由堆栈中的多个层构成,每层由 Dockerfile 中的一部分命令构建。
  2. 每个层有一个哈希标识。
  3. 上传镜像到注册表时,只上传注册表中不存在的层(由哈希标识确定)。
  4. 在 GitHub 构建机上使用 GitHub Actions 缓存 重新构建镜像会将所有未受影响的层从缓存中拉取到构建机上。请注意,如果你的项目中有大量不会更改的依赖项,它们仍将在构建过程中从缓存中复制到构建机上。
  5. Docker 构建不是确定性的。如果使用完全相同的内容构建两个镜像,可能每次都会产生不同的哈希值。(虽然与本文直接相关性不强,但我们想观察这个意外的结果。作为一个极端情况,请考虑,一个刚构建的大层与已存在于注册表中的层完全相同,但仍然会作为一个新的层被上传。
启动 Docker 容器
关于启动 Docker 容器,我们使用亚马逊云科技 Fargate,需要 45~90 秒的时间来提供和启动一个镜像。它不提供任何镜像缓存。启动一个新的容器会将所有层从注册表下载到已提供的容器中。
其他限制
在 Docker 镜像构建和启动后,我们运行用户的代码来提取元数据,并在 UI 中显示。这是不可避免的,并且可能需要几秒钟、30 秒或更多时间,具体取决于如何计算元数据(例如可能会连接数据库以读取模式)。此代码服务器保持活动状态并服务元数据请求,直到推送代码的新版本,然后启动一个新的容器。
我们有一个关键要求是可重复性:我们需要能够多次重新部署完全相同的代码和环境。使用 Docker 镜像哈希作为代码和环境的标识符非常适合这个要求。
我们的备选方案
下面是我们探索和讨论过的一些备选方案:
  1. 切换到 EC2 以加快容器启动速度。这会增加我们的运营负担,需要我们预先预配、监控和扩展集群。我们仍然会面临 Docker 构建速度慢的问题。
  2. 切换到其他的 Docker 构建系统,例如 AWS CodeBuild。这需要更多的实现工作,并与 GitHub 进行更深入的集成。不确定收益是否值得。
  3. 切换到 AWS Lambda,启动时间更快。Lambda 环境附带其自己的基础镜像,如果需要进行定制则更加困难。它还对执行时间施加了 15 分钟的限制,这将需要为运行时间更长的服务器实施复杂的解决方案。
  4. 通过仅构建和上传更改的代码到相同的服务器,来重复使用长时间运行的代码服务器。这里的挑战是实现打包和运行时机制,以确保可靠和可重复的执行环境。我们研究了各种打包和分发 Python 环境的方法,包括 rsync、poetrynixshivpex我们还考虑使用 EFS 卷以及这些工具来挂载 Python 环境。
做出这个决定的关键因素是因为我们意识到,虽然 Docker 镜像是行业标准,但在只需要同步一个小改变时,传输数百兆字节的镜像似乎是不必要的。考虑 git——它只传输差异,但却产生了完整且一致的代码库。
这让我们倾向于选择方案 4……如果我们能找到一个适合我们大部分工作工具的话。经过一些实验,我们发现 PEX 具有许多特性,非常适合我们的使用情况——在下一节中会详细介绍。
什么是 PEX?
PEX 是 Python 可执行文件的缩写,是一个将 Python 包打包成名为 pex 文件的工具。这些可执行文件包含 Python 包和一些引导代码。例如,我们可以将 dagster 包和其依赖项打包成单个文件,然后运行它:
% pex dagster --python=python3.8 -o dagster.pex% ./dagster.pexPython 3.8.16 (default, Dec 72022, 01:24:57)[Clang 14.0.0 (clang-1400.0.29.202)] on darwinType "help", "copyright", "credits"or"license"for more information.(InteractiveConsole)>>> import dagster>>>
将整个环境存储在单个文件中非常方便,可以轻松地将其传输到 S3 中进行存储。PEX 提供了更多功能,不仅仅是“文件中的虚拟环境” - 这里是我们使用的其他功能:
  • 隔离性
在运行时,pex 环境与其他全局包完全隔离。在环境中只有捆绑在 pex 文件中的包。我们将多个 pex 文件一起发送到同一台机器上,而不必担心环境隔离问题。
  • 确定性
使用相同的输入包会生成完全相同的 pex 文件:
$ pex dagster pandas -o out.pex | sha256sume3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -$ pex dagster pandas -o out.pex | sha256sume3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -
这让我们可以使用内容寻址来识别这些 pex 文件,从而对实现可重复性更有信心。为了实现可重复性,除了使用 Docker 镜像哈希之外,我们还使用 pex 文件哈希。
  • 组合
多个 pex 文件可以在运行时合并,有效地将多个环境合并为一个环境。
% pex pandas -o pandas.pex% pex dagster -o dagster.pex% PEX_PATH=pandas.pex ./dagster.pexPython 3.8.16 (default, Dec 72022, 01:24:57)[Clang 14.0.0 (clang-1400.0.29.202)] on darwinType "help", "copyright", "credits"or"license"for more information.(InteractiveConsole)>>> import pandas>>> import dagster>>>
我们使用这个功能将代码分成两个部分,在运行时合并起来:一个包含所有依赖项的 deps.pex 文件,和一个仅包含用户代码的 source.pex 文件。
  • 跨平台构建
我们在 Serverless Cloud 中使用 Linux python:* -slim 衍生的基础镜像。只要有包的 wheel,pex 工具就可以在任何平台上为 Linux 构建 pex 文件。
快速部署
使用 pex 和 S3 存储 pex 文件,我们构建了一个系统,其中快速路径避免了构建和启动 Docker 镜像的开销。
我们的系统工作方式如下:当你将代码提交到 GitHub 时,GitHub 操作根据你的依赖关系是否与上一次部署不同,执行全量构建或快速构建。我们跟踪在 setup.pyrequirements.txt 中指定的依赖关系集。
对于全量构建,我们将你的项目依赖项构建为 deps.pex 文件,将你的代码构建为 source.pex 文件。这两个文件都会上传到 Dagster Cloud。对于快速构建,我们只构建并上传 source.pex 文件。
在 Dagster Cloud 中,我们可能会重复使用现有容器或为代码服务器提供新的容器。我们将 deps.pexsource.pex 文件下载到此代码服务器上,并在隔离环境中使用它们运行你的代码。我们从不跨用户共享容器,容器上的所有环境都属于同一用户。快速部署的最佳时间和最差时间如下所示:
这里的要点是,在快速路径中——当我们进行快速构建并重用现有容器时——整个过程只需要大约 40 秒,而不是之前的 3 分钟多。我们称这个功能为快速部署,现在已经默认开启了所有新的无服务器注册。
权衡和问题
这种方式可以显著提高部署速度(4~5 倍),但也带来了一些权衡和其他需要调整的因素:
  1. 虽然现在我们可以在一个代码服务器上运行多个环境,并且在代码方面是隔离的,但它们仍然共享相同的 RAM 和 CPU。如果我们将太多的环境放在一个容器中,而一个环境占用了太多的内存,就可能对在同一个容器中运行的其他环境产生不利影响。
  2. Docker 可以在任何操作系统上为 Linux 构建 Python 包,因为在构建过程中目标 Linux 操作系统和 Python 解释器都可用。pex 仅为提供 wheel 的包构建 Linux 的 pex 文件。如果出现问题,我们在构建过程中使用 Docker 容器来处理源分发未来,这一步骤可以移动到单独的共享服务中。
  3. 在构建 Docker 镜像时可以进行深度定制,例如,你可以指定自定义基础镜像而不是默认的 python:*-slim 镜像之一。为了实现功能的平等,我们实现了一种方法,允许用户指定他们自己的基础 Docker 镜像,我们将其用于快速部署。
GitHub 工作流和 PEX
你可能已经注意到,在最初的图表中,Download Docker based action 的操作大约需要 10 秒钟。我们是如何完全消除这个步骤的呢?
我们曾经将 GitHub action 代码打包成 Docker 镜像,并使用 Docker container action。现在,我们将我们的 action 代码打包为 pex 文件,将其检入我们的 action 存储库并直接在 GitHub runner 上运行。这消除了下载和启动 Docker action 镜像所花费的时间,同时仍允许我们打包所有依赖项。
我们做出的另一个小优化是只使用一个 GitHub 工作流作业。在 GitHub 中,每次作业启动需要大约 10 秒钟来准备一个新的 runner。
总   结
将部署时间从超过 3 分钟缩短到 40 秒是一个显著的加速,我们对这个结果非常满意,特别是在测试自己的服务时。使用 pex 使我们能够在 Docker 之上构建一个可重复、一致的环境,我们很高兴能够探索使用 pex-on-docker 组合的其他可能性。
如果有兴趣,可以看 PEX 团另一篇关于使用 Docker 进行部署的博客文章,它描述了如何使用 pex 文件作为中间目标来加速 Docker 镜像构建。
原文链接:
https://dagster.io/blog/fast-deploys-with-pex-and-docker
声明:本文为 InfoQ 翻译,未经许可禁止转载。
点击底部阅读原文访问 InfoQ 官网,获取更多精彩内容!
今日好文推荐
继续阅读
阅读原文