在优步容器化 Apache Hadoop 基础设施

2021-07-23 20:20:32

随着 Uber 业务的增长,我们在 5 年内将 Apache Hadoop(本文中称为“Hadoop”)部署扩展到 21000 多台主机,以支持各种分析和机器学习用例。我们组建了一支拥有各种专业知识的团队,以应对在裸机上运行 Hadoop 所面临的挑战:主机生命周期管理、部署和自动化、Hadoop 核心开发以及面向客户的门户。随着 Hadoop 基础设施的复杂性和规模越来越大,团队越来越难以承担管理如此庞大的团队的各种职责。使用脚本和工具的舰队范围内的操作消耗了大量的工程时间。坏主机在没有及时维修的情况下开始堆积。随着我们继续维护自己的 Hadoop 裸机部署,公司的其他成员在微服务领域取得了重大进展。容器编排、主机生命周期管理、服务网格和安全性的解决方案奠定了基础,使微服务的管理更加高效和简单。 2019 年,我们开始了重新架构 Hadoop 部署堆栈的旅程。快进 2 年,超过 60% 的 Hadoop 在 Docker 容器中运行,为团队带来了重大的运营优势。作为该计划的结果,该团队将他们的许多职责移交给了其他基础架构团队,并且能够更多地专注于核心 Hadoop 开发。本文总结了我们面临的问题,以及我们如何解决这些问题。在进入架构之前,有必要简要描述一下我们操作 Hadoop 的旧方式及其缺点。几个分解的解决方案协同工作,为 Hadoop 的裸机部署提供了动力。这涉及:在幕后,这是通过几个 Golang 服务、大量 Python 和 Bash 脚本、Puppet 清单和一些 Scala 代码来实现的。早期我们使用了 Cloudera Manager(免费版)并评估了 Apache Ambari。然而,由于 Uber 的自定义部署模型,这两个系统都被证明是不够的。

生产主机的手动就地突变导致了后来让我们感到惊讶的漂移。工程师经常对部署过程进行辩论,因为在事件响应期间没有审查和确定某些更改。整个舰队的变化需要永远手动计划和编排。我们上次的操作系统升级被推迟了,最终花了 2 年多的时间才完成。几个月后,管理不善的配置导致了事故。我们错误地配置了 dfs.blocksize,这最终导致我们的一个集群中的 HDFS RPC 队列时间下降。自动化与人类交互之间缺乏良好的契约会导致意想不到的严重后果。由于主机意外退役,我们丢失了一些副本。 “宠物”主人的存在和越来越多的“宠物”的手动牵手导致了高影响的事件。我们的 HDFS NameNode 迁移之一导致了影响整个批处理分析堆栈的事件。当我们开始设计新系统时,我们遵循以下一组原则: 对 Hadoop 核心的更改应该最少,以避免偏离开源(例如,Kerberos 以确保安全)

应尽可能重用和利用优步内部基础设施以避免重复以下部分详细介绍了定义新架构的一些关键解决方案。我们已经到了使用命令式、基于操作的脚本来配置主机和操作集群不再可行的地步。鉴于工作负载的有状态 (HDFS) 和批处理性质 (YARN),以及部署操作所需的自定义,我们决定将 Hadoop 加入 Uber 的内部有状态集群管理系统。几个基于通用框架和库构建的松散耦合组件使集群管理系统能够运行 Hadoop。下图代表了我们今天拥有的当前架构的简化版本。黄色的组件描述了核心集群管理系统,而绿色标记的组件代表了专门为 Hadoop 构建的自定义组件。集群管理员与集群管理器界面 Web 控制台交互以触发对集群的操作。意图被传播到集群管理器服务,然后触发改变集群目标状态的 Cadence 工作流。集群管理系统维护预先配置的主机,称为托管主机。一个节点代表一组部署在托管主机上的 Docker 容器。目标状态定义了集群的整个拓扑,包括节点放置信息(主机位置)、集群到节点的属性、节点资源(CPU、内存、磁盘)的定义及其环境变量。持久数据存储存储目标状态,允许从集群管理系统前所未有的故障中快速恢复。我们非常依赖 Uber 开发的开源解决方案 Cadence 来协调集群上的状态变化。 Cadence 工作流负责所有操作,无论是添加或停用节点,还是升级整个队列中的容器。 Hadoop 管理器组件定义了所有工作流。

