微信邦 发表于 2020-7-22 21:29:04

向成熟化迈进 - 腾讯Ozone千台能力突破



背景介绍


腾讯目前在HDFS上存储了海量的数据,但HDFS在可扩展性上的缺陷,以及对小文件的不友好,限制了HDFS在许多场景下的应用。
为了寻找能解决这些问题的存储系统,Ozone走入了我们的视野。Ozone是继HDFS的下一代统一数据湖对象存储系统,数据湖是一种在系统或存储库中以自然格式存储数据的方案,它有助于以各种模式和结构形式配置数据,通常是对象块或文件。
HDFS缺陷
Apache Hadoop HDFS从出现到现在经过10多年的发展,已经到了非常成熟的状态,广泛应用于业界,解决海量文件的存储需求。但随着数据量的增长,以及对于数据使用方式的多样化 ,HDFS的架构局限性就逐渐被放大,NameNode在大规模场景很容易成为瓶颈:
•元数据的扩展性:NameNode是一个中央元数据服务节点,也是集群管理节点,文件系统的元数据以及块位置关系全部在内存中。NameNode对内存的要求非常高,需要定制大内存的机器,内存大小也限制了集群扩展性。京东的NameNode采用内存512GB的机器,字节跳动的NameNode采用内存1TB的机器。此外,NameNode的堆分配巨大,京东的NameNode需要360GB的堆大小,对GC的要求比较高,京东定制化的JDK11+G1GC在GC时性能良好,但是一般规模的公司不具备维护JDK能力,该方案不具备普遍性。字节跳动把NameNode修改成C++版本,这样分配、释放内存都由程序控制,也达到了不错的性能,该方案仍然不具普遍性,因为开发和维护C++版本的NameNode也需要不小规模的团队。•块汇报风暴:HDFS块大小默认128M,启动几百PB数据量的集群时,NameNode需要接受所有块汇报才可以退出安全模式,因此启动时间会达数小时。当集群全量块汇报、下线节点、balance集群存储,也会对集群元数据服务的性能造成影响,这些根本原因都是DataNode需要把所有块汇报给NameNode。•全局锁:NameNode 有一把FSNamesystem全局锁,每个元数据请求时都会加这把锁。虽然是读写分开的,且有部分流程对该锁的持有范围进行了优化,但依然大问题。同时FSNamesystem内部的FSDirectory(Inode树)还存在一把单独的锁,用来保护整棵树以及BlockMap的访问和修改。Ozone优势为了解决HDFS的上述问题,Hadoop社区推出分布式key-value对象存储系统Ozone,兼容文件访问接口。利用Hadoop Compatible FileSystem接口, Ozone可以用于大数据生态;利用CSI,S3协议, Ozone可以作为云存储服务云上用户。

Ozone架构分为三个部分,OzoneManager、StorageContainerManager、Datanode。OzoneManager相当于HDFS的Namespace元数据;StorageContainerManager相当于HDFS的Block Manager,但管理的是Container而不是HDFS的Block。而Datanode使用Raft实现的Ratis保证写一致性。

HDFS的三个局限,Ozone采用如下方式解决:
•Ozone把Namespace元数据服务和Block Manager拆分为两个服务。OzoneManager负责元数据服务;StorageContainerManager负责数据块管理、节点管理、副本冗余管理。两个服务可以部署在两台机器,各自利用机器资源。Ozone的元数据不像NameNode存储在内存中,不管是OzoneManager的元数据,还是StorageContainerManager中的Container信息都维护在RocksDB中,极大降低对内存的依赖,理论上元数据可以无限扩展。•StorageContainerManager无须管理默认128MB的Block,只需管理默认5GB的Container。极大地减少了StorageContainerManager管理的数据量,从而提升StorageContainerManager的服务性能。因为StorageContainerManager是以Container作为汇报单位,汇报数量比HDFS大大减少。无论是全量块汇报,增删副本,balancer集群存储,都不会给StorageContainerManager性能造成很大影响。•OzoneManager内部的锁是Bucket级别,可以达到Bucket级的写并发。Ozone是对象存储,对象语义的操作,不存在目录和树的关系,因此不需要维护文件系统树,可以达到高吞吐量。Ozone稳定性提升腾讯正将越来越多的业务接入Ozone,包括数据仓库、机器学习平台、K8S集群挂载盘等等。因此亟需提升Ozone的成熟度,而大规模集群上能否长时间稳定运行是检验成熟度的一个非常重要的标志。因此我们采用线上业务真实数据,在千台以上Datanode的单集群上,进行读、写、删操作,观察集群稳定性,以及数据正确性。在经过长时间的反复验证改进后,内部Ozone能够以千台以上的规模长时间稳定运行,且无须人工运维介入。在验证改进过程中,我们做了大量的优化工作来改进性能,提升稳定性。在接下来的内容中,我们将会分享几个重点的改进案例。


