深度解析Cat源码系列专栏点击访问
持续更新中
文章目录
CAT源码分析 - 客户端(上-消息结构)
由于cat-client目录下的代码官方已经不再维护更新,所以分析lib目录下的cat客户端源码
1. 官方Demo
CAT客户端的源码分析以官方的使用QuickStart入手
如上图代码所示:
- 通过Cat客户端开启了一个Transaction,记录整个方法的运行时间。
- 在过程中“记录”(插入)了成功的Event事件,和两个Metric业务指标。
- 最后设置Transaction成功
SUCCESS
,标记结束complete()
。
2. 客户端初始化
CAT客户端使用时,不需要显式的预初始化,而是在客户端调用时再“懒”初始化。
Cat
类是一个单例,封装了客户端对Message消息的一些列API操作。
以newTransation方法为例分析代码:
- 首先获取MessageProcuder,通过MessageProducer来创建Transaction。
- MessageProducer是一个消息提供类,包括了消息的创建、上下文管理、初始化等逻辑。
注:这段代码可以看到NullMessage.TRANSACTION,包括getProducer()的实现,都使用了“空对象”模式,来减少空判断。这种模式在cat源码中大量使用。具体介绍-空对象模式
public static Transaction newTransaction(String type, String name) {
if (isEnabled()) { // 判断客户端是否生效,初始化失败会为false
try {
// 获取Producer工程,
return Cat.getProducer().newTransaction(type, name);
} catch (Exception e) {
errorHandler(e);
return NullMessage.TRANSACTION;
}
} else {
return NullMessage.TRANSACTION;
}
}
public static MessageProducer getProducer() {
try {
// 检查是否执行初始化
checkAndInitialize();
if (producer != null) {
// 返回producer
return producer;
} else {
return NullMessageProducer.NULL_MESSAGE_PRODUCER;
}
} catch (Exception e) {
errorHandler(e);
return NullMessageProducer.NULL_MESSAGE_PRODUCER;
}
}
- 初始化方法校验是否已经初始化,之后通过Java SPI读取用户是否设置了自定义的ClientConfigProvider实现。这为用户提供了自定义配置和获取配置的方式。
默认情况下,使用CAT默认的配置进行初始化。
private static void checkAndInitialize() {
try {
// 由于是懒加载,避免重复初始化
if (!init) {
// 通过SPI读取自定义的配置
ClientConfig clientConfig = getSpiClientConfig();
if (clientConfig == null) {
// 执行初始化
initializeInternal();
} else {
initializeInternal(clientConfig);
}
}
} catch (Exception e) {
errorHandler(e);
}
}
private static ClientConfig getSpiClientConfig() {
// 通过SPI接口,获取/META-INF/service下是否有自定义配置
ServiceLoader<ClientConfigProvider> clientConfigProviders = ServiceLoader.load(ClientConfigProvider.class);
if (clientConfigProviders == null) {
return null;
}
Iterator<ClientConfigProvider> iterator = clientConfigProviders.iterator();
if (iterator.hasNext()){
//只支持一个ClientConfigProvider的实现,默认取查询结果第一个
ClientConfigProvider clientConfigProvider = (ClientConfigProvider)iterator.next();
return clientConfigProvider.getClientConfig();
} else {
return null;
}
}
真正的初始化逻辑com.dianping.cat.Cat#initializeInternal():
- 校验是否配置了app.name(/META-INFO/app.properties),作为Domain上报Server使用
- 单例双重检查是否初始化
- 引用MessageProducer实例和MessageManager实例(下文详解)
- 开启客户端通信TcpSocketSender
- 启动HeartBeat消息的检查线程
private static void initializeInternal() {
// 检查是否配置appname
validate();
if (isEnabled()) {
try {
// 单例的双重检查
if (!init) {
synchronized (instance) {
if (!init) {
// 一系列的初始化操作
producer = DefaultMessageProducer.getInstance();
manager = DefaultMessageManager.getInstance();
StatusUpdateTask heartbeatTask = new StatusUpdateTask();
TcpSocketSender messageSender = TcpSocketSender.getInstance();
Threads.forGroup("cat").start(heartbeatTask);
Threads.forGroup("cat").start(messageSender);
Threads.forGroup("cat").start(new LocalAggregator.DataUploader());
CatLogger.getInstance().info("Cat is lazy initialized!");
init = true;
}
}
}
} catch (Exception e) {
errorHandler(e);
disable();
}
}
}
private static void validate() {
// 系统变量检查是否开启CAT客户端,默认开启
String enable = Properties.forString().fromEnv().fromSystem().getProperty("CAT_ENABLED", "true");
if ("false".equals(enable)) {
CatLogger.getInstance().info("CAT is disable due to system environment CAT_ENABLED is false.");
enabled = false;
} else {
// 检查是否从系统变量读取自定义的Domain
String customDomain = getCustomDomain();
// 没有特殊定义,默认从/META-INF/app.properties中读取app.name
if (customDomain == null && UNKNOWN.equals(ApplicationEnvironment.loadAppName(UNKNOWN))) {
CatLogger.getInstance().info("CAT is disable due to no app name in resource file /META-INF/app.properties");
enabled = false;
}
}
}
private static String getCustomDomain() {
// 通过系统变量检查cat-client-config,是否自定义了配置
// 默认cat-client.xml
String config = System.getProperty(Cat.CLIENT_CONFIG);
if (StringUtils.isNotEmpty(config)) {
try {
ClientConfig clientConfig = DefaultSaxParser.parse(config);
return clientConfig.getDomain();
} catch (Exception e) {
// ignore
}
}
return null;
}
以上,完成了客户端的初始化,可以继续执行消息的操作逻辑。
3. 消息管理
Cat客户端的消息收集和管理逻辑如下图所示。为了解决线程安全问题,使用ThreadLocal保存消息上下文。每一条独立的Message通过MessageId在MessageTree中串联组织。
当messageTree标记complete时,通过MessageQueue异步将tree编码发送到服务端。
3.1 MessageProducer & MessageManager
初始化代码 com.dianping.cat.Cat#initializeInternal()
中,第一步就是获取Producer和Manager两个单实例。
Producer是一个较为上层的抽象,它引用了Manager,通过后者的API创建Transaction、Event、logError等Message。
3.1.1 Manager初始化
Manager更贴近底层,是管理CAT客户端消息树 MessageTree
的核心,负责设定、创建消息树、添加节点,标记完成等。
获取Manager实例时,Manager也执行了初始化逻辑
private DefaultMessageManager() {
initialize();
}
private void initialize() {
// 1. 读取domain,itern方法用于返回同字面量的String对象,可以实现==判断,提升性能
domain = String.valueOf(configService.getDomain()).intern();
// 2. 获取本地的HostName和IP
hostName = NetworkInterfaceManager.INSTANCE.getLocalHostName().intern();
ip = NetworkInterfaceManager.INSTANCE.getLocalHostAddress().intern();
// initialize domain and IP address
try {
// 3. MessageIdFactory初始化
factory.initialize(domain);
} catch (Exception e) {
LOGGER.error("error when create mark file", e);
}
}
- 通过com.dianping.cat.configuration.DefaultClientConfigService读取domain,该类在实例化时进行初始化。与之前的初始化逻辑类似,同样读取默认的Client配置。
private DefaultClientConfigService() {
// 判断是否有额外的配置
String config = System.getProperty(Cat.CLIENT_CONFIG);
if (StringUtils.isNotEmpty(config)) {
try {
this.config = com.dianping.cat.configuration.client.transform.DefaultSaxParser.parse(config);
LOGGER.info("setup cat with config:" + config);
} catch (Exception e) {
LOGGER.error("error in client config " + config, e);
}
}
if (this.config == null) {
// 默认从/META-INF/app.properties中读取
String appName = ApplicationEnvironment.loadAppName(Cat.UNKNOWN);
// 读Client配置
ClientConfig defaultConfig = ApplicationEnvironment.loadClientConfig(appName);
// 设置ClientConfig
defaultConfig.setDomain(appName);
this.config = defaultConfig;
LOGGER.info("setup cat with default configuration:" + this.config);
}
}
com.dianping.cat.configuration.ApplicationEnvironment#loadClientConfig读取默认的client配置client_cache.xml和client.xml
public static ClientConfig loadClientConfig(String domain) {
String xml = null;
try {
// client_cache.xml
File cacheFile = new File(Cat.getCatHome() + CACHE_FILE);
// client.xml
File configFile = new File(Cat.getCatHome() + CLIENT_FILE);
if (cacheFile.exists() && !isDevMode()) {
xml = Files.forIO().readFrom(cacheFile, "utf-8");
} else if (configFile.exists()) {
xml = Files.forIO().readFrom(configFile, "utf-8");
} else {
xml = ApplicationEnvironment.loadRemoteClientConfig();
}
ClientConfig config = DefaultSaxParser.parse(xml);
config.setDomain(domain);
return config;
} catch (Exception e) {
CatLogger.getInstance().info("load client config error: " + xml, e);
File cacheFile = new File(Cat.getCatHome() + CACHE_FILE);
if (cacheFile.exists()) {
cacheFile.delete();
return loadClientConfig(domain);
}
throw new RuntimeException("Error when get cat router service, please contact cat support team for help!", e);
}
}
- NetWorkInterfaceManager中,同样实例化时调用JavaAPI获取本机的IP和HostName,不多赘述
NetworkInterfaceManager() {
System.setProperty("java.net.preferIPv4Stack", "true");
// 支持高级的手动设定
load();
ip = local.getHostAddress().intern();
String hostname;
try {
hostname = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
hostname = local.getHostName();
}
if (hostname == null) {
hostname = "unknown";
} else if (hostname.contains(".sankuai.com") || hostname.contains(".office.mos")) {
hostname = hostname.substring(0, hostname.indexOf("."));
}
hostName = hostname;
}
3.1.2 ThreadLocal管理消息
DefaultManager实现类中包含一个ThreadLocal的context上下文,为每个Cat客户端线程管理消息,解决线程安全问题。
注:这里先不考虑异步线程的问题,CAT作者也说开源版本对异步的支持并不好
public class DefaultMessageManager implements MessageManager {
// 其他代码
private ThreadLocal<Context> context = new ThreadLocal<Context>();
// 其他代码
}
Context上下文是包含MessageTree,存储所有的消息,并使用一个栈结构管理Transaction消息,用于为tansaction消息注入子message
public class Context {
private MessageTree tree;
private Stack<Transaction> stack;
private int length;
private boolean traceMode;
private long totalDurationInMicros;
private Set<Throwable> knownExceptions;
public Context(String domain, String hostName, String ipAddress) {
tree = new DefaultMessageTree();
stack = new Stack<Transaction>();
Thread thread = Thread.currentThread();
String groupName = thread.getThreadGroup().getName();
tree.setThreadGroupName(groupName);
tree.setThreadId(String.valueOf(thread.getId()));
tree.setThreadName(thread.getName());
tree.setDomain(domain); // 设置appName
tree.setHostName(hostName);
tree.setIpAddress(ipAddress);
length = 1;
}
// 其他代码
}
3.2 Message & MessageTree
Message接口是客户端采集上报数据的抽象,其主要子接口包括Transaction、Event、Trace、HeartBeat,Metric,以及多个实现类,如DefaultTransaction等
MessageTree在Context中被引用,虽然该类名中包含一个单词“tree”,但实际上它并不是一个树形结构。而是后续串联消息树的一个节点。
每个Message都对应一个MessageTree,后者引用前者和串联消息树必备的消息Id等标示
public class DefaultMessageTree implements MessageTree {
private ByteBuf buf;
private String domain;
private String hostName;
private String ipAddress;
// 每个Message都引用一个Message
private Message message;
private String messageId;
private String parentMessageId;
private String rootMessageId;
private String sessionToken;
private String threadGroupName;
private String threadId;
private String threadName;
private MessageId formatMessageId;
private boolean discard = true;
private boolean hitSample = false;
// 解码时使用
private List<Transaction> transactions;
private List<Event> events;
private List<Heartbeat> heartbeats;
private List<Metric> metrics;
private List<ForkableTransaction> forkableTransactions;
3.3 MessageIdFactory与MessageId
3.3.1 MesageId的结构
MessageId在CAT中用来串联独立的Message。MessageId具有固定的结构,由domain、当前时间的小时表示、机器IP,每小时内独立自增的索引index。
[应用名Domain]-[ip16进制字符串]-[timeStamp当前时间/3600]-[本小时的顺序index]
例如"UserService-ac648dc2-445256-2073"的各段分别是
- UserService - domain应用名
- ac648dc2 - ip16进制2位一段,对应172.100.141.194
- 445256 - timeStamp系统时间/3600后的结果
- 2073 - index当前小时的顺序号
3.3.2 MessageIdFactory
MessageIdFactory随着MessageManager初始化,主要用来为IdFactory读取domain、时间戳、机器ip、和index。
其代码com.dianping.cat.message.internal.MessageIdFactory#initialize如下
- 读取本机IP,将4段IP存入byte数组,并将byte转换为16进制表示的字符串,每一段用两个字符表示如,172 -> “ac”,6 -> “06”
- 转换时之所以先将String类型的ip转换为byte,再通过toHexString将高低位分开计算是为了保证,每个整数一定都被转换为2个字符表。由于IP一定小于等于255,所以也不会超过2个字符。如果直接使用String转Int后,直接toHexString小于15的数会用1位表示,MessageId长度将不规范
- 创建并写入MarkFile,MarkFile是记录ID中domain对应的自增索引的文件。创建文件时,读取
CAT_HOME
系统变量中设置的目录,如果没有设置,默认为/data/appdatas/cat/
。创建时会判断是否已存在或不可写,异常情况均会写临时目录
目录下的 .mark
文件,其格式为16进制数文件,内容如
- 创建文件成功后,通过NIO的方式读写文件,开始拼装ID,具体NIO的API操作不赘述
- 读文件前,首先拼装idPrefix,并获取当前时间戳。读取时,首先获取上次记录的时间戳和索引,如果时间戳相等,新index+1000开始重新计数。并读取map中的数据(cat多实例的用法,此处可以略过)。不相等时,说明不再同一小时,从0开始重新计数
- 初始化读取完成后,更新mark文件,并注册结束的监听关闭文件读写
// MessageIdFactory初始化逻辑
public void initialize(String domain) throws IOException {
String ip = NetworkInterfaceManager.INSTANCE.getLocalHostAddress();
List<String> items = Splitters.by(".").noEmptyItem().split(ip);
// IP分4段,每段8bit,用4个byte存储
byte[] bytes = new byte[4];
for (int i = 0; i < 4; i++) {
bytes[i] = (byte) Integer.parseInt(items.get(i));
}
// 个人认为此处应该是乘2,8个byte正好满足下面每段2个字符存储的设计
// 除以2没有意义
StringBuilder sb = new StringBuilder(bytes.length / 2);
for (byte b : bytes) {
// 先将高4位写入sb,后写入低4位
sb.append(Integer.toHexString((b >> 4) & 0x0F));
sb.append(Integer.toHexString(b & 0x0F));
}
this.domain = domain;
ipAddress = sb.toString();
//根据domain创建markfile
File mark = createMarkFile(domain);
// NIO读写文件
markFile = new RandomAccessFile(mark, "rw");
// 默认大小1M
byteBuffer = markFile.getChannel().map(MapMode.READ_WRITE, 0, 1024 * 1024L);
// 通过时间戳获取ID的前缀
idPrefix = initIdPrefix(getTimestamp(), false);
// CAT客户端默认是单例,但也支持多实例模式,不常用,暂不分析
idPrefixOfMultiMode = initIdPrefix(getTimestamp(), true);
// 文件有内容可读时
if (byteBuffer.limit() > 0) {
try {
long lastTimestamp = byteBuffer.getLong();
// 上一次发送的index
int index = byteBuffer.getInt();
if (lastTimestamp == timestamp) {
// 当前小时,last+1000开始计数
this.index = new AtomicInteger(index + 1000);
int mapLength = byteBuffer.getInt();
for (int i = 0; i < mapLength; i++) {
int domainLength = byteBuffer.getInt();
byte[] domainArray = new byte[domainLength];
byteBuffer.get(domainArray);
int value = byteBuffer.getInt();
map.put(new String(domainArray), new AtomicInteger(value + 1000));
}
} else {
// 新小时,从0开始计数
this.index = new AtomicInteger(0);
}
} catch (Exception e) {
retry++;
// 报错,删除旧文件并重试初始化。有且只有1次
if (retry == 1) {
mark.delete();
initialize(domain);
}
}
}
// 保存更新mark文件
saveMark();
// 注册关闭的监听,回收资源,记录文件
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
close();
}
});
}
// 读timeStamp,除小时
protected long getTimestamp() {
return System.currentTimeMillis() / HOUR;
}
// 拼装ID前缀
private String initIdPrefix(long timestamp, boolean multiMode) {
StringBuilder sb = new StringBuilder(domain.length() + 32);
int processID = getProcessID();
if (multiMode && processID > 0) {
// 多实例模式可以忽略
sb.append(domain).append('-').append(ipAddress).append(".").append(processID).append('-').append(timestamp).append('-');
} else {
// ID的格式
sb.append(domain).append('-').append(ipAddress).append('-').append(timestamp).append('-');
}
return sb.toString();
}
// 先保存当前的文件,再关闭文件流
public void close() {
try {
saveMark();
markFile.close();
} catch (Exception e) {
// ignore it
}
}
// 写buffer
public synchronized void saveMark() {
try {
// nio操作
byteBuffer.rewind();
byteBuffer.putLong(timestamp);
byteBuffer.putInt(index.get());
byteBuffer.putInt(map.size());
// 写multi部分
for (Entry<String, AtomicInteger> entry : map.entrySet()) {
byte[] bytes = entry.getKey().toString().getBytes();
byteBuffer.putInt(bytes.length);
byteBuffer.put(bytes);
byteBuffer.putInt(entry.getValue().get());
}
byteBuffer.force();
} catch (Throwable e) {
// ignore it
}
}