
Pinterest 的批处理平台 Monarch 由 30 多个 Hadoop YARN 构建了 17k + 个节点的集群完全在顶部 AWS EC2。在 2021 年初,Monarch 仍然在 Hadoop 2.7.1 上,已经有五年的历史了。由于反向移植上游更改 (功能和错误修复) 的复杂性越来越高,我们决定是时候投资版本升级了。我们选择了 Hadoop 2.10.0,这是当时 Hadoop 2 的最新版本。
本文分享我们将 Monarch 升级到 Hadoop 2.10.0 的经验。为简单起见,我们使用 Hadoop 2.10 来指代 Hadoop 2.10.0,使用 Hadoop 2.7 来指代 Hadoop 2.7.1。
挑战
自从 Pinterest 的批处理平台开始 (大约在 2016 年) 以来,我们一直在使用 Hadoop 2.7。随着时间的推移,我们平台处理的工作负载不断增长和发展,作为回应,我们进行了数百次内部更改以满足这些需求。这些内部补丁大多数是 Pinterest 特定的,需要大量的时间投入才能将它们移植到 Hadoop 2.10。
业务最关键的批处理工作负载在 Monarch 上运行,因此我们的最高优先级是执行升级,以避免集群停机或对这些工作负载的性能 /SLA 影响。
升级策略
由于许多用户定义的应用程序与 Hadoop 2.7 紧密耦合,因此我们决定将升级过程分为两个独立的阶段。第一阶段是将平台本身从 Hadoop 2.7 升级到 Hadoop 2.10,第二阶段是将用户定义的应用程序升级到 2.10。
在升级的第一阶段,我们将允许用户的作业继续使用 Hadoop 2.7 依赖项,而我们专注于平台升级。这增加了额外的开销,因为我们需要使 Hadoop 2.7 作业与 Hadoop 2.10 平台兼容,但它将允许我们额外的时间在第二阶段工作。
由于我们拥有的平台和用户应用程序的规模,上述两个阶段都需要逐步完成:
我们需要逐一升级君主集群
我们需要升级用户应用程序,以批量捆绑 2.10 而不是 2.7
当时,我们没有一个灵活的构建管道,允许我们构建两个单独版本的作业工件,分别使用单独的 hadoop 直接依赖和传递依赖。同样,我们要求所有用户 (公司的其他工程师) 单独验证从 2.7 到 2.10 的 10,000 多个单独的工作迁移也是不合理的。为了支持上面描述的增量升级,我们需要在迁移之前运行许多验证,以确保使用 Hadoop 2.7 构建的绝大多数应用程序将继续在 2.10 集群中运行。
我们为升级过程提出的高级步骤是:
Hadoop 2.10 发布准备: 将 Hadoop 2.7 内部分支上的所有补丁移植到香草 Apache Hadoop 2.10
将 Monarch 集群增量升级到 Hadoop 2.10 (逐个)
升级用户应用程序以增量方式 (批量) 使用 Hadoop 2.10
Hadoop 2.10发布准备
Pinterest 2.7 版本在开源 Hadoop 2.7 之上进行了许多内部更改,需要将其移植到 Hadoop 2.10。但是,Hadoop 2.7 和 Hadoop 2.10 之间发生了重大变化。因此,将 Pinterest Hadoop 2.7 更改应用到 vanilla Hadoop 2.10 是一项艰巨的任务。
以下是我们在 Hadoop 2.7 上制作的几个内部补丁示例,然后移植到 Hadoop 2.10:
Monarch 构建在 EC2 之上,并使用 S3 作为持久存储。任务的输入和输出通常在 s3 上。添加了 directoutputfilemitter,以使任务能够将结果直接写入目标位置,从而避免在 s3 中复制结果文件的开销。
添加 application master 和 history server 终结点,以便为给定作业的所有任务获取特定计数器的值。
在提供容器日志时添加范围支持,这允许获取指定容器日志的一部分。
为日志聚合添加 Node-Id 分区,使得集群的不同节点的日志可以写入不同的 S3 分区,这有助于避免达到 S3 访问速率限制。
新创建的 namenode 可能具有不同的 IP 地址,这是在故障转移时解析 NN RPC 套接字地址的一项新增功能。
如果分配的映射器数量与总映射器数量之比超过配置的阈值,则禁用抢占缩减器。
将磁盘使用情况监视线程添加到 AM,这样,如果磁盘使用情况超过配置的限制,应用程序将被终止。
将Monarch集群升级到Hadoop 2.10
集群升级方法探索
我们评估了将 Monarch 集群升级到 Hadoop 2.10 的多种方法。每种方法都有自己的优点和缺点,我们在下面概述。
方法一: 使用 CCR
中提到的高效的资源管理文章中,我们开发了跨集群路由 (CCR) 来平衡各个集群之间的工作负载。为了最大限度地减少对现有 2.7 集群的影响,一种选择是构建新的 Hadoop 2.10 集群,并逐步将工作负载移动到新集群。如果出现任何问题,我们可以将工作负载路由回其原始集群,修复问题,然后再次路由回 2.10 集群。
我们从这种方法开始,并在一些小型生产和开发集群上对其进行了评估。没有任何重大问题,但我们发现了一些缺点:
我们必须为每个集群迁移构建一个新的并行集群。这对于大型 YARN 集群 (多达数千个节点) 变得昂贵
工作负载需要批量迁移,这非常耗时。因为 Monarch 是一个很大的平台,这个升级过程可能需要很长时间才能完成。
方法二: 滚动升级
理论上,我们可以尝试滚动升级工作节点,但滚动升级可能会影响集群上的所有工作负载。如果我们遇到任何问题,回滚将是昂贵的。
方法三: 就地升级
利用与将集群从一种实例类型就地升级到另一种实例类型类似的方法,我们:
将新实例类型的多个 canary 主机作为节点的新自动缩放组 (canary ASG) 插入到集群中
相对于 (现有实例类型的) 基本 ASG 评估 canary ASG
横向扩展 canary ASG
基本 ASG 中的比例
通常,这对于没有服务级别更改的小型基础架构级别更改非常有效。作为一个探索,我们想看看我们是否可以用 Hadoop 2.10 升级做同样的事情。我们必须做出的一个关键假设是,Hadoop 2.7 和 2.10 组件之间的通信是兼容的。这种方法的步骤是:
将 Hadoop 2.10 canary 工作节点 (运行 HDFS datanode 和 YARN NodeManager) 添加到 Hadoop 2.7 集群
识别并解决出现的问题
增加 Hadoop 2.10 worker 节点的数量,减少 Hadoop 2.7 worker 节点的数量,直到 2.7 节点完全替换为 2.10 节点
升级所有管理器节点 (Namenodes、JournalNodes、ResourceManagers、历史服务器等)。这个过程的工作原理与替换 worker 节点类似,方法是将它们替换为 Hadoop 2.10 节点。
在将这种有风险的方法应用于生产集群之前,我们对 dev Monarch 集群进行了广泛的评估。原来是一个无缝的升级体验,除了一些小问题,我们将在后面描述。
最终确定的升级方法
如前所述,作业工件最初是使用 Hadoop 2.7 依赖项构建的。这意味着它们可以将 Hadoop 2.7 jar 携带到分布式缓存中。然后在运行时,我们将用户类路径放在集群上存在的库路径之前。这可能会导致 Hadoop 2.10 金丝雀节点的依赖问题,因为 Hadoop 2.7 和 2.10 可能依赖于不同版本的第三方 jar。
在使用方法 I 对一些小型集群进行升级之后,我们确定此方法将花费太长时间来完成所有 Monarch 集群的升级。此外,考虑到我们最大的 monarch 集群的规模 (最多 3k 个节点!),我们无法获得足够的 EC2 实例来替换这些集群。我们评估了利弊,并决定采用方法 III,因为我们可能会大大加快升级过程,并且大多数依赖性问题都可以快速解决。如果我们无法快速解决某些作业的问题,我们可以使用 CCR 将作业路由到另一个 Hadoop 2.7 集群,然后花时间解决问题。
问题和解决方案
在我们最终确定方法 III 之后,我们的主要重点是确定任何问题并尽快解决这些问题。从广义上讲,我们遇到了三类问题: 由于 Hadoop 2.7 和 Hadoop 2.10 之间的不兼容而导致的服务级别问题,用户定义的应用程序中的依赖性问题以及其他杂项问题。
不相容的行为问题
重新启动 Hadoop 2.10 NM 导致容器被杀死。我们发现 Hadoop 2.10 引入了一个新的配置纱线.nodemanager. 恢复. 监督默认为 FALSE。为了防止容器在重新启动 NMs 时被杀死,我们需要设置 TRUE。启用此配置后,正在运行的 NodeManager 不会尝试清理容器,因为它会立即重新启动并恢复容器。
在 2.10 金丝雀节点上安排 AM 时,作业卡住: 添加了应用程序优先级MAPREDUCE-6515假定始终在 PB 响应中设置此字段。在拆分版本的集群 (2.7.1 ResourceManager + 2.10 worker) 中,情况并非如此,因为 RM 返回的 PB 响应将不包含 appPriority 字段。我们检查这个字段是否在 protobuf 中,如果不是,我们忽略更新 applicationPriority。
HADOOP-13680制造fs.s3a.readahead.range从 Hadoop 2.8 开始使用 getLongBytes,并支持格式为 “32m” 的值 (内存后缀 K,M,G,T,P)。但是,Hadoop 2.7 代码无法处理这种格式。这会中断混合 Hadoop 版本集群中的作业。我们为 Hadoop 2.7 添加了一个修复程序,使其与 hadop2.10 行为兼容。
Hadoop 2.10 意外地在 io.serialization config 的多个值之间引入了空格,这导致了 ClassNotFound 错误。我们做了一个修复,以删除配置值中的空格。
依赖性问题
当我们执行 inplace Hadoop 2.7 到 2.10 升级时,我们面临的大多数依赖问题是由于 Hadoop 服务和用户应用程序之间共享的依赖关系版本不同。解决方案是修改用户的作业以与 Hadoop 平台依赖项兼容,或者在我们的作业工件或 Hadoop 平台分发中着色版本。这里有一些例子:
Hadoop 2.7 jar 被放入分布式缓存中,并导致 Hadoop 2.10 canary 节点上的依赖问题。我们在 Hadoop 2.7 版本中实现了一个解决方案,以防止将这些 jar 添加到分布式缓存中,以便所有主机都使用已经部署到主机的 Hadoop jar。
Woodstox 核心包。Hadoop-2.10.0 依赖于 woodstox-core-5.0.3.jar,而一些应用程序依赖于另一个依赖于 wstx-asl-3.2.7.jar 的模块。woodstox-core-5.0.3.jar 和 wstx-asl-3.2.7.jar 之间的不兼容导致作业失败。我们的解决方案是在 Hadoop 2.10 中对 woodstox-core-5.0.3.jar 进行着色。
我们有一些基于 Hadoop 2.7 实现的内部库或类。它们不能在 Hadoop 2.10 上运行。例如,我们有一个名为 S3DoubleWrite 的类,它同时将输出写入两个 s3 位置。它的开发是为了帮助我们在 3 个桶之间迁移日志。由于我们不再需要这个类,我们弃用它来解决依赖问题。
一些 Hadoop 2.7 库被打包到用户的 bazel jar 中,并在运行时导致一些依赖问题。我们采取的解决方案是将用户应用程序与 Hadoop jar 分离。更多细节可以在后面的相关章节中找到。
杂项其他问题
我们在开发集群上执行的验证之一是确保我们可以在升级过程中途回滚。当我们尝试将 NameNode 回滚到 Hadoop 2.7 时,出现了一个还原问题。我们发现 NameNode 没有从升级的 datanode 收到块报告。我们确定的解决方法是手动触发块报告。我们后来发现潜在的问题 HDFS-12749 (NN 重新启动后,DN 可能不会向 NN 发送块报告),并将其反向移植。
当与 Hadoop 2.7 jar 捆绑在一起的 Hadoop 流作业部署到 Hadoop 2.10 节点时,预期的 2.7 jar 不可用。这是因为我们使用集群提供的 jar 来满足大多数用户工件分布的依赖关系,以减少工件的大小。但是,所有 Hadoop 依赖项都有在 jar 名称中编码的版本。解决方案是使 Hadoop 流作业捆绑 Hadoop jar 没有版本字符串,因此提供的 Hadoop 依赖项始终在运行时的类路径中,而不管它运行的节点是 Hadoop 2.7 或 2.10。
将用户应用程序升级到Hadoop 2.10
要将用户应用程序升级到 Hadoop 2.10,我们需要确保在编译时和运行时都使用 Hadoop 2.10。第一步是确保 Hadoop 2.7 jar 没有随用户 jar 一起提供,以便在运行时使用部署到集群的 Hadoop jar (2.7 节点中的 2.7 jar,和 2.10 节点中的 2.10)。然后我们改变了用户应用程序构建环境,使用 Hadoop 2.10 而不是 2.7。
将用户应用程序与Hadoop jar分离
在 Pinterest,大多数数据管道都使用 Bazel 构建的 fat jar。这些 jar 包含所有依赖项,包括升级前的 Hadoop 2.7 客户端库。我们总是尊重来自那些胖 jar 的类,而不是来自本地环境的类,这意味着在使用 Hadoop 2.10 的集群上运行那些胖 jar 时,我们仍然会使用 Hadoop 2.7 类。
为了永久解决这个问题 (在 2.10 集群中使用 2.7 jar),我们决定将用户的 Bazel jar 与 Hadoop 库分离; 也就是说,我们不再在 fat 用户 Bazel jar 中提供 Hadoop jar,并且已经部署到集群节点的 Hadoop jar 将在运行时使用。
Bazel java_binary 规则有一个名为 deploy_env 的参数,其值是表示此二进制文件的部署环境的其他 java_binary 目标的列表。我们设置此属性以从用户 jar 中排除所有 Hadoop 依赖项及其子依赖项。这里面临的挑战是,许多用户应用程序都依赖于 Hadoop 所依赖的库。这些公共库很难识别,因为它们没有被明确指定,因为它们已经作为 NodeManager 部署的一部分在 Hadoop workers 中提供。在测试过程中,我们付出了很多努力来识别这类情况,并修改了用户的 bazel 规则,以明确添加那些隐藏的依赖关系。
将Hadoop bazel目标从2.7升级到2.10
在将用户应用程序与 Hadoop jar 分离之后,我们需要将 Hadoop bazel 目标从 2.7 升级到 2.10,以便我们可以确保构建和运行时环境中使用的 Hadoop 版本是一致的。在这个过程中,Hadoop 2.7 和 Hadoop 2.10 之间存在一些依赖冲突。我们通过构建测试确定了这些依赖项,并相应地将它们升级到正确的版本。
摘要
将 17k + 节点从一个 Hadoop 版本升级到另一个版本,同时又不会对应用程序造成重大中断,这是一个挑战。我们设法做到了质量,合理的速度和成本效益。我们希望以上分享的经验对社区有益。
确认书
感谢来自批处理平台团队的张昂、郭恒哲、Sandeep Kumar、Bogdan Pisica、Connell Donaghy,他们在整个升级过程中给予了很大的帮助。感谢 Soam Acharya,Keith Regier 致力于解决问题FGAC集群。感谢 Jooseong Kim,Evan Li 和 Chunyan Wang 一路走来的支持。感谢工作流团队、查询团队和我们的平台用户团队的支持。
评论区