集群管理器不了解 Hadoop 的内部操作以及管理 Hadoop 基础设施的复杂性。 Hadoop Manager 实施自定义逻辑(类似于 K8s Custom Operator)以在 Hadoop 的操作范围内以安全的方式管理 Hadoop 集群和建模工作流。例如,我们所有的 HDFS 集群都有两个 NameNode。 Guardrails,比如不要同时重启它们,进入Hadoop Manager 组件。 Hadoop Worker 是在分配给 Hadoop 的每个节点上启动的第一个代理。系统中的所有节点都在 SPIRE 注册,SPIRE 是一个开源身份管理和工作负载证明系统。 Hadoop Worker 组件在容器启动时使用 SPIRE 进行身份验证,并接收 SVID(X.509 证书)。 Hadoop Worker 使用它与其他服务通信以获取其他配置和机密(例如 Kerberos 密钥表)。 Hadoop 容器代表在 Docker 容器中运行的任何 Hadoop 组件。在我们的架构中,所有 Hadoop 组件(HDFS NameNode、HDFS DataNode 等)都部署为 Docker 容器。 Hadoop Worker 定期从集群管理器中获取节点的目标状态,并在节点上本地执行操作以实现目标状态(控制循环,也是 K8s 的核心概念)。该状态将定义要启动、停止或停用的 Hadoop 容器以及其他设置。在运行 HDFS NameNode 和 YARN ResourceManager 的节点上,Hadoop Worker 负责更新“主机文件”(例如, dfs.hosts 和 dfs.hosts.exclude )。这些文件指示需要包含在集群中或从集群中排除的 DataNodes/NodeManager 主机。 Hadoop Worker 还负责将节点的实际状态(或当前状态)报告回集群管理器。集群管理器在启动新的 Cadence 工作流时结合使用实际状态和目标状态,将集群收敛到定义的目标状态。与集群管理器良好集成的系统会持续检测主机问题。集群管理器做出智能决策,例如限制速率以避免同时停用过多的坏主机。 Hadoop 管理器在采取任何行动之前确保集群在不同的系统变量中是健康的。 Hadoop 管理器中包含的检查可确保集群中没有丢失或复制不足的块,并且在运行关键操作之前,DataNode 之间的数据平衡以及其他检查。使用声明式操作模型(使用 Goal State ),我们减少了对操作集群的手动参与。一个很好的例子是自动检测到坏主机并将其安全地从集群中退出以进行修复。系统通过为取出的每台坏主机添加一个新主机来保持集群容量不变(如目标状态中所定义)。下图显示了由于不同问题在一周时间段内的任何时间点退役的 HDFS DataNode 数量。每种颜色描绘了一个不同的 HDFS 集群。

过去我们遇到过多种情况,其中基础设施的可变性让我们感到意外。使用新架构,我们在不可变 Docker 容器中运行所有 Hadoop 组件( NodeManagers 、DataNodes 等)和 YARN 应用程序。当我们开始工作时,我们在生产环境中为 HDFS 运行 Hadoop v2.8,为 YARN 集群运行 v2.6。 v2.6 中不存在对 YARN 的 Docker 支持。鉴于依赖 YARN 的不同系统(Hive、Spark 等)对 v2.x 的紧密依赖,将 YARN 升级到 v3.x(以获得更好的 Docker 支持)是一项艰巨的任务。我们最终将 YARN 升级到了 v2.9,它支持 Docker 容器运行时,并从 v3.1( YARN-5366 、 YARN-5534 )向后移植了几个补丁。 YARN 节点管理器在主机上的 Docker 容器中运行。主机 Docker 套接字挂载到 NodeManager 容器,使用户的应用程序容器能够作为兄弟容器启动。这绕过了运行 Docker-in-Docker 会引入的所有复杂性,并使我们能够在不影响客户应用程序的情况下管理 YARN NodeManager 容器的生命周期(例如重启)。为了促进超过 150,000 多个应用程序从裸机 JVM ( DefaultLinuxContainerRuntime ) 到 Docker 容器 ( DockerLinuxContainerRuntime ) 的无缝迁移,我们添加了补丁以在 NodeManager 启动应用程序时支持默认的 Docker 映像。此映像包含所有依赖项(python、numpy、scipy 等),这些依赖项使环境看起来与裸机主机完全一样。在应用程序容器启动期间拉取 Docker 镜像会产生额外的开销,这可能会导致超时。为了规避这个问题,我们通过 Kraken 分发 Docker 镜像,Kraken 是一个最初在 Uber 内部开发的开源点对点 Docker 注册表。我们通过在启动 NodeManager 容器时预取默认应用程序 Docker 映像进一步优化了设置。这可确保在请求进入之前默认应用程序 Docker 映像可用,以启动应用程序容器。所有 Hadoop 容器( DataNodes 、 NodeManagers )都使用卷挂载来存储数据(YARN 应用程序日志、HDFS 块等)。这些卷在节点放置在受管主机上时提供,并在节点从主机退役 24 小时后删除。在迁移过程中,我们逐渐翻转要使用默认 Docker 映像启动的应用程序。我们还有一些客户使用自定义 Docker 镜像,这些镜像使他们能够带来自己的依赖项。通过容器化 Hadoop,我们通过不可变部署减少了可变性和出错的机会,并为客户提供了更好的体验。

