vlambda博客
学习文章列表

HDFS优化之RPC时延变大问题处理

一、问题现象

公司大数据平台在运行一段时间后,收到用户反馈:执行hdfs命令很慢,一条普通ls命令消耗5S以上,严重影响数仓等业务程序的执行效率。通过ambari平台自带的grafana监控信息工具,发现NameNodeRPC响应时间平均在500ms以上,甚至部分时候达到了3S以上,如图所示。

图一 NameNode RPC时延较大监控


二、处理过程

对于这个问题,我们首先想到的是对NameNode参数进行调优,提升NameNode的性能,包括增加namenode handler数量、GC优化、FairCallQueue优化、异步editlog、服务端调用与客户端调用的端口分离等,但收到的效果都是微乎其微,没有在本质上解决问题。

在试过所有参数后,我们开始转换思路,怀疑是当前集群数据量过大导致,于是开始从清理集群的方向入手。在当时,我们集群的文件量达到2亿,集群中有大量的低价值文件。通过count命令检查集群所有目录的文件量,发现仅MR的日志文件就达到了惊人的8000万,而这些日志文件绝大部分是不需要再次访问的。检查集群参数发现ambari平台默认把日志的保留时间(yarn.log-aggregation.retain-seconds)设置为了30天,实际我们对日志的访问需求一般在3天左右,所以我们修改该参数的值为3天,并手动清理HDFS上的不需要的日志文件后,NameNodeRPC性能得到了极大的改善。

虽然在修改日志保留时间后,RPC性能问题得到改善,但还没能完全解决,RPC时延偶尔也会升到3S。此时我们只能迎难而上,找出根因。于是我们对NameNode增加监控,当RPC变大超过3S时,打印NameNode当前的栈。通过对栈信息进行多轮次的分析,发现一些可疑信息(图二所示):

HDFS优化之RPC时延变大问题处理

图二 NameNode权限检查栈信息

该栈信息代表NameNode收到一个getListingRPC请求,在处理该RPC请求时先进行权限校验,权限校验时先调用rangerhdfs插件模块进行检查,最后再返回hdfs自身的权限检查模块的checkSubAccess进行权限检查。分析checkSubAccess源码,它会以访问点为根,递归扫描目录树下的所有节点进行权限检查。如果根节点以下的子目录树很大,该调用会非常耗时,RPC时延自然也会变大。此时,我们已经接近问题真相,但我们还有两个问题需要回答:

1、什么样的操作会使NameNode在调用ranger权限校验之后,还需回调自身的权限校验模块?

2、回调hdfs自身的权限校验模块是否真的有必要调用checkSubAccess这种耗时的操作,有无规避方法?

针对问题1,基于ranger的权限检验会首先检查ranger的策略库,只有ranger策略库中没有需要访问的目录对访问用户的策略设置时才需要调用hdfs自身的权限管理模块进行权限校验(如图三所示),所以我们怀疑是有人访问了没有在ranger中设置过策略的目录。通过分析hdfsaudit log,我们发现某租户每隔几分钟就会访问根目录,而根目录正好是没有做ranger策略设置的。与租户一起沟通排查后发现,租户在上传数据前会ls根目录来判断哪个节点为Active NameNode,然后用NameNode全路径方式上传数据。而根目录下有1.2亿文件,如果每次ls根目录都进行一次递归遍历,那NameNodeRPC必然会下降。在与租户商量后,修改业务代码逻辑,取消Active NN的判断,转而用NameService做为schema来直接进行数据上传。租户完成修改后,NameNodeRPC性能的确有了很大提升,这也就证明了我们分析的方向是正确的。

HDFS优化之RPC时延变大问题处理

图三 ranger权限检查流程

针对问题2,我们仔细阅读了hdfsranger的源码,发现hdfs权限检查模块只有在subAccess标志位不为空时才需要进行checkSubAccess调用,而该标志位是RPC指令在调用权限检查模块时先传入rangerranger再回传给hdfs。但ranger回调时,把原本为null的入参改成了FsAction.None,这样所有回调到hdfs权限校验模块RPC指令都会调用checkSubAccess,从而导致性能下降(如图四所示)。所以我们只要修复此BUG,即可避免NameNode因调用checkSubAccess导致的性能下降。

