阅读 519

业务增长400%,Uber如何快准稳扩容HDFS集群?

作者 | Ang Zhang , Wei Yan
编译 | 李瑞丰
编辑 | Emily Chen

三年前,Uber 采用 Hadoop 作为大数据分析系统中海量存储(HDFS)和并行计算(YARN)的底层架构方案。随着业务的发展,Uber 不断对这套系统的稳定性、可用性以及用户体验进行了持续的改善。

Uber 使用 Hadoop 的场景有很多,包括批处理和流式计算。其涵盖的业务场景有反欺诈、机器学习,以及到达时间预估系统等等。随着 Uber 业务在近几年的发展,其 Hadoop 集群数据存储容量大小和访问频次都在以指数级别的方式增长。仅在 2017 年,HDFS 存储数据量就增长了 400% 以上。

而完成一个兼顾扩展性和高性能的架构设计,绝对不是一件容易的事情。为了达到这个目标, Uber 的数据架构团队对整个架构进行了彻底仔细的检查和分析,将目标拆分为几个大的方向去进行优化。具体方向包括: View File System (ViewFs),快速的 HDFS 版本升级,NameNode 垃圾回收 调优,限制系统中小文件数量,提供了 HDFS 加载管理服务,以及增加了一个 NameNode 的只读副本。接下来我们针对上述内容进行一个详细介绍,看 Uber 是如何构建一个快速增长的、稳定的,并且可靠的存储系统的。

更多干货内容请关注微信公众号“AI 前线”,(ID:ai-front)

扩容的挑战

HDFS 的设计初衷是一个支持快速扩展的分布式文件系统,能够在一个集群内支持上千个节点。只要硬件充足,将存储系统的容量扩展到 100 PB 以上应该是很容易,并且能快速实现的。

但是对于 Uber 来说,每周有数千个业务方用户查询(通过 hive 或者 presto)Uber 的数据系统。随着业务的高速发展,Uber 既想快速扩容,又不想影响正常的查询性能,其实是一件很不容易的事情。

在 Uber 的系统中 ,Presto 用户占到了一半以上,并且 90% 的 Presto 查询要持续 100s 以上。如果 HDFS 系统过载,这样就会导致 Presto 查询在队列中堆积,最终使得查询延迟。这种情况下,Uber 需要解决的问题是,如何用最快速度准备好一个查询结果相关的数据。

在原来的数据架构中,Uber 设计了 ETL 系统,在每个集群内应用来支持用户的查询。ETL 包括数据的提取、转换和加载。这样在同一个集群当中,可以减少由复制导致的延迟。但这种方式存在一个副作用,就是会产生大量的小文件,而小文件也是阻塞查询队列的重要原因。

除了上述问题之外,还需要注意到大部分的业务团队在进行数据访问和使用的时候,所涉及的数据是现有数据的绝大部分。这样导致 Uber 很难通过按照用户场景或者组织架构层面去做集群的拆分的方式,来降低业务快速增长给集群带来的压力。

而导致 Uber 不能实现快速扩容又不降低用户体验的主要瓶颈,其实是在 NameNode 这个节点的吞吐量上。NameNode 是整个集群的文件索引,索引中记录了数据文件在集群中的位置。客户端要想访问 HDFS 集群来获取数据的话,第一步必须要先访问 NameNode。更糟糕的是,在 NameNode 中是通过一个 ReadWriteLock 来实现对元数据区域的读写控制(为了保证数据一致性,译者注)。这使得读写 NameNode 的性能更加糟糕,因为每次写操作都需要抢占这个锁,并且强制其他请求在队列中等待。

在 2016 年年末,Uber 分析了高请求量下(RPC 请求)请求的响应时间,并整理如下图。从图中可以看到,偶尔有请求的执行时间超过了 500ms,最长的甚至都到了接近 1s。这意味着每个 HDFS 请求都要在队列中等待至少半秒的时间。而正常请求的响应时间通常只有 10ms。

图 1:NameNode RPC 请求平均时间能超过半秒。

扩容 & 性能调优

Uber 对 HDFS 系统的目标是在能支持高性能查询的同时,也能做到随着业务增长,快速对 HDFS 集群进行扩容。为了实现这个目标,Uber 团队设计了一些方案,能够在短期内避免上述问题。与此同时,这些解决方案中也包含了在中长期计划当中如何支持系统的高可用和水平扩展能力,从而应对业务的快速发展。