我们所有的 Hadoop 集群都通过 Kerberos 进行保护。作为集群一部分的每个节点都需要在 Kerberos (dn/hdfs-dn-host-1.example.com) 中注册的主机特定服务主体(身份)。在启动任何 Hadoop 守护程序之前,需要生成相应的密钥表并将其安全地发送到节点。 Uber 使用 SPIRE 进行工作负载证明。 SPIRE 实现了 SPIFFE 规范。形式为 spiffe://example.com/some-service 的 SPIFFE ID 用于表示工作负载。这通常与部署服务的主机名无关。很明显,SPIFFE 和 Kerberos 都是它们自己独特的身份验证协议,围绕身份和工作负载证明具有不同的语义。在 Hadoop 中重新连接整个安全模型以与 SPIRE 一起工作并不是一个可行的解决方案。我们决定同时利用 SPIRE 和 Kerberos,彼此之间没有任何交互/交叉证明。这简化了我们的技术解决方案,其中涉及以下自动化步骤序列。我们“信任”集群管理器和它为从集群中添加/删除节点而执行的目标状态操作。在 Hashicorp Vault 中保留密钥表。设置适当的 ACL,使其只能由 Hadoop Worker 读取。集群管理器代理获取节点的目标状态并启动 Hadoop 工作器。通常,人为参与会导致密钥表管理不善,从而破坏系统的安全性。通过此设置,Hadoop Worker 由 SPIRE 进行身份验证,Hadoop 容器由 Kerberos 进行身份验证。上述整个过程是端到端的自动化,无需人工参与,确保更严格的安全性。

在 YARN 中,分布式应用程序的容器作为提交应用程序的用户(或服务帐户)运行。用户组在 Active Directory (AD) 中进行管理。我们的旧架构涉及通过 Debian 软件包安装用户组定义(从 AD 生成)的定期快照。这让位于整个车队的不一致,由包版本差异和安装失败引起。未被发现的不一致会持续数小时到数周,直到影响用户。在过去 4 年多的时间里,由于跨主机的用户组信息不一致导致权限问题和应用程序启动失败,我们遇到了几个问题。此外,这导致了大量的手动调试和修复工作。 Docker 容器内 YARN 的用户组管理有其自身的一系列技术挑战。维护另一个守护进程 SSSD(如 Apache 文档中所建议)会增加团队的开销。由于我们正在重新构建整个部署模型,因此我们花费了额外的精力来设计和构建用于 UserGroups 管理的稳定系统。我们的设计涉及利用内部强化的信誉良好的配置分发系统将用户组定义中继到部署 YARN NodeManager 容器的所有主机。 NodeManager 容器运行 UserGroups Process ,它观察 UserGroups 定义(在 Config Distribution System 内)的更改并将其写入与所有 Application Containers 共享为只读的卷挂载。应用程序容器使用自定义 NSS 库(内部开发并安装在 Docker 映像中)来查找用户组定义文件。使用此解决方案,我们能够在 2 分钟内实现用户组范围内的一致性,从而显着提高客户的可靠性。我们运营着 40 多个服务于不同用例的集群。对于旧系统,我们在单个 Git 存储库中独立管理每个集群的配置(每个集群一个目录)。复制粘贴配置和管理跨多个集群的部署变得难以管理。通过新系统,我们改进了管理集群配置的方式。该系统利用了以下 3 个概念:

我们将模板和 Starlark 文件中总共 66,000 多行的 200 多个 .xml 配置文件减少到约 4,500 行(行数减少了 93% 以上)。事实证明,这种新设置对团队来说更具可读性和可管理性,尤其是因为它与集群管理系统更好地集成。此外,该系统被证明有利于为批处理分析堆栈中的其他相关服务(例如 Presto)自动生成客户端配置。从历史上看,将 Hadoop 控制平面(NameNode 和 ResourceManager)移动到不同的主机一直很麻烦。这些迁移通常会导致整个 Hadoop 集群滚动重启,并与许多客户团队协调以重启相关服务,因为客户端使用主机名来发现这些节点。更糟糕的是,某些客户端倾向于缓存主机 IP,并且不会在出现故障时重新解析它们——我们从一个严重的事件中学到了这一点,该事件使整个区域批处理分析堆栈降级。 Uber 的微服务和在线存储系统在很大程度上依赖于内部开发的服务网格来进行发现和路由。 Hadoop 对服务网格的支持远远落后于其他 Apache 项目,例如 Apache Kafka。 Hadoop 的用例以及将其与内部服务网格集成所涉及的复杂性并不能证明工程工作的投资回报率是合理的。相反,我们选择利用基于 DNS 的解决方案,并计划将这些更改逐步贡献回开源( HDFS-14118 、 HDFS-15785 )。我们有 100 多个团队每天都与 Hadoop 进行交互。他们中的大多数使用过时的客户端和配置。为了提高开发人员的生产力和用户体验,我们正在对整个公司的 Hadoop 客户端进行标准化。作为这项工作的一部分,我们正在迁移到集中式配置管理解决方案,客户不必为初始化客户端指定典型的 *-site.xml 文件。利用上述相同的配置生成系统,我们能够为客户端生成配置并将配置推送到我们的内部配置分发系统。配置分发系统以可控和安全的方式在整个车队范围内推出它们。服务/应用程序使用的 Hadoop 客户端将从主机本地 Config Cache 获取配置。标准化客户端(具有 DNS 支持)和集中配置完全从 Hadoop 客户那里抽象出发现和路由。此外,它还提供了一组丰富的可观察性指标和日志记录,可以更轻松地进行调试。这进一步改善了我们客户的体验,并使我们能够在不中断客户应用程序的情况下轻松管理 Hadoop 控制平面。自从 Hadoop 于 2016 年首次部署在生产环境中,我们已经开发了几个(100 多个)松散耦合的 python 和 bash 脚本来操作集群。重新构建 Hadoop 的自动化堆栈意味着重写所有这些逻辑。努力意味着重新实现价值超过 4 年的逻辑,同时牢记系统的可扩展性和可维护性。

对 21,000 多台 Hadoop 主机进行了大修,以迁移到容器化部署,并因多年的脚本而失去可操作性,随之而来的是最初的怀疑。我们开始将该系统用于没有 SLA 的新开发级集群,然后用于集成测试。几个月后,我们开始向我们的主要集群(用于数据仓库和分析)添加 DataNodes 和 NodeManagers,并逐渐建立了信心。在一系列内部演示和编写良好的运行手册使其他人能够使用新系统之后,团队被转移到容器化部署的好处所吸引。此外,新架构解锁了旧系统无法支持的某些原语(为了效率和安全性)。该团队开始接受新架构的好处。很快,我们在新旧系统之间架起了几个组件,以实现从现有系统到新系统的迁移路径。我们采用新架构的原则之一是机群中的每一台主机都必须是可更换的。由旧架构管理的可变主机积累了多年的技术债务(陈旧的文件和配置)。作为迁移的一部分,我们决定重新映像我们机群中的每个主机。目前,自动化工作流程以最少的人工参与来协调迁移。在较高的层次上,我们的迁移工作流程是一系列 Cadence 活动,迭代大量节点。活动表现......