1. 背景
笔者的开发大数据平台XSailboat中包含基于Flink的可视化计算管道开发和运维功能。状态存储器中数据的查看和节点的日志查看功能是其重要的辅助支撑功能。它能使得在大数据平台上就能完全实现计算管道的开发、调试、部署,逐渐摆脱Flink的原生界面。
此篇文档是继《Flink日志采集、集中存储、可视化查询实践》文档之后,进一步描述如何实现节点调试日志动态开启/关闭的文档。
在计算管道的开发调试过程中,为了了解某一个节点实例在运行过程中,输入/输出或某些变量、状态存储器的值,通常都是通过Aviator函数p()来打印(我们的计算管道可视化开发的数据处理逻辑是使用Aviator表达式语言来描述的)。这么做至少有以下不便之处:
- 计算管道调试完之后,需要一点点把调试代码删掉。如果万一没删,则会在TaskManager的Stdout里面输出大量的日志。如果发现这种情况,又得再删,再部署,费时费力。
- 如果有多个节点都有调试打印的代码,日志混杂在一起,分析日志非常不方便。
为了解决这些问题,我们设计了计算管道节点日志动态开启/关闭功能,它实现以下功能目标:
- 在一般情况下,调试日志不开启。如果要查看调试日志,可以主动开启,调试完之后关闭。这里调试日志时开启之后才会输出调试日志,不开启后台时不会输出调试日志的,所以无法查看调试日志开启之前的日志。
- 调试日志代码,在不开启调试日志时,对逻辑的影响不大,不输出日志,这样相关代码没有必要在发布到生产环境时一定要删除。
- 节点日志按节点分开查看,不混杂在一起。调试日志开启/关闭控制到计算管道节点的级别。
2. 逻辑架构
SailMSPivot服务提供调试日志开启/关闭的相关接口。它将这些状态数据存储在Zookeeper中。
SailFlink框架在讲计算管道转换成Flink Job时,会创建一个和计算管道实例(即Flink Job)相对应的ZK临时节点,用以存储某个节点的调试日志时开启还是关闭的配置数据。ZK节点的路径
/xz/{系统环境,dev、test、prod}/flink/{工作环境,dev或prod}/debug/{集群名称.计算管道id}
它的配置数据格式:
{
"节点1id":true ,
"节点2id":true ,
...
}
计算管道节点配置采用的是“以既定节点逻辑结构为框架,以表达式语言表达细节逻辑为填充的思想”(见《Flink的DAG可视化开发实践》),计算管道节点转换成Flink算子逻辑的过程是在SailFlink框架中实现的,日志控制就是在其中实现的。
对于Flink算子的RichFunction,最主要的是open(初始化)和run(数据处理主体处理逻辑,不同节点方法名和参数不同)。在open方法中进行这个算子实例(SubTask)的日志记录器初始化工作,
// sDebugLogger是org.slf4j.Logger
// mLogger是TaskLogger
@Override
public void open(Configuration aParameters) throws Exception
{
RuntimeContext rc = getRuntimeContext() ;
// 如果当前进程还没有监听节点所属计算管道实例相对应的ZK节点,则监听它。
// 同时初始化这个节点的日志记录器TaskLogger,保存一些信息
mLogger.init(Integer.toString(rc.getIndexOfThisSubtask()+1) , sDebugLogger , rc.getExecutionConfig().getGlobalJobParameters()) ;
...
}
run方法中总是下面这种代码形式:
// 设置MDC,collect_log、where、subTaskIndex
FlinkLogHelper.logBegin(mLogger) ;
try
{
...
}
finally
{
// 从MDC中移除collect_log、where、subTaskIndex
FlinkLogHelper.logEnd() ;
}
3. 日志记录的相关代码
类:FlinkLogHelper
package com.cimstech.sailboat.flink.run.base.log;
import java.util.Map;
import java.util.Set;
import org.apache.log4j.MDC;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.cimstech.sailboat.common.IZKProxy;
import com.cimstech.sailboat.common.ZKProxy;
import com.cimstech.xfront.common.collection.CS;
import com.cimstech.xfront.common.excep.ExceptionAssist;
import com.cimstech.xfront.common.json.JSONObject;
import com.cimstech.xfront.common.math.Bool;
import com.cimstech.xfront.common.text.XString;
public class FlinkLogHelper
{
static final Logger sLogger = LoggerFactory.getLogger(FlinkLogHelper.class) ;
static final Set<String> sWatchedPaths = CS.hashSet() ;
static final Map<String , Bool> sNodeIdDebugMap = CS.concurrentHashMap() ;
public static Bool getOrCreateNodeDebug(String aNodeId)
{
synchronized (aNodeId.intern())
{
Bool b = sNodeIdDebugMap.get(aNodeId) ;
if(b == null)
{
b = new Bool() ;
sNodeIdDebugMap.put(aNodeId, b) ;
}
return b ;
}
}
public static TaskLogger newLogger(String aRunUid , String aNodeId)
{
return new TaskLogger(aRunUid , aNodeId , getOrCreateNodeDebug(aNodeId)) ;
}
public static void init( Map<String, String> aParams)
{
String zkPath = aParams.get("zookeeper.cpipe.path") ;
if(XString.isNotEmpty(zkPath) && !sWatchedPaths.contains(zkPath))
{
synchronized (FlinkLogHelper.class)
{
if(!sWatchedPaths.contains(zkPath))
{
// 注册
String zkQuorum = aParams.get("zookeeper.quorum") ;
try
{
IZKProxy proxy = ZKProxy.get(zkQuorum) ;
Watcher watcher = new Watcher()
{
@Override
public void process(WatchedEvent aEvent)
{
sLogger.info("Zookeeper节点[{}]的内容发生改变!" , zkPath) ;
try
{
String data = proxy.getNodeData_Str(zkPath) ;
if(XString.isNotEmpty(data))
{
new JSONObject(data)
.forEachBool((nodeId , debug)->{
Bool b = sNodeIdDebugMap.get(nodeId) ;
if(b != null)
{
b.set(debug.booleanValue()) ;
}
else
{
b = new Bool(debug.booleanValue()) ;
sNodeIdDebugMap.put(nodeId, b) ;
}
}) ;
}
}
catch (Exception e)
{
sLogger.error(ExceptionAssist.getClearMessage(FlinkLogHelper.class , e)) ;
}
}
} ;
proxy.watchNode(zkPath , watcher) ;
watcher.process(null) ; // 执行一次
sWatchedPaths.add(zkPath) ;
}
catch (Exception e)
{
sLogger.error(ExceptionAssist.getClearMessage(FlinkLogHelper.class , e)) ;
}
}
}
}
}
public static void logBegin(String aRunUid , String aNodeId , String aSubTaskIndex)
{
MDC.put("collect_log", true) ;
MDC.put("where" , aRunUid+"."+aNodeId) ;
MDC.put("subTaskIndex" , aSubTaskIndex) ;
}
public static void logBegin(TaskLogger aLogger)
{
TaskLogger logger = LogContext.get() ;
if(logger == null || logger.number != aLogger.number)
{
MDC.put("collect_log", true) ;
MDC.put("where" , aLogger.runUid +"." +aLogger.nodeId) ;
MDC.put("subTaskIndex" , aLogger.subTaskIndex) ;
LogContext.LOG.set(aLogger) ;
}
}
public static void logEnd()
{
MDC.remove("collect_log") ;
MDC.remove("where") ;
MDC.remove("subTaskIndex") ;
}
}
类:TaskLoggger
package com.cimstech.sailboat.flink.run.base.log;
import java.io.Serializable;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.flink.api.common.ExecutionConfig.GlobalJobParameters;
import org.slf4j.Logger;
import com.cimstech.xfront.common.math.Bool;
public class TaskLogger implements Serializable
{
private static final long serialVersionUID = 1L;
static final AtomicLong sLoggerNo = new AtomicLong(0) ;
public final String runUid ;
public final String nodeId ;
public String subTaskIndex ;
transient Bool debug ;
public transient Logger logger ;
public final long number ;
TaskLogger(String aRunUid , String aNodeId , Bool aDebug)
{
runUid = aRunUid ;
nodeId = aNodeId ;
number = sLoggerNo.incrementAndGet() ;
debug = aDebug ;
}
public void init(String aSubTaskIndex , Logger aLogger , GlobalJobParameters aParams)
{
subTaskIndex = aSubTaskIndex;
logger = aLogger ;
FlinkLogHelper.init(aParams.toMap()) ;
}
public void info(String aMsg)
{
logger.info(aMsg) ;
}
public void info(String aMsg , Object...aArgs)
{
logger.info(aMsg, aArgs) ;
}
public void debug(String aMsg)
{
if(isDebug())
logger.info("[框架调试]"+aMsg) ;
}
public boolean isDebug()
{
if(debug == null)
debug = FlinkLogHelper.getOrCreateNodeDebug(nodeId) ;
return debug.get() ;
}
}