
发展进程越快,科技公司的成长速度就越快。
不幸的是,现代应用程序对我们不利——我们的系统必须实时更新,不能打扰任何人或导致停机或中断。 部署到此类系统变得具有挑战性,即使对于小型团队来说也需要复杂的持续交付管道。
这些管道通常范围狭窄、缓慢且不可靠。 开发人员必须首先手动构建它们,然后管理它们,公司通常雇用整个 DevOps 团队来完成此任务。
开发的速度取决于这些管道的速度。 最好的团队可以在 5-10 分钟内完成部署,但通常需要更长的时间,每次部署需要几个小时。
在黑暗中这需要 50 毫秒。 五十。 毫秒。 黑暗的 - 专为持续交付而构建,Dark 的每个方面(包括语言本身)都是在构建时考虑到安全、即时部署。
为什么持续交付管道如此缓慢?
假设我们有一个 Python Web 应用程序,并且我们已经创建了一个精彩且现代化的持续交付管道。 对于每天忙于该项目的开发人员来说,部署一个小更改将如下所示:
改造
- 在git中创建一个新分支
- 在功能开关后面进行更改
- 单元测试以测试有或没有功能开关的更改
拉取请求
- 提交更改
- 将更改推送到远程 github 存储库
- 拉取请求
- CI 在后台自动构建
- 代码审查
- 如果需要的话,可以多加一些评论
- 将更改合并到 git master 中。
CI在master上运行
- 通过 npm 安装前端依赖项
- HTML+CSS+JS资源的组装与优化
- 在前端运行单元和功能测试
- 从 PyPI 安装 Python 依赖项
- 在后端运行单元和功能测试
- 测试两端的集成
- 将前端资源发送到CDN
- 为 Python 程序构建容器
- 将容器提交到注册表
- 更新 Kubernetes 清单
用新代码替换旧代码
- Kubernetes 启动新容器的多个实例
- Kubernetes 等待实例变得健康
- Kubernetes 将实例添加到 HTTP 负载均衡器
- Kubernetes 等待,直到旧实例不再使用
- Kubernetes 停止旧实例
- Kubernetes 重复这些操作,直到新实例取代所有旧实例
启用新功能开关
- 新代码仅供您自己使用,以确保一切正常
- 为 10% 的用户启用新代码,跟踪运营和业务指标
- 为 50% 的用户启用新代码,跟踪运营和业务指标
- 为 100% 的用户启用新代码,跟踪运营和业务指标
- 最后重复整个过程以删除旧代码并切换
该过程取决于工具、语言和面向服务的架构的使用,但总的来说它看起来像这样。 我没有提到数据库迁移部署,因为它们需要大量规划,但我将在下面向您展示 Dark 如何处理此问题。
这里有很多组件,其中许多组件很容易减慢速度、崩溃、导致临时争用或使工作系统崩溃。
而且由于这些管道几乎总是为特殊情况而创建,因此很难依赖它们。 许多人都会遇到代码无法部署的情况,因为 Dockerfile 存在问题,数十个服务之一崩溃了,或者必要的专家正在休假。
更糟糕的是,其中许多步骤根本没有任何用处。 以前,当我们直接向用户部署代码时,这些是需要的,但现在我们可以切换新代码,并且这些进程已被分离。 结果,部署代码的步骤(用新代码替换旧代码)现在根本就变成了不必要的风险。
当然,这是一个非常深思熟虑的管道。 创建它的团队不遗余力地快速部署它。 通常,部署管道要慢得多且更不可靠。
在黑暗中实施持续交付
持续交付对于 Dark 来说非常重要,因此我们从一开始就以亚秒级为目标。 我们完成了管道的所有步骤,删除了所有不必要的内容,并完善了其余部分。 这就是我们删除步骤的方法。
杰西·弗雷泽()在雷克雅未克举行的软件开发未来会议上创造了一个新词“deployless”(不需要部署)
我们立即决定 Dark 将基于“无需部署”的概念(感谢 对于新词)。 无部署意味着任何代码都可以立即部署并准备在生产中使用。 当然,我们不会允许损坏或不完整的代码通过(我将在下面描述安全原则)。
在 Dark 演示中,经常有人问我们如何能够如此快速地部署。 奇怪的问题。 人们可能认为我们已经提出了某种超级技术,可以比较代码、编译代码、将其打包到容器中、启动虚拟机、冷启动容器,以及诸如此类的一切 - 所有这些都在 50 毫秒内完成。 这几乎是不可能的。 但我们创建了一个特殊的部署引擎,不需要这一切。
Dark 在云中运行解释器。 假设您正在函数、HTTP 或事件处理程序中编写代码。 我们将 diff 发送到服务器的抽象语法树(我们的编辑器和服务器内部使用的代码实现),然后在请求到来时运行该代码。 因此,部署看起来只是数据库中的一个适度的条目 - 即时且基本。 部署之所以如此快,是因为它涉及的内容最少。
未来,我们计划将 Dark 打造成基础设施编译器,它将创建和运行理想的基础设施,以实现应用程序的高性能和可靠性。 当然,即时部署将继续存在。
安全部署
结构化编辑器
Dark 中的代码是在 Dark 编辑器中编写的。 结构化编辑器不允许语法错误。 事实上,Dark 甚至没有分析仪。 当您打字时,我们直接使用抽象语法树(AST),例如 , , , и .
Dark 中任何未完成的代码都具有有效的执行语义,例如 。 例如,如果您更改函数调用,我们会存储旧函数,直到新函数可用为止。
Dark 中的每个程序都有其自己的含义,因此未完成的代码并不妨碍已完成的代码运行。
编辑模式
在两种情况下,您可以在 Dark 中编写代码。 首先:您正在编写新代码,并且您是唯一的用户。 例如,它位于 REPL 中,其他用户永远不会访问它,或者它是您不在任何地方引用的新 HTTP 路由。 在这里您可以在没有任何预防措施的情况下工作,现在这几乎就是您在开发环境中的工作方式。
第二种情况:该代码已被使用。 如果有流量通过您的代码(函数、事件处理程序、数据库、类型),您需要小心。 为此,我们阻止所有使用的代码,并要求使用更结构化的工具来编辑它。 我将介绍下面的结构工具:HTTP 和事件处理程序的函数开关、强大的数据库迁移平台以及函数和类型的新版本控制方法。
功能开关
一种方法 在黑暗中 - 用一种解决方案解决多个问题。 功能开关执行许多不同的任务:替换本地开发环境、git 分支、部署代码,当然还有传统的缓慢且受控的新代码发布。
创建和部署功能开关只需在我们的编辑器中通过一项操作即可完成。 它为新代码创建空白区域,并提供对旧代码和新代码的访问控制,以及用于逐步采用或消除新代码的按钮和命令。
功能开关内置于 Dark 语言中,即使是不完整的开关也能达到其目的 - 如果不满足开关中的条件,则会执行旧的阻塞代码。
开发环境
功能开关取代了本地开发环境。 如今,团队发现很难确保每个人都使用相同版本的工具和库(代码格式化程序、linter、包管理器、编译器、预处理器、测试工具等)。使用 Dark,无需在本地安装依赖项,管理本地 Docker 安装,或采取其他措施来确保开发和生产环境之间至少有表面上的平等。 ,我们甚至不会假装我们正在为此努力。
Dark 中的交换机不是创建克隆的本地环境,而是在生产中创建一个新的沙箱来替换开发环境。 将来我们还计划对应用程序的其他部分(例如即时数据库克隆)进行沙箱处理,尽管现在这似乎并不那么重要。
分支和部署
现在有多种方法可以将新代码引入系统:git 分支、部署阶段和功能切换。 他们在工作流程的不同部分解决了相同的问题:预部署阶段的 git、从旧代码过渡到新代码的部署以及新代码受控发布的功能切换。
最有效的方式是功能开关(同时也是最容易理解和使用的)。 有了它们你就可以完全放弃其他两种方法。 删除部署特别有用 - 如果我们无论如何都使用功能开关来启用代码,那么将服务器迁移到新代码的步骤只会产生不必要的风险。
Git 很难使用,特别是对于初学者来说,并且有很大的限制,但它确实有方便的分支。 我们已经解决了 git 的许多缺点。 Dark 可以实时编辑,并支持 Google 文档式协作,因此您无需提交代码,并且可以减少变基和合并的频率。
功能开关是安全部署的核心。 与即时部署相结合,它们使您能够以小而低风险的方式快速测试概念,而不是做出可能导致系统瘫痪的重大更改。
版本控制
我们使用版本控制来更改函数和类型。 如果您想更改某个函数,Dark 会创建该函数的新版本。 然后,您可以使用 HTTP 或事件处理程序中的开关来调用此版本。 (如果它是调用图深处的函数,则在此过程中会创建每个函数的新版本。这可能看起来有些过分,但除非您使用它们,否则这些函数不会妨碍它们,所以您甚至不会注意。)
出于同样的原因,我们也使用版本类型。 我们详细讨论了我们的类型系统 .
通过对函数和类型进行版本控制,您可以逐步对应用程序进行更改。 您可以检查每个处理程序是否适用于新版本,而无需立即对应用程序进行所有更改(但如果您愿意,我们有工具可以快速执行此操作)。
这比像现在这样一次性完全部署所有内容要安全得多。
新的软件包版本和标准库
当您在 Dark 中更新包时,我们不会立即替换整个代码库中每个函数或类型的使用。 这是不安全的。 代码继续使用它所使用的相同版本,并且您可以使用开关根据具体情况将函数和类型的使用更新到新版本。
Dark 中部分自动过程的屏幕截图,显示了 Dict::get 函数的两个版本。 Dict::get_v0 返回 Any 类型(我们正在丢弃),而 Dict::get_v1 返回 Option 类型。
我们经常提供标准库的新功能并排除旧版本。 代码中使用旧版本的用户仍然可以访问它们,但新用户将无法访问它们。 我们将提供工具,将用户从旧版本迁移到新版本,同样使用功能开关。
Dark 还提供了一个独特的功能:由于我们运行您的生产代码,因此我们可以自己测试新版本,比较新旧查询的输出以通知您更改。 因此,通常盲目完成的软件包更新(或需要广泛的安全测试)带来的风险要小得多,并且可以自动发生。
新版本的黑暗
从 Python 2 到 Python 3 的过渡跨越了十年,而且仍然是一个挑战。 由于我们正在构建 Dark 来实现持续交付,因此我们需要考虑这些语言变化。
当我们对语言进行小的更改时,我们创建了一个新版本的 Dark。 老版本Dark中保留旧代码,新版本中使用新代码。 您可以使用开关或功能版本升级到新版本的 Dark。
考虑到深色最近才被引入,这一点特别有用。 对语言或库的许多更改可能会失败。 增量语言版本控制允许我们进行较小的更新,这意味着我们可以慢慢来,推迟有关语言的许多决定,直到我们拥有更多用户,从而获得更多信息。
数据库迁移
为了安全的数据库迁移有 :
- 重写代码以支持新旧格式
- 将所有数据转换为新格式
- 删除旧数据访问
因此,数据库迁移需要很长时间并且需要大量资源。 我们最终会遇到堆积如山的过时模式,因为即使是修复表或列名之类的简单任务也不值得付出努力。
Dark 有一个高效的数据库迁移平台(我们希望)将使该过程变得如此简单,以至于您不会害怕它。 Dark 中的所有数据存储(键值存储或持久哈希表)都有一个类型。 要迁移数据存储,您只需为其分配新类型以及回滚和前滚函数以在两种类型之间转换值。
Dark 中的数据存储是通过版本化变量名称访问的。 例如,Users 数据存储最初将命名为 Users-v0。 当创建不同类型的新版本时,名称将更改为 Users-v1。 如果数据是通过 Users-v0 保存的并且您通过 Users-v1 访问它,则应用前滚功能。 如果数据是通过Users-v1保存的,并且您通过Users-v0访问它,则应用回滚功能。
数据库迁移屏幕显示旧数据库字段名称、翻转和回滚表达式以及启用迁移的说明。
使用功能开关将呼叫路由到 Users-v0 到 Users-v1。 一次可以通过一个 HTTP 处理程序来完成此操作,以降低风险,并且这些开关基于每个用户工作,因此您可以验证一切是否按预期工作。 当没有剩余 Users-v0 时,Dark 会将后台所有剩余数据从旧格式转换为新格式。 你甚至不会注意到它。
测试
黑暗是 和不可变的值,因此与动态类型的面向对象语言相比,它的测试面要小得多。 但你仍然需要测试。
在 Dark 中,编辑器会自动在后台为您正在编辑的代码运行单元测试,并默认为所有功能开关运行这些测试。 将来,我们希望使用静态类型来自动模糊代码以查找错误。
此外,Dark 在生产中运行您的基础设施,这开辟了新的可能性。 我们在 Dark 框架中自动保存 HTTP 请求(我们现在保存所有请求,但随后我们想切换到获取)。 我们针对它们测试新代码并运行单元测试,如果您愿意,您可以轻松地将有趣的查询转换为单元测试。
我们摆脱了什么?
由于我们没有部署,但有功能开关,因此大约 60% 的部署管道被遗漏了。 我们不需要 git 分支或拉取请求、构建后端资源和容器、将资源和容器推送到注册表或部署到 Kubernetes 的步骤。
标准持续交付管道(左)和暗持续交付(右)的比较。 Dark交付由6个步骤和35个周期组成,而传统版本包括3个步骤和XNUMX个周期。
在 Dark 中,部署只有 6 个步骤和 1 个周期(重复多次的步骤),而现代持续交付管道由 35 个步骤和 3 个周期组成。 在 Dark 中,测试会自动运行,您甚至看不到它; 依赖项会自动安装; 不再需要与 git 或 Github 相关的所有内容; 无需构建、测试和发布 Docker 容器; 不再需要部署到 Kubernetes。
就连黑暗中剩下的步骤也变得更加容易。 由于可以通过单个操作来控制功能切换,因此无需再次遍历整个部署管道来删除旧代码。
我们尽可能简化代码交付,减少持续交付的时间和风险。 我们还使更新包、数据库迁移、测试、版本控制、依赖项安装、开发和生产环境之间的平等以及快速安全的语言版本升级变得更加容易。
我在以下网站回答有关此问题的问题 .
要了解有关 Dark 设备的更多信息,请阅读 , (或 )或 。 如果你九月要去 StrangeLoop, .
来源: habr.com
