【前言】
在测试过程中,发现了一个问题,记录相关的分析过程、内部原理与优化。
【问题与分析】
在测试过程中,发现一个问题:单个dn在配置一块磁盘,存储900w 的block的情况下,重启dn后等待2个多小时才正常提供服务。
结合《DN整体架构与启动流程》中介绍的DN启动流程与实际的日志,很快就发现了耗时很长的地方:从磁盘加载block到内存中耗时非常长。
初略的阅读了相关流程的源码,整个流程无外乎就是遍历每个BP下finalized和rbw中的block文件,然后构造出对应的对象存放到ReplicaMap中,感觉理论上应该不至于这么慢。
本着怀疑的心态,将相关逻辑流程的代码抽出来执行了下,还真是花了这么长的时间。再仔细分析下整个代码,终于发现了问题的所在,有问题的代码如下:
循环对目录下的所有文件进行遍历,如果是block文件,则查找block文件对应的meta文件,并从meta文件命中获取block的时间戳。
而查找meta文件时,又是一次循环遍历,这样的一个双循环直接导致了整个加载block的动作非常耗时。
实际测试将第二层循环查找block对应meta文件的逻辑删除,整个遍历的速度还是非常快的(只遍历block文件,900w文件耗时将近500s)。
注:贴出的代码的版本为2.8.5,在新的版本中,这个逻辑已经进行了优化,list目录下的文件后先进行一次排序,由于block文件和对应meta文件具有相同的前缀,这样查找meta文件获取时间戳的耗时几乎就没有了(详细可以查看最新版本的代码)。
【优化】
上面的测试场景是DN只配置一个磁盘的情况,那么如果配置多块磁盘,将block分散存储到不同的磁盘目录上,这样启动加载block的速度是否会提升呢?
答案是显而易见的,实际测试下来,单个DN配置9块磁盘,同样数量(900w)的block,DN启动加载block的耗时为300s,较之前的2个多小时有了大幅的提升。
在阅读相关代码后,确认DN在启动时,针对每个目录都启动一个线程进行block的扫描加载,起到了加速的效果。
同时,在阅读代码的时候,还发现了另外一个逻辑:在扫描目录加载block之前,会先读取一个缓存文件,如果加载成功,则不再扫描目录加载block。也就是说这个文件缓存了所有block的信息,因此成功加载这个文件等同于扫描目录加载block的逻辑。
那么这个文件是什么时候生成的呢?
又是一番代码的走读,找到了对应的位置:dn在响应shutdown(停止dn)的rpc请求时,会将内存中记录的block信息持久化到这个文件中,启动时优先读取该文件,如果成功,则不进行后续的扫描加载逻辑。
进一步说明就是,采用下面的命令停止dn时,实际上是给dn发送了rpc请求,dn收到请求后会将block信息写入到文件中,然后进程退出。
代码语言:javascript复制hdfs dfsadmin -shutdownDatanode <datanode_host:ipc_port>
而采用下面的命令,本质上是以"kill -9"的方式停止dn,dn内部的回调函数中没有特殊的逻辑处理。
代码语言:javascript复制hadoop-daemon.sh stop datanode
既然原理都已经清楚了,那就不妨直接测试下:
- dn配置1块磁盘,900w的block,采用shutdown的方式停止后,dn启动加载block耗时81s。
- dn配置9块磁盘,900w的block,采用shutdown的方式停止后,dn启动加载block耗时77s
由此可见,采用shutdown的方式停止dn是更优的方式。但是有一个问题需要注意,dn启动加载这个文件之前会进行判断,如果这个文件的最后修改时间与当前时间相差5分钟以上,dn会认为该文件已经失效,转而重新进入扫描目录加载block的逻辑。
在当前版本(2.8.5)的代码中,5分钟为硬编码写死,没有任何地方可以配置,而在最新版本中,该时间是可以进行配置的。从社区的讨论来看,也是更倾向于鼓励使用该缓存文件,从而减少dn启动的预热时间。
【总结】
当数据量到达一定程度后,任何小的优化,效果都可能是巨大的。