使用 Kafka 泳道处理不平衡流量


HubSpot 的客户使用工作流程来自动化其业务流程。工作流由触发器和操作集合组成,触发器告诉工作流何时“注册”要处理的记录,操作集合告诉工作流如何处理这些注册的记录。有数百万个活动工作流程,每天总共执行数亿个操作,每秒执行数万个操作。

一旦触发工作流,就会创建注册,并且工作流引擎开始执行工作流中的操作。理论上,这可以在接收触发器的同一进程中同步发生。然而,按照 HubSpot 的运营规模,这种方法很快就会失效:我们无法控制工作流程何时被触发,因此,如果触发器到来得太快,我们的线程池就会溢出并开始丢弃注册。不好!

注册和执行系统使用 Apache Kafka 进行通信,这是 HubSpot 大量使用的排队系统。 Kafka 允许我们将最初请求任务的时间与实际处理任务的时间分离; 消息由主题生成=7>生产者,然后由一个或多个消费者消费。生产者和消费者是相互独立的:生产者不知道消费者何时真正处理消息,只是保证消费者会收到消息。

现在,我们已经解决了尝试同步处理每个触发器的问题:我们可以在触发器发生时尽快接受它们,并且我们将在 Kafka 消费者中尽快处理它们

但我们遇到了一个新问题:如果我们处理消息的速度不够快,我们的 Kafka 消费者将会产生 lag,这意味着操作应该执行的时间和实际执行的时间之间存在延迟。

如果突然出现大量关于我们主题的消息,那么我们必须处理积压的消息。我们可以扩大消费者实例的数量,但这会增加我们的基础设施成本;我们可以添加自动缩放,但添加新实例需要时间,而且客户通常希望工作流程能够近乎实时地处理注册。那么我们该怎么办?

泳道swimlanes
根本问题是,我们所有客户的所有流量都产生到同一个队列。如果该队列的使用者遇到延迟,那么我们所有的流量都会延迟。

引入泳道使我们能够对流量隔离分片。

  • 我们可以将部分流量发送到“溢出”泳道,而不是将所有流量发送到“实时”流量泳道(我们希望尽可能减少延迟)。
  • 两个泳道以完全相同的方式处理消息,但可以独立地产生延迟。
  • 如果我们突然出现流量激增,其速度超过了实时泳道所能容纳的速度,我们将通过将多余的流量发送到“溢出”泳道来保护“实时”泳道。

从表面上看,这似乎是我们只是将问题转移到其他地方(在某些方面确实如此),但这一策略已在 HubSpot 广泛部署,并取得了巨大成功。通常只有少数客户在任何时间点产生大量流量。通过将流量转移到其他地方,我们将这些客户与流量水平更稳定的数千名其他客户隔离开来,为我们的大多数客户提供更快的体验。

有许多策略可用于确定将消息路由到哪个泳道。一般来说,这些可以分为“手动”(被动)和“自动”(主动)策略。自动策略减少了运营负担,因为它们不需要工程师的任何干预,但手动策略也很有帮助,因为如果我们需要重新路由一部分流量,它们会给我们一个“逃生舱口”。

自动处理突发
我们知道我们想要将突发流量路由到溢出泳道,但是我们如何实际检测突发流量呢?
有时我们只需查看 Kafka 消息上的字段就可以知道。
例如,工作流有一个称为“批量注册”的功能,允许客户快速将数百万条记录注册到工作流中。我们可以查看Kafka消息上的原始注册源,如果是批量注册,则自动路由到溢出泳道。

HubSpot 的另一种常见模式是在 Kafka 消息上有一个“回填”标志,以指示消息是由于一次性作业而生成的,不需要立即处理;这也可以用于泳道路由。

不过,有时我们无法仅通过查看消息来判断。在这些情况下,可以使用速率限制器。速率限制器(例如 Guava RateLimiter)强制执行某些处理可以发生的最大速率。当我们决定将流量路由到哪个泳道时,我们将根据每个客户的速率限制检查每条消息,并在速率限制器开始拒绝流量后将流量路由到溢出泳道。

速率限制由阈值(例如 250 个请求)和存储桶大小(例如每秒或每分钟)组成。较小的存​​储桶大小对流量突发的响应速度更快,但可能会过度惩罚小突发。例如,如果我们的速率限制为 250 个请求/秒,并且我们在一秒钟内收到 300 个请求,然后没有进一步的请求,那么即使请求总数相当合理,我们也会对 50 个请求进行速率限制。为了解决这个问题,我们可以实施多个速率限制,具有不同的阈值和存储桶大小(例如,一个速率限制为 500 个请求/秒,另一个速率限制为 1000 个请求/分钟)。

设置速率限制阈值需要一些领域知识。一般来说,我们会查看工作人员在负载下时的指标,以估计其最大吞吐量。然后,我们会将每个客户的速率限制设置为低于该阈值,以防止个别客户主导整体容量。最好将限制设置为可配置的,以便可以根据观察到的吞吐量和延迟随时更改它们。


自动路由流量的其他方法
到目前为止,我们只讨论了泳道作为卸载突发流量的一种方式,但它们实际上比这更通用。由于工作流执行流量非常异构,我们还利用泳道将“快”流量与“慢”流量隔离。与我们如何查看原始注册源来决定路由到哪个泳道类似,我们还将查看正在执行的操作类型,并且我们将执行通常需要更长时间的操作类型(例如 自定义代码操作,允许客户在专用泳道中编写任意 Node 或 Python 脚本。

手动流量路由
所有自动路由意味着大多数时候,工作流引擎在自动驾驶仪上运行。但时不时就会出现需要手动干预的问题,当这种情况发生时,我们很高兴有手动路由选项。我们经常使用的一种手动策略是可配置的客户列表,以强制重新路由。如果特定客户出现问题(例如,操作执行速度特别慢),我们可以将该客户隔离到自己的泳道中,以在我们调查和解决问题时减少问题的影响。

为了使手动路由有效,对我们的系统有良好的可见性非常重要,这样我们就知道哪些流量子集需要重新路由。对我们来说幸运的是,HubSpot 拥有世界一流的内部开发人员工具,这使得通过搜索我们的日志或查看我们的指标可以轻松发现问题。我们记录自定义指标,这些指标为我们提供了多个维度(例如操作类型)的执行延迟,并且我们的日志记录可以告诉我们每个操作发生了什么。

结论
Kafka 是用于异步任务处理的强大工具,但由于队列是共享基础设施,因此生成到队列的消息之间没有隔离。泳道为我们提供了一种隔离交通的方法。我们已经列出了泳道背后的基本原则,但这些模式可以以多种不同的方式应用。我们总共有大约十几个不同的泳道为我们的执行引擎提供动力,有助于保持工作流程快速可靠。