图四 ranger权限检查代码流程


三、源码分析

为了加深对问题的理解,我们以ls命令为例对源码进行剖析。当客户端执行ls命令时,会向NameNode发起getListingRPC调用,该调用最终会执行fsDirectoy.checkPathAccess进行权限校验:

void checkPathAccess(FSPermissionChecker pc, INodesInPath iip, FsAction access) 

throws AccessControlException { 

 checkPermission(pc, iip, false, null, null, access, null);

}

留意checkPermission的最后一个参数subAccess值为null。函数会调用FSPermissionChecker.checkPermission函数:

void checkPermission(INodesInPath inodesInPath, boolean doCheckOwner, 

FsAction ancestorAccess, FsAction parentAccess, FsAction access, 

FsAction subAccess, boolean ignoreEmptyDir)  

throws AccessControlException {

......  

AccessControlEnforcer enforcer =  getAttributesProvider().getExternalAccessControlEnforcer(this);  

enforcer.checkPermission(fsOwner, supergroup, callerUgi, 

inodeAttrs, inodes, pathByNameArr, snapshotId, path, 

ancestorIndex, doCheckOwner,  ancestorAccess, parentAccess, access, 

subAccess, ignoreEmptyDir);

}

其中enforcer由参数dfs.namenode.inode.attributes.provider.class指定,开启ranger后其值为:org.apache.ranger.authorization.hadoop.RangerHdfsAuthorizer。然后进入ranger进行权限校验:

public void checkPermission(String fsOwner, String superGroup, UserGroupInformation ugi,                    

 INodeAttributes[] inodeAttrs, INode[] inodes, byte[][] pathByNameArr,                     

int snapshotId, String path, int ancestorIndex, boolean doCheckOwner,                     

FsAction ancestorAccess, FsAction parentAccess, FsAction access,                     

FsAction subAccess, boolean ignoreEmptyDir) throws AccessControlException {

......   

authzStatus = isAccessAllowed(ancestor, ancestorAttribs, ancestorAccess, user, groups, plugin, auditHandler);

//如果在ranger中没有找到策略设置  

 if (authzStatus == AuthzStatus.NOT_DETERMINED) {     

 authzStatus = checkDefaultEnforcer(fsOwner, superGroup, ugi, inodeAttrs, inodes,           

pathByNameArr, snapshotId, path, ancestorIndex, doCheckOwner,            

 FsAction.NONE, FsAction.NONE, access, FsAction.NONE, ignoreEmptyDir,            

 isTraverseOnlyCheck, ancestor, parent, inode, auditHandler);    }

}

private AuthzStatus checkDefaultEnforcer(String fsOwner, String superGroup, UserGroupInformation ugi,                     

 INodeAttributes[] inodeAttrs, INode[] inodes, byte[][] pathByNameArr,                    

 int snapshotId, String path, int ancestorIndex, boolean doCheckOwner,                     

FsAction ancestorAccess, FsAction parentAccess, FsAction access,                     

 FsAction subAccess, boolean ignoreEmptyDir,                                  

boolean isTraverseOnlyCheck, INode ancestor,                               

INode parent, INode inode, RangerHdfsAuditHandler auditHandler                             

 ) throws AccessControlException {

  ......

try {

// 回调hdfs的权限校验    

defaultEnforcer.checkPermission(fsOwner, superGroup, ugi, inodeAttrs, inodes,                           

 pathByNameArr, snapshotId, path, ancestorIndex, doCheckOwner,                           

 ancestorAccess, parentAccess, access, subAccess, ignoreEmptyDir);  

}

}

NameNode通过反射调用rangercheckPermission接口,该接口会调用ranger模块提供的isAccessAllowd()函数进行权限校验,如果该函数返回NOT_DETERMINED(未到相应策略),则通过checkDefaultEnforcer函数返回NameNode原来的权限校验模块进行校验。这里注意到调用checkDefaultEnforcer时,subAccess参数不论传入的值是什么,在回调时都统一写成了FsAction.None。下面我们再看一下NameNode自己是如何进行权限校验的:

public void checkPermission(String fsOwner, String supergroup,    

   UserGroupInformation callerUgi, INodeAttributes[] inodeAttrs,    

  INode[] inodes, byte[][] pathByNameArr, int snapshotId, String path,    

int ancestorIndex, boolean doCheckOwner, FsAction ancestorAccess,    

FsAction parentAccess, FsAction access, FsAction subAccess,    

boolean ignoreEmptyDir)   

 throws AccessControlException { 

......

if (subAccess != null) {    

  checkSubAccess(pathByNameArr, inodeAttrs.length - 1, rawLast,       

          snapshotId, subAccess, ignoreEmptyDir); 

 }

}

private void checkSubAccess(byte[][] pathByNameArr, int pathIdx, INode inode,    

   int snapshotId, FsAction access, boolean ignoreEmptyDir)   

throws AccessControlException {  

Stack<INodeDirectory> directories = new Stack<INodeDirectory>();  

for(directories.push(inode.asDirectory()); !directories.isEmpty(); ) {    

INodeDirectory d = directories.pop();    

ReadOnlyList<INode> cList = d.getChildrenList(snapshotId);    

if (!(cList.isEmpty() && ignoreEmptyDir)) {      

  check(getINodeAttrs(pathByNameArr, pathIdx, d, snapshotId),         

 inode.getFullPathName(), access);   

 }

    for(INode child : cList) {     

 if (child.isDirectory()) {        

directories.push(child.asDirectory());      

     }   

   } 

 }

}

由于FsAction.None不等于null,所以只要回调到NameNode进行权限校验的RPC调用都会进行访问点下整个子目录的遍历,浪费了NameNode的性能。


四、解决办法

我们只能修改ranger的代码来解决此BUG。在查询社区最新版代码后,发现社区对该问题已经解决,我们只需要参考社区对线上集群进行修改即可。修改后的代码如下:

public void checkPermission(String fsOwner, String superGroup, UserGroupInformation ugi,                     

 INodeAttributes[] inodeAttrs, INode[] inodes, byte[][] pathByNameArr,                    

 int snapshotId, String path, int ancestorIndex, boolean doCheckOwner,                   

 FsAction ancestorAccess, FsAction parentAccess, FsAction access,                   

FsAction subAccess, boolean ignoreEmptyDir) throws AccessControlException {

......   

authzStatus = isAccessAllowed(ancestor, ancestorAttribs, ancestorAccess, user, groups, plugin, auditHandler);   

if (authzStatus == AuthzStatus.NOT_DETERMINED) {     

        authzStatus = checkDefaultEnforcer(fsOwner, superGroup, ugi, inodeAttrs, inodes,          

                pathByNameArr, snapshotId, path, ancestorIndex, doCheckOwner,           

                null, null, access, null, ignoreEmptyDir,            

                isTraverseOnlyCheck, ancestor, parent, inode, auditHandler);    

}

}

本段代码属于ranger项目的hdfs-agent子项目,我们只需要对该子项目打包,并把相应的jar包放到NameNode节点的lib目录,重启NameNode即可。


五、结果验证

为了验证结果,我们选择ls命令来做修复前和修复后的对比,并打开客户端的debug日志来获取每个RPC调用的具体执行时间。以下是对比结果:

规模

场景

修复前(ms)

修复后(ms)

10W

ranger已设置策略

1.7

2.8


ranger未设置策略

14.7

3.7

100W

ranger已设置策略

1.3

2.6


ranger未设置策略

34.33

2.6

800W

ranger已设置策略

1.3

2.17


ranger未设置策略

275

3

表一 BUG修改前后ls命令执行时间对比

可以发现:

1、如果ranger中有相应的策略,则RPC时延不会随目录规模变大而变大,稳定在2ms以内。

2、修复前,如果ranger中没有相应的策略,RPC时延随目录规模变大而大。

3、修复后,不论ranger中是否有相应的策略,目录的规模是否变大,RPC都稳定在2ms左右。

BUG修复上线后,我们还进行了上线前和上线后RPC性能对比,RPC性能得到了极大的改善,如下图:

图五 BUG修改前后NameNode RPC监控对比


六、结语

大数据平台开发不仅仅是搭环境、监控、业务支撑这样一份偏运维和运营的工作,我们还需要在了解各组件原理的基础之上,深入到组件的源码,读懂并能修改源码,才能应对一些实际的问题,为业务的发展保驾护航。