在Debricked,我们已经将Symfony用于我们的Web后端已有一段时间了。它为我们提供了很好的服务,当他们在Symfony 4.1中宣布Messenger组件时,我们很想尝试一下。从那时起,我们就使用该组件进行异步排队电子邮件。
最近,出于性能原因,需要将我们从GitHub集成中收到的GitHub事件的处理从Web后端分离到单独的微服务,以分开处理。我们决定利用Messenger组件提供的生产者/消费者模式,因为它使我们可以将各种事件异步地分派到队列中,然后立即将该事件确认给GitHub。
但是,与发送电子邮件相比,处理一些GitHub事件可能很耗时。我们也无法控制何时发生这些事件,因此负载既不可预测又不规则-我们需要自动缩放使消费者。
使用Kubernetes 自动扩展
由于我们已经将大部分基础架构部署到Google Cloud上的Kubernetes,因此有必要尝试为我们的消费者利用它。 Kubernetes提供了一种称为Horizontal Pod Autoscaler的工具,它可以让您根据某些度量标准自动缩放您的Pod。
自动缩放器具有一个内置的指标,即CPU指标。它使我们可以跨Pod设置目标CPU负载,然后Kubernetes将自动调整Pod的数量以匹配目标。我们将使用此指标来确保我们始终有合理数量的Pod,可以运行我们的消费者。
准备消费者的Docker镜像
得出结论,Kubernetes可以帮助我们,我们现在需要一个合适的Docker镜像来运行我们的Pod。我们将消费者镜像基于我们的基础镜像,该基础镜像基于Debian,并包含我们的后端逻辑,包括GitHub事件消费者/消息处理程序的逻辑。
Symfony建议使用一种名为“Supervisor”的工具来控制消费者的执行,因此我们将其添加到镜像中,并在Docker的CMD指令中启动它,请参见下面的示例代码:
如果仔细看,我们还会添加两个与运行Supervisor(d)有关的文件。这些文件如下所示:
配置文件
Bash脚本
这是一个相当标准的Supervisor配置,其中有几项值得注意。我们正在执行一个bash脚本,该脚本又将以状态代码37退出,在下一部分中进一步介绍该脚本,或者使用我们的GitHub事件使用者执行Messenger组件的消耗命令。我们还将Supervisor配置为在意外失败时自动重启,意外失败是任何状态代码都不为37。
在我们的案例中,由于负载受网络IO的限制,我们将同时运行大量使用者(70个)。通过同时运行70个消费者,我们可以完全饱和我们的CPU。这是HPA的CPU指标正常工作所必需的,因为否则负载会太低-不管队列有多长,缩放都会卡在配置的最小副本上。
优雅缩容 pods/consumers
当HPA决定了负载过高启动新的Pod。由于Messenger组件的异步特性,我们无需担心诸如竞态条件之类的并发问题。它可以在具有多个消费者的情况下开箱即用,因此增加的pod/消费者数量不会引起任何问题,但是当负载太低且缩放器决定缩小实例时会发生什么呢?
默认情况下,如果自动缩放器决定不再需要它时,它将突然终止正在运行的pod。这会导致消费者出现问题,因为它可能正处于处理邮件的过程中。我们需要一种方法来正常关闭Pod,使用我们当前正在处理的消息,然后退出。
在Dockerfile的上一节中,您可能已经注意到我们将名为pre_stop.sh
的文件复制到了镜像中。该文件如下所示:
执行时,此bash脚本将创建一个/tmp/debricked-stop-work.txt
文件。由于该脚本还调用php/app/bin/console messenger:stop-workers
,因此它将优雅地停止当前的工作程序/消费者,从而导致Supervisord重新启动supervisord_githubeventconsumer.sh
。脚本重新启动后,由于存在/tmp/debricked-stop-work.txt,它现在将立即以状态码37退出。反过来导致Supervisor退出,因为37是我们的预期退出状态码。
一旦Supervisor退出,作为Supervisor的Docker镜像就会成为我们的CMD,并且pre_script.sh也将终止,因为supervisord_githubeventconsumer.sh在删除37之前会删除/tmp /debricked-stop-work.txt
。我们已经实现了正常关机!
但是何时执行“pre_script.sh”?您可能会问自己。我们将在Kubernetes容器的PreStop
生命周期事件中执行它。
每当容器即将终止时(例如,由自动缩放器终止时),都会发生此事件。这是一个阻塞事件,这意味着在完成该脚本之前,不会删除容器,这正是我们想要的。
要配置生命周期事件,我们只需要将几行代码添加到我们的部署配置中,如以下示例所示:
不知所措?这是关闭流程的示意图:
结论
在本文中,我们学习了如何根据负载来动态扩展Symfony Messenger消费者,包括正常关闭它们。以经济有效的方式导致消息的高吞吐量。