下面就列举了 Uber 团队是如何做到在 HDFS 容量实现了 400% 增长的前提下,还能保证整个系统的高性能和高可用。

使用 ViewFs 实现水平扩展

Uber 受到 Twitter 的启发,采用了 View File System (ViewFs) 的方案来实现对多个物理 HDFS 集群的管理。并且多个 HDFS 集群都通过挂载的方式接入 ViewFs,这样在外部使用者看来,就像一个系统一样。

为了实现拆分,Uber 参考 YARN 以及 Presto 集群相关操作分布的情况,将底层 HBase 进行了拆分。拆分之后效果明显,除了大幅降低了主集群的负载之外,还使得 HBase 更加稳定,并且集群重启时间也由原来的数个小时缩短到了几分钟。

Uber 还特意为 YARN 的日志收集 单独搭建了一套 HDFS 集群。 YARN-3269 的日志收集处理之后,可以被用来优化 ViewFs。Uber 的 Hive scratch directory 也放到了这个集群当中。目前看来,这种方式的效果很棒:新的集群支撑了大概 40% 左右的写请求,并且将几乎全部的小文件的写都转移到了该集群,从而帮助主集群缓解其自身的压力。除此之外,这个迁移非常顺畅,因为对现有的用户应用几乎没有任何影响。

最终,Uber 选用了多个 HDFS 集群的方案,而没有采纳 Hadoop 官方提供的 HDFS Federation。这种方案可以帮助 Uber 尽可能地规避了采用单一的 HDFS 集群这种方案的潜在风险(如宕机)。此外,物理层面的隔离还提高了整个系统的可靠性。不过这种方案有一个缺点,就是会增加 HDFS 集群运维的复杂度。

图 2:Uber 的 HDFS 部署方案:基于多数据中心的 ViewFs

HDFS 升级

Uber 遇到的第二个问题,就是如何解决 HDFS 集群在大规模环境下做版本升级的问题。Uber 在一年之内完成了 HDFS 两个主要版本的升级,分别是从 CDH 5.7.2(存在大量补丁一个版本)到 Apache 2.7.3,之后又升级到了 2.8.2 版本。为了完成快速的升级,Uber 没有采用第三方集群管理工具,而是基于 Puppet 以及 Jenkins 构建了自己的部署系统。

版本升级带给 Uber 生产环境中大规模 HDFS 集群的好处是显而易见的,主要包括:HDFS-9710, HDFS-9198, 以及 HDFS-9412。举个例子,升级到 Apache 2.7.3 版本之后,上报的增量块数量明显减少,从而使 NameNode 负载显著降低。

升级 HDFS 的风险还是比较大的,可能会导致集群宕机、性能下降,甚至于数据丢失。为了解决这些隐患, Uber 在将其生产环境升级到 2.8.2 之前,花费了数月个的时间去进行相关验证。但就算如此,Uber 在升级的过程中,在其最大的 HDFS 集群上还是碰到了一个意料之外的 BUG HDFS-12800,并且这个 BUG 是在上线一段时间之后才发现。此时 Uber 的集群隔离、稳定的升级步骤,以及降级回滚策略帮了大忙,让 Uber 能将这个问题的影响降到最低。

能同时支持不同版本的 YARN 和 HDFS 在同一个集群中同时运行,对扩容来说也是一个非常关键的点。YARN 和 HDFS 都是 Hadoop 的一部分,通常来讲他们都是一起进行升级的。但是 YARN 的大版本升级通常需要很久的时间来验证,可能是因为 YARN 的 API 接口发生变化,或者 JAR 依赖变化后导致冲突的发生。Uber 不想让 HDFS 的升级过程受到 YARN 升级的干扰。为了解决这个问题,Uber 会在升级 YARN 的同时,运行一个旧版本的、能够很好支持现有应用的 YARN(需要注意的是,当使用了 Erasure Coding 这样的特性的时候,这种方式可能就不生效了。因为这个特性需要客户端也做出改动)。

NameNode 垃圾回收 调优

垃圾回收(GC)调优在 Uber 的日常优化中起到了很重要的作用。Uber 通过 GC 调优,能为存储集群的扩容节省出很大的空间。

