NameNode锁细粒度优化在B站的实践

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的读写性能提升方案。

图片

图1-1 B站HDFS整体架构图

2. HDFS 细粒度锁优化整体方案

2.1 面临的问题

NameNode是整个HDFS的核心组件,集中管理HDFS集群的所有元数据,主要包括文件系统的目录树、数据块集合和分布以及整个集群的拓扑结构。HDFS在对NameNode的实现上做了大胆取舍,如图2-1所示,锁机制上使用全局锁来统一来控制并发读写。这样处理的优势非常明显,全局锁进一步简化锁模型,不需要额外考虑锁依赖关系,同时降低复杂度,减少工程量。但是问题比优势更加突出,核心问题就是全局唯一锁制约性能提升。

图片

图2-1 B站HDFS整体架构图

在多年的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 设计选型

为了更好地理解使用全局锁存在的问题,首先梳理全局锁管理的主要数据结构,大致分成三类:

  1. NameSpace目录树:文件系统的全局目录视图。获取目录树上任一节点的信息必须先拿到全局读锁;目录树上任一节点新增、删除、修改都必须先拿到全局写锁。

  2. BlockPool层数据块集合:文件系统的全量数据信息。获取其中任一数据块信息必须先拿到全局读锁;新增、删除,修改都必须先拿到全局写锁。

  3. 集群信息:HDFS集群节点信息的集合。获取节点信息等必须先拿到全局读锁;注册,下线或者变更节点信息请求处理时必须先拿到全局写锁。

具体实现上,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层读写锁也拆成更细粒度的读写锁;

目前我们已经基本完成第一部分和第二部分的工作。

图片

图2-2 NameNode 锁优化过程

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所示。

  1. NameSpace层请求(getListing/getFileInfo等请求),只需要获取NameSpace层锁;

  2. BlockPool层请求(BlockReport/IncrementalBlockReport等请求),只需要获取BlockPool层锁,这块我们发现有块上报过程中,有一段更新Quota的逻辑需要获取NameSpace层锁,我们无法做到完美的适配,考虑到我们的Quota采用的外置计算的方式,所以做了相应的取舍,只获取了BlockPool层的锁;

  3. 同时访问NameSpace层和BlockPool层的请求(setReplication/getBlockLocation),需要同时获取NameSpace层的锁和BlockPool层的锁。

通过对不同请求按不同类型锁要求划分后,我们基本可以做到访问部分不同层数据的请求的并行执行,但仍然有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整体写入能力。

图片

图2-5 NameSpace层锁细粒度拆分

要将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层锁细粒度拆分剩下的问题都是如何预防死锁和数据错乱,因此我们对加锁行为进行规划,总体遵循如下原则。

  • 普通Client端的RPC请求采用自上而下的加锁方式,对特殊操作如Rename等操作进行特殊处理;

  • Client端的RPC请求进行全链路加锁,部分请求考虑最后的INode和Edge加写锁;

如图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也需要加写锁。

图片

图2-7 INodeLock 加锁方式

除了访问单个路径的请求,还有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 由于还未存在,则放弃加锁;

图片

图2-8 Rename RPC操作加锁方式

在上述的工作中,我们完成了不同请求的加锁方式,针对部份加锁场景中存在的INode缺失场景(如文件不存在等场景),如下图2-9所示针对相对典型的是create请求列举了不同RPC类型的加锁逻辑。

  • create 路径/a/b/c文件,如果当前已经存在存在/a/b 路径,则最终会在Edge b->c 加写锁;

  • create 路径/a/b/c文件,如果已经存在/a/b/c路径,则最终会在Edge b→c 和INode c上加写锁;

  • create 路径/a/b/c文件,如果只存在 /a 路径,则会在Edge a->b 这条边上加上写锁。

图片

图 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%,整体性能有很大的提升。

图片

图2-10 锁优化性能对比

四. 总结与展望

NameNode的性能优化已经告一段落了,第一步和第二部的拆锁已经在我们的生产集群上稳定运行了一段时间,整体性能提升明显,整体RPC Queue Time相比于拆锁之前有数量级的下降,当前已经可以支持绝大多数应用场景,包括之前的描述的任务日志聚合和Flink CheckPoint 路径等场景,在接下来计划中,我们也正在考虑是否将BlockPool层锁做进一步细粒度拆分,进一步提升NameNode的性能。

同时考虑到NameNode元数据都存储在内存中,限制了NameNode元数据总量的扩展,特别是小文件场景,我们也将在未来规划引入Ozone或者将NameNode的元数据信息持久化至RocksDB或者KV中,进一步提升单组Namespace的承载量彻底解决小文件问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/313750.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Quarto Dashboards 教程 2:Dashboard Layout

「写在前面」 学习一个软件最好的方法就是啃它的官方文档。本着自己学习、分享他人的态度,分享官方文档的中文教程。软件可能随时更新,建议配合官方文档一起阅读。推荐先按顺序阅读往期内容: 1.quarto 教程 1:Hello, Quarto 2.qu…

C语言扫雷游戏完整实现(上)

文章目录 前言一、新建好头文件和源文件二、实现游戏菜单选择功能三、定义游戏函数四、初始化棋盘五、 打印棋盘函数六、布置雷函数七、玩家排雷菜单八、标记功能的菜单九、标记功能菜单的实现总结 前言 C语言从新建文件到游戏菜单,游戏函数,初始化棋盘…

【C语言】深入解析选择排序算法

一、算法原理二、算法性能分析三、C语言实现示例四、总结 一、算法原理 选择排序(Selection Sort)是一种简单直观的排序算法。它的工作原理是不断地选择剩余元素中的最小(或最大)元素,放到已排序的序列的末尾&#xff…

普乐蛙VR航天航空体验馆VR双人旋转座椅元宇宙VR飞船