优化历程


在千台集群规模之前,已提前进行小规模的测试,并测出若干导致集群不稳定的因素,例如内存泄露、吞吐量下降、Ratis Group不稳定等问题。这些问题的修复,保证了千台Datanode的集群能长时间稳定运行。接下来介绍三个优化实例:内存泄露内存是影响Ozone Datanode和S3gateway稳定的最主要因素。长期运行中发现若干内存泄露问题,导致Datanode、S3gateway宕机。本文介绍两个典型内存泄露问题。案例一绝大部分Datanode宕机后产生core.pid文件,但没有产生crash log,也没有产生heap dump;少部分Datanode宕机后产生crash log,但没有产生core.pid,也没有产生heap dump。Ozone本身采用Java实现,但也使用C++实现的RocksDB。因此可以确认core文件是RocksDB异常退出时产生,而crash log是JVM crash时产生。首先用gdb分析core.pid,堆栈如下,可以看出rocksdb创建线程时宕机,而且崩溃在libstdc++里,这是C++标准库,基本不可能出现问题,因此怀疑其他原因导致创建线程失败。

注意到gdb打开core文件时,有大量New LWP,LWP为轻量级进程即线程。因此怀疑线程数过多,用info threads打出所有线程,可以看到有32599个线程都在wait锁。




接下来分析32599个线程都是什么线程,随机选出几十个线程分析,用thread threadid命令切换到对应线程,并查看线程堆栈,发现绝大部分线程来源于/usr/java/jdk1.8.0_191-amd64/jre/lib/amd64/server/libjvm.so,这表示绝大部分线程都是jvm创建出来的。因此问题仍然在Java代码里,大量Java线程,占用太多内存,导致创建C++线程时宕机。


继续分析crash log,可以看出JVM创建线程时因为OOM崩溃,从crash log可以找到26000多个Datanode State Machine Thread线程,都处于block状态。查看代码发现Datanode State Machine Thread是使用newCachedThreadPool创建的线程池里的线程,而newCachedThreadPool在所有线程block时,会不断创建新的线程,导致产生26000多个线程。至此可确认,产生core.pid的宕机和产生crash log的宕机是同一个原因:Datanode State Machine Thread线程太多。


