1. 背景
一般情况下,用户会以项目为维度提交作业。因为项目用户的拥有项目下的所有权限。如下所示,个人用户bob将在project_sa项目空间下提交作业,HDFS会通过project_sa进行鉴权并访问:
上述方案有一个问题,如果HDFS中的auditlog中记录的操作用户是project_us,无法分辨具体由哪个用户提交的作业。如果能够在auditlog中增加真实提交的用户,在作业相关问题时可以节省大量人力成本。
2. 方案
为了实现HDFS auditlog打印真实用户信息,需要将真实用户bob的身份信息通过计算组件传递给NameNode:
通过调研发现,所有计算组件在访问NameNode时,都会使用FileSystem获取HDFS客户端对象。客户端在像NameNode放松RPC请求时,会携带FileSystem中的callercontext信息发送给NameNode中。因此,可以将真实用户bob信息写入到FileSystem中的callercontext中,这样namenode获取到真实用户后即可打印出来:
3. CallerContext线程处理流程分析
首先,回忆一下在https://blog.51cto.com/u_15327484/7779462文章中,介绍了NameNode服务端的RPC的线程模型,每个RPC请求都是由一个handler线程处理的:
同样,在客户端和服务端中,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在服务端的处理逻辑:
- 服务端接收到来自客户端的Rpc请求时,从请求的header中构建服务端的一个新的CallerContext对象,放到Call对象中。
- 服务端从callQueue中取出请求后,创建新的Handler线程进行处理,在Handler线程中,设置Call对象中的CallerContext为当前线程的CallerContext。这样确保服务端中每个CallerContext只由一个线程处理,符合CallerContext的ThreadLocal定义。
其关键代码如下所示:
其处理逻辑可以简化成如下流程:
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自动写入设置的真实用户信息:
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