screen_shot_2014-05-30_AT_1.37.33_PM.

Salsify智慧工程博客

用于高吞吐量服务的延迟作业

张贴了Tafadzwa Pasipanodya

找到我:

2019年9月3日8:14:29

多租户SaaS往往面临的挑战是确保每个租户获得平台资源的公平份额。在Salsify,我们必须在我们的延迟工作基于后台任务执行基础设施。因为我们的客户有不同的用例,他们倾向于运行不同复杂度和大小的任务。随着时间的推移,我们制定了租户公平的工作预留策略,这使得我们的工作系统很难扩大,甚至不可能扩大。在这篇文章中,我将讨论我们如何通过扩展Delayed Job来解决租户公平问题。

狂热的延迟工作

延迟作业的核心由作业、队列和工人组成。作业包含要运行的业务逻辑,并在应用程序代码将作业排队时写入数据库表。队列作为工人保留和运行作业的流发挥作用。工人负责管理作业生命周期,并确保作业在各自的环境中正确运行。我们的核心存储库服务在此配置中使用了延迟作业;从Postgres数据库运行100名工作人员。

简化了延迟工作的代表

我们的早期解决方案

工作人员可以使用的最简单的策略之一是FIFO排序,它可以通过让数据库按插入顺序挂起作业来实现。不幸的是,在多租户环境中,高使用率租户可以通过在短时间内连续排队大量作业,轻松锁定所有可用的工作者。例如,假设我们运行两个工人,有两个租户(Jane和Jon)。如果Jane要在Jon之前放入2个或更多的任务,那么Jon将被有效地锁定在无法处理任何后台任务,直到Jane的所有任务都完成。Jon会对他的用户体验感到非常沮丧,这是可以理解的。

多租户系统中的FIFO工作处理

我们在解决这个问题时的第一次迭代使用随机算法来帮助确保每个租户的下一个待处理作业被选为下一个作业的可能性相同。这意味着如果Jane在Jon之前加入了3个作业,Jon和Jane将被平等对待,Jon将不会被锁定在运行他的任务之外。

多租户系统中的随机工作处理

虽然随机策略确保了工作保留的公平性,但如果租户倾向于拥有长期运行的工作,那么他们很可能最终会锁定大部分(如果不是所有的话)可用的工人。还有一个非零的可能性是,一个租户会以其他租户的损失为代价被反复选中。为了解决这个问题,我们添加了租户限制,以确保租户可以同时运行的作业总数是有限的。虽然所有这些变化都极大地提高了公平性和用户感知的延迟,但它也给我们的工作系统带来了一些挑战。

您的延迟作业设置如何保留作业的自定义需要在SQL中实现负载平衡算法。算法越复杂,实现,测试,优化和规模越具挑战性。萨尔西耶生长;以越来越大的客户,我们每天进程的工作数量增加到数十万岁。我们的工作选择查询放慢平均为600ms。在很大的负载下,在高吞吐量队列上保留工作可能只需4S。相比之下,平均工作持续时间为3.8s。保留工作贡献了50%至80%的数据库CPU负载;这通常徘徊在60%和90%之间。我们使用延迟作业的能力,为客户提供更好的平台体验。

选择选项

其他作业系统通过使用非关系数据存储解决了慢查询问题。例如,Resque使用Redis。经过分析,我们意识到我们也可以使用Redis强大的数据结构以一种简单且可扩展的方式对我们的算法进行建模。我们不能完全切换到像Resque这样的现成解决方案,因为这样做意味着我们必须通过修补Resque的内部来实现我们的算法。此外,我们的整个平台是围绕延迟作业及其一致性保证构建的。例如,Salsify中的一个常见模式涉及为业务工作流创建一个数据库记录,并让作业排队运行它。将数据导入平台的用户将创建一个数据库记录来跟踪导入,并将启动所有所需处理的作业排入队列。

没有交易,用户最终可能会随机孤立工作流程,这将是一个可怕的用户体验。多年来,我们还开发了许多关键任务延迟工作特定的延期,如工作团体工人池可连锁钩子心跳依赖相同的一致性保证的插件。例如,作业组插件依赖于事务以确保以故障安全的方式协调作业集。改变这将是一个冒险的大升力。

我们当前的解决方案

