1. 背景

一般情况下,用户会以项目为维度提交作业。因为项目用户的拥有项目下的所有权限。如下所示,个人用户bob将在project_sa项目空间下提交作业,HDFS会通过project_sa进行鉴权并访问:

Untitled.png

上述方案有一个问题,如果HDFS中的auditlog中记录的操作用户是project_us,无法分辨具体由哪个用户提交的作业。如果能够在auditlog中增加真实提交的用户,在作业相关问题时可以节省大量人力成本。

2. 方案

为了实现HDFS auditlog打印真实用户信息,需要将真实用户bob的身份信息通过计算组件传递给NameNode:

Untitled 1.png

通过调研发现,所有计算组件在访问NameNode时,都会使用FileSystem获取HDFS客户端对象。客户端在像NameNode放松RPC请求时,会携带FileSystem中的callercontext信息发送给NameNode中。因此,可以将真实用户bob信息写入到FileSystem中的callercontext中,这样namenode获取到真实用户后即可打印出来:

Untitled 2.png

3. CallerContext线程处理流程分析

首先,回忆一下在https://blog.51cto.com/u_15327484/7779462文章中,介绍了NameNode服务端的RPC的线程模型,每个RPC请求都是由一个handler线程处理的:

Untitled 3.png

同样,在客户端和服务端中,callerContext是线程级别,每一个Handler线程拥有对应rpc请求传过来的callercontext:

  private static final class CurrentCallerContextHolder {
    static final ThreadLocal<CallerContext> CALLER_CONTEXT =
        new InheritableThreadLocal<>();
  }

https://issues.apache.org/jira/browse/HDFS-9184中,新增了CallerContext特性。通过分析这个patch可以发现CallContext在服务端的处理逻辑:

  1. 服务端接收到来自客户端的Rpc请求时,从请求的header中构建服务端的一个新的CallerContext对象,放到Call对象中。
  2. 服务端从callQueue中取出请求后,创建新的Handler线程进行处理,在Handler线程中,设置Call对象中的CallerContext为当前线程的CallerContext。这样确保服务端中每个CallerContext只由一个线程处理,符合CallerContext的ThreadLocal定义。

其关键代码如下所示:

Untitled 4.png

其处理逻辑可以简化成如下流程:

Untitled 5.png

4. 代码二次开发

如下,客户端新增hadoop.caller.context.env配置,可以设置要额外打印的信息。例如额外打印Job地址信息,真实用户信息:

<property>
  <name>hadoop.caller.context.env</name>
  <value>JOB_TASKID,REAL_USER,...</value>
</property>

新增一个类,用户读取环境变量或者配置文件中设置的JOB_TASKID,REAL_USER值,写入并替换到线程对应的CallerContext中:

public class CallerContextExtension {
    private static final Logger LOG = LoggerFactory.getLogger(CallerContextExtension.class);

    public static void impoveCallerContext(Configuration conf){
        conf.addResource("hdfs-site.xml");
        CallerContext.Builder ctxBuilder = null;
        String currentContent = null;
        if(CallerContext.getCurrent() == null){
            ctxBuilder = new CallerContext.Builder(null);
        }else{
            currentContent = CallerContext.getCurrent().getContext();
            ctxBuilder = new CallerContext.Builder(currentContent);
        }
        //get env property, set Configuration, append callercontext
        String contextEnv = conf.get("hadoop.caller.context.env");
        if(contextEnv != null){
            String[] envList = contextEnv.split(",");
            for(int i = 0; i < envList.length; ++i){
                String envKey = envList[i];
                String envVal = System.getenv(envKey);
                String confVal = conf.get(envKey);
                if(confVal != null && currentContent != null && !currentContent.contains(envKey)){
                    ctxBuilder.append(envKey, confVal);
                }
            }
        }
        String newCtxText = ctxBuilder.getContext();
        String realCtx;
        int maxSize = conf.getInt("hadoop.caller.context.max.size", 128);
        if (newCtxText == null || newCtxText.length() <= maxSize) {
            realCtx = newCtxText;
        } else {
            String finalCallerContext = newCtxText.substring(0, maxSize);
            LOG.warn(
                    "Truncated Hadoop caller context from "
                            + newCtxText
                            + " to "
                            + finalCallerContext);
            realCtx = finalCallerContext;
        }
        CallerContext.setCurrent(new CallerContext.Builder(realCtx).build());
    }
}

最后,客户端在调用FileSystem.get()时,调用CallerContextExtension自动写入设置的真实用户信息:

Untitled 6.png

5. 上线测试

在客户端环境变量中增加REAL_USER信息:

export REAL_USER=gdc_sa

通过执行MR、Spark、Hive,或者直接访问HDFS,NameNode都会打印REAL_USER信息。例如,执行以下命令访问HDFS:

hdfs dfs -ls /job_1698398895366_3124388/job.split

如下所示,callercontext中打印了真实用户:

2023-11-06 17:45:18,213 INFO org.apache.hadoop.hdfs.server.namenode.FSNamesystem.audit: allowed=true    ugi=xxxx/scheduler@xxxx (auth:TOKEN)        ip=/xxxxxx        cmd=open        src=/job_1698398895366_3124388/job.split    dst=null        perm=null       proto=rpc       callerContext=REAL_USER:gdc_sa