Uber 采用 CMS 垃圾回收器来进行垃圾回收,并且有针对性的调整了一些参数的值,如 CMSInitiatingOccupancyFraction,UseCMSInitiatingOccupancyOnly 以及 CMSParallelRemarkEnabled 来优化老年代的垃圾回收效果。这些参数能够通过提高对 CPU 的利用率的方式,更快的完成垃圾回收操作。

当远程调用 (RPC)负载很高的时候,大量的短暂存活的对象会在年轻代创建。这会导致年轻代的垃圾回收器频繁触发 stop-the-world collection 操作,来进行垃圾回收。Uber 通过将年轻代的堆大小从 1.5GB 调整到 16GB 以及将 ParGCCardsPerStrideChunk 大小指定为 32768 这种方式,将 NameNode GC 的 STW 总时间占比从 13% 优化到了 1.7%,整体的吞吐量提升了 10% 之多。性能测试结果表明,只读场景下性能提升更多。

下面列出了 JVM 中针对 GC 调优涉及到的一些参数(NameNode 堆大小是 160GB 的情况下):

  • XX:+UnlockDiagnosticVMOptions

  • XX:ParGCCardsPerStrideChunk=32768 -XX:+UseParNewGC

  • XX:+UseConcMarkSweepGC -XX:+CMSConcurrentMTEnabled

  • XX:CMSInitiatingOccupancyFraction=40

  • XX:+UseCMSInitiatingOccupancyOnly

  • XX:+CMSParallelRemarkEnabled -XX:+UseCondCardMark

  • XX:+DisableExplicitGC

Uber 也在尝试验证 G1 垃圾回收器的效果,来决定是否要在系统中采用 G1 完成垃圾回收。不过之前的测试中尚未发现 G1 相比 CMS 而言有特别明显的优势。Uber 表示也会持续关注新版本 JVM 中 G1 垃圾回收器性能的提升,很可能接下来就需要重新审视垃圾回收器的选择了。

图 3:增加年轻代大小(从 1.5GB 调整到 16GB)和优化 ParGCCardsPerStrideChunk 参数与 GC 暂停时间之间的关系展示(从 13% 到 1.7%)。

控制小文件的数量

NameNode 会加载所有的元数据到内存当中,所以如果系统中存在很多的小文件,对 NameNode 的压力就会特别大。此外,访问相同数据量的情况下,小文件过多还会导致 client 发起的读文件 RPC 请求过多(写入小文件的时候也是这样)。针对这种情况,Uber 主要采用了两种方式来进行优化:

首先,Uber 数据团队构建了一个新版本的数据管道(基于 Hoodie 库),这样会比原生的数据管道创建更少的小文件。作为切换到新数据管道之前的一个过渡方案,Uber 数据团队也提供了一个工具(内部称之为 stitcher)来对现有的小文件进行合并,保证大部分文件都在 1GB 大小之上。

其次,Uber 严格限定了 Hive 数据库和应用的目录所在的空间的配额。为了实现这一点,Uber 维护了一个管理工具,可以帮助用户管理自己应用所在的空间目录的配额。默认每个文件都是 256MB 大小,用户可以通过 Hadoop 团队提供的优化建议和工具来合并相应的文件,从而更合理的组织管理自己应用的文件。例如,通过调整 Hive auto-merge 配置和 reducers 的数量,可以显著改善由 Hive 插入 - 重写 操作产生的小文件。

HDFS 负载管理服务

运维一个大规模、多租户的基础架构系统,比如 HDFS,有一个很大的挑战就是发现系统突增负载的来源,并且能快速解决其带来的问题。Uber 团队构建了一个叫做 Spotlight 的 HDFS 内部负载的管理服务。

在 Spotlight 现有的实现当中,NameNode 会输出一份审计日志,然后通过 Flink 和 Kafka 进行实时处理。审计日志分析结果可以通过系统面板进行查看,并且自动禁用那些导致 HDFS 性能下降的账户,或者直接干掉有问题的工作流。

图 4:Spotlight 系统使得 Uber 能发现并处理导致 HDFS 性能出现问题的账户。

观察者(Observer) NameNode

