Kubernetes 提示和技巧:NGINX 和 PHP-FPM 中优雅关闭的功能

在 Kubernetes 中实施 CI/CD 时的典型条件:应用程序必须能够在完全停止之前不接受新的客户端请求,最重要的是,成功完成现有请求。

Kubernetes 提示和技巧:NGINX 和 PHP-FPM 中优雅关闭的功能

满足此条件可以让您在部署期间实现零停机。 然而,即使使用非常流行的捆绑包(例如 NGINX 和 PHP-FPM),您也可能会遇到困难,从而导致每次部署时出现大量错误......

理论。 Pod 如何生活

我们已经发布了有关 pod 生命周期的详细信息 本文。 在所考虑的主题背景下,我们感兴趣的是以下内容:pod 进入状态的那一刻 终止,新请求停止发送给它(pod 已移除 来自服务的端点列表)。 因此,为了避免部署过程中出现停机,我们只要解决正确停止应用程序的问题就足够了。

您还应该记住,默认宽限期是 30秒:在此之后,pod 将被终止,应用程序必须有时间处理在此期间之前的所有请求。 注意:虽然任何需要超过 5-10 秒的请求已经是有问题的,并且优雅的关闭将不再有帮助......

为了更好地理解 pod 终止时会发生什么,只需查看下图:

Kubernetes 提示和技巧:NGINX 和 PHP-FPM 中优雅关闭的功能

A1、B1 - 接收有关炉床状态的变化
A2 - 出发 SIGTERM
B2 - 从端点删除 Pod
B3 - 接收更改(端点列表已更改)
B4 - 更新 iptables 规则

请注意:删除端点 pod 和发送 SIGTERM 不是按顺序发生的,而是并行发生的。 并且由于 Ingress 不会立即收到更新的 Endpoints 列表,来自客户端的新请求将被发送到 pod,这将在 pod 终止期间导致 500 错误 (有关此问题的更详细材料,我们 翻译的)。 这个问题需要通过以下方式解决:

  • 发送连接:在响应标头中关闭(如果这涉及 HTTP 应用程序)。
  • 如果无法更改代码,则以下文章介绍了一种解决方案,可让您在宽限期结束之前处理请求。

理论。 NGINX 和 PHP-FPM 如何终止其进程

NGINX

让我们从 NGINX 开始,因为一切都或多或少是显而易见的。 深入研究这一理论,我们了解到 NGINX 有一个主进程和多个“工作进程”——这些是处理客户端请求的子进程。 提供了一个方便的选项:使用命令 nginx -s <SIGNAL> 以快速关闭或正常关闭模式终止进程。 显然,我们感兴趣的是后一种选择。

那么一切都很简单:你需要添加到 前停止钩子 将发送正常关闭信号的命令。 这可以在部署中的容器块中完成:

       lifecycle:
          preStop:
            exec:
              command:
              - /usr/sbin/nginx
              - -s
              - quit

现在,当 Pod 关闭时,我们将在 NGINX 容器日志中看到以下内容:

2018/01/25 13:58:31 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down
2018/01/25 13:58:31 [notice] 11#11: gracefully shutting down

这将意味着我们需要的:NGINX 等待请求完成,然后终止进程。 然而,下面我们还将考虑一个常见问题,由于该问题,即使使用命令 nginx -s quit 该进程错误终止。

到了这个阶段,我们已经完成了 NGINX:至少从日志中你可以了解到一切都在正常工作。

PHP-FPM 有什么关系? 它如何处理优雅关闭? 让我们弄清楚一下。

PHP-FPM

对于 PHP-FPM,信息有点少。 如果你专注于 官方手册 根据 PHP-FPM,它会说接受以下 POSIX 信号:

  1. SIGINT, SIGTERM — 快速关闭;
  2. SIGQUIT - 优雅的关闭(我们需要的)。

其余信号在此任务中不需要,因此我们将省略它们的分析。 要正确终止进程,您需要编写以下 preStop 挂钩:

        lifecycle:
          preStop:
            exec:
              command:
              - /bin/kill
              - -SIGQUIT
              - "1"

