1. 背景
随着业务的高速开展,针对HDFS元数据的访问恳求量呈指数级回升。在之前的上班中,咱们曾经经过引入HDFS Federation和Router机制成功NameNode的平行扩容,在肯定水平上满足了元数据的扩容需求;也经过引入Observer NameNode读写分别架构优化单组NameSpace的读写才干,在肯定水平上减缓了读写压力。但随着业务场景的开展变动,NameSpace数量也在回升至30+组后,Active+Standby+Observer NameNode 的架构曾经不可满足一切的元数据读写场景,咱们肯定思考优化NameNode读写才干,来应答始终回升的元数据读写要求。
如图1-1 所展现的B站离线存储全体架构所示,随着业务的始终增量开展,经过引入HDFS Router机制成功NameNode的平行扩容,目前NameSpace的数量曾经超越30+组,总存储量EB级,每日恳求访问量超越200亿次。各个NameSpace之间的读写恳求更是散布十分不平衡,在一些不凡场景下,局部NameSpace的全体负载更高。如Flink义务的CheckPoint 场景,Spark和MR义务的log日志上行场景,这两类场景的数据写入要求要远远高于一般场景。此外还有局部数据回刷场景,存在短期间写入恳求参与300%以上的状况,极易触发NameNode的写入性能瓶颈,影响其余义务的反常访问。为了应答这个疑问,咱们针对性的提出了NameNode的读写性能优化方案。
2. HDFS 细粒度锁优化全体方案
2.1 面临的疑问
NameNode是整个HDFS的**组件,集中治理HDFS集群的一切元数据,重要包括文件系统的目录树、数据块汇合和散布以及整个集群的拓扑结构。HDFS在对NameNode的成功上做了大胆取舍,如图2-1所示,锁机制上经常使用全局锁来一致来控制并发读写。这样处置的长处十分显著,全局锁进一步简化锁模型,不须要额外思考锁依赖相关,同时降低复杂度,缩小工程量。然而疑问比长处愈加突出,**疑问就是全局惟一锁制约性能优化。
在多年的HDFS通常上班中,咱们发现NameNode全局惟一的读写锁曾经成为NameNode读写性能最大瓶颈之一,社区曾经做了很多的上班来优化相关性能,如将一些日志操作异步化,移动日志操作到锁外,针对DU恳求驳回分段锁,大删除异步化等一系列优化措施,但关于咱们这种数据量的HDFS集群来说,依然难以满足局部消费场景。为了进一步优化HDFS读写性能,满足业务场景,咱们方案对全局锁启动细粒度拆分,为此咱们也面临着许多艰巨。
首先是疑问复杂度高,Hadoop开展到当天曾经超越十年,其中HDFS经过屡次迭代演进,架构曾经十分复杂。针对NameNode组件来说,架构上模块划分不够明晰,外部**数据结构和上班线程之间耦合十分重大,成功细节上,还存在少量相互依赖,不一而足。
其次是社区的能源无余,在全局惟一的读写锁的裁减性疑问上,社区做过屡次尝试,重要就有 HDFS-8966:Separate the lock used in namespace and block management layer 和 HDFS-5453:Support fine grain locking in FSNamesystem 等方面的尝试,然而并没有产出可以启动消费化部署的成绩。详细要素还是能源无余,由于NameNode性能针对小规模部署的集群来说大体上曾经足够,也有经过Federation和Router机制启动裁减,满足肯定的需求。
为了处置这个难题,咱们参考了业界的拆锁方案和Alluxio的LockPool成功机制,方案成功针对NameNode全局惟一锁的细粒度拆分。
2.2 设计选型
为了更好地理解经常使用全局锁存在的疑问,首先梳理全局锁治理的重要数据结构,大抵分红三类:
详细成功上,NameNode经常使用了JDK提供的可重入读写锁(ReentrantReadWriteLock),ReentrantReadWriteLock对并行恳求有严厉限度,允许读恳求并行处置,写恳求具备排他性。针对不同RPC恳求的处置逻辑,依照须要失掉锁粒度,咱们可以把一切恳求形象为全局读锁和全局写锁两类。全局读锁包括客户端恳求(getListing/getBlockLocations/getFileInfo)、服务治理接口(monitorHealth/getServiceStatus)等;全局写锁则包括客户端写恳求(create/mkdir/rename/append/truncate/complete/recoverLease)、服务治理接口(transitionToActive/transitionToStandby/setSafeMode)和主从节点之间恳求(rollEditLog)等。在一次性RPC处置环节中,假设不能及时失掉到锁,这次RPC将处于排队期待形态,直到成功取得锁,锁期待期间间接影响恳求照应性能,极其场景下假设长期间不能取得锁,将形成IPC队列沉积,TCP衔接队列被打满,客户端出现恳求卡住,新建衔接超时失败等各种意外疑问。从全局来看,写锁由于排它对性能影响愈加显著。假设有写恳求正在被处置,其余一切恳求都肯定排队期待,直到写恳求被处置成功监禁锁后再竞争全局锁。因此咱们宿愿对全局锁启动细粒度划分,最终成功NameNode服务的大局部的RPC恳求都能并行处置。
咱们方案经过3步成功 NameNode 锁的细粒度划分,如图2-2所示。
第一步,将NameNode 全局锁拆分为 Namespace层读写锁和 BlockPool层读写锁;
第二部,将NameSpace读写锁拆成颗粒度更细的Inode层的读写锁;
第三步,将BlockPool层读写锁也拆成更细粒度的读写锁;
目前咱们曾经基本成功第一局部和第二局部的上班。
3. HDFS 细粒度锁优化成功
3.1 NameNode全局惟一锁拆成
NameSpace层锁和BlockPool层锁
在通常中发现,客户端恳求访问NameNode环节中,局部恳求须要同时访Namespace层和BlockPool层,有些恳求只有要访问 Namespace层,同时服务端恳求如DataNode的IBR/BlockReport等请务实践上也只有要访问 BlockPool层,这两层的锁调用可以拆分,成功对两层数据的并行访问。因此拆锁的第一步, 就是将NameNode 全局锁拆分为 Namespace层读写锁和 BlockPool层读写锁,如图2-3所示,经过这种拆分红功访问的这两层数据的RPC恳求能够并⾏处置。在通常环节中,咱们引入了BlockManagerLock,独自处置BlockPool层锁事情。
图2-3 NameNode全局惟一锁拆成NameSpace层锁和BlockPool层锁
在实践的拆锁环节中,咱们发现NameSpace层和BlockPool层之间有十分多的耦合,这里咱们参考了社区的一局部上班HDFS-8966:Separate the lock used in namespace and block management layer, 曾经协助咱们解除了局部的依赖,除了社区列进去的这局部依赖之外咱们还发现一些BlockPool层对NameSpace层的反相依赖,重要是Block的正本消息和storagePolicy属性消息,这块咱们将这局部消息在BlockPool层启动冗余存储,同时确保出现变卦时NameSpace层的消息及时同步至BlockPool层。在解除了BlockPool层对NameSpace层的反相依赖后,开局针对不同类型的恳求失掉何种类型的锁启动区分,如图2-4所示。
经过对不同恳求按不同类型锁要求划分后,咱们基本可以做到访问局部不同层数据的恳求的并行口头,但依然有2个疑问须要处置。首先是死锁疑问,为此咱们确保一切恳求的加锁顺序的分歧性,一切须要同时失掉NameSpace层锁和BlockPool层的恳求都是NameSpace层锁在前,BlockPool层的锁在后;其次是分歧性疑问,NameNode外部自身是写分歧性,并发读取场景,针对同时访问NameSpace层和BlockPool层的恳求,须要确保NameSpace层加锁范围齐全蕴含BlockPool层加锁范围,防止读取到两边形态。
图2-4 不同类型的恳求加锁场景
经过上述这种形式,咱们基本成功了BlockPool层和NameSpace层的锁拆分,这局部优化战略曾经在消费环境运转了一段期间,NameNode全体性能大概优化了50%左右。
3.2 NameSpace层锁拆分红INode粒度锁
在成功了FSN层和BP层锁拆分之后,NameNode性能曾经有了肯定的优化,消费环境中对HDFS的NameNode元数据恳求的rpc processtime和queuetime也有显著的降低,但依然有一些场景不可满足,因此咱们继续优化,对NameSpace层的锁启动更细粒度的拆分如图2-5所示,将锁细粒度到INode层,宿愿能进一步优化NameSpace层RPC并发才干,优化NameNode全体写入才干。
要将NameSpace层锁拆分到INode层级粒度,肯定要为对应的INode调配锁对象,在这里咱们面临了许多疑问。
首先是内存限度,咱们目前单组Namespace元数据容量阈值基本在10亿左右,假设每个INode调配一个INode锁,单是INode锁的内存简直就须要120GB左右的内存,再加上自身NameNode就十分消耗内存,的主机类型很难满足。为了处置这个疑问,咱们参考了 Alluxio 的LockPool 的概念,也就是有一个锁资源池,每个INode须要Lock加锁的时刻,就去资源池里放开锁,同时援用计数会参与,用完之后unlock掉的时刻,援用计数会缩小,同时性能不同的高下水位,活期清算掉援用计数为0的锁,确保总体内存可控。
其次是锁对象的治理,这方面咱们引入了INodeLockManager 用于治理INode和锁对象的之间的映射,咱们经过INodeLockManager新增了INode锁的LockPool 和 Edge锁的LockPool,如图2-6所示,治理整个NameSpace层的INode层级的细粒度锁。
图2-6 NameSpace层的INode层级的细粒度锁治理
成功了锁对象的治理后,Namespace层锁细粒度拆分剩下的疑问都是如何预防死锁和数据杂乱,因此咱们对加锁行为启动布局,总体遵照如下准则。
如图2-7所示,咱们性能了3种类型的锁,区分时Read锁,Write_Inode 锁和 WRITE_EDGE锁区分应答不同类型的客户端RPC恳求。针对读恳求,咱们正向遍历INodeTree从ROOT节点开局依次加锁 对对整个门路上的INode和Edge都加读锁;针对addBlock ,setReplication 这类不影响INodeTree的恳求,咱们也是正向遍历INodeTree从ROOT节点开局依次加读锁,然而对最后一个INode加写锁;针对create ,mkdir恳求,咱们正向遍历INodeTree ,对最后INode节点和最后的Edge都加写锁,假设最后INode不存在,对最后的Edge也须要加写锁。
除了访问单个门路的恳求,还有rename等访问多个门路的rpc恳求,如图2-8所示,从 /a/b/c rename 成 /a/b/e,咱们对这种场景做了不凡处置。咱们首先门路/a/b/c和/a/b/e按字典序确定先后,再自上而下加锁,如图2-8所示,门路/a/b/c排序在前,咱们先对/a/b/c 门路加锁,正向遍历INodeTree从ROOT节点开局依次加锁,边b-c,INode c都加上写锁,门路/a/b/e排序在后,咱们在对门路/a/b/c加锁成功后,对门路/a/b/e加锁,雷同遍历INodeTree,Edge b→e加上写锁,INode e 由于还未存在,则丢弃加锁;
在上述的上班中,咱们成功了不同恳求的加锁形式,针对部份加锁场景中存在的INode缺失场景(如文件不存在等场景),如下图2-9所示针对相对典型的是create恳求罗列了不同RPC类型的加锁逻辑。
图 2-9 不同RPC类型的加锁逻辑举例
经过成功上述2步拆锁环节,NameNode性能曾经有了很大优化,如图2-10展现了咱们在测试环境中的性能对比,经过Namespace层读写锁和BlockPool层读写锁拆分后,相比于社区版本,单NameSpace的写性能大概优化了50% ,经过Namespace层细粒度锁拆分后,写性能相比于社区版本有3倍左右的优化,此时NameNode性能瓶颈曾经集中在Edits和审计日志同步以及BlockPool层的自身的锁竞争上。在实践消费环节中,咱们把之前须要2组NameSpace允许的义务日志采集门路收归为一组NamesSpace允许,在写入QPS回升3倍的场景下,全体rpc queue time 降低了90%,全体性能有很大的优化。
四. 总结与展望
NameNode的性能优化曾经告一段落了,第一步和第二部的拆锁曾经在咱们的消费集群上稳固运转了一段期间,全体性能优化显著,全体RPC Queue Time相比于拆锁之前有数量级的降低,曾经可以允许绝大少数运行场景,包括之前的形容的义务日志聚合和Flink CheckPoint 门路等场景,在接上去方案中,咱们也正在思考能否将BlockPool层锁做进一步细粒度拆分,进一步优化NameNode的性能。
同时思考到NameNode元数据都存储在内存中,限度了NameNode元数据总量的裁减,特意是小文件场景,咱们也将在未来布局引入Ozone或许将NameNode的元数据消息耐久化至RocksDB或许KV中,进一步优化单组Namespace的承载量彻底处置小文件疑问。