在 LinkedIn,我们使用 Hadoop 作为大数据分析和机器学习的支柱。 随着数据量呈指数级增长,并且公司在机器学习和数据科学方面进行了大量投资,我们的集群规模逐年翻了一番,以匹配计算工作负载的增长。 我们最大的集群现在有大约 10,000 个节点。 多年来,扩展 Hadoop YARN 已成为我们基础架构中最具挑战性的任务之一。
在这篇博文中,我们将首先讨论我们在接近 10,000 个节点时观察到的 YARN 集群减速以及我们为这些减速开发的修复程序。 然后,我们将分享我们主动监控未来性能下降的方法,包括我们编写的一个现已开源的工具 DynoYARN,它可以可靠地预测任意大小的 YARN 集群的性能。 最后,我们将描述 Robin,这是一种内部服务,它使我们能够将集群水平扩展至超过 10,000 个节点。
当它成为一个问题
与 Hadoop 分布式文件系统 (HDFS) NameNode 的所有文件系统元数据都存储在一台机器上相比,YARN 资源管理器非常轻量级,只维护少量元数据。 我们比使用 YARN 更早地遇到了 HDFS 的可扩展性问题,我们在 2016 年开始投资扩展 HDFS。相比之下,Hadoop 中的计算组件 YARN 在我们的系统中一直是一个非常稳定和和平的部分。
我们的集群每年增长 2 倍,我们知道有一天 YARN 可扩展性会成为一个问题,因为资源管理器的单线程调度机制无法维持无限增长。 尽管如此,我们从未真正投入了解 YARN 的扩展限制,并认为它可能会在下一项技术浮出水面之前发挥作用。 这种方法一直有效到 2019 年初。
从历史上看,我们在我们的一个数据中心中构建了两个 Hadoop 集群:主集群服务于主要流量并受存储和计算的约束,而为数据混淆而构建的辅助集群主要是与空闲计算资源绑定的存储。 为了提高资源利用率,我们将辅助 Hadoop 集群中的计算节点合并到我们的主 Hadoop 集群中,作为一个单独的分区。
不幸的是,大约两个月后,事情开始分崩离析。
观察
计算节点合并后,集群有两个分区,分别有约 4,000 和约 2,000 个节点(我们称它们为“主节点”和“辅助节点”)。 很快,Hadoop 用户在他们的工作提交被安排之前经历了长达数小时的延迟。 但是,集群中有丰富的可用资源。
在寻找延迟的原因时,我们最初认为 Hadoop YARN 中处理软件分区的逻辑有问题,但经过调试和调查,我们没有发现那段代码有任何问题。 我们还怀疑将集群大小增加 50% 会使资源管理器过载,并且调度程序无法跟上。
我们仔细查看了队列的 AggregatedContainerAllocation,它表示容器分配速度。 合并前,主集群的平均吞吐量为每秒 500 个容器,辅助集群为每秒 250 个容器; 合并后,聚合的平均分配速度约为每秒 600 个容器,但在经过一段时间之后,分配速度也经常下降到每秒 50 个容器。
我们进行了几轮分析,发现一些昂贵的操作,如 DNS,在 @synchronized 注释后面,这限制了并行性。 将这些操作移出同步块后,我们观察到吞吐量提高了大约 10%,但对于用户而言,延迟仍然很明显。
通过重新定义fairness来缓解
在从资源管理器解析审计日志后,我们注意到调度器经常被困在一个队列中的调度容器很长时间,然后才切换到其他队列。 即使在性能合理的时期(每秒 600 个容器),某些队列中的用户也经历了数小时的延迟,而其他队列中的用户几乎没有遇到延迟。 某些队列的容器分配速度是正常的,但对于其他队列,则几乎降至零。 这一观察使我们重新审视调度程序如何决定为调度容器优先考虑哪个队列。 在 LinkedIn,我们使用容量调度程序,它根据利用率对队列进行排序,并首先将容器分配给利用率最低的队列。
假设我们有两个队列 A 和 B,如果 A 的利用率为 10%,B 的利用率为 20%,调度程序将首先为队列 A 调度容器,然后再为 B 服务。这在大多数情况下都有效;但是,在容器流失率高的环境中可能会出现暂时的僵局。假设队列 B 中运行的大部分作业都是相对较长的作业,而队列 A 中运行的作业是非常短暂的。由于 A 只有 10% 的利用率,容器将被调度到队列 A 中而不是队列 B。由于队列 A 中的容器流失率远高于队列 B,因此当调度程序完成队列 A 中的一次调度工作负载迭代时,队列 A 的利用率可能保持不变甚至下降,但仍将远低于队列 B,例如 9.5%,而队列 B 的利用率略微下降至 19%。在队列利用率收敛并且队列 A 的利用率超过队列 B 之前,调度程序不会接收提交给队列 B 的工作负载,但由于队列工作负载的不同特征,这可能需要几个小时。从观察者的角度来看,调度程序似乎被卡在队列 A 中的调度工作负载中,而队列 B 中的工作负载资源匮乏。
我们问自己为什么这只是在合并两个集群后才成为问题,并意识到主分区队列中的工作负载主要由 AI 实验和数据分析组成,这些工作被实现为运行时间更长的 Spark 作业,而辅助分区队列中的工作负载 主要是快速搅动的 MapReduce 工作。 如果资源管理器可以任意快速地调度容器,这将不是问题; 然而,由于集群合并极大地降低了调度速度,分配公平问题浮出水面。
我们提出的缓解措施是在调度程序分配容器时以相等的概率选择队列; 换句话说,我们随机选择队列而不是基于利用率。 瞧! 我们的问题暂时得到缓解。 我们后来将补丁贡献给了 Apache Hadoop。
效率低下的根本原因
尽管队列公平性有所缓解,但调度缓慢的根本原因仍未解决。 我们知道在我们的 YARN 集群中仍然存在迫在眉睫的扩展问题。 我们必须深入挖掘!
回顾合并之前和之后的总调度吞吐量,在最好的情况下,我们达到了 80% 的性能平价(每秒约 600 个容器与每秒 750 个容器),在最坏的情况下,我们的性能为 7% 奇偶校验(每秒约 50 个容器与每秒 750 个容器)。 这个差距直观地导致我们重新审视分区的调度逻辑,在那里我们发现了一个引起我们注意的不规则性。
默认情况下,YARN 资源管理器使用同步调度,即节点向资源管理器发送心跳,这会触发调度器在该节点上调度未完成的容器。 未完成的容器被提交到主分区或辅助分区,如果容器的分区和节点的分区不匹配,则不会在该节点上分配容器。
在队列中调度应用程序时,调度程序以先进先出 (FIFO) 的顺序遍历它们。 现在假设主分区中的一个节点向资源管理器发送心跳; 调度程序选择队列 A 进行调度,队列 A 中的前 100 个未完成的应用程序正在从辅助分区请求资源。 我们发现调度程序仍然会尝试将这些应用程序中的容器与该节点匹配的低效率,即使匹配总是会失败。 由于两个分区都处理了大量的工作负载,这会在每个心跳上产生大量开销,从而导致速度变慢。
为了解决这个挑战,我们优化了逻辑,如果一个节点从主(或辅助)分区心跳到资源管理器,调度器在调度时只考虑提交到主(或辅助)分区的应用程序。 更改后,我们观察到合并前后总平均吞吐量的奇偶性,并且在两个分区都忙的最坏情况下提高了 9 倍! 我们还在Hadoop社区贡献了这种优化。
被测量的东西得到固定
为了应对 YARN 可扩展性挑战,我们遵循了我们以前的工程主管 David Henke 的智慧,“测量得到的东西得到修复。” 紧接着的下一步是构建度量和工具来衡量和监控可扩展性。
在我们的集群达到今天的规模之前,用户遇到的任何缓慢都可以用用户队列中的资源不足来解释——一个只会影响在该队列中运行的团队的后勤问题。 要找到任何缓慢的根本原因,我们可以简单地找出哪些工作负载在该队列中消耗了不成比例的资源,并要求他们调整他们的工作流程。
然而,我们的集群最终达到了资源管理器级别的可伸缩性问题引发用户缓慢的规模。 因此,我们需要 (1) 一种测量和响应资源管理器缓慢的方法,以及 (2) 在我们继续扩大集群规模和工作负载时预测未来资源管理器性能的工具。
设置可扩展性指标和警报
我们能够利用现有的资源管理器指标来衡量性能问题。 最相关的是:
1) Apps pending:
2)容器分配吞吐量(“AggregateContainersAllocated”):
3)NodeManager心跳处理率:
应用程序待处理指标让我们对用户看到的性能有一个整体的了解。 有许多待处理的应用程序意味着队列已备份,并且许多用户的应用程序尚未运行。
在资源管理器方面,容器分配吞吐量指标告诉我们资源管理器是否无法足够快地调度容器; 长时间(例如 30 分钟)持续的低吞吐量表明可能有问题。 但是,仅低容器分配吞吐量并不表示资源管理器性能问题。 例如,如果集群被充分利用并且容器流失率较低,我们可能会看到吞吐量较低,但这是因为集群资源不足。
在容量调度器中,我们使用同步调度容器的默认设置,即在节点管理器心跳上。 节点管理器心跳处理速率指标告诉我们这个关键代码路径是否有任何缓慢。 例如,在一次事件中,我们注意到资源管理器在功能推出后花费了更多的 CPU 周期。 使用该指标帮助我们确定该功能对节点心跳处理逻辑进行了更改,从而浪费了通过网络发送的序列化和反序列化节点管理器心跳容器状态对象,从而导致 CPU 周期过多。 优化此代码路径后,节点更新吞吐量显着增加,资源管理器 CPU 使用率恢复到以前的水平。
用于保护 YARN 可扩展性的 DynoYARN
评估 YARN 可扩展性的另一个重要差距是能够预测未来资源管理器的性能。 虽然我们从历史容量分析中知道我们的工作负载和集群规模同比增长了 2 倍,但我们不知道资源管理器将如何应对这种增长,也不知道资源管理器的性能在什么时候将不再能够维持这些增长。 与我们编写的用于评估未来 HDFS NameNode 性能的规模测试工具 Dynamometer 类似,我们编写了 DynoYARN,这是一个启动任意大小的模拟 YARN 集群然后在这些集群上重放任意工作负载的工具。
DynoYARN 由两个组件组成:一个用于启动模拟 YARN 集群的“驱动程序”,以及一个用于在该集群上重放模拟工作负载的“工作负载”。 两者都实现为 YARN 应用程序; 我们本质上是在 YARN 集群中运行 YARN 集群,但资源限制要低得多。 例如,要启动一个 1,200 个节点的模拟集群,驱动程序应用程序将分配一个容器来运行模拟资源管理器,并分配一个容器来运行模拟节点管理器。 后面的每个容器都可以运行多个模拟节点管理器; 在我们的设置中,我们可以在单个 50GB 容器中运行 30 个模拟节点管理器。 因此,在 256GB 主机上,我们可以运行 5 个容器,每个容器带有 30 个模拟节点管理器,或者在每个真实的 256GB 主机上运行 150 个模拟节点管理器。 因此,1200 个节点的模拟集群只需要 1200/150 = 8 个真实主机。
为了评估资源管理器的性能,工作负载应用程序从我们的生产集群中解析审计日志,并将它们提供给驱动程序应用程序的模拟集群。生产工作负载忠实地在模拟资源管理器上重放。从审计日志中,我们提取每个应用程序请求的容器数量、每个容器的内存/vcore 要求、应用程序提交的时间以及其他元数据,例如什么用户提交应用程序(模拟用户限制约束)以及将应用程序提交到什么队列。结果是一个非常接近我们在生产中看到的性能的模拟,因为工作负载几乎完全相同。
为了预测未来的可扩展性,我们在工作负载应用程序中实现了一项功能,允许我们修改已解析的审计日志以模拟预计的工作负载。 例如,我们经常模拟的一个用例是将生产工作负载“乘以”一个固定数字,例如 1.5 倍或 2 倍。 在 1.5 倍的情况下,每个申请有 50% 的机会被提交两次; 在 2x 的情况下,每个申请将有 100% 的机会被提交两次。 使用这种策略,我们保留了生产中的高级工作负载模式(例如,Spark 应用程序的比例、长期运行与短期运行的应用程序的比例等),同时扩大它们以预测未来的性能。
通过在许多细粒度乘法器(例如,1.5x、1.6x、1.7x、1.8x、1.9x、2x)上重新运行模拟,我们可以准确地了解资源管理器的性能如何随着我们逐步扩大生产集群规模而发生变化。 以下是这些模拟的结果:
Multiplier | Number of Node Managers | Applications Per Day | p95 application delay (minutes) |
1 | 7152 | 237472 | 4.633 |
1.5 | 10728 | 354600 | 8.769 |
1.6 | 11443 | 377962 | 10.278 |
1.7 | 12158 | 401440 | 19.653 |
1.8 | 12873 | 424540 | 22.815 |
1.9 | 13588 | 441090 | 43.029 |
可扩展性结果
我们的目标是将 p95 应用程序延迟保持在 10 分钟或以下。 根据我们的模拟,我们发现一个 11,000 个节点的集群可以将应用程序延迟大致保持在 10 分钟的时间窗口内(一个 11,443 个节点的集群给我们一个 10.278 分钟的延迟,仅略高于我们的 10 分钟目标),但是一个 12,000 个节点的集群会 给我们 19.653 分钟的应用程序延迟,远远超出我们的 SLA。
根据这一预测,我们可以推断(基于我们 2 倍的同比增长)我们何时会达到这个里程碑,从而得知我们有多少时间处理遇到由于扩展而导致的严重资源管理器性能问题。
开源 DynoYARN
除了确定未来的扩展性能外,在 LinkedIn,我们使用 DynoYARN 来评估大型功能在将它们推向生产之前的影响,并在将我们的集群升级到更高的上游版本时确保性能均衡。 例如,在将 Hadoop 集群从 Hadoop 2.7 升级到 2.10 时,我们使用 DynoYARN 来比较资源管理器的性能。 我们还使用此工具对前面讨论的资源管理器优化进行 A/B 测试。 它是我们确定 YARN 性能路线图以及自信地推出大型资源管理器功能和升级的有用工具。 我们认为 YARN 社区也可以从中受益,因此我们很高兴地宣布我们将在 GitHub 上开源 DynoYARN。 该仓库可在 https://github.com/linkedin/dynoyarn 获得。 欢迎评论和投稿!
使用 Robin 进行水平扩展
虽然我们能够快速推出多项优化来缓解我们在资源管理器中发现的瓶颈,但很明显,单个 YARN 集群很快将不再能够维持 LinkedIn 当前的计算增长(主要是由于基本的单线程调度程序的设计限制)。 因此,我们踏上了寻找未来几年可以依赖的长期解决方案的旅程。
我们首先评估了来自开源 Hadoop 社区的两个潜在解决方案,即 Global Scheduling 和 YARN Federation。
全局调度的主要目标是解决默认心跳驱动调度程序无法满足的复杂资源放置要求(例如,反亲和性)。 它还引入了多线程调度,结合乐观并发控制,以提高整体集群调度吞吐量。 然而,我们没有观察到在 DynoYARN 模拟中使用我们的生产跟踪的默认单线程调度程序有明显改进(可能是由于调度线程之间的过度锁争用或在最后提交步骤中容器分配的高拒绝率)。 鉴于我们只能通过使用 YARN 进行调度优化来实现有限的(相对于我们的增长率)改进,我们没有进一步追求这个方向。
另一方面,专为解决单个 YARN 集群的可扩展性限制而设计的 YARN Federation 对我们来说似乎是一个更有希望的长期计划。 它允许应用程序跨越由数万个节点组成的多个集群,同时呈现单个 YARN 集群的视图,当我们添加更多集群以适应未来的计算增长时,这对于保持对 YARN 应用程序和我们的用户完全透明是非常理想的。 但是,出于几个原因,我们决定不在 LinkedIn 使用它。
- 控制平面(全局策略生成器)的当前实现是基于 CLI 的手动过程,无法处理我们 Hadoop 集群中的动态级别。 操作复杂性也可能很重要,即使不是不可能,也需要处理。
- 它引入了新的依赖项(MySQL 用于策略存储,Zookeeper 用于 YARN 注册表)并需要我们从未测试或使用过的许多功能,例如 YARN Reservation、非托管 AM 和 AMRMProxy。 这些复杂性对于我们这样规模的团队来说意义重大。
请注意,设计的大多数复杂性来自于允许 YARN 应用程序跨越多个集群,如果应用程序可以停留在单个 YARN 集群的边界内,我们可以避免大部分复杂性并为 YARN 构建一个特定于域的负载均衡器,非常很像规范的 L7 负载均衡器。
Robin
我们设想我们的 Hadoop 集群由子集群组成,每个子集群有大约 5,000 个节点,因此所有应用程序都可以停留在一个子集群的边界内。 有了这个假设,我们可以构建一个集群编排器来协调底层 YARN 集群之间的作业。 输入 Robin:一个负载均衡器,用于将 YARN 应用程序动态分布到多个 Hadoop 集群,我们在 LinkedIn 构建它以扩展我们的 YARN 集群。
在高层次上,Robin 提供了一个简单的 REST API,它为给定的作业返回一个 YARN 集群。 在提交作业之前,YARN 客户端会与 Robin 核对以确定作业应路由到哪个集群,然后将作业发送到正确的集群。 提交后,作业从开始到结束都保留在集群中。 虽然应用程序只允许消耗单个集群的容量,但我们没有发现这对我们的工作负载有限制。
在 LinkedIn,大部分工作负载由 Azkaban 管理,我们的工作流编排引擎代表最终用户充当 YARN 客户端。 传统上它只支持单个物理 Hadoop 集群; 因此,我们必须改造 Azkaban 以支持动态作业提交并添加 Robin 集成,以便在我们扩展逻辑集群并在其下添加物理集群时向最终用户呈现单个逻辑集群的视图。 结果,大多数最终用户都没有意识到 Robin(请参见下面的架构图)。
虽然 Robin 的整体理念及其设计很简单,但我们必须在这里解决一些其他值得一提的问题。
高可用性:Azkaban 是我们 Hadoop 最终用户的核心接口。 Azkaban的每项工作都依赖于Robin,因此Robin始终保持活跃至关重要。
- Robin 会在后台定期检查每个 YARN 集群的可用资源,并仅根据最新的快照做出路由决策,因此即使 Robin 到 YARN 连接间歇性失败,也可以路由作业。
- Robin 被设计为无状态的,因此我们可以通过添加或删除副本来扩大和缩小规模。 它部署在 Kubernetes (K8s) 上,以实现其提供的自我修复功能。
负载均衡策略:选择正确的负载均衡策略对于保持每个集群的工作负载均衡、减少资源竞争和作业延迟至关重要。 我们已经尝试了一些策略,例如:
- 大多数绝对的支配资源公平 (DRF) 资源可用,即将作业路由到具有最多可用原始资源的集群。 例如,如果集群 A 在 100 TB 中有 20 TB 可用,而集群 B 在 200 TB 中有 30 TB 可用,则将作业路由到集群 B。
- 大多数可用的相关 DRF 资源,即将作业路由到可用资源百分比最高的集群。 例如,如果集群 A 在 100 TB 中有 20 TB 可用(20% 余量),而集群 B 在 200 TB 中有 30 TB 可用(15% 余量),则将作业路由到集群 A。
- 队列级绝对可用资源,即,将作业路由到该作业将在其中运行的队列中具有最多可用资源的集群。(我们的集群具有相同的队列结构。)
我们使用生产工作负载跟踪模拟了 DynoYARN 中的每个策略,发现最绝对的 DRF 资源可用策略最大限度地减少了应用程序延迟,使其成为我们的最佳策略。
数据局部性:如今,LinkedIn 的工作负载工作性能在很大程度上依赖于数据局部性。 作为将 Robin 部署到我们最大的生产集群的一部分,我们将现有的 YARN 集群拆分为两个大小相等的 YARN 子集群,相同的底层 HDFS 集群与两个 YARN 子集群共享相同的节点。 因为每个作业现在只能访问 50% 的 HDFS 数据,与拆分前它可以访问的 100% 数据相比,数据局部性有所损失。 为了缓解这种情况,我们必须在两个子集群之间平均分配每个机架上的机器(也称为机架条带化),以便无论作业在哪个集群上运行,仍然可以从同一个机架访问数据。 事实证明,这对于处理机架和 Pod 网络交换机上的当前流量是有效的。
下一步工作
LinkedIn 正在积极迁移到 Azure。 作为下一步,我们正在研究在云上管理和扩展 YARN 集群的最佳方式。 将我们的本地 Hadoop YARN 集群的 10,000 多个节点提升到云端有很多令人兴奋的挑战,包括处理带宽抖动、磁盘利用率感知调度以降低成本以及作业缓存; 同时,在 Azure 中也有令人兴奋的探索机会,例如 Spot 实例、自动扩展等。我们还计划扩展 Robin 以支持跨本地和云集群的路由,然后开源 Robin。 如果您有兴趣加入在云上扩展最大的 Hadoop YARN 集群之一的旅程,请加入我们!
本文转载自Linkedin Engineering,原文链接:https://engineering.linkedin.com/blog/2021/scaling-linkedin-s-hadoop-yarn-cluster-beyond-10-000-nodes。