本帖最后由 Mirinda 于 2021-9-13 11:08 编辑
问题导读:
1.你是否遇到过Hadoop基础设施的复杂性和规模的不断增长的情况?
2.你是怎样进行UserGroup管理的?
3.文章对应用得Hadoop系统有啥启示?
介绍随着 Uber 业务的增长,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 的自定义部署模型,这两个系统都被证明是不够的。 我们旧的方法遇到了一些挑战,包括但不限于以下几点: 人工手动的在生产主机上进行操作,后来造成一些奇怪的问题。工程师经常对部署过程进行辩论,因为在事件响应期间,有些变更没有被审查和识别。 比较大的变化需要很长时间才能手工规划和协调。比如我们上次的操作系统升级被推迟了,最终花了两年多的时间才完成。 管理不善的配置导致了几个月后的事故。比如我们错误的配置了 dfs.blocksize,这最终导致我们的一个集群中的 HDFS RPC 队列时间下降。 自动化和人类交互之间缺乏良好的契约,导致了意想不到的严重后果。比如我们有些主机意外出现 decommissioned 状态,导致我们失去了一些副本。 主节点的存在以及手工维护这些主节点导致了高影响事件。比如我们的一个 HDFS NameNode 迁移导致了影响整个批处理分析任务的事件。 在重新架构的过程中,我们考虑了以前的经验教训。
新架构在开始设计新系统时,我们遵循了以下原则: 对 Hadoop 核心的更改应该是最小的,以避免偏离开源(例如,用于安全性的 Kerberos) Hadoop 守护进程必须容器化,以实现不可变和可重复的部署; 集群运维必须使用声明性概念建模,而不是基于动作的命令式模型; 集群中的任何主机都必须在故障或降级时易于更换 Uber 内部的基础设施应该被尽可能地重用和利用,以避免重复 以下部分详细介绍了新架构的一些关键解决方案。
集群管理现在我们使用命令行、基于操作脚本来维护机器,这个已经到了不可行的地步。考虑到工作负载的有状态 (HDFS) 和批处理性质 (YARN),以及部署操作所需的定制,我们决定将 Hadoop 加入 Uber 内部有状态集群管理系统。 基于通用框架和类库构建的几个松散耦合组件使 Cluster Management 系统能够管理 Hadoop。下图代表了我们现在所拥有的当前体系结构的简化版本。黄色的组件描述核心集群管理系统,而绿色标记的组件表示专门为 Hadoop 构建的自定义组件。
集群管理员通过集群管理器的 web 控制台来触发对集群的操作。意图被传播到集群管理器服务,然后触发改变集群目标状态的 Cadence 工作流。 集群管理系统维护预配置的主机(pre-provisioned hosts),称为托管主机(Managed Host)。一个节点代表一组部署在托管主机上的 Docker 容器。目标状态(Goal State)定义了集群的整个拓扑结构,包括节点放置信息(主机位置)、集群到节点的归属、节点资源(CPU、内存、磁盘)的定义及其环境变量。目标状态会被持久化,从而允许集群管理系统从故障中快速恢复。 我们非常依赖 Uber 开发的开源解决方案 Cadence 来协调集群上的状态变化。Cadence 工作流负责所有操作,无论是添加或停用节点,还是升级整个队列中的容器。Hadoop 管理器组件定义了所有工作流。 集群管理器不了解 Hadoop 的内部操作以及管理 Hadoop 基础设施的复杂性。Hadoop Manager 实现了自定义逻辑(类似于 K8s Custom Operator)以安全的方式在 Hadoop 的操作范围内管理 Hadoop 集群和模型工作流。例如,我们所有的 HDFS 集群都有两个 NameNode。这种方式使得我们不会同时重启这两个 NameNode。 Hadoop Worker 是在分配给 Hadoop 的每个节点上启动的第一个代理。系统中的所有节点都在 SPIRE 注册,SPIRE 是一个开源身份管理和工作负载认证系统。Hadoop Worker 组件在容器启动时使用 SPIRE 进行身份验证,并接收 SVID(X.509 证书)。Hadoop Worker 使用它与其他服务通信以获取其他配置和密钥(例如 Kerberos keytabs)。 Hadoop Container 表示运行在 Docker 容器中的任何 Hadoop 组件。在我们的架构中,所有的 Hadoop 组件(HDFS NameNode、HDFS DataNode等)都是以 Docker 容器的形式部署。 Hadoop Worker 定期从集群管理器中获取节点的目标状态(Goal State),并在节点本地执行操作以实现目标状态。状态可以是启动(started)、停止(stopped)或停用(decommissioned)Hadoop Container 或者是其他设置。在运行 HDFS NameNode 和 YARN ResourceManager 的节点上,Hadoop Worker 负责更新“主机文件”(例如,dfs.hosts 和 dfs.hosts.exclude)。这些文件记录需要从集群中包含或排除的 DataNodes/NodeManager 主机。Hadoop Worker 还负责将节点的实际状态(或当前状态)报告回集群管理器。集群管理器在启动新的 Cadence 工作流程时结合实际状态和目标状态,将集群收敛到定义的目标状态。 与集群管理器良好集成的系统会持续检测主机问题。集群管理器会做出智能决策,例如限制速率以避免同时停用(decommissioning)过多的坏主机。Hadoop 管理器在采取任何行动之前确保集群在不同的系统变量中是健康的。Hadoop 管理器中包含的检查可确保集群中没有丢失或副本不足的块,并且在运行关键操作之前确保数据在 DataNode 之间已经是平衡。 使用声明式操作模型(使用 Goal State),我们减少了手动操作集群。一个很好的例子是自动检测到坏主机并将其安全地从集群中退出(decommissioned),系统通过为取出的每台坏主机添加一个新主机来保持集群容量不变(如 Goal State 中定义的那样)。 下图显示了一周内由于不同问题导致的 HDFS datanode 节点退役的数量。每种颜色表示不同的HDFS集群。
Hadoop 容器化在过去遇到的许多情况中,基础设施的可变性让我们感到束手无措。在新的架构下,我们在不可变的 Docker 容器中运行所有的 Hadoop 组件(NodeManagers、DataNodes 等)和 YARN 应用程序。 当我们开始容器化改造时,我们生产环境中的 HDFS 版本为 v2.8,YARN 集群的版本为 v2.6。但是 Docker 不支持运行 YARN v2.6,为了能够更好地在 Docker 中运行 YARN 需要将 YARN 的版本升级到 v3.x 版本,但是我们不同的系统(Hive、Spark 等)对 v2.x 的紧密依赖,所以升级 YARN 是一项艰巨的任务。我们最终将 YARN 升级 v2.9,这个版本支持在 Docker 容器中运行,并从 v3.1(YARN-5366、YARN-5534)向后移植了几个补丁。 YARN NodeManagers 运行在主机上的 Docker 容器中。主机 Docker 套接字被挂载到 NodeManager 容器,以允许用户的应用程序容器作为兄弟容器启动。这就绕过了运行 Docker-in-Docker 所带来的所有复杂性,并使我们能够在不影响客户应用的情况下管理 YARN NodeManager 容器的生命周期(例如重启)。
为了方便超过 150,000 多个应用程序从裸机 JVM (DefaultLinuxContainerRuntime) 到 Docker 容器 (DockerLinuxContainerRuntime) 的无缝迁移,我们添加了相关补丁以在 NodeManager 启动应用程序时支持默认 Docker 镜像。此镜像包含所有依赖项(python、numpy、scipy 等),这些依赖项使环境看起来与裸机主机完全一样。 在应用程序容器启动期间拉取 Docker 镜像会产生额外的开销,这可能会导致超时。为了规避这个问题,我们通过 Kraken 分发 Docker 镜像,Kraken 是一个最初在 Uber 内部开发的开源点对点(peer-to-peer) Docker registry。我们通过在启动 NodeManager 容器时预先拉取(pre-fetching)默认应用程序 Docker 映像。这可确保在请求进入之前默认应用程序 Docker 映像可用,以启动应用程序容器。 所有 Hadoop 容器(DataNode、NodeManager)都使用卷挂载(volume mounts)来存储数据(YARN 应用程序日志、HDFS 块等)。这些卷在节点放置在受管主机上时提供,并在节点从主机退役(decommissioning) 24 小时后删除。 在迁移过程中,我们使用默认 Docker 映像启动应用程序。我们还有一些客户使用自定义 Docker 镜像,这些镜像使他们能够带来自己的依赖项。通过容器化 Hadoop,我们通过不可变部署减少了可变性和出错的机会,并为客户提供了更好的体验。
Kerberos 集成我们所有的 Hadoop 集群都通过 Kerberos 进行保护。集群中的每个节点都需要在 Kerberos (dn/hdfs-dn-host-1.example.com) 中注册特定于主机的服务主体(身份)。在启动任何 Hadoop 守护程序之前,需要生成相应的 keytab 并将其安全地发送到节点。 Uber 使用 SPIRE 进行 workload 认证,SPIRE 实现了 SPIFFE 规范。形式为 spiffe://example.com/some-service 的 SPIFFE ID 用于表示 workloads。这通常与部署服务的主机名无关。 很明显,SPIFFE 和 Kerberos 都是各自不同的身份验证协议,围绕身份和工作负载证明具有不同的语义。在 Hadoop 中重写整个安全模型以与 SPIRE 一起使用并不是一个可行的解决方案。我们决定同时利用 SPIRE 和 Kerberos,彼此之间没有任何交互/交叉认证。 这简化了我们的技术解决方案,其中包括以下自动化步骤的序列。我们“信任”集群管理器和它执行的从集群中添加/删除节点的目标状态操作。
使用拓扑信息(目标状态)从集群拓扑中获取所有节点。 将所有节点的相应主体注册到 Kerberos 中并生成相应的 keytab。 在 Hashicorp Vault 中保留 keytab。设置适当的 ACL,使其只能由 Hadoop Worker 读取。 集群管理器代理(Cluster Manager Agent)获取节点的目标状态并启动 Hadoop Worker。 Hadoop Worker 通过 SPIRE Agent 认证。 Hadoop Worker: 获取 keytab(在步骤 2 中生成) 将其写入 Hadoop 容器可读的只读挂载 启动 Hadoop Container Hadoop Container (DataNode, NodeManager 等): 从挂载中读取 keytab 在加入集群之前使用 Kerberos 进行身份验证。 通常,人为参与很容易导致 keytab 配置不当,从而破坏系统的安全性。通过此设置,Hadoop Worker 由 SPIRE 进行身份验证,Hadoop 容器由 Kerberos 进行身份验证。上述整个过程是端到端的自动化,无需人工参与,确保更严格的安全性。
UserGroups 管理在 YARN 中,分布式应用程序的容器以提交应用程序的用户(或服务帐户)运行。用户组(UserGroups)在 Active Directory (AD) 中进行管理。我们的旧架构涉及通过 Debian 软件包安装用户组定义(从 AD 生成)的定期快照。因为软件包版本差异和安装失败的原因,使得集群节点之间可能存在不一致的状态,。 未检测到的不一致性会持续数小时到数周,直到影响用户。在过去4年多的时间里,我们经历了一些问题,这些问题来自于权限问题和应用程序启动失败,原因是不同主机间的 UserGroups 信息不一致。此外,这导致了大量的手动调试和修复工作。 在 Docker 容器中管理 YARN 的 UserGroups 也有它自己的一组技术挑战。维护另一个守护进程 SSSD(如 Apache 文档中所建议的)会增加团队的开销。由于我们正在重新构建整个部署模型,所以我们花费了额外的精力来设计和构建一个稳定的 UserGroups 管理系统。
我们的设计涉及到利用可靠的配置分发系统(Config Distribution System),该系统可以将 UserGroups 定义转发到部署了 YARN NodeManager 容器的所有主机。NodeManager 容器运行 UserGroups 进程,该进程观察 UserGroups 定义(在配置分发系统内)的变化,并将其写入一个卷挂载,该卷挂载与所有应用程序容器以只读方式共享。 应用程序容器使用自定义 NSS 库(内部开发并安装在 Docker 映像中)来查找 UserGroups 定义文件。通过这个解决方案,我们能够在2分钟内为用户组实现集群范围的一致性,从而显著提高了客户的可靠性。
配置生成我们维护着 40 多个服务于不同用例的集群。在旧系统中,我们在单个 Git 存储库中独立管理每个集群的配置(每个集群一个目录)。复制粘贴配置和管理跨多个集群的部署变得难以管理。 在新系统中,我们改进了管理集群配置的方式。该系统利用了以下3个概念: .xml 和 .properties 文件的 Jinja 模板,与集群无关 在部署节点期间注入运行时环境变量(磁盘挂载、JVM设置等)
我们将模板和 Starlark 文件中总共 66,000 多行的 200 多个 .xml 配置文件减少到约 4,500 行(行数减少了 93% 以上)。 事实证明,这种新设置对团队来说更具可读性和可管理性,尤其是因为它与集群管理系统更好地集成。 此外,该系统被证明有利于为批处理分析技术栈中的其他相关服务(例如 Presto)自动生成客户端配置。
Discovery & Routing从历史上看,将 Hadoop master 节点(NameNode 和 ResourceManager)移动到不同的主机一直很麻烦。这些迁移通常会导致整个 Hadoop 集群滚动重启,并与许多客户团队协调以重启相关服务,因为客户端使用主机名来发现这些节点。更糟糕的是,某些客户端倾向于缓存主机 IP 并且不会在出现故障时重新解析它们——我们从一次重大事件中学到了这一点,该事件使整个区域批处理分析任务进行降级。 Uber 的微服务和在线存储系统在很大程度上依赖于内部开发的服务网络(service mesh)来进行发现和路由。Hadoop 对服务网络的支持远远落后于其他 Apache 项目,例如 Apache Kafka。Hadoop 的用例以及将其与内部服务网络集成所涉及的复杂性并不能证明工程工作的投资回报率(ROI)是合理的。相反,我们选择利用基于 DNS 的解决方案,并计划将这些更改逐步贡献回开源(HDFS-14118、HDFS-15785)。 我们有 100 多个团队每天都与 Hadoop 进行交互。他们中的大多数使用过时的客户端和配置。为了提高开发人员的生产力和用户体验,我们正在对整个公司的 Hadoop 客户端进行标准化。作为这项工作的一部分,我们正在迁移到集中式配置管理解决方案,客户无需为初始化客户端指定典型的 *-site.xml 文件。 利用上述相同的配置生成系统,我们能够为客户端生成配置并将配置推送到我们的内部配置分发系统。配置分发系统以可控和安全的方式将它们推广到整个公司。服务/应用程序使用的 Hadoop 客户端将从主机本地配置缓存中获取配置。
标准化客户端(支持 DNS)和集中配置完全从 Hadoop 客户那里抽象出发现和路由。此外,它还提供了一组丰富的可观察性指标和日志记录,可以更轻松地进行调试。这进一步改善了我们客户的体验,并使我们能够在不中断客户应用程序的情况下轻松管理 Hadoop 主节点。
心态的转变自从 Hadoop 于 2016 年首次部署在生产环境中,我们已经开发了很多个(100 多个)松散耦合的 python 和 bash 脚本来操作集群。重新构建 Hadoop 的自动化堆栈意味着重写所有这些逻辑。这意味着重新实现价值超过 4 年的逻辑,同时牢记系统的可扩展性和可维护性。 对 21,000 多台 Hadoop 主机进行大修,以迁移到容器化部署并因多年的脚本而失去可操作性,随之而来的是最初的怀疑。我们开始将该系统用于没有 SLA 保证的新开发级集群,然后用于集成测试。几个月后,我们开始向我们的主要集群(用于数据仓库和分析)添加 DataNodes 和 NodeManagers,并逐渐建立了信心。 在进行了一系列内部测试和编写了良好的使用手册使其他人能够利用它使用新的系统,团队相信迁移到容器化部署的确实有很多好处。此外,新架构解锁了旧系统无法支持的某些原语(为了效率和安全性)。团队开始接受新架构的好处。很快,我们在新旧系统之间架起了几个组件,以实现从现有系统到新系统的迁移路径。
集群迁移我们采用新架构的原则之一是集群中的每一台主机都必须是可更换的。由旧架构管理的可变主机积累了多年的技术债务(陈旧的文件和配置)。作为迁移的一部分,我们决定为集群中的每台主机重新制作镜像。 目前,自动化工作流程以最少的人工参与来协调迁移。在较高的层次上,我们的迁移工作流程是一系列 Cadence 作业,其中处理大量的节点。作业执行各种检查以确保集群稳定,智能地选择和停用节点,为它们提供新配置,并将它们添加回集群。 最初估计完成迁移需要两年。我们花了相当多的时间调优集群,以找到一个最佳点,使迁移进展足够快,同时又不会损害我们的 SLA。在9个月内,我们成功迁移了整个集群的 60%(12,500/21,000个主机)。我们将在未来6个月内完成大部分节点迁移。
经验教训如果我们说整个迁移是很顺利的,那我们就是在撒谎。迁移的初始阶段非常顺利。然而,当我们开始迁移对变化更敏感的集群时,我们发现了意想不到的问题。 块丢失我们最大的集群之一有多个运维工作流并发执行。在集群范围内的 DataNode 升级和集群其他节点的迁移同时进行,触发了 NameNode RPC 延迟的降低。后来发生了一系列意想不到的事件,最终在集群中丢失了一些数据块(blocks),我们不得不从另一个区域恢复这些块。这迫使我们为自动化和运维程序设置更多的保护和安全机制。 磁盘使用统计逻辑处理集群管理器代理(Cluster Manager Agent)定期执行磁盘使用情况统计以进行使用分析,并将其提供给公司范围的效率计划。不幸的是,该逻辑意味着“遍历”存储在 DataNode 上的 24 x 4TB 磁盘上的所有 HDFS 块。这导致了大量的磁盘 i/o。它不会影响我们不太忙的集群。然而,这对我们最繁忙的集群之一产生了负面影响,增加了 HDFS 客户端读/写延迟,这促使我们增强了磁盘使用统计逻辑。
关键要点和未来工作在过去的两年中,我们在运维 Hadoop 集群的方式上做出了巨大的改变。从脚本和 Puppet 清单的大杂烩,我们升级了我们的部署,我们升级了我们的部署,以在 Docker 容器中运行大型 Hadoop 生产集群。 从脚本和工具过渡到通过成熟的 UI 操作 Hadoop 是团队的重大文化转变。花在集群运维上的时间减少了 90% 以上。我们让自动化控制整个集群的运维和更换坏主机。我们不再让 Hadoop 管理我们的时间。 以下是我们从这次迁移中得到的关键启示: 如果没有先进卓越的运维方式,Hadoop 可能会变成一个难以驯服的庞然大物。定期重新评估部署架构并定期偿还技术债务,以免为时已晚。大规模基础设施重新架构需要时间。建立强大的工程团队,专注于进步而不是完美,生成环境中出现问题总是预料中的。我们的新的架构非常稳固,这要感谢所有参与的基础设施团队的工程师。与 Hadoop 领域之外的人合作可以收获到不同的观点。当我们看到迁移接近尾声时,我们将注意力转移到更令人兴奋的工作上。利用底层容器编排的好处,我们对未来有以下计划: 单击一次多集群就可以开通以及 region 内多个 zones 的进行集群平衡(cluster balancing) 主动检测和修复服务降级、故障的自动化解决方案。通过桥接云(bridging cloud)和本地容量来提供按需弹性存储和计算资源。 感谢本文中描述的工作之所以成为可能,是因为多个 Uber 基础设施团队的工程师的一致努力。感谢在我们迁移中支持我们的每个人,以及所有审查和协助改进本文的人。
|