乍一看,这就是在两个容器中执行正常关闭所需的全部操作。 然而,这项任务比看起来更困难。 下面是两种情况,在部署过程中,正常关闭不起作用并导致项目短期不可用。

实践。 正常关闭可能出现的问题

NGINX

首先,记住:除了执行命令之外,这是有用的 nginx -s quit 还有一个阶段值得关注。 我们遇到了一个问题,NGINX 仍然会发送 SIGTERM 而不是 SIGQUIT 信号,导致请求无法正确完成。 类似的案例还可以找到,例如 这里。 不幸的是,我们无法确定此行为的具体原因:对 NGINX 版本存在怀疑,但尚未得到证实。 症状是在 NGINX 容器日志中观察到消息 “打开连接 10 中剩余的套接字 #5”,之后吊舱停止了。

我们可以观察到这样一个问题,例如,从我们需要的Ingress上的响应中:

Kubernetes 提示和技巧:NGINX 和 PHP-FPM 中优雅关闭的功能
部署时的状态码指示

在这种情况下,我们只收到来自 Ingress 本身的 503 错误代码:它无法访问 NGINX 容器,因为它不再可访问。 如果您使用 NGINX 查看容器日志,它们包含以下内容:

[alert] 13939#0: *154 open socket #3 left in connection 16
[alert] 13939#0: *168 open socket #6 left in connection 13

更改停止信号后,容器开始正确停止:不再观察到 503 错误这一事实证实了这一点。

如果您遇到类似的问题,那么弄清楚容器中使用的停止信号以及 preStop 钩子到底是什么样子是有意义的。 原因很可能正是在于此。

PHP-FPM...以及更多

PHP-FPM 的问题可以用一个简单的方式来描述:它不会等待子进程完成,而是终止子进程,这就是为什么在部署和其他操作期间会出现 502 错误。 自 2005 年以来,bugs.php.net 上有多个错误报告(例如 这里 и 这里),描述了这个问题。 但您很可能不会在日志中看到任何内容:PHP-FPM 将宣布其过程完成,没有任何错误或第三方通知。

值得澄清的是,问题本身可能或多或少取决于应用程序本身,并且可能不会表现出来,例如在监控中。 如果您确实遇到它,首先想到一个简单的解决方法:添加一个 preStop 钩子 sleep(30)。 它将允许您完成之前的所有请求(并且我们不接受新的请求,因为 pod 已经 能够 终止),30 秒后,pod 本身将结束并发出信号 SIGTERM.

事实证明, lifecycle 容器将如下所示:

    lifecycle:
      preStop:
        exec:
          command:
          - /bin/sleep
          - "30"

但由于30秒 sleep 我们 非常 我们将增加部署时间,因为每个 Pod 都会被终止 最低限度 30秒,这很糟糕。 关于这个还能做什么?

让我们转向负责直接执行应用程序的一方。 在我们的例子中是 PHP-FPM默认情况下不监控其子进程的执行:master进程立即终止。 您可以使用指令更改此行为 process_control_timeout,它指定子进程等待来自主进程的信号的时间限制。 如果将该值设置为 20 秒,这将覆盖容器中运行的大部分查询,并在完成后停止主进程。

有了这些知识,让我们回到上一个问题。 如前所述,Kubernetes 不是一个整体平台:不同组件之间的通信需要一些时间。 当我们考虑 Ingress 和其他相关组件的操作时尤其如此,因为由于部署时的延迟,很容易出现 500 个错误激增的情况。 例如,在向上游发送请求的阶段可能会发生错误,但组件之间交互的“时滞”非常短——不到一秒。

因此, 总共 与已经提到的指令 process_control_timeout 您可以使用以下结构 lifecycle:

lifecycle:
  preStop:
    exec:
      command: ["/bin/bash","-c","/bin/sleep 1; kill -QUIT 1"]

在这种情况下,我们将使用命令补偿延迟 sleep 而且我们并没有显着增加部署时间:毕竟,30 秒和 XNUMX 秒之间的差异很明显?.. 事实上,这是 process_control_timeoutlifecycle 仅用作滞后情况下的“安全网”。

