作者|黄东发背景介绍在一个典型的分布式文件系统中,目录文件的元数据操作(包括创建目录或文件、重命名、修改权限等)在整个文件系统操作中占据了很大的比重,因此元数据服务扮演着重要的角色在整个文件系统中。随着大规模机器学习、大数据分析、企业级数据湖等应用,分布式文件系统的数据规模从PB发展到EB。大多数分布式文件系统(如HDFS等)目前都面临着元数据可扩展性的挑战。以谷歌、Facebook、微软为代表的公司基本都实现了可以管理EB级数据的分布式文件系统。这些系统的共同架构特点是依赖底层的分布式数据库能力来实现元数据性能的水平扩展,比如GoogleColossus是基于BigTable,Facebook是基于ZippyDB,MicrosoftADLSv2是基于TableStorage,还有一些包括CephFS、HopsFS在内的开源文件系统基本实现了横向扩展的能力。由于这些文件系统实现对底层分布式数据库的依赖,对文件系统的语义支持程度也不同。例如,大多数基于分布式文件系统的计算和分析框架都依赖底层目录的原子重命名操作来提供数据的原子更新。但是Tectonic和Colossus不保证跨目录Rename的原子性,因为底层数据库不支持跨分区事务,而ADLSv2支持任意目录的原子Rename。DanceNN是公司研发的目录树元信息存储系统,致力于解决所有分布式存储系统(包括但不限于HDFS、NAS等)的目录树需求,大大简化目录树的复杂度上层存储系统所依赖的操作。包括但不限于原子Rename、递归删除等。解决超大规模目录树存储场景下异构系统间的扩展性、性能、全局统一命名空间等问题,打造全球领先的通用分布式目录树服务。目前DanceNN已经为公司线上的ByteNAS和线下的HDFS两大分布式文件系统提供了目录树元数据服务。(本文主要介绍离线大数据场景下DanceNN在HDFS文件系统中的应用,考虑到篇幅,DanceNN在ByteNAS中的应用会在后续系列文章中介绍,敬请期待)元数据演进ByteHDFS元数据系统分为三个阶段演化:NameNode最初使用的是原始的HDFSNameNode。虽然做了很多优化,但仍然面临以下问题:元数据(包括目录树、文件和块副本等)在内存场景下,GC暂停时间比较长,严重影响SLA。使用全局读写锁,读写吞吐性能较差。随着集群数据规模的增大,重启恢复时间达到小时级别。DanceNNv1DanceNNv1的设计目标就是解决上述NameNode问题。到问题。主要设计点包括:重新实现HDFS协议层,将目录树文件??相关的元数据存储到RocksDB存储引擎,提供10倍的元数据负载,使用C++避免GC问题,使用高效的数据结构来组织内存块信息,减少内存实现一套细粒度的目录锁机制,大大提高了不同目录文件操作之间并发请求路径的异步性,支持请求优先级处理,优化块上报和重启加载过程,减少不可用时间.DanceNNv1终于在2019年全部上线,上线效果基本达到了设计目标。下面是一个超过十亿文件规模的集群。切换后的总体性能对比:DanceNNv1在开发中遇到了很多技术挑战。完全兼容原有的HadoopHDFS协议。DistributedDanceNN一直采用Federation方式管理HDFS中的目录树,将全局Namespace按照路径映射到多组元数据独立的DanceNNv1集群。单个DanceNNv1集群存在单机瓶颈,可处理的吞吐量和容量有限。随着公司业务数据的增长,单个DanceNNv1集群达到了性能极限,需要在两个集群之间频繁迁移数据。为了保证数据的一致性,在迁移过程中需要停止上层业务,对业务影响比较大,而且数据量大的时候迁移很慢。这些问题给整个系统的运维带来了很大的压力,降低了服务的稳定性。Distributed版本主要设计目标:通用目录树服务,支持多协议包括HDFS、POSIX等单一全局Namespace容量,吞吐量支持水平扩展,高可用,秒级故障恢复,包括跨目录Rename等写操作,支持事务高性能,基于C++实现,依托Brpc等高性能框架,DistributedDanceNN已经在部分HDFS集群上线,正在进行现有集群的平滑迁移。文件系统概述分层架构最新的HDFS分布式文件系统实现采用分层架构,主要包括三层:数据层:用于存储文件内容和处理块级IO请求,由DataNode节点提供命名空间层:负责目录树相关元数据,处理目录和文件创建、删除、重命名和身份验证请求。DistributedDanceNN集群提供服务。文件块层:负责文件相关的元数据,文件与Block的映射,以及块拷贝位置信息,处理文件的创建和删除,文件块的添加等,需要一个BSGroup负责管理某个文件的元数据集群中的块。它由多个DanceBS组成,提供高可用服务。BSGroup的动态扩展用于适应集群负载。当一个BSGroup即将达到性能极限时,可以控制写入DanceProxyC++实现,基于高性能框架Brpc,实现HadoopRPC协议,支持高吞吐,无缝对接现有HDFSClient。主要负责HDFSClient请求的解析。拆分处理后,将Namespace相关的请求发送到DanceNN集群,将文件块相关的请求路由到对应的BSGroup进行处理。当所有后端请求都得到回复后,生成最终的客户端响应。DanceProxy通过一定的请求路由策略实现多个BSGroup的负载均衡。DanceNN接口DistributedDanceNN为文件系统提供主要接口如下:classDanceNNClient{public:DanceNNClient()=default;虚拟~DanceNNClient()=默认值;//...//递归创建目录,例如:MkDir/home/tiger.ErrorCodeMkDir(constMkDirReq&req);//删除一个目录,eg:RmDir/home/tiger.ErrorCodeRmDir(constRmDirReq&req);//更改文件或目录的名称或位置,//例如:重命名/tmp/foobar.txt/home/tiger/foobar.txt。错误代码重命名(constRenameReq&req);//创建一个文件,例如:创建/tmp/foobar.txt。错误代码创建(constCreateReq&req,CreateRsp*rsp);//删除一个文件,例如:Unlink/tmp/foobar.txt。ErrorCodeUnlink(constUnlinkReq&req,UnlinkRsp*rsp);//总结一个文件或目录,eg:Du/home/tiger.ErrorCodeDu(constDuReq&req,DuRsp*rsp);//获取文件或目录的状态,例如:Stat/home/tiger/foobar.txt。ErrorCodeStat(constStatReq&req,StatRsp*rsp);//列出目录内容,例如:Ls/home/老虎。ErrorCodeLs(constLsReq&req,LsRsp*rsp);//创建一个名为link_path的符号链接,其中包含字符串target。//例如:Symlink/home/foo.txt/home/bar.txtErrorCodeSymlink(constSymlinkReq&req);//读取符号链接的值。错误码ReadLink(constReadLinkReq&req,ReadLinkRsp*rsp);//更改文件或目录的权限。ErrorCodeChMod(constChModReq&req);//更改文件或目录的所有权。ErrorCodeChOwn(constChOwnReq&req);//更改文件上次访问和修改时间。ErrorCodeUTimeNs(constUTimeNsReq&req,UTimeNsRsp*rsp);//设置扩展属性值。ErrorCodeSetXAttr(constSetXAttrReq&req,SetXAttrRsp*rsp);//列出扩展属性名称。ErrorCodeGetXAttrs(constGetXAttrsReq&req,GetXAttrsRsp*rsp);//删除一个扩展属性。ErrorCodeRemoveXAttr(constRemoveXAttrReq&req,RemoveXAttrRsp*rsp);//...};DanceNN架构功能介绍分布式DanceNN是基于底层分布式事务KV存储构建的,实现容量和吞吐量的扩展。主要功能:HDFS等协议层高效实现无状态服务,支持高可用服务节点快速扩缩容提供高性能低延迟访问将Namespace划分为子树,充分利用子树CacheLocality集群调度子树根据负载均衡策略模块划分SDK缓存集群子树、NameServer位置等信息,分析用户请求路由到后端服务节点。如果服务节点非法响应请求,可能会强制SDK刷新对应的集群缓存NameServer作为一个服务节点,是无状态的,支持水平扩展。HDFS/POSIX协议层:处理客户端请求,实现HDFS等协议层语义,包括路径分析、权限验证、删除到回收站等子树管理器:管理分配给当前节点的子树,负责用户请求检查、子树迁移处理等。Heartbeater:进程启动后,会自动注册到集群中,并定期向NameMaster更新心跳和负载信息。DistributedLockManager:基于LockTable,对跨目录的Rename请求进行并发控制LatchManager:锁定所有路径读写请求,减少底层事务冲突,支持并发访问CacheStrongConsistentCache:保持dentry和inode的强一致性当前节点子树的CacheData访问层:对底层KV存储的访问接口抽象,上层的读写操作会映射到底层KV存储请求NameMaster作为管理节点,无状态,多路,通过选择themaster,主节点提供服务AdminTaskScheduler:后台管理相关任务调度执行,包括子树切分、扩容等LoadBalancer:根据集群NameServer的负载情况,通过子树自动迁移完成负载均衡NameServerManager:监控NameServer的健康状态,并执行corre响应宕机处理Statistics:实时统计并展示DistributedbyconsumingclusterchangelogsTransactionalKVStore数据存储层使用自研强一致性KV存储系统ByteKV提供水平扩展性支持分布式事务,并提供Snapshot隔离级别支持多机房数据容灾。BinLogStoreBinLog存储采用自研低延迟分布式日志系统ByteJournal支持ExactlyOnce语义,实时从底层KV存储系统中提取数据变更日志。主要用于PITR等组件的实时消费。GC(Garbagecollector)从BinLogStore实时消费变更日志。文件块服务针对用户认领的目录发出删除命令,及时清理用户数据配额,并会周期性实时增量计算文件总数和空间总量,容量超过限制后,限制用户写入关键设计存储格式。基于分布式存储的元数据格式,一般有两种方案:方案一类似GoogleColossus,以全路径为key,元数据为value存储。优点是:路径解析非常高效,直接通过用户请求扫描目录的路径从底层KV存储中读取对应inode的元数据。可以通过前缀扫描KV存储,但是有如下缺点:跨目录Rename开销大,需要扫描目录下的所有文件和文件。移动目录时key占用的空间比较大。另一种类似于FacebookTectonic和开源的HopsFS,使用父目录inodeid+目录或文件名作为key,metadata作为value存储。这样的好处是:跨目录Rename很轻只需要修改源节点和目标节点及其父节点就??可以扫描目录了。也可以使用父目录inodeid作为前缀进行扫描。缺点:路径解析网络延迟高,需要从Root递归读取相关节点元数据到目标节点例如:MkDir/tmp/foo/bar.txt,有四种元数据网络访问:/,/tmp,/tmp/foo和/tmp/foo/bar.txt级别越小,访问热点越明显,导致底层存储负载严重不均衡。比如根目录/的元数据,每次请求都需要读取一次。考虑到在线集群中跨目录Rename请求占比较高,且大目录rename延迟不可控,DanceNN主要采用方法二的方案,方案2的两个缺点可以通过下面的子树划分来解决.DanceNN将全局Namespace划分为子树分区,子树指定为NameServer实例维护子树缓存。子树缓存维护该子树下所有目录和文件元数据的强一致性缓存。缓存项有一定的淘汰策略,包括LRU、TTL等,该子树下的所有请求路径都可以直接访问本地缓存。需要从底层KVLoad中获取miss并填充缓存,通过在缓存项中添加一个version来指定某个目录下所有元数据的缓存过期时间,有利于子树的快速迁移和清理。通过使用子树的本地缓存,路径解析和读请求基本可以命中缓存。整体延迟降低,避免了靠近根节点的热点问题。路径冻结在子树迁移、跨子树重命名等操作中,为了避免请求读取过时的子树缓存,需要冻结相关路径。在冻结期间,该路径下的所有操作都会被阻塞,SDK负责重试,整个过程在亚秒级内完成。路径被冻结后,目录中的所有缓存项都将被设置为过期。冻结的路径信息会持久化到底层KV存储,重启后会重新加载刷新子树管理子树。树的管理主要由NameMaster负责:支持通过管理员命令手动进行子树分裂和子树迁移,定时监控集群节点的负载状态,动态调整子树在集群中的分布,定时统计子树的访问吞吐量,以及提供子树分裂的建议。支持启发式算法选择子树完成分裂。例如,如下图所示:目录/被调度到NameServer#1,目录/b被调度到NameServer#2,目录/b/d被调度到NameServer#3MkDir/a请求被发送到NameServer#1。发送到其他NameServer会校验失败返回重定向错误,让SDK刷新缓存重试Stat/b/d请求会发送到NameServer#3,直接读取本地缓存就可以发送ChMod/b请求到NameServer#2,更新并持久化目录b的权限信息,刷新NameServer#2和NameServer#3上的缓存,最后回复客户端并发控制。底层KV存储系统ByteKV支持单条记录的Put、Delete和Get语义。Put支持CAS语义,也提供了多条记录的原子写接口WriteBatch。客户端写入操作通常涉及更新多个文件或目录。比如Create/tmp/foobar.txt会更新/tmp的mtime记录,创建foobar.txt记录等,而DanceNN会将多条记录更新转换为ByteKVWriteBatch请求,保证整个操作的原子性。分布式锁管理虽然ByteKV提供了事务的ACID属性,支持Snapshot隔离级别,但如果涉及底层数据变化的多个并发写操作之间没有Overlap,仍然会出现WriteSkew异常,可能会导致元数据完整性受损。破坏。一个例子是并发Rename异常,如下图所示:单个Rename/a/b/d/e操作或单个Rename/b/d/a/c操作符合预期,但如果两者并发执行(并且可以成功),会导致目录a、c、d、e的元数据出现环,破坏了目录树结构的完整性。我们选择使用分布式锁机制来解决问题。针对可能导致异常的并发请求的串行处理,我们设计了基于底层KV存储的LockTable,支持对元数据记录加锁,提供持久化、水平扩展、读写锁,锁超时清理和幂等函数。Latch管理为了支持子树内部缓存的并发访问和更新,保持缓存的强一致性,它会对操作涉及的缓存项进行锁定(Latch),例如:创建/home/tiger/foobar。txt,它会先将Latch添加到tiger和foobar.txt对应的缓存项中,然后执行更新操作;stat/home/tiger会将Latch添加到tiger缓存项中,然后读取它。为了提高服务的整体性能,进行了大量的优化。下面列出了两个重要的优化:在热目录中创建和删除大量文件。比如一些业务,比如大规模的MapReduce任务,会一次性在同一个目录下创建上千个目录或者文件。一般来说,按照文件系统的语义创建文件或目录,会更新父目录相关的元数据(例如HDFS协议更新父目录的mtime,POSIX要求更新父目录的mtime、nlink等).),从而导致在同一目录下创建文件的操作。更新父目录的元数据会产生严重的事务冲突。另外,底层KV存储系统部署在多个机房,机房的延迟较高,进一步降低了这些操作的并发度。DanceNN只是读取热点目录中的创建、删除等操作的latch,然后放入一个ExecutionQueue中。一个轻量级的Bthread协程进行后台异步串行处理,将这些请求组合成一定大小的batch发送到底层KV存储,避免了底层事务冲突,提高了数十倍的吞吐量。请求之间相互阻塞在某些场景下,目录的更新请求可能会阻塞该目录下的其他请求,例如:SetXAttr/home/tiger和Stat/home/tiger/foobar.txt不能并发执行,因为第一个是为tiger缓存项添加writeLatch,后续读取tiger元数据缓存项的请求会被阻塞。DanceNN使用类似的Read-Write-CommitLock实现来管理Latch。每个Latch都有三种类型:Read、Write和Commit。其中Read-Read和Read-Write请求可以并发,Write-Write和Any-Commit请求是互斥的。.基于此实现,可以在保证数据一致性的情况下,并发执行上述两个请求。请求幂等性当客户端因超时或网络故障而失败时,重试会导致同一个请求多次到达服务端。某些请求(例如Create或Unlink)是非幂等请求。对于这样的操作,需要在服务器端对其进行识别,以保证它们只被处理一次。在单机场景下,我们通常使用内存Hash表来处理重试请求。Hash表的key是{ClientId,CallId},value是{State,Response}。当请求A到来时,我们将{InprocessState}插入Hash表;之后,如果重试请求B到来,则直接阻塞请求B,等待第一个请求A执行成功后再唤醒B。当A执行成功后,我们将{FinishedState,Response}写入Hash表并唤醒B,B看到更新后的Finished状态后会响应客户端。类似DanceNN的写请求,会在底层的WriteBatch请求中添加一条Request记录,可以保证后续的重试请求操作一定会导致底层的事务CAS失败。上层发现后,会读取Request记录,直接响应客户端。另外,在删除Request记录的时候,我们会为该记录设置一个比较长的TTL,以保证TTL结束后,该记录一定已经处理完毕。性能测试压测环境:DanceNN使用1台NameServer,分布式KV存储系统使用100+数据节点,部署三机房五副本(2+2+1),跨机房延时约2-3ms,客户端通过NNTroughputBenchmark元数据压测脚本分别使用单线程和6K线程并发进行压测。截取的部分延迟和吞吐量数据如下:测试结果表明:读吞吐量:单个NameServer支持500K读请求,吞吐量基本可以随着NameServer数量的增加呈线性增长;写吞吐量:目前取决于底层KV存储的写事务性能,随着底层KV节点数据量的增加也可以实现线性增长。