多长假来袭!!想为门店寻找更多新鲜有趣的吸粉体验?想丰富景区体验?别着急,小编为你准备了一款爆款设备——时光穿梭机,720无死角旋转!!吸睛、刺激体验,将亲子、闺蜜、情侣…

微信小程序酒店选择日期和入住人数(有效果图)

效果图 app.vue onLaunch:function(options){this.defaultcache()}defaultcache(){// 入住信息缓存var arr this.getDateTime();var ReserVation {reservType:0,//1 人数 2日期InCheckin:{},//入离日期peopleArr:[{title:成人,num:2},{title:儿童,num:0},{title:宝子,num:1…

宁盾LDAP统一用户认证与单点登录:构建高效安全的企业身份认证

在信息化时代,企业面临着众多的应用系统和数据资源,如何有效地管理和保护这些资源,确保信息安全和高效利用,成为了企业信息化建设的核心问题。LDAP统一用户认证和单点登录(SSO)作为一种高效、安全的身份验证…

全开源小狐狸Ai系统 小狐狸ai付费创作系统 ChatGPT智能机器人2.7.6免授权版

内容目录 一、详细介绍二、效果展示1.部分代码2.效果图展示 三、学习资料下载 一、详细介绍 测试环境:Linux系统CentOS7.6、宝塔、PHP7.4、MySQL5.6,根目录public,伪静态thinkPHP,开启ssl证书 具有文章改写、广告营销文案、编程…

一个java项目中,如何使用sse协议,构造一个chatgpt的流式对话接口

前言 如何注册chatGPT,怎么和它交互,本文就不讲了;因为网上教程一大堆,而且你要使用的话,通常会再包一个算法服务,用来做一些数据训练和过滤处理之类的,业务服务基本不会直接与原生chatGPT交互。…

Git操作与异常处理

文章目录 常用操作1、代码拉取2、代码提交3、暂存区状态4、提交代码5、推送远程仓库 异常处理【1】报错信息:Cannot pull into a repository with state: MERGING【2】报错信息:You have not concluded your merge (MERGE_HEAD exists)【3】报错信息&…

BGP的基本概念和工作原理

AS的由来 l Autonomous System 自治系统,为了便于管理规模不断扩大的网络,将网络划分为不同的AS l 不同AS通过AS号区分,AS号取值范围1-65535,其中64512-65535是私有AS号 l IANA机构负责AS号的分发 AS之…

NumPy简单学习(需要结合书本)

NumPy简单学习(需要结合书本:Python数据分析与应用) 文章目录 NumPy简单学习(需要结合书本:Python数据分析与应用)前言导库: 一、大概内容1.掌握NumPy数组对象ndarray(1)…

Excel 公式的定义、语法和应用(LOOKUP 函数、HLOOKUP 函数、VLOOKUP 函数;MODE.MULT 函数; ROUND 函数)

一、公式的定义和语法 二、公式的应用 附录 查找Excel公式使用方法的官方工具【强烈推荐!!!】:Excel 函数(按字母顺序)【微软官网】 excel 函数说明语法LOOKUP 函数在向量或数组中查找值LOOKUP(lookup_va…

Linux-文件系统

1. 物理结构 计算机的存储硬件有很多,这里讲磁盘。 磁盘的物理结构大致分为: 磁盘(数据存储)磁头音圈马达主轴 所有的数据都存储在磁盘上,磁盘有很多片,每一个面都有对应的磁头来对数据进行更改 磁头是…

轻松处理文件名,告别重复命名烦恼!一键覆盖复制操作,让文件管理更高效!

我们每天都在与大量的文件打交道。从工作文档到生活照片,从学习资料到娱乐视频,每一个文件都承载着我们的记忆和辛勤付出。然而,随着文件数量的不断增加,文件名冲突、重复命名等问题也愈发突出,给我们的文件管理带来了…

Python:解析pyserial串口通讯

简介:串行接口简称串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方式的扩展接口。串行接口 (Serial Interface)是指数据一位一位地顺序传送。其特点是通信线路简单&#…

DC-DC电源芯片规格书上的各种参数详解

1.输出电压精确度 输出电压的精确度,也被称为设定点精度,它描述了输出电压的允许误差。该参数通常是在常温,满载和额定输入电压的条件下测得的,它是这样定义的: 输出电压之所以产生误差,是因为元器件本身存在误差,特别是输出端的分压电阻,它将输出电压降低后比PWM比较…

【白盒测试】单元测试的理论基础及用例设计技术(6种)详解

目录 🌞前言 🏞️1. 单元测试的理论基础 🌊1.1 单元测试是什么 🌊1.2 单元测试的好处 🌊1.3 单元测试的要求 🌊1.4 测试框架-Junit4的介绍 🌊1.5 单元测试为什么要mock 🏞️…

BGP配置和应用案例

策略路由的配置步骤 l 策略路由的配置步骤如下: 创建route-map 通过ACL匹配感兴趣的数据,定义策略动作 在指定接口下通过ip policy 命令应用route-map l 最终实现对通过该接口进入设备的数据进行检查,对匹配的数据执行规定的策略…

pytest参数化数据驱动(数据库/execl/yaml)

常见的数据驱动 数据结构: 列表、字典、json串 文件: txt、csv、excel 数据库: 数据库链接 数据库提取 参数化: pytest.mark.parametrize() pytest.fixture()…

大语言模型在研究领域的应用——信息检索中的大语言模型

信息检索中的大语言模型 大语言模型提升信息检索任务利用大语言模型进行信息检索大语言模型增强的信息检索模型. 检索增强的大语言模型输入优化策略.指令微调策略.预训练策略. 总结应用建议未来方向 大语言模型对于传统信息检索技术与应用范式带来了重要影响。这两者在技术路径…