Uber 正在致力于开发一个作为观察者(Observer)的 NameNode(HDFS-12975)。这是 HDFS 的一个新特性,设计的初衷是通过提供一个只读的 NameNode 副本的方式,来减少当前 Active NameNode 上的负载压力。因为 Uber 目前主要的 HDFS RPC 压力来自于 Presto 查询发起的只读请求,所以 Uber 期望能通过 Observer 来提升 NameNode 的吞吐量。Observer 目前正处于验证阶段,Uber 也开始着手准备将其迁移到生产环境当中。

图 5:Uber 设计的采用了 Observer NameNode 之后, HDFS 的高可用架构方案。

关键措施

Uber 总结了自己在 HDFS 架构方面的一些最佳实践(其他同样采用 HDFS 的公司或者组织同样可能面临的一些问题),具体如下:

  • 分期规划解决方案:像 Observer NameNode 和 拆分 HDFS 集群这种解决方案,属于长期规划;而短期的、临时的解决方案,比如 GC 调优以及合并小文件,可以为长期规划提供缓冲的时间。两类方案结合规划施行,更加合理。

  • 小文件对 HDFS 是一个很大的隐患,关于小文件的治理,越早开始越好。团队可以通过提供工具、文档,以及对用户培训的方式,在大规模的、多租户的 HDFS 集群中来尽可能地避免小文件的产生。

  • 积极参与到社区当中:Hadoop 已经发行了有 10 年多的时间了,它的社区也比之前活跃了很多。每个新的版本中,都会引入社区中的一些可扩展性和功能性方面的新特性。参与到 Hadoop 社区当中,提供自己的工具或者介绍自己的发现,也会对你自己的基础架构有很大帮助。

快速发展

尽管 Uber 在过去的几年当中取得了很大的进步,但是接下来为了提升 HDFS 基础架构的稳定性,还有很多的工作要做。

例如,Uber 在接下来计划集成几个新的服务到他们的存储系统当中,就像图 6 中展示的一样。这些新服务会帮助 Uber 后续的基础架构扩展,并且使 Uber 的存储系统更加易用。

图 6:Uber 未来的 HDFS 基础架构。这套架构中新增的部分,会帮助 Uber 存储系统的发展更加顺利和稳定。

下面就着重介绍下两个主要的项目:基于路由的 HDFS 集群 和分层存储:

基于路由的 HDFS 联邦集群

Uber 目前遇到系统负载压力的时候,采用 ViewFs 的方式来进扩容。这种方式遇到的主要问题是,每次 ViewFs 存在挂载点的变动时,都要更新客户端的配置。并且当出现配置问题的时候,更新操作很难在生产环境做到应用无感知的回滚。这也是我们目前只对那些客户端较少的应用采用这种方案来进行扩容的原因,例如 YARN log 的收集。

Microsoft 在 HDFS 2.9 版本中提供的新特性:基于路由方案的联邦集群(HDFS-10467, HDFS-12615),就是对现有 ViewFs 架构的一个很好的补充。这种方案在集群层面增加了一个由软件管理的中心化的 HDFS 命名空间。这种方案提供的接口跟之前的 WebHDFS 接口是互相兼容的,可以让用户的应用无需任何修改就能完成子集群之间的互相访问,同时还能独立维护自己的数据。

通过在各集群之上提供一个负载均衡工具,联邦集群(federation)能支持在子集群之前的数据无缝迁移,从而降低某个子集群上的负载,还能够实现分层存储。联邦集群层会记录当前全局命名空间的状态,并且可以同时启用多个路由器来将用户请求合理映射到不同的子集群当中。

Uber 目前正在积极致力于将基于路由的 HDFS 联邦集群解决方案落地到自己的生产环境当中,并且帮助 Hadoop 社区改进相关开源代码,包括 WebHDFS support。

分层存储

随着存储量的不断增加,Uber 认为减少存储的花费也是很有必要的。最近 Uber 技术团队研究发现,用户主要的数据访问集中在最近产生的数据(热数据),而一些历史数据(冷数据)的访问频次是很低的。所以很有必要通过将历史数据迁移到一个独立的且资源不敏感的存储当中以大幅减少存储方面的开销。对于这种方案,HDFS Erasure Coding、Router-based Federation、高密度存储硬件(大于 250TB),以及数据迁移服务是其中的关键因素。Uber 计划在接下来的文章中分析相关的分层存储经验。

查看英文原文:https://eng.uber.com/scaling-hdfs/


关注下面的标签,发现更多相似文章
评论