接着分析大量Datanode State Machine Thread线程被block的原因,在每个Datanode线程数超过7000时,自动执行jstack打出所有线程堆栈,发现有7939个Datanode State Machine Thread线程,其中7938个处于等锁状态,只有1个编号5500的线程拿到锁但卡在rpc调用里。
相关JIRA:HDDS-3933(https://issues.apache.org/jira/p ... ilter=allopenissues,请复制本链接到浏览器查看)

案例二
压测S3gateway时发现20分钟内,内存涨到16G并开始宕机,首先用Jmap确定内存泄露是堆内还是堆外,在内存涨到10G时用Jmap发现tenured generation占用5909M,使用率达到99%内存,基本可确认泄露发生在堆内。然后dump下堆发现有262144个InternalSubchannel,每个InternalSubchannel在grpc-java里代表一个连接,因此可确定内存泄露原因是大量连接未断开。最终发现S3gateway为每个请求建立一个连接,但请求使用完后未断开连接。

修复后重新测试,发现S3gateway迅速占满所有CPU,24核的机器S3gateway使用CPU达到2381%。

CPU使用如此之高,无法运行arthas进行perf分析,只能采用其他策略。首先用top -Hp pid命令打出进程pid的所有线程及每个线程的CPU消耗。然后计算出使用CPU最高的线程号的十六进制表示0x417,再用jstack -l pid > jstack.txt命令打出所有线程状态,用0x417在jstack.txt查询消耗CPU最高的线程,即下图所示ThreadPoolExecutor里的线程,该线程一直处于RUNNABLE,且队列为empty,基本确认该部分线程出了问题,因为正常的线程不会一直空转,状态会有TIMED_WAITING的时刻。因为线程堆栈不包含业务代码,都是JDK的源码,因此用线程堆栈搜索JDK相关问题,最终发现是JDK8的Bug:JDK-8129861,该Bug在创建大小为0的线程池时容易触发。
相关JIRA:HDDS-3041(https://issues.apache.org/jira/browse/HDDS-3041,请复制本链接到浏览器查看)

性能优化
案例一使用S3gateway读文件时,文件越大读速越慢,读1G文件,速度只有2.2M每秒,使用perf未发现线索。

然后用tcpdump在读200M文件时抓包分析,从下图可看到读200M文件,共有10个GET请求:GET /goofys-bucket/test.dbf HTTP/1.1,每个GET请求读20M文件,每个GET请求读完后回复:HTTP/1.1 200 OK。第1个GET请求到达S3gateway时间为0.2287秒,第10个GET请求到达S3gateway时间为1.026458秒。第1个GET请求完成时间为1.869579秒,第10个GET请求完成时间为23.640925秒。可见10个GET请求在1秒内全部到达S3gateway,但每个请求耗时越来越长。因此只需要分析后续的GET请求读同样大小的数据块,比前序GET请求多做了哪些事情即可。

最后通过分析日志和阅读代码发现,Ozone采用的第三方库commons-io采用read实现skip。例如读第10个GET请求时,实际只需要读[180M, 200M),但commons-io实现skip前180M时,会将前180M读出来,导致第10个GET请求读完整的[0M, 200M),因此GET请求越来越慢。优化后,性能提升一百倍。
相关JIRA:HDDS-3223(https://issues.apache.org/jira/browse/HDDS-3223,请复制本链接到浏览器查看)

总结

经过各个方面的优化改进,在单集群千台以上的规模Ozone已能长时间稳定运行,保证数据正确。但是我们在Ozone上的改进不会就此结束,腾讯会持续推进Ozone在更多线上业务落地,部署更大规模的生产集群。并提高Ozone的可靠性、扩展性、稳定性、性能,将Ozone发展为继HDFS的下一代存储系统。

相关工作

在部署千台集群中,腾讯针对Ozone集群稳定和性能稳定做了诸多工作。集群稳定主要是保证集群宕机率低,使用中发现Ozone的OzoneManager和StorageContainerManager未发生过宕机,但Datanode和S3gateway因为内存溢出存在宕机问题。而性能稳定主要是为了保证集群规模扩大时,吞吐量可线性增长。另外Ozone采用Ratis保证写一致性,因此也做了若干优化,保证Ratis主从组成的Group能稳定的写数据。相关工作如下所示,部分工作腾讯内部已完成,在向社区推进中。稳定内存HDDS-3933. Fix memory leak because of too many Datanode State Machine ThreadHDDS-3630. Merge rocksdb in datanodeHDDS-3514. Fix memory leak of RaftServerImplHDDS-3041. Fix memory leak of s3g by releasing the connection resourceRATIS-935.   Fix memory leak by ungister metricsRATIS-925.   Fix memory leak of RaftServerImpl for no remove from static RaftServerMetrics::metricsMapRATIS-845.   Fix memory leak of RaftServerImpl for no unregister from reporterRATIS-840.   Fix memory leak of log appender性能优化HDDS-3223. Improve s3g read 1GB object efficiency by 100 timesHDDS-3745. Improve OM and SCM performance with 64% by avoid collect datanode information to s3gHDDS-3240. Improve write efficiency by creating container in parallelHDDS-3244. Improve write efficiency by opening RocksDB only onceHDDS-3168. Improve read efficiency by merging a lot of RPC call getContainerWithPipeline into oneHDDS-3770. Improve getPipelines performanceHDDS-3737. Avoid serialization between UUID and StringHDDS-3481. SCM ask too many datanodes to replicate the same containerHDDS-3743. Avoid NetUtils#normalize when get DatanodeDetails from protoHDDS-3742. Improve OM performance with 5.29% by avoid stream.collectHDDS-3734. Improve the performance of SCM with 3.86% by avoid TreeSet.addAllRATIS-821.   Fix high processor load for ScheduledThreadPoolExecutor with 0 core threads稳定Ratis GroupRATIS-995. Leader balance in multi raftRATIS-993. Pre vote before request voteRATIS-987. Fix Infinite install snapshotRATIS-983. Check follower state before ask for votesRATIS-982. Fix RaftServerImpl illegal transition from RUNNING to RUNNINGRATIS-980. Fix leader election happens too fastRATIS-989. Avoid change state from CLOSING to EXCEPTION in LogAppenderRATIS-977. Fix gRPC failed to read message
页: [1]
查看完整版本: 向成熟化迈进 - 腾讯Ozone千台能力突破