Laravel 零停机部署

已发表: 2022-03-11

在更新实时应用程序时,有两种根本不同的方法。

在第一种方法中,我们对系统状态进行增量更改。 例如,我们更新文件、修改环境属性、安装额外的必需品等等。 在第二种方法中,我们拆除整台机器并使用新图像和声明性配置(例如,使用 Kubernetes)重建系统。

Laravel 部署变得简单

本文主要介绍相对较小的应用程序,这些应用程序可能不会托管在云中,尽管我会提到 Kubernetes 如何在“无云”场景之外的部署中极大地帮助我们。 我们还将讨论一些常见问题和提示,以执行可能适用于各种不同情况的成功更新,而不仅仅是 Laravel 部署。

出于演示的目的,我将使用 Laravel 示例,但请记住,任何 PHP 应用程序都可以使用类似的方法。

版本控制

对于初学者来说,了解当前部署在生产环境中的代码版本至关重要。 它可以包含在某个文件中,或者至少包含在文件夹或文件的名称中。 至于命名,如果我们遵循语义版本控制的标准做法,我们可以在其中包含更多信息,而不仅仅是单个数字。

查看两个不同的版本,这些添加的信息可以帮助我们轻松了解它们之间引入的更改的性质。

显示语义版本控制解释的图像。

版本控制从版本控制系统开始,例如 Git。 假设我们已经准备了一个用于部署的版本,例如 1.0.3 版。 在组织这些版本和代码流时,有不同的开发风格,例如基于主干的开发和 Git 流,您可以根据团队的偏好和项目的具体情况进行选择或混合。 最后,我们很可能会在我们的主分支上相应地标记我们的版本。

提交后,我们可以创建一个简单的标签,如下所示:

git tag v1.0.3

然后我们在执行 push 命令时包含标签:

git push <origin> <branch> --tags

我们还可以使用它们的哈希将标签添加到旧提交。

将发布文件传送到目的地

Laravel 部署需要时间,即使它只是复制文件。 但是,即使不需要太长时间,我们的目标是实现零停机时间

因此,我们应该避免就地安装更新,并且不应该更改实时提供的文件。 相反,我们应该部署到另一个目录,并且只有在安装完全准备好后才进行切换。

实际上,有各种工具和服务可以帮助我们进行部署,例如 Envoyer.io(由 Laravel.com 设计师 Jack McDade 设计)、Capistrano、Deployer 等。我还没有在生产中使用所有这些工具和服务,所以我不能提出建议或写一个全面的比较,但让我展示这些产品背后的想法。 如果其中一些(或全部)无法满足您的要求,您始终可以创建自定义脚本,以您认为合适的最佳方式自动化流程。

出于本演示的目的,假设我们的 Laravel 应用程序由 Nginx 服务器从以下路径提供服务:

/var/www/demo/public

首先,每次进行部署时,我们都需要一个目录来放置发布文件。 此外,我们需要一个指向当前工作版本的符号链接。 在这种情况下, /var/www/demo将作为我们的符号链接。 重新分配指针将使我们能够快速更改版本。

Laravel 部署文件处理

如果我们正在处理 Apache 服务器,我们可能需要在配置中允许以下符号链接:

Options +FollowSymLinks

我们的结构可以是这样的:

 /opt/demo/release/v0.1.0 /opt/demo/release/v0.1.1 /opt/demo/release/v0.1.2

可能有一些文件我们需要通过不同的部署来持久化,例如日志文件(如果我们不使用 Logstash,显然)。 在 Laravel 部署的情况下,我们可能希望保留存储目录和 .env 配置文件。 我们可以将它们与其他文件分开,并使用它们的符号链接。

为了从 Git 存储库中获取我们的发布文件,我们可以使用克隆或存档命令。 有些人使用 git clone,但您无法克隆特定的提交或标记。 这意味着获取整个存储库,然后选择特定标签。 当存储库包含许多分支或大量历史记录时,它的大小会比发布存档大得多。 因此,如果您在生产中并不特别需要 git repo,您​​可以使用git archive 。 这使我们可以通过特定标签仅获取文件存档。 使用后者的另一个优点是我们可以忽略一些不应该出现在生产环境中的文件和文件夹,例如测试。 为此,我们只需要在.gitattributes file中设置 export-ignore 属性。 在 OWASP 安全编码实践检查表中,您可以找到以下建议: “在部署之前删除测试代码或任何不适合生产的功能。”

如果我们从源版本控制系统获取版本,git archive 和 export-ignore 可以帮助我们满足这个要求。

让我们看一个简化的脚本(它在生产中需要更好的错误处理):

