几个月前,我们留意到,银行集成服务部署缓慢正在影响团队发布代码的能力。工程师要花至少 30 分钟才能通过多个过渡环境和生产环境构建、部署和监视变更,这将消耗大量宝贵的工程时间。随着团队越来越大,我们天天发布的代码也越来越多,这一点变得越来越不可接受。
固然我们计划实现长期改进,比如将基于 Amazon ECS 服务的基础设施迁移到 Kubernetes 上,但是,为了在短期内进步迭代速度,有必要快速解决下这个题目。因此,我们决定实践自定义的“快速部署”机制。
我们的银行集成服务由 4000 个 Node.js 进程组成,这些进程运行在专用的 Docker 收留器上,这些收留器托管并部署在 Amazon 的收留器编排服务 ECS 上。在分析了我们的部署过程之后,我们将增加的部署延迟回结到三个不同的组件上:
启动任务会导致延迟。除了应用程序启动时间之外,ECS 健康检查也会导致延迟,它决定收留器何时预备好开始处理流量。控制这个过程的三个参数是 interval、retry 和 螺蛳粉 startPeriod。假如没有对健康检查进行仔细调优,收留器可能会卡在“启动”状态,即使它们已经预备好为流量服务。封闭任务会导致延迟。当我们运行 ECS 服务更新时,一个 SIGTERM 信号被发送到所有正在运行的收留器。为了处理这个题目,我们在应用程序代码中使用了一些逻辑,以便在完全封闭服务之前占用现有资源。我们启动任务的速度限制了部署的并行性。尽管我们将 MaximumPercent 参数设置为 200%,但是 ECS start-taskAPI 调用的硬限制是每个调用只能执行 10 个任务,千航国际,而且速度有限。我们需要调用 400 次才能将所有收留器投进生产。
我们考虑并试验了一些不同的潜伏解决方案,以逐步实现总体目标:
减少生产中运行的收留器总数。这当然是可行的,但它涉及到对服务架构进行重大修改,以使其能够处理相同的请求吞吐量,在进行这样的修改之前,还需要进行更多研究。通过修改健康检查参数来调整 ECS 配置。我们尝试通过减少 interval 和 startPeriod 的值来加强健康检查,但是 ECS 在启动时将健康的收留器错误地标记为不健康,导致我们的服务永远无法完全稳定在 100% 健康状态。由于根本题目(ECS 部署缓慢)依然存在,对这些参数进行迭代是一个缓慢而费力的过程。在 ECS 集群中启动更多实例,以便可以在部署期间同时启动更多任务。这样做可以减少部署时间,但不会减少太多。从长远来看,这也不划算。通过重构初始化和关机逻辑优化服务重启时间。只需要做一些小小的修改,我们就能够在每个收留器中节省大约 5 秒的时间。
尽管这些更改将总体部署时间减少了几分钟,但是我们仍然需要将时间进步至少一个数目级,才能以为题目已解决。这将需要一个根本不同的解决方案。
Node require cache 是一个 JavaScript 对象,它根据需要缓存模块。这意味着多次执行 require(‘foo’) 或 import * as 螺蛳粉 foo from 'foo’只会在第一次时请求 foo 模块。神奇的是,删除 require cache 中的条目(我们可以使用全局 require.cache 对象访问)将迫使 Node 在下次导进模块时从磁盘重新读取该模块。
为了绕过 ECS 部署过程,我们尝试使用 Node 的 require cache 在运行时执行应用程序代码的“热重载”。一旦接收到外部触发(我们将实在现为银行集成服务上的 gRPC 端点),应用程序将下载新代码来替换现有的构建,清除 require cache,从而强制重新导进所有相关模块。通过这种方法,我们能够消除 ECS 部署中存在的大部分延迟,优化整个部署过程。
在 Plaiderdays (我们的内部黑客马拉松)期间,来自不同团队的一组工程师聚在一起,为我们所谓的“快速部署”实现了一个端到真个概念验证。当我们一起想法构建一个原型时,有一件事似乎出了题目:假如下载新构建的 Node 代码也试图使失效缓存,跨境铁路 国际物流,那么下载器代码本身将如何重新加载就不清楚了。(有一种方法可以解决这个题目,就是使用 Node EventEmitter ,但是会给代码增加相当大的复杂性)。更重要的是,千航国际,还存在运行未同步代码版本的风险,这可能导致应用程序意外失败。
由于我们不愿意在银行集成服务的可靠性上妥协,这种复杂性需要重新考虑“热重载”方法。
在过往,为了在所有服务中运行一系列同一的初始化任务,我们编写了自己的进程封装器,它的名称非常贴切,叫做 Bootloader。Bootloader 的核心包含设置日志管道、转发信号和读取 ECS 元数据的逻辑。每个服务都是通过将应用程序可执行文件的路径以及一系列标志传递给 Bootloader 来启动的,这些文件在执行初始化步骤之后会作为子进程执行。
我们没有清除 Node 的 require cache,而是在下载预期的部署构建后,使用特殊的退出代码来调用 process.exit 实现服务更新。我们还在 Bootloader 中实现了自定义逻辑,以触发使用此代码退出的任何子进程的进程重载。与“热重载”方法类似,这使我们能够绕过 ECS 部署的本钱并快速引导新代码,同时避免“热重载”的陷阱。此外,Bootloader 层的这种“快速部署”逻辑答应我们将其推广到在 Plaid 运行的任何其他服务。
下面是终极解决方案:
Jenkins 部署管道向银行集成服务的所有实例发送 RPC 请求,指示它们“快速部署”特定的提交散列。应用程序接收 gRPC 请求进行快速部署,并根据接收到的提交散列从 Amazon S3 下载构建好的压缩包。然后,它替换文件系统上的现有构建,并使用 Bootloader 识别的特殊退出代码退出。Bootloader 看到应用程序使用这个特殊的“Reload”退出代码退出,然后重新启动应用程序。服务运行新的代码。
下面这张图简单说明了这个过程。
结果
我们能够在 3 周内交付这个“快速部署”项目,并将 90% 生产收留器的部署时间从 30 多分钟减少到 1.5 分钟。
上图显示了我们为银行集成服务部署的收留器数目(按提交表示为不同的颜色)。假如留意下黄线,就可以看到它在 12:15 左右增长趋于平稳,这代表我们的收留器长尾仍然在占用资源。
这个项目极大进步了 Plaid 集成工作的速度,答应我们更快地发布特性及进行 Bug 修复,并将浪费在上下文切换和监视仪表板上的工程时间最小化。这也证实了我们的工程文化,即通过黑客马拉松得来的想法实现具有实质性影响的项目。
最后,我自己是一名从事了多年开发的JAVA老程序员,辞职目前在做自己的java私人定制课程,今年年初我花了一个月整理了一份最适合2019年学习的java学习干货,可以送给每一位喜欢java的小伙伴,想要获取的可以关注我的头条号并在后台私信我:java,即可免费获取。
本文转载至微信公众号——InfoQ,如有侵权请联系立删!
郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。
千航国际 |
国际空运 |
国际海运 |
国际快递 |
跨境铁路 |
多式联运 |