一般来说 所描述的行为和相应的解决方法不仅适用于 PHP-FPM。 使用其他语言/框架时可能会以某种方式出现类似的情况。 如果您无法通过其他方式修复正常关闭(例如,通过重写代码以便应用程序正确处理终止信号),您可以使用所描述的方法。 它可能不是最美丽的,但它确实有效。

实践。 负载测试以检查 pod 的运行情况

负载测试是检查容器如何工作的方法之一,因为此过程使其更接近用户访问站点时的真实战斗条件。 要测试上述建议,您可以使用 Yandex.Tankom:它完美地满足了我们的所有需求。 以下是通过 Grafana 和 Yandex.Tank 本身的图表根据我们的经验进行测试的清晰示例的提示和建议。

这里最重要的是 逐步检查更改。 添加新修复后,运行测试并查看结果与上次运行相比是否发生了变化。 否则,将很难识别无效的解决方案,从长远来看,它只会造成损害(例如,增加部署时间)。

另一个细微差别是在终止期间查看容器日志。 那里有记录正常关机的信息吗? 访问其他资源(例如,访问相邻的 PHP-FPM 容器)时日志中是否有任何错误? 应用程序本身存在错误(如上述 NGINX 的情况)? 我希望本文的介绍性信息能够帮助您更好地了解容器在终止期间会发生什么。

因此,第一次测试运行是在没有 lifecycle 并且无需应用程序服务器的附加指令(process_control_timeout 在 PHP-FPM 中)。 此测试的目的是确定错误的大致数量(以及是否存在错误)。 此外,从其他信息中,您应该知道每个 Pod 的平均部署时间约为 5-10 秒,直到完全准备就绪。 结果是:

Kubernetes 提示和技巧:NGINX 和 PHP-FPM 中优雅关闭的功能

Yandex.Tank 信息面板显示 502 个错误激增,这些错误发生在部署时,平均持续长达 5 秒。 大概这是因为旧 pod 终止时现有的请求也被终止。 此后,出现了 503 错误,这是 NGINX 容器停止的结果,该容器还由于后端而断开了连接(这阻止了 Ingress 连接到它)。

让我们看看如何 process_control_timeout PHP-FPM中的将帮助我们等待子进程的完成,即纠正此类错误。 使用此指令重新部署:

Kubernetes 提示和技巧:NGINX 和 PHP-FPM 中优雅关闭的功能

第 500 次部署期间没有再出现错误! 部署成功,正常关闭。

然而,值得记住 Ingress 容器的问题,由于时间滞后,我们可能会收到一小部分错误。 为了避免它们,剩下的就是添加一个结构 sleep 并重复部署。 然而,在我们的特定情况下,没有看到任何变化(同样,没有错误)。

结论

为了优雅地终止进程,我们期望应用程序出现以下行为:

  1. 等待几秒钟,然后停止接受新连接。
  2. 等待所有请求完成并关闭所有未执行请求的保持活动连接。
  3. 结束你的进程。

然而,并非所有应用程序都能以这种方式工作。 Kubernetes 现实中的问题的一种解决方案是:

  • 添加一个将等待几秒钟的预停止钩子;
  • 研究我们后端的配置文件以获取适当的参数。

NGINX 的示例清楚地表明,即使应用程序最初应该正确处理终止信号,也可能无法这样做,因此在应用程序部署期间检查 500 错误至关重要。 这也使您能够更广泛地看待问题,而不是专注于单个 Pod 或容器,而是将整个基础设施视为一个整体。

作为测试工具,您可以将 Yandex.Tank 与任何监控系统结合使用(在我们的示例中,数据是从具有 Prometheus 后端的 Grafana 获取用于测试)。 在基准测试可能生成的重负载下,正常关闭的问题显而易见,并且监控有助于在测试期间或测试后更详细地分析情况。

针对文章的反馈:值得一提的是,这里描述的问题和解决方案与 NGINX Ingress 相关。 对于其他情况,还有其他解决方案,我们可以在该系列的以下材料中考虑。

PS

K8s 提示和技巧系列中的其他内容:

来源: habr.com

添加评论