部署.sh

 #!/bin/bash # Terminate execution if any command fails set -e # Get tag from a script argument TAG=$1 GIT_REMOTE_URL='here should be a remote url of the repo' BASE_DIR=/opt/demo # Create folder structure for releases if necessary RELEASE_DIR=$BASE_DIR/releases/$TAG mkdir -p $RELEASE_DIR mkdir -p $BASE_DIR/storage cd $RELEASE_DIR # Fetch the release files from git as a tar archive and unzip git archive \ --remote=$GIT_REMOTE_URL \ --format=tar \ $TAG \ | tar xf - # Install laravel dependencies with composer composer install -o --no-interaction --no-dev # Create symlinks to `storage` and `.env` ln -sf $BASE_DIR/.env ./ rm -rf storage && ln -sf $BASE_DIR/storage ./ # Run database migrations php artisan migrate --no-interaction --force # Run optimization commands for laravel php artisan optimize php artisan cache:clear php artisan route:cache php artisan view:clear php artisan config:cache # Remove existing directory or symlink for the release and create a new one. NGINX_DIR=/var/www/public mkdir -p $NGINX_DIR rm -f $NGINX_DIR/demo ln -sf $RELEASE_DIR $NGINX_DIR/demo

为了部署我们的版本,我们可以执行以下操作:

deploy.sh v1.0.3

注意:在这个例子中,v1.0.3 是我们发布的 git 标签。

生产方面的作曲家?

您可能已经注意到该脚本正在调用 Composer 来安装依赖项。 尽管您在许多文章中都看到了这一点,但这种方法可能存在一些问题。 通常,最佳实践是创建应用程序的完整构建并通过基础架构的各种测试环境推进此构建。 最后,您将拥有一个经过全面测试的构建,可以安全地部署到生产环境中。 尽管每个构建都应该可以从头开始重现,但这并不意味着我们应该在不同的阶段重新构建应用程序。 当我们在生产环境中安装 composer 时,这与测试版本并不完全相同,这可能会出错:

  • 网络错误可能会中断下载依赖项。
  • 库供应商可能并不总是遵循 SemVer。

很容易发现网络错误。 我们的脚本甚至会因错误而停止执行。 但是如果不运行测试,库中的重大更改可能很难确定,而这在生产中是无法做到的。 在安装依赖项时,Composer、npm 和其他类似工具依赖于语义版本控制——major.minor.patch。 如果你在composer.json中看到~1.0.2,表示安装的是1.0.2版本或者最新的补丁版本,比如1.0.4。 如果您看到 ^1.0.2,则表示安装版本 1.0.2 或最新的次要或补丁版本,例如 1.1.0。 我们相信库供应商在引入任何重大更改时会增加主要编号,但有时会忽略或不遵循此要求。 过去有过这样的案例。 即使您将固定版本放在 composer.json 中,您的依赖项也可能在它们的 composer.json 中有 ~ 和 ^。

如果可以访问,我认为更好的方法是使用工件存储库(Nexus、JFrog 等)。 最初会创建一次包含所有必要依赖项的发布版本。 该工件将存储在存储库中,并从那里获取用于各种测试阶段。 此外,这将是部署到生产环境的构建,而不是从 Git 重建应用程序。

保持代码和数据库兼容

我第一眼就爱上 Laravel 的原因是它的作者非常注重细节,考虑到开发者的便利性,并且在框架中融入了很多最佳实践,比如数据库迁移。

数据库迁移使我们能够使我们的数据库和代码同步。 他们的两个更改都可以包含在单个提交中,因此可以包含单个发布。 但是,这并不意味着可以在不停机的情况下部署任何更改。 在部署期间的某个时间点,将运行不同版本的应用程序和数据库。 万一出现问题,这一点甚至可能变成一个周期。 我们应该始终尝试使它们都与之前版本的同伴兼容:旧数据库-新应用程序,新数据库-旧应用程序。

例如,假设我们有一个address列,需要将其拆分为address1address2 。 为了保持一切兼容,我们可能需要多个版本。

  1. 在数据库中添加两个新列。
  2. 尽可能修改应用程序以使用新字段。
  3. address数据迁移到新列并将其删除。

这个案例也是一个很好的例子,说明小改动如何更好地部署。 他们的回滚也更容易。 如果我们在几周或几个月内更改代码库和数据库,则可能无法在不停机的情况下更新生产系统。

Kubernetes的一些令人敬畏的地方

尽管我们的应用程序的规模可能不需要云、节点和 Kubernetes,但我仍然想提一下 K8s 中的部署是什么样的。 在这种情况下,我们不会对系统进行更改,而是声明我们想要实现什么以及应该在多少个副本上运行什么。 然后,Kubernetes 确保实际状态与所需状态匹配。

每当我们准备好新版本时,我们都会构建一个包含新文件的镜像,用新版本标记该镜像,并将其传递给 K8s。 后者将在集群中快速启动我们的图像。 根据我们提供的就绪检查,它将在应用程序准备就绪之前等待,然后将流量重定向到新应用程序并终止旧应用程序。 我们可以很容易地运行我们的应用程序的多个版本,这将使我们只需几个命令即可执行蓝/绿或金丝雀部署。

如果您有兴趣,可以在 Burr Sutter 的演讲“9 Steps to Awesome with Kubernetes”中进行一些令人印象深刻的演示。

相关:完整的用户身份验证和访问控制 - Laravel Passport 教程,Pt。 1