在我们的2018年夏季Hackathon期间,我们将一个工作系统原型,该系统使用Postgres作为具有有前途的结果的工作编排的真理和redis来源。使用200,000个简单的NO-OP工作均均匀分布在200个租户和6名工人的现有系统中运行了一项初始的基准,以100%的MacBook Pro固定数据库CPU使用率,并看到了15个作业的作业吞吐量。相比之下,使用我们的原型运行395名工人导致了50%的数据库CPU使用率和2,700个作业的作业吞吐量。这款基准测试是我们通过单独使用数据库推动延迟作业的开销惩罚的伟大插图。我们选择REDIS由于其强大的数据结构和原子脚本支持,简化了建模复杂的线程安全算法。这个想法在作业泵中达到了高潮;一种高吞吐作业制度,使我们的复杂公平战略廉价且快速地保留工作。

这个怎么运作

简化作业泵的表示

作业泵由泵和工人流程以及相当数量的Lua脚本组成。一旦应用程序代码将新作业写入数据库,泵过程会将编排所需的最小作业元数据集推入Redis。以下代码段显示了简化版本。

工作进程类似于默认的Delayed Job进程,尽管它的作业来源不同。它不是直接查询数据库,而是在从Postgres加载和运行Redis之前轮询下一个运行的任务。一旦任务成功或失败,它将在数据库和Redis中被标记为成功或失败。

由于我们的作业系统运行数百个工人,Redis中的数据操作需要是原子的。Redis允许使用Lua;一种类似于c的低级脚本语言。本文更详细地介绍了其工作原理。我们利用了这一点,并将公平性算法实现为Lua脚本,在保留、重新调度和完成任务时,工作人员会调用这些脚本。每个脚本都原子地操作Redis结构,以确保每个租户在下一个被选中的可能性相等,而不超过设置的限制。我们使用以下的Redis结构:

钥匙 目的
作业: 包含作业元数据的哈希,用于动态发现与作业相关的所有其他数据结构。
_pending_jobs: 给定租户和队列的待处理作业ID列表。
租客限额 一个队列映射到一个租户允许并发运行的最大作业数。
<队伍> _running_jobs: < tenant_id > 包含租户当前在给定队列中运行的所有作业的id的集合。
<队列> _runnable_tenants 包含有待处理工作的所有租户的IDS集合,并没有达到其容量。

发布脚本在Redis中初始化作业的元数据。这储备脚本使用租户和未决工作所知的内容来确定运行的工作。这重新安排脚本做出重新运行的工作删除脚本从redis中删除一份工作。由于一些元编程魔术,我们的工人可以通过借助作为Ruby函数的每个脚本redis_script_runner.

可靠性

这个项目最具挑战性的方面是确保容错性,因为Redis已经成为我们基础设施中一个新的关键任务。例如,我们的Redis部署使用间隔1秒的异步主从复制磁盘持久性。如果我们在灾难场景中失去了Redis,我们可能会失去几秒钟的数据。我们的Job系统需要能够重新发布任何被删除的写,并最终保持一致。为了实现这一点,所有Lua脚本都必须是幂等的。例如,发布脚本在填充任何Redis结构之前,检查工作是否已经存在于Redis中。此外,流程必须能够检测恢复场景并自动触发重新发布。最后,Postgres和Redis之间没有分布式交易,这进一步巩固了Redis最终一致的需求。单单解决这个问题就值得写一篇博客。

结果

以这种方式扩展延迟作业允许我们消除最昂贵的数据库操作。确定要运行的作业的速度提高了99.97%,数据库CPU使用率下降了65%。作业吞吐量增加了20.18%。这是由于减少了保留和完成作业的开销。自从做了这些改变后,我们已经能够使用相同的硬件运行更多的工作人员,这导致了用户可以注意到的延迟改善。我们的核心存储库服务和其他两个高吞吐量服务已经在使用我们的新作业系统,并取得了类似的结果。

下一步是什么?

除了这些性能收益外,还改变我们的租户公平算法已经变得令人难以置信。我们计划将特定租户限制添加为对我们目前的队列特定限制的提高。这将使我们能够为客户提供更多工人,而不是非支付审判客户。较大的客户还可以要求更多工人运行更多并发任务。我们还计划通过引入爆发来添加队列预先抢占。当少数其他租户有待决任务时,这将临时允许租户超出其限制。

您是否必须解决类似的工作系统缩放问题?这会是你喜欢看到开放的工具吗?我们想听听它。

话题:数据结构软件开发RubyRuby在Rails.延迟工作软件工程数字商务电子商务战略

评论由反驳

最近的帖子

    Baidu