3年前,Uber采用了Hadoop作为大数据分析的存储(HDFS)和计算(YARN)基础设施。借助于这套系统,Uber的服务能力得到了增强,用户体验也得到了提升。
Uber将基于Hadoop的批量和流式分析应用在了广泛的场景中,例如反作弊、机器学习和ETA计算等。随着过去几年的业务增长,Uber的数据容量和访问负载也呈现了指数级增长的趋势,如:仅2017年,存储在HDFS中的数据量就增加了超过400%。
同时保证系统扩展能力和高性能并不是一件容易的事情。Uber的数据基础设施团队采用了多种方式来扩展HDFS系统,例如视图文件系统(ViewFS)、频繁的HDFS版本更新、NameNode的垃圾回收调优、减少小文件的数量、HDFS负载管理服务、只读的NameNode副本等。下面将详细介绍,Uber是如何通过这些改进措施来保证存储系统的持续增长、稳定和可靠的。
一.扩展带来的挑战
HDFS被设计成在单一集群上部署的、能够包含数千个节点的、可扩展的分布式文件系统。只要硬件配置满足,单一集群可以方便又快速的扩展到超过100PB的存储容量。
可是在Uber,由于业务的快速增长,每周都有成千上万个用户进行百万次的Hive和Presto查询,在这种情况下,既要可靠的扩展系统,又要不影响数据分析业务是很难的。
目前,一半的HDFS访问都来自Presto,90%的Presto查询需要约100秒的时间。如果我们的HDFS系统过载,Presto查询就会因为在队列中长时间等待而延迟。除此之外,当一个查询来到时,我们还需要保证HDFS中的数据是立即可用的。
在之前的存储系统中,为了减少数据复制导致的延迟,数据抽取、转换、加载(ETL)和用户查询都在同一个集群中进行。为了适应这种频繁写入和更新,系统产生了很多的小文件,这进一步导致了队列的阻塞。
此外,每个业务方都需要使用到大部分的存储数据,如果按照场景用例和组织架构来拆分集群,势必会增加数据的冗余存储、降低效率和增加成本。
扩展HDFS和用户体验之间的主要矛盾和瓶颈在于NameNode的性能和吞吐能力。NameNode保存了整个系统的目录树以及数据的位置信息。正是因为NameNode保存了所有的元数据信息,所有的客户请求都必须先访问它。而且,NameNode对整个命名空间只使用一把读写锁,使得任何写请求都会阻塞其它请求,这进一步限制了NameNode的吞吐量。
2016年下半年,我们开始遇到上述原因带来的问题 - 超长的NameNode RPC请求排队时间。有时候,NameNode的请求排队时间会超过500毫秒(最长的排队时间达到了1秒),这就意味着每个HDFS请求都需要在队列中至少等待半秒钟 。相比起常态10毫秒的处理时间,这无疑是巨大的性能下降。
图1. 2016年,NamoNode单个请求的RPC排队时间超过了0.5秒
二.扩展系统 & 提升性能
为了保证HDFS的持续扩展能力和高性能,我们同时开发了多个方案。它们既避免了短期内数据增长可能导致的服务中断,也为我们长期建立一个更可靠、扩展性更强的系统来应对未来的业务发展提供了支持。
下面将详细讲述这些改进方案,它们不仅把HDFS系统的容量增加了400%,还提高了系统的整体性能。
2.1 使用ViewFs来进行横向扩展
受到Twitter的一些相似工作的启发,我们使用视图文件系统将HDFS拆分到多个命名空间中,并且使用ViewFs的挂载点来给用户呈现一个虚拟的命名空间。
HBase从Yarn和Presto所在的HDFS集群中分离了出来,这个调整不仅大大降低了主集群上的负载,而且使得HBase更加的稳定,将HBase集群的启动时间从几小时缩减到了几分钟。
我们还为Yarn上的作业汇聚日志创建了专有的HDFS集群(汇聚日志要支持ViewFS,需要包含YARN-3269,)。Hive的scratch目录也被放在了这个专有集群上。现在,新的集群服务了大概40%的写请求,而且这个集群上的大部分文件都是小文件,这样也减轻了文件数量给主集群带来的压力。该方案不需要客户端的代码改动,因此整个迁移过程很平滑。
我们最终选择在ViewFS的下层部署独立的HDFS集群,而不是HDFS联邦集群。这样,每个HDFS集群都可以单独升级,减少了大规模故障的风险。此外,集群隔离也提高了系统的可靠性。这个方案的一个缺点是,维护多个独立的HDFS集群,会带来更高的运维成本。
图2. 我们在多个数据中心部署了ViewFS来管理多个HDFS命名空间
2.2 HDFS升级
第二个方案就是保持HDFS升级到最新的发布版本。我们一年内,进行了两次大版本升级,先从CDH 5.7.2 (HDFS 2.6.0的基础上打了很多补丁)升级到Apache 2.7.3,然后升级到2.8.2。为了进行升级,我们还在Puppet和Jenkins的基础上,重新构建了部署框架,来取代第三方的集群管理工具。
升级(HDFS-9710, HDFS-9198,和HDFS-9412)能够显著提升HDFS集群的扩展能力。例如,升级到Apache 2.7.3后,增量数据块的报告减少了,NameNode的负载也降低了。
升级HDFS是有风险的,因为可能导致停机、性能下降或者数据丢失。为了避免这些情况,我们在部署到生产环境前,花费了数个月的时间来验证2.8.2版本。可是在升级我们最大的集群时,还是意外的发现了一个bug(HDFS-12800)。集群隔离、分阶段升级流程和紧急回滚计划帮助我们减轻了影响。
在我们的扩展过程中,在同一个集群上采用不同版本的YARN和HDFS也是非常关键的决定。YARN和HDFS是Hadoop的组成部分,通常是一起升级的。但是,YARN的大版本升级可能导致API的变更和JAR包冲突,使得生产环境中的作业失败,因此需要很长的验证和实施时间。因为我们不需要对YARN集群进行扩容,我们也不希望HDFS的关键升级被YARN阻塞住,所以我们目前部署的YARN版本要老于HDFS。在我们的场景中,这种模式运行良好。(可是,如果要使用Erasure Coding特性的话,会涉及到客户端API的变更,这种模式就不适用了。)
2.3 NameNode垃圾回收调优
垃圾回收调优在我们的扩容过程中,也扮演了很重要的角色,给了我们更多的缓冲时间来进行后续存储系统的横向扩展。
我们通过参数调整(CMSInitiatingOccupancyFraction、UseCMSInitiatingOccupancyOnly和CMSParallelRemarkEnabled),促使CMS更加主动的进行老生代的垃圾回收,从而避免长时间的GC停顿。虽然这样会增加CPU的使用率,但我们CPU的配置足够支撑。
当RPC处于高负载时,新生代会产生大量的临时对象,新生代的垃圾回收器会进行频繁的stop-the-world回收操作。通过把新生代的空间从1.5GB增加到16GB,同时调整ParGCCardsPerStrideChunk(设置成32768),生产环境NameNode的GC停顿时间占比从13%降低到了1.7%,吞吐量也增加了超过10%。图3的基准测试曲线,展示了只读场景下NameNode的GC优化结果。
图3. 通过把新生代的空间从1.5G增加到16G以及调整ParGCCardsPerStrideChunk的值,NameNode的GC停顿时间占比从13%降到了1.7%
我们NamNode的堆内存是160G,以下是配置的参数。
· XX:+UnlockDiagnosticVMOptions
· XX:ParGCCardsPerStrideChunk=32768 -XX:+UseParNewGC
· XX:+UseConcMarkSweepGC -XX:+CMSConcurrentMTEnabled
· XX:CMSInitiatingOccupancyFraction=40
· XX:+UseCMSInitiatingOccupancyOnly
· XX:+CMSParallelRemarkEnabled -XX:+UseCondCardMark
· XX:+DisableExplicitGC
我们还评估了是否要使用G1垃圾回收器。尽管我们在过去的G1使用经验中,没有发现明显的优点,但是新版本的JVM总会带来额外的GC性能提升,未来替换我们的垃圾回收机制也是有可能的。
2.4 控制小文件的数量
NameNode会把所有文件的元数据信息存在内存中,存储小文件会给NameNode带来巨大的内存压力。读取和新增同样数据量的情况下,小文件也会导致更大量的RPC请求。因此,我们采用了两种方法来减少小文件的数量。
首先,Hadoop数据平台组基于Hoodie构建了新的数据摄取管道,可以生成比原来的数据管道更大的文件。在此之前,我们使用了一个临时方案,就是构建了一个工具(内部叫做stitcher)来合并小文件,大部分都被合并成大于1GB的文件。
其次,我们给Hive数据库和各个应用目录设置了更加严格的命名空间配额。我们还提供了一个自服务的工具,来帮助用户管理各自组织下的配额。配额是以256MB为基本单位分配给文件的,这可以促使用户优化他们的输出文件大小。Hadoop组还提供了优化指南和文件合并工具给用户。例如:在Hive上开启自动合并机制,并且调优reducer的数目,可以大大减少Hive插入复写请求所生成的文件数。
2.5 HDFS负载管理工具
运维一个像HDFS这样大型多租户分布式平台的最大挑战之一,就是快速检测出哪个应用导致了异常的高负载,并且能够迅速解决问题。为此,我们构建了一个HDFS负载管理服务,名字叫做Spotlight。
Spotlight目前的实现是通过Kafka获取Active NameNode上的审计日志,并通过Flink实时处理。分析结果会通过大盘方式显示出来,并且会自动封禁或杀死导致HDFS变慢的账号和作业。
图4. Splotlight可以帮助识别和封禁导致HDFS性能问题的账号
2.6 Observer NameNode
我们目前在开发Observer NameNode(HDFS-12975),这是一个新的特性,旨在通过只读的NameNode副本来降低Active NameNode的负载。由于有一半以上的HDFS RPC请求来自于只读的Presto请求,我们希望第一次发布的Observer NameNode可以把NameNode吞吐量增加100%。目前验证已经完成,并且正在部署到生产环境中。
图5. Uber目前的HDFS架构集成了HA和Observer NameNodes
2.7 要点
通过HDFS的扩展经验,我们总结了以下一些最佳实践,希望对其它面对相似问题的同行有帮助。
- 对解决方案分层:实现Observer NameNode和拆分HDFS集群都需要大量的工作,短期的举措例如GC调优、合并小文件等给了我们很多的缓冲时间来开发长期解决方案。
- 越大越好:小文件对HDFS是个危险,越早处理越好。为用户提供工具、文档和培训,都是有效的帮助用户采取最佳实践的好方法,尤其是在多租户系统中。
- 参与到社区中:Hadoop已经有十几年的历史,它的社区也比以往更加活跃,今年的每一个新版本发布都带来了扩展性和功能的提升。参与到社区中,提供你的发现和工具,这对于你的大数据基础设施的持续扩展很重要。
三.继续前进
尽管过去几年,我们在改进HDFS基础设施上取得了很大的进步,但是依然很多东西需要继续去做。
如图6,我们准备把更多的新服务集成到存储系统中来,这样可以进一步扩展我们的基础设施,并且使得Uber的存储生态更加高效和易用。
图6. Uber近期的HDFS架构将会增加多个特性,有助于扩展存储系统容量
下面介绍一下我们准备做的两个主要项目 - 基于路由的HDFS联邦集群和分层存储。
3.1基于路由的HDFS联邦集群(Router-based HDFS Federation)
我们目前是使用ViewFs来扩展HDFS集群。这种方式的问题是,每次增加或者替换ViewFs的某个挂载点时,都需要对客户端的配置进行更改,因此想要对生产环境无损的进行调整是非常困难的。这也导致了我们目前只对不需要进行大范围客户侧改动的数据进行了拆分,例如Yarn的作业聚合日志。
微软的 new initiative and implementation 和Router-based Federation (HDFS-10467, HDFS-12615)是对基于ViewFs的分区联邦系统的自然延伸。整个系统增加了一个联邦层来集成各个命名空间。联邦层通过提供HDFS兼容的接口(RPC和WebHDFS),可以使得用户透明的访问子集群,同时又能够让子集群独立管理各自的数据。
通过提供一个rebalancing工具,联邦层还支持集群之间数据的透明迁移,从而实现负载均衡和存储分层。联邦层在一个中心化的状态存储中,维护了一个全局的命名空间,通过运行多个路由器将用户的请求路由到正确的子集群中。
我们目前正在尝试把基于路由的联邦集群部署到Uber的生产环境中,同时在一些开源的改进点上和Hadoop社区保持着密切的合作,例如WebHDFS support。
3.2 分层存储
随着基础设施规模的扩大,缩减存储成本也变得十分重要。我们的技术小组所做的研究表明,用户访问新数据(热数据)比访问老数据(温数据)要频繁的多。将老数据转存到独立的,资源更不密集的分层可以显著降低存储成本。HDFS Erasure Coding、Router-based Federation、高密度(大于250TB)硬件存储,数据迁移服务(在hot层和warm层进行数据迁移)是我们将来分层存储设计的关键组件。我们将在后续的文章分享分层存储的经验。
原文链接:https://eng.uber.com/scaling-hdfs/
阿里巴巴开源大数据技术团队成立Apache Spark中国技术社区,定期推送精彩案例,技术专家直播,问答区近万人Spark技术同学在线提问答疑,只为营造纯粹的Spark氛围,欢迎钉钉扫码加入!
对开源大数据和感兴趣的同学可以加小编微信(下图二维码,备注“进群”)进入技术交流微信群。