美团CAT源码分析 - 客户端(上-消息结构)

深度解析Cat源码系列专栏点击访问
持续更新中

CAT源码分析 - 客户端(上-消息结构)

由于cat-client目录下的代码官方已经不再维护更新,所以分析lib目录下的cat客户端源码

1. 官方Demo

CAT客户端的源码分析以官方的使用QuickStart入手
官方Demo
如上图代码所示:

  • 通过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);
    }
}
  1. 通过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);
    }
}
  1. 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等

Message Transaction Event Trace HeartBeat Metric

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如下

  1. 读取本机IP,将4段IP存入byte数组,并将byte转换为16进制表示的字符串,每一段用两个字符表示如,172 -> “ac”,6 -> “06”
  2. 转换时之所以先将String类型的ip转换为byte,再通过toHexString将高低位分开计算是为了保证,每个整数一定都被转换为2个字符表。由于IP一定小于等于255,所以也不会超过2个字符。如果直接使用String转Int后,直接toHexString小于15的数会用1位表示,MessageId长度将不规范
  3. 创建并写入MarkFile,MarkFile是记录ID中domain对应的自增索引的文件。创建文件时,读取 CAT_HOME 系统变量中设置的目录,如果没有设置,默认为 /data/appdatas/cat/ 。创建时会判断是否已存在或不可写,异常情况均会写临时目录

目录下的 .mark 文件,其格式为16进制数文件,内容如
在这里插入图片描述

  1. 创建文件成功后,通过NIO的方式读写文件,开始拼装ID,具体NIO的API操作不赘述
  2. 读文件前,首先拼装idPrefix,并获取当前时间戳。读取时,首先获取上次记录的时间戳和索引,如果时间戳相等,新index+1000开始重新计数。并读取map中的数据(cat多实例的用法,此处可以略过)。不相等时,说明不再同一小时,从0开始重新计数
  3. 初始化读取完成后,更新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
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值