三年前,Uber Engineering采用了这种方法Hadoop作为存储(HDFS)和计算(纱)的大数据分析基础设施。这种分析为我们的服务提供了动力,使我们能够交付更加无缝和可靠的用户体验。
我们使用Hadoop进行批处理和流分析跨越广泛的用例,如欺诈检测、机器学习和ETA计算。随着优步业务在过去几年的增长,我们的数据量和相关访问负载呈指数级增长;仅在2017年,HDFS上存储的数据量就增长了400%以上。
在保持高性能的同时扩展我们的基础设施并非易事。为了实现这一点,Uber的数据基础设施团队通过实现一些新的调整和特性,对我们的HDFS扩展方法进行了大规模的改革,包括查看文件系统(ViewFs), HDFS版本升级频繁,NameNode垃圾收集优化、限制通过系统过滤的小文件数量、HDFS负载管理服务和只读NameNode副本。继续阅读,了解Uber如何实现这些改进,以促进存储系统的持续增长、稳定性和可靠性。
扩展的挑战
HDFS被设计为一个可伸缩的分布式文件系统,在单个集群中支持数千个节点。有了足够的硬件,在一个集群中扩展到超过100 pb的原始存储容量是很容易且快速的。
然而,对于Uber来说,我们业务的快速增长使得我们很难在不放缓数据分析的情况下可靠地扩大规模,我们的数千名用户创造了数百万Hive或转眼间查询每一个星期。
目前,Presto占到HDFS访问的一半以上,90%的Presto查询需要大约100秒的时间来处理。如果我们的HDFS基础设施过载,队列中的Presto查询就会堆积起来,导致查询完成延迟。其中一个问题是,我们需要HDFS上的数据尽快可用。
对于我们最初的存储基础设施,我们将提取、转换和加载(ETL)设计为在用户运行查询的相同集群中进行,以减少复制延迟。这些集群的双重职责导致生成小文件来容纳频繁的写入和更新,这进一步阻塞了队列。
除了这些挑战之外,多个团队需要大量存储的数据,这使得不可能在没有重复的情况下按用例或组织拆分集群,从而降低了效率,同时增加了成本。
这些慢下来的根源——我们在不影响ux的情况下扩展HDFS的能力的主要瓶颈——是性能和吞吐量NameNode,系统中所有文件的目录树,它跟踪数据文件的保存位置。因为所有的元数据都存储在NameNode中,客户端对HDFS集群的请求必须首先经过它。更复杂的是,一个ReadWriteLock在NameNode命名空间上限制了NameNode可以支持的最大吞吐量,因为任何写请求都会独占写锁并强制任何其他请求在队列中等待。
在2016年底,由于这种组合,我们开始经历NameNode远程过程调用(RPC)队列时间过长。有时,每个请求的NameNode队列时间可能超过500毫秒(最慢的队列时间接近1秒),这意味着每个HDFS请求在队列中等待至少半秒——与我们正常的10毫秒以下的处理时间相比,这是一个明显的放缓。
支持扩展和改进性能
为了确保HDFS操作的高性能,同时继续扩展,我们并行开发了几个解决方案,以避免短期内出现中断。与此同时,这些解决方案使我们能够构建一个更可靠和可扩展的系统,能够支持未来的长期增长。
下面,我们概述了一些改进,使我们能够将HDFS基础设施扩展超过400%,同时提高系统的整体性能:
使用viewf向外缩放
受到类似努力的启发推特,我们利用查看文件系统(ViewFs)将我们的HDFS拆分为多个物理命名空间,并使用ViewFs挂载点向用户呈现单个虚拟命名空间。
为了做到这一点,我们把我们的HBase从我们的YARN和Presto操作相同的HDFS集群。这一调整不仅大大降低了主集群的负载,而且使我们的HBase更加稳定,将HBase集群的重启时间从几小时减少到几分钟。
我们还创建了一个专用的HDFS集群聚合的YARN应用程序日志.纱- 3269需要使日志聚合支持viewf。我们的蜂巢划痕目录也被移到了这个集群。这种新增功能的结果非常令人满意;目前,新集群服务大约40%的写请求,其中大多数文件都是小文件,这也减轻了主集群的文件计数压力。因为现有的用户应用程序不需要任何客户端更改,所以这个转换非常顺利。
最后,我们在ViewFs后面实现了独立的HDFS集群,而不是基础架构HDFS联合会.有了这个设置,HDFS的升级可以逐步推出,以最小化大规模中断的风险;此外,完全隔离还有助于提高系统的可靠性。然而,这种修复方法的一个缺点是维护独立的HDFS集群会略微增加操作成本。
HDFS升级
针对我们的扩展性挑战的第二个解决方案是升级我们的HDFS以跟上最新的发布版本。我们在一年内安装了两次主要的升级,第一次从CDH 5.7.2 (HDFS 2.6.0有很多补丁)到Apache 2.7.3,然后是2.8.2。为了在此基础上执行,我们还必须在其之上重新构建部署框架木偶而且詹金斯替换第三方集群管理工具。
这些升级为我们带来了关键的可伸缩性改进,包括hdfs - 9710,hdfs - 9198,hdfs - 9412.例如,在升级到Apache 2.7.3之后,增量块报告的数量减少了,导致NameNode的负载减少。
升级HDFS可能有风险,因为它可能导致停机、性能下降或数据丢失。为了解决这些可能的问题,在将2.8.2部署到生产环境之前,我们花了几个月的时间验证它。然而,仍有一个错误(hdfs - 12800),这让我们措手不及,当时我们正在升级我们最大的生产集群。尽管我们发现它的时间较晚,但是拥有独立的集群、分阶段的升级过程和应急回滚计划帮助我们减轻了它的影响。
在同一台服务器上同时运行不同版本的YARN和HDFS的能力也被证明对我们的扩展工作非常关键。由于YARN和HDFS都是Hadoop的一部分,它们通常一起升级。然而,主要的YARN升级需要更长的时间才能完全验证和推出,因为由于YARN API的更改,一些运行在YARN上的生产应用程序可能需要更新JARYARN和这些应用程序之间的依赖冲突。虽然YARN的可伸缩性在我们的环境中不是问题,但我们不希望关键的HDFS升级被YARN升级阻塞。为了防止可能的阻塞,我们目前运行的是比HDFS更早版本的YARN,这在我们的用例中工作得很好。(然而,当我们采用诸如擦除编码因为需要客户端更改。)
NameNode垃圾收集调优
垃圾收集(GC)调优在我们的优化中也发挥了重要作用,并在我们继续扩展存储基础设施时为我们提供了必要的喘息空间。
我们通过强制防止长时间的GC暂停并发标记扫描收集器(CMS)进行更积极的老一代收集,通过调优CMS参数,如CMSInitiatingOccupancyFraction,UseCMSInitiatingOccupancyOnly,CMSParallelRemarkEnabled.虽然这增加了CPU利用率,但我们幸运地拥有足够的空闲CPU周期来支持此功能。
在重远程过程调用(RPC)负载,即在年轻代中创建大量短期对象,这迫使年轻代收集器执行停止一切集合频繁。通过将年轻代的大小从1.5GB增加到16GB,并调整ParGCCardsPerStrideChunk值(设置为32,768),我们的生产NameNode用于GC暂停的总时间从13%下降到1.7%,吞吐量增加了10%以上。基准测试结果(图3)显示了只读场景中的其他改进。
作为参考,我们为堆大小为160GB的NameNode定制的gc相关Java虚拟机(JVM)参数是:
- XX: + UnlockDiagnosticVMOptions
- XX: ParGCCardsPerStrideChunk = 32768 - XX: + UseParNewGC
- XX: + UseConcMarkSweepGC - XX: + CMSConcurrentMTEnabled
- XX: CMSInitiatingOccupancyFraction = 40
- XX: + UseCMSInitiatingOccupancyOnly
- XX: + CMSParallelRemarkEnabled - XX: + UseCondCardMark
- XX: + DisableExplicitGC
我们也在评估是否要整合Garbage first垃圾收集器(G1GC)与我们的系统。尽管我们在过去使用G1GC时没有看到优势,但JVM的新版本带来了额外的垃圾收集器性能改进,因此偶尔有必要重新考虑我们的收集器和配置的选择。
控制小文件的数量
因为NameNode将所有文件元数据加载到内存中,所以存储小文件会增加NameNode的内存压力。此外,当客户端读取文件时,小文件会增加访问相同数量数据的读RPC调用,生成文件时也会增加RPC调用。为了减少存储中小文件的数量,我们采取了两种主要方法。
首先,我们的Hadoop数据平台团队基于我们的连帽衫库生成的文件比原始数据管道创建的文件大得多。然而,作为这些可用的临时解决方案,我们还构建了一个工具(内部称为“缝合器”),将小文件合并为更大的文件,大多数大于1GB大小。
其次,我们对Hive数据库和应用程序目录设置了严格的命名空间配额。为了实现这一点,我们为用户创建了一个自助服务工具,以便在他们的组织内管理配额。配额按每个文件256MB的比例分配,以鼓励用户优化输出文件的大小。Hadoop团队还提供优化指南和文件合并工具,以帮助用户采用这些最佳实践。例如,在Hive上打开自动合并并调优reducer的数量可以大大减少Hive insert-overwrite查询生成的文件数量。
HDFS负载管理服务
运行像HDFS这样的大型多租户基础设施的最大挑战之一是检测哪些应用程序导致异常大的负载,并从那里采取快速行动来修复它们。为此,我们构建了一个内部HDFS负载管理服务,称为Spotlight。
在我们当前的Spotlight实现中,审计日志来自活动的NameNode,并通过基于Flink和Kafka的后端实时处理。然后,分析输出通过仪表板显示,并用于自动禁用帐户或杀死导致HDFS放缓的工作流。
观察者NameNode
我们正在开发观察者NameNode (hdfs - 12975),这是HDFS的一个新特性,设计为一个只读的NameNode副本,旨在减少活动NameNode集群的负载。因为HDFS超过一半的RPC量和增长来自只读的Presto查询,我们希望Observer NameNode能够在第一个版本中帮助我们将整个NameNode吞吐量提高近100%。我们已经完成了对该工具的验证,并正在将其投入生产。
关键的外卖
当我们扩展我们的HDFS基础结构时,我们收集了一些最佳实践,可能对其他面临类似问题的组织有价值,概述如下:
- 层的解决方案:实现大型的可伸缩性改进,如Observer NameNode或将HDFS拆分为更多的集群,都需要付出很大的努力。短期措施,如GC调优和通过我们的缝合器将较小的文件合并为较大的文件,为我们开发长期解决方案提供了很大的喘息空间。
- 越大越好:由于小文件对HDFS是一种威胁,所以最好尽早处理它们。为用户提供工具、文档和培训都是非常有效的方法,可以帮助执行对运行多租户HDFS基础设施至关重要的最佳实践。
- 参与社区:Hadoop已经存在了10多年,它的社区比以往任何时候都更加活跃,导致几乎每个版本都引入了可伸缩性和功能改进。通过贡献您自己的发现和工具来参与Hadoop社区对于您的基础设施的持续扩展非常重要。
前进
虽然我们在过去的几年里已经取得了很大的进步,但是要进一步改进HDFS的基础设施,我们还需要做更多的工作。
例如,在不久的将来,我们计划将各种新的服务集成到我们的存储系统中,如图6所示。这些增加将使我们能够进一步扩大我们的基础设施,并使Uber的存储生态系统更有效和更容易使用。
下面,我们将重点介绍我们的两个主要项目Router-based HFDS联合会和分层存储:
Router-based HDFS联合会
当子集群过载时,我们目前使用ViewFs来扩展HDFS。这种方法的主要问题是,每当我们在ViewFs上添加或替换一个新的挂载点时,都需要更改客户端配置,而且很难在不影响生产工作流的情况下推出这些调整。这种困境是我们目前只分割不需要大规模客户端更改的数据的主要原因之一,例如YARN日志聚合。
微软的新措施及实施的Router-based联合会(hdfs - 10467,hdfs - 12615),它目前包含在HDFS 2.9版本中,是对基于viewfs的分区联邦的自然扩展。这种联合增加了一个能够集中HDFS名称空间的软件层。通过提供相同的接口(RPC和WebHDFS),它的额外层为用户提供了对任何子集群的透明访问,并让子集群独立地管理它们的数据。
通过提供一个再平衡工具,联合层还将支持跨子集群的透明数据移动,以平衡工作负载和实现分层存储。联合层在一个集中的状态存储中维护全局名称空间的状态,并允许多个活动路由器启动和运行,同时将用户请求引导到正确的子集群。
我们正积极致力于将基于路由器的HDFS Federation引入Uber的生产环境,同时与Hadoop社区密切合作,进行进一步的开源改进,包括WebHDFS支持.
分层存储
随着基础设施规模的扩大,降低存储成本的重要性也在增加。雷竞技是骗人的在我们的技术团队中进行的研究表明,用户访问最近数据(“热门”数据)比访问旧数据(“热门”数据)更频繁。将旧数据转移到一个独立的、资源密集型较低的层将显著降低存储成本。HDFS擦除编码,Router-based联合会、高密度(超过250TB)硬件和数据移动服务(处理“热”层集群和“热”层集群之间的数据移动)是我们即将进行的分级存储设计的关键组件。我们计划在以后的文章中分享我们在分层存储实现方面的经验。
如果您对扩展大型分布式系统感兴趣,请考虑申请一个角色在我们的团队!






