1. 介绍

1.1 概述

CAT(Central Application Tracking)基于Java开发的实时监控平台,主要包括移动端监控,应用侧监控,核心网络层监控,系统层监控等。

CAT是一个提供实时监控报警,应用性能分析诊断的工具。

1.2 CAT能做什么

在此之前,先来想一想对于线上应用我们希望能监控些什么?可能有如下这些:

  • 机器状态信息。CPU负载、内存信息、磁盘使用率这些是必需的,另外可能还希望收集Java进程的数据,例如线程栈、堆、垃圾回收等信息,以帮助出现问题时快速debug。
  • 请求访问情况。例如请求个数、响应时间、处理状态,如果有处理过程中的时间分析那就更完美了。
  • 异常情况。譬如缓存服务时不时出现无响应,我们希望能够监控到这种异常,从而做进一步的处理。
  • 业务情况。例如订单量统计,销售额等等。

CAT支持的监控消息类型包括:

  • Transaction 适合记录跨越系统边界的程序访问行为,比如远程调用,数据库调用,也适合执行时间较长的业务逻辑监控,Transaction用来记录一段代码的执行时间和次数。
  • Event 用来记录一件事发生的次数,比如记录系统异常,它和transaction相比缺少了时间的统计,开销比transaction要小。
  • Heartbeat 表示程序内定期产生的统计信息, 如CPU%, MEM%, 连接池状态, 系统负载等。
  • Metric 用于记录业务指标、指标可能包含对一个指标记录次数、记录平均值、记录总和,业务指标最低统计粒度为1分钟。
  • Trace 用于记录基本的trace信息,类似于log4j的info信息,这些信息仅用于查看一些相关信息

在一个请求处理中可能产生有多种消息,CAT将其组织成消息树的形式。

在处理开始时,默认开始一个类型为URL的Transaction,在这个Transaction中业务本身可以产生子消息。例如,产生一个数据库访问的子Transaction或者一个订单统计的Metric。结构如下所示:

开源网络监控系统搭建_开源网络监控系统搭建

 

1.3 分布式监控系统要求

  • 方便安装
  • 要求轻量
  • 界面尽可能友好
  • 监控策略丰富,监控元素多样化
  • 可以嵌套开发
  • 占用服务器资源小,使用时不过多占用机器硬件方面资源,对实际业务影响较小

1.4 CAT使用特点

  • 异步化传输数据,不太影响正常业务
  • 实时监控
  • 轻量,部署简单
  • 嵌入简单
  • 有问题跟踪报表
  • 消息树形化
  • 日志不落地本地磁盘,较少IO,但很消耗网络资源
  • 监控消息,按照分业务传输数据,如业务场景,时间等要求传输数据
  • 有报警机制
  • 可能复杂的消息存储和消息ID查询看起来麻烦,需要建立查询索引(目前不考虑这个东东)
  • 消息队列异步化发送
  • 开源(这个最重要)

2. CAT设计

2.1 整体设计

开源网络监控系统搭建_客户端_02

2.2 客户端设计

开源网络监控系统搭建_消息队列_03

2.3 服务端设计

 

开源网络监控系统搭建_客户端_04

2.4 领域建模

 

开源网络监控系统搭建_客户端_05

3. 模块划分

 

开源网络监控系统搭建_客户端_06

3.1 模块说明

3.1.1 client端

cat-client 提供给业务以及中间层埋点的底层SDK。

3.1.2 server端

cat-consumer 用于实时分析从客户端提供的数据。

cat-home 作为用户给用户提供展示的控制端 ,并且cat-home做展示时,通过对cat-consumer的调用获取其他节点的数据,将所有数据汇总展示。

consumer、home以及路由中心都是部署在一起的,每个服务端节点都可以充当任何一个角色。

开源网络监控系统搭建_客户端_07

CAT服务端在整个实时处理中,基本上实现了全异步化处理:

  • 消息消费基于Netty的NIO实现(Netty-Server);
  • 消息消费到服务端就存放内存队列,然后程序开启一个线程会消费这个消息做消息分发(异步消费处理);
  • 每个消息都会有一批线程并发消费各自队列的数据,以做到消息处理的隔离。(每报表每线程,分别按照自己的规则解析消费这个消息,并且可以动态控制对某种报表类型的处理线程个数);
  • 消息(原始的消息logView)存储是先存入本地磁盘,然后异步上传到HDFS文件,这也避免了强依赖HDFS;

4. 设计原理

4.1 cat-client设计

作为一个日志上报的通用客户端,考虑点至少有如下这些:

  • 为了尽可能减少对业务的影响,需要对消息进行异步处理。即业务线程将消息交给CAT客户端与CAT客户端上报这两个过程需要异步。
  • 为了达到实时的目的以及适应高并发的情况,客户端上报应该基于TCP而非HTTP开发。
  • 在线程安全的前提下尽可能的资源低消耗以及低延时。我们知道,线程竞争的情况是由于资源共享造成的,要达到线程安全通常需要减少资源共享或者加锁,而这两点则会导致系统资源冗余和高延时。

CAT客户端实现并不复杂,但这些点都考虑到了。它的架构如下所示:

开源网络监控系统搭建_开源网络监控系统搭建_08

大概步骤为:

  • 业务线程产生消息,交给消息Producer,消息Producer将消息存放在该业务线程消息栈中;
  • 业务线程通知消息Producer消息结束时,消息Producer根据其消息栈产生消息树放置在同步消息队列中;
  • 消息上报线程监听消息队列,根据消息树产生最终的消息报文上报CAT服务端。

4.1.1 cat-client包结构



└─com
    ├─dianping
    │  └─cat
    │      ├─build
    │      ├─configuration
    │      ├─log4j
    │      ├─message
    │      │  ├─internal
    │      │  ├─io
    │      │  └─spi
    │      │      ├─codec
    │      │      └─internal
    │      ├─servlet
    │      └─status
    └─site
        ├─helper
        └─lookup
            └─util



4.1.2 com.dianping.cat.message包介绍

包结构如下:

开源网络监控系统搭建_服务端_09

com.dianping.cat.message中主要包含了internal、io、spi这三个目录:

  • internal目录包含主要的CAT客户端内部实现类;
  • io目录包含建立服务端连接、重连、消息队列监听、上报等io实现类;
  • spi目录为上报消息工具包,包含消息二进制编解码、转义等实现类。

其uml图如下所示(可以放大看):

开源网络监控系统搭建_开源网络监控系统搭建_10

类的功能如下:

  • Message为所有上报消息的抽象,它的子类实现有Transaction、Metric、Event、HeartBeat、Trace这五种。
  • MessageProducer封装了所有接口,业务在使用CAT时只需要通过MessageProducer来操作。
  • MessageManager为CAT客户端核心类,相当于MVC中的Controller。
  • Context类保存消息上下文。
  • TransportManager提供发送消息的sender,具体实现有DefaultTransportManager,调用其getSender接口返回一个TcpSocketSender。
  • TcpSocketSender类负责发送消息。

1)Message

上面说到,Message有五类,分别为Transaction、Metric、Event、HeartBeat、Trace。其中Metric、Event、HeartBeat、Trace基本相同,保存的数据都为一个字符串;而Transaction则保存一个Message列表。换句话说,Transaction的结构为一个递归包含的结构,其他结构则为原子性结构。

下面为DefaultTransaction的关键数据成员及操作:



public class DefaultTransaction extends AbstractMessage implements Transaction {
    private List<Message> m_children;
    private MessageManager m_manager;
    ...

    //添加子消息
    public DefaultTransaction addChild(Message message) {
        ...
    }

    //Transaction结束时调用此方法
    public void complete() {
        ...
        m_manager.end(this); //调用MessageManager来结束Transaction 
        ...
    }



 

值得一提的是,Transaction(或者其他的Message)在创建时自动开始,消息结束时需要业务方调用complete方法,而在complete方法内部则调用MessageManager来完成消息。

2)MessageProducer

MessageProducer对业务方封装了CAT内部的所有细节,它的主要方法如下:



public void logError(String message, Throwable cause);
public void logEvent(String type, String name, String status, String nameValuePairs);
public void logHeartbeat(String type, String name, String status, String nameValuePairs);
public void logMetric(String name, String status, String nameValuePairs);
public void logTrace(String type, String name, String status, String nameValuePairs);
...
public Event newEvent(String type, String name);
public Event newEvent(Transaction parent, String type, String name);
public Heartbeat newHeartbeat(String type, String name);
public Metric newMetric(String type, String name);
public Transaction newTransaction(String type, String name);
public Trace newTrace(String type, String name);



 

logXXX方法为方法糖(造词小能手呵呵),这些方法在调用时需要传入消息数据,方法结束后消息自动结束。

newXXX方法返回相应的Message,业务方需要调用Message方法设置数据,并最终调用Message.complete()方法结束消息。

MessageProducer只是接口封装,消息处理主要实现依赖于MessageManager这个类。

3)MessageManager

MessageManager为CAT的核心类,但它只是定义了接口,具体实现为DefaultMessageManager。DefaultMessageManager这个类里面主要包含了两个功能类,ContextTransportManager,分别用于保存上下文和消息传输。TransportManager运行期间为单例对象,而Context则包装成ThreadLocal为每个线程保存上下文。

我们通过接口来了解DefaultMessageManager的主要功能:



public void add(Message message);
public void start(Transaction transaction, boolean forked);
public void end(Transaction transaction);

public void flush(MessageTree tree);



 

add()方法用来添加原子性的Message,也就是Metric、Event、HeartBeat、Trace。

start()和end()方法用来开始和结束Transaction这种消息。

flush()方法用来将当前业务线程的所有消息刷新到CAT服务端,当然,是异步的。

4)Context

Context用来保存消息上下文,我们可以通过它的主要接口来了解它功能:



public void add(Message message) {
    if (m_stack.isEmpty()) {
         MessageTree tree = m_tree.copy();

         tree.setMessage(message);
         flush(tree);
    } else {
         Transaction parent = m_stack.peek();

         addTransactionChild(message, parent);
     }
 }



add方法主要添加原子性消息,它先判断该消息是否有上文消息(即判断是否处于一个Transaction中)。如果有则m_stack不为空并且将该消息添加到上文Transaction的子消息队列中;否则直接调用flush来将此原子性消息刷新到服务端。



public void start(Transaction transaction, boolean forked) {
    if (!m_stack.isEmpty()) {
        ...
        Transaction parent = m_stack.peek();
        addTransactionChild(transaction, parent);
    } else {
        m_tree.setMessage(transaction);
    }

    if (!forked) {
        m_stack.push(transaction);
    }
}



start方法用来开始Transaction(Transaction是消息里比较特殊的一种),如果当前消息栈为空则证明该Transaction为第一个Transaction,使用消息树保存该消息,同时将该消息压栈;否则将当前Transaction保存到上文Transaction的子消息队列中,同时将该消息压栈。



public boolean end(DefaultMessageManager manager, Transaction transaction) {
if (!m_stack.isEmpty()) {
        Transaction current = m_stack.pop();
        ...
        if (m_stack.isEmpty()) {
            MessageTree tree = m_tree.copy();

            m_tree.setMessageId(null);
            m_tree.setMessage(null);
            ...
            manager.flush(tree); //刷新消息到CAT服务端
            return true;
        }
    }

    return false;
}



end方法用来结束Transaction,每次调用都会pop消息栈,如果栈为空则调用flush来刷新消息到CAT服务端。

综上,Context的m_stack的结构如下:

开源网络监控系统搭建_开源网络监控系统搭建_11

Transaction之间是有引用的,因此在end方法中只需要将第一个Transaction(封装在MessageTree中)通过MessageManager来flush,在拼接消息时可以根据这个引用关系来找到所有的Transaction :)。

5)TransportManager和TcpSocketSender

这两个类用来发送消息到服务端。MessageManager通过TransportManager获取到MessageSender,调用sender.send()方法来发送消息。 TransportManager和MessageSender关系如下:

开源网络监控系统搭建_开源网络监控系统搭建_12

TCPSocketSender为MessageSender的具体子类,它里面主要的数据成员为:



private MessageCodec m_codec;
private MessageQueue m_queue = new DefaultMessageQueue(SIZE);
private ChannelManager m_manager;



  • MessageCodec:CAT基于TCP传输消息,因此在发送消息时需要对字符消息编码成字节流,这个编码的工作由MessageCodec负责实现。
  • MessageQueue:还记得刚才说业务方在添加消息时,CAT异步发送到服务端吗?在添加消息时,消息会被放置在TCPSocketSender的m_queue中,如果超出queue大小则抛弃消息。
  • ChannelManager:CAT底层使用netty来实现TCP消息传输,ChannelManager负责维护通信Channel。通俗的说,维护连接。

TCPSocketSender主要方法为initialize、send和run,分别介绍如下:



public void initialize() {
    m_manager = new ChannelManager(m_logger, m_serverAddresses, m_queue, m_configManager, m_factory);

    Threads.forGroup("cat").start(this);
    Threads.forGroup("cat").start(m_manager);
    ...
}



initialize方法为初始化方法,在执行时主要创建两个线程,一个用来运行自身run方法(TCPSocketSender实现了Runnable接口)监听消息队列;另一个则用来执行ChannelManager维护通信Channel。



public void send(MessageTree tree) {
    if (isAtomicMessage(tree)) {
        boolean result = m_atomicTrees.offer(tree, m_manager.getSample());

        if (!result) {
            logQueueFullInfo(tree);
        }
    } else {
        boolean result = m_queue.offer(tree, m_manager.getSample());

        if (!result) {
            logQueueFullInfo(tree);
        }
    }
}



send方法被MessageManager调用,把消息放置在消息队列中。



public void run() {
    m_active = true;

    while (m_active) {
        ChannelFuture channel = m_manager.channel();

        if (channel != null && checkWritable(channel)) {
            try {
                MessageTree tree = m_queue.poll();

                if (tree != null) {
                    sendInternal(tree);
                    tree.setMessage(null);
                }

            } catch (Throwable t) {
                m_logger.error("Error when sending message over TCP socket!", t);
            }
        } else {
            try {
                Thread.sleep(5);
            } catch (Exception e) {
                // ignore it
                m_active = false;
            }
        }
    }
}

private void sendInternal(MessageTree tree) {
    ChannelFuture future = m_manager.channel();
    ByteBuf buf = PooledByteBufAllocator.DEFAULT.buffer(10 * 1024); // 10K

    m_codec.encode(tree, buf);

    int size = buf.readableBytes();
    Channel channel = future.channel();

    channel.writeAndFlush(buf);
    if (m_statistics != null) {
        m_statistics.onBytes(size);
    }
}



run方法会一直执行直到进程退出,在循环里先获取通信Channel,然后发送消息。值得注意的是,sendInternal方法在执行时调用m_codec.encode(tree, buf),参数为消息树缓冲区。消息树里面其实只保存了一个消息,还记得刚才说的Transaction上下文引用吗?m_codec在encode的时候会判断消息类型是否为Transaction,如果为Transaction则会递归获取子Transaction,否则直接将该消息编码。具体实现可以参考源代码的PlainTextMessageCodec类的encode方法,此处不再赘述。

4.1.3 cat-client 主要类介绍

cat-client的主要入口是cat-client包中的Cat类

Cat类以及Cat的依赖类层级结构如下:

开源网络监控系统搭建_服务端_13

接口层

Cat类以及MessageProducer类。主要功能是为外部提供api,Cat主要作用是与plexus框架做集成,MessageProducer是处理api的主要类

PS:额外说一下,Cat这个项目很【有特色】地用了plexus作为管理容器,初次接触的时候真是让人头大,plexus的基本功能和spring可以说别无二致,但是很多地方的注入竟然都需要手动处理,真是让人尴尬,虽然作者说spring太重了,plexus的作用已经足够

消息处理层

MessageManager以及其内部类Context。主要功能是管理消息的发送,Transaction类消息的归集,等消息的管理工作。在MessageManager中,使用了ThreadLocal类型作为当前线程消息管理的上下文,通过这个对象线程安全地实现消息的添加,合并,发送等等。

PS:MessageManager管理的消息Message是基于Cat的监控模型创建的,其中最主要的区别是Transaction类和其他消息不太一样,Transaction消息是一个链表的模型,每一个消息后面都链接着下一个消息,所以MessageManager对Transaction的处理也不同,别的消息都是放到Context中直接从消息处理层flush到下一层,Transaction是放到Context的栈中,直到过了预定时间,或者消息达到规定的最大长度才flush到下一层。

消息传输层

TransportManager以及TcpSocketSender以及ChannelManager。主要功能是把消息管理层发下来的消息进行发送,对于与多个发送的目的服务器进行Channel管理,保证有可用服务器能接受消息。TransportManager主要功能是根据配置文件初始化TcpSocketSender,TcpSocketSender主要实现把Message进行编码(如果是Transaction还会进行合并)并放置到待发送队列中,再同时由ChannelManager消费队列中的消息,将消息发送给状态为active的server端

PS:暂存消息的队列用的是LinkedBlockingQueue,实际上LinkedBlockingQueue属于生产消费者队列的标配了,因为这个类对于添加和移除的消耗小,线程安全,而且达到队列容量时会成为blocking状态,所以基本上都会用这个类,或者基于这个类进行扩展来实现相关需求。相对来说还有ConcurrentLinkedQueue可以用,和blockingqueue的主要区别是,Concurrent超过主要容量会直接返回false,不会block,所以如果想马上就返回的可以用Concurrent队列。

4.1.4 Cat入口类

1)测试用例



//静态方法获取Transaction对象
        Transaction t=Cat.newTransaction("logTransaction", "logTransaction");

        TimeUnit.SECONDS.sleep(30);
        t.setStatus("0");
        t.complete();



2)Cat源码



private static Cat s_instance = new Cat();
    private static volatile boolean s_init = false;

    private static void checkAndInitialize() {
        if (!s_init) {
            synchronized (s_instance) {
                if (!s_init) {
                    initialize(new File(getCatHome(), "client.xml"));
                    log("WARN", "Cat is lazy initialized!");
                    s_init = true;
                }
            }
        }
    }
    private Cat() {
    }

    public static MessageProducer getProducer() {
        checkAndInitialize();

        return s_instance.m_producer;
    }



Cat lazy Init

可以看到类加载时已经完成了Cat对象的初始化,内存中有且仅有一个Cat Object(static Cat s_instance = new Cat();),但是包含配置信息的完整的Cat对象并没有完全初始化完成。调用Cat时会先尝试获取producer对象,并在获取之前检查客户端配置是否加载完毕(checkAndInitialize)。

checkAndInitialize()通过使用doublecheck来对Cat相关配置填充的单次初始化加载。

cat-client首先会使用plexus(一个比较老的IOC容器)加载配置文件/META-INF/plexus/plexus.xml,完成IOC容器的初始化。

接着使用../../client.xml文件完成cat对象的配置信息填充初始化。并且启动这四个daemon线程,后文详细说明:

  • cat-StatusUpdateTask 用来每秒钟上报客户端基本信息(JVM等信息)
  • cat-merge-atomic-task(消息合并检查)
  • cat-TcpSocketSender-ChannelManager(NIO 连接服务端检查)
  • cat-TcpSocketSender(消息发送服务端)

4.1.5 CatClientModule

由于Cat用了十分low的plexus作为容器,所以在加载Cat类的时候会从静态方法中加载各个Module,CatClientModule就是Cat client工程中首要Module



public class CatClientModule extends AbstractModule {
    public static final String ID = "cat-client";

    @Override
    protected void execute(final ModuleContext ctx) throws Exception {
        ctx.info("Current working directory is " + System.getProperty("user.dir"));

        // initialize milli-second resolution level timer
        MilliSecondTimer.initialize();

        // tracking thread start/stop,此处增加经典的hook,用于线程池关闭的清理工作。
        Threads.addListener(new CatThreadListener(ctx));

        // warm up Cat
        Cat.getInstance().setContainer(((DefaultModuleContext) ctx).getContainer());

        // bring up TransportManager
        ctx.lookup(TransportManager.class);

        ClientConfigManager clientConfigManager = ctx.lookup(ClientConfigManager.class);

        if (clientConfigManager.isCatEnabled()) {
            // start status update task
            StatusUpdateTask statusUpdateTask = ctx.lookup(StatusUpdateTask.class);

            Threads.forGroup("cat").start(statusUpdateTask);
            LockSupport.parkNanos(10 * 1000 * 1000L); // wait 10 ms

            // MmapConsumerTask mmapReaderTask = ctx.lookup(MmapConsumerTask.class);
            // Threads.forGroup("cat").start(mmapReaderTask);
        }
    }



这里plexusIOC的具体的初始化加载逻辑在org\unidal\framework\foundation-service\2.5.0\foundation-service-2.5.0.jar中,有兴趣可以仔细查看。 
当准备工作做完之后,会执行具体的消息构造:

DefaultMessageProducer.newTransaction(String type, String name)



@Override
    public Transaction newTransaction(String type, String name) {
        // this enable CAT client logging cat message without explicit setup
        if (!m_manager.hasContext()) {
            //详细可见下文源码,此处就是用ThreadLocal存储一个Context对象:ctx = new Context(m_domain.getId(), m_hostName, m_domain.getIp());
            m_manager.setup();

        }

        if (m_manager.isMessageEnabled()) {
            DefaultTransaction transaction = new DefaultTransaction(type, name, m_manager);

//向Context中填充构造的消息体:Context.m_tree;Context.m_stack;稍后看看Context这个对象
            m_manager.start(transaction, false);
            return transaction;
        } else {
            return NullMessage.TRANSACTION;
        }
    }



DefaultMessageManager.start(Transaction transaction, boolean forked)



@Override
    public void start(Transaction transaction, boolean forked) {
        Context ctx = getContext();//这里获取上文中说到的ThreadLocal中构造的Context对象

        if (ctx != null) {
            ctx.start(transaction, forked);

            if (transaction instanceof TaggedTransaction) {
                TaggedTransaction tt = (TaggedTransaction) transaction;

                m_taggedTransactions.put(tt.getTag(), tt);
            }
        } else if (m_firstMessage) {
            m_firstMessage = false;
            m_logger.warn("CAT client is not enabled because it's not initialized yet");
        }
    }



DefaultMessageManager.Context.start(Transaction transaction, boolean forked)



public void start(Transaction transaction, boolean forked) {
            if (!m_stack.isEmpty()) {//
                 {
                    Transaction parent = m_stack.peek();
                    addTransactionChild(transaction, parent);
                }
            } else {
                m_tree.setMessage(transaction);//在这里把返回的transaction放在tree上,如果有嵌套结构,后边继续在tree上添枝加叶
            }

            if (!forked) {
                m_stack.push(transaction);
            }
        }



这部分代码可以看出, 

通过ThreadLocal<Context.>,使Context中实际的消息的构造保证了线程安全。

如果当前Context的栈m_stack不为空,那么接着之前的消息后边,将当前消息构造为一个孩子结点。如果当前消息之前没有其他消息,放入m_stack中,并setMessage.也就是当前消息时父节点。

至此,消息体构造完毕。 

这里需要看一下Context类,是DefaultMessageManager包私有的内部类。

Context.java



class Context {
        private MessageTree m_tree;//初始化的时候构建一个MessageTree

        private Stack<Transaction> m_stack;

        private int m_length;

        private boolean m_traceMode;

        private long m_totalDurationInMicros; // for truncate message

        private Set<Throwable> m_knownExceptions;

        public Context(String domain, String hostName, String ipAddress) {
            m_tree = new DefaultMessageTree();
            m_stack = new Stack<Transaction>();

            Thread thread = Thread.currentThread();
            String groupName = thread.getThreadGroup().getName();

            m_tree.setThreadGroupName(groupName);
            m_tree.setThreadId(String.valueOf(thread.getId()));
            m_tree.setThreadName(thread.getName());

            m_tree.setDomain(domain);
            m_tree.setHostName(hostName);
            m_tree.setIpAddress(ipAddress);
            m_length = 1;
            m_knownExceptions = new HashSet<Throwable>();
        }



每个线程通过使用ThreadLocal构造一个Context对象并存储。Context主要包含当前的消息体m_tree,和多个嵌套消息体填充的栈:m_stack :

开源网络监控系统搭建_开源网络监控系统搭建_11

再回到我们原来的UnitTest代码, 



Transaction t=Cat.newTransaction("logTransaction", "logTransaction");



这行代码完成了客户端plexusIOC容器的初始化,cat-client的加载初始化、启动了四个daemon线程,并返回了Transaction对象。



t.setStatus("0");//很简单,就是这是一个属性值
t.complete();



消息完成后,将消息放入一个队列中,从而保证异步上报。

transaction.complete();的具体代码如下:



........
    public void complete() {
        try {
            if (isCompleted()) {
                // complete() was called more than once
                DefaultEvent event = new DefaultEvent("cat", "BadInstrument");

                event.setStatus("TransactionAlreadyCompleted");
                event.complete();
                addChild(event);
            } else {
                m_durationInMicro = (System.nanoTime() - m_durationStart) / 1000L;

                setCompleted(true);

                if (m_manager != null) {
                    m_manager.end(this);
                }
            }
        } catch (Exception e) {
            // ignore
        }
    }
........
    @Override
    public void end(Transaction transaction) {
        Context ctx = getContext();

        if (ctx != null && transaction.isStandalone()) {
            if (ctx.end(this, transaction)) {
                m_context.remove();
            }
        }
    }
........

        public boolean end(DefaultMessageManager manager, Transaction transaction) {
            if (!m_stack.isEmpty()) {
                Transaction current = m_stack.pop();//Context的成员变量m_stack弹出栈顶元素,LIFO当然是最新的current元素。

                if (transaction == current) {
                    m_validator.validate(m_stack.isEmpty() ? null : m_stack.peek(), current);
                } else {
                    while (transaction != current && !m_stack.empty()) {
                        m_validator.validate(m_stack.peek(), current);

                        current = m_stack.pop();
                    }
                }

                if (m_stack.isEmpty()) {//如果当前线程存储的Context中m_stack无元素
                    MessageTree tree = m_tree.copy();

                    m_tree.setMessageId(null);//清理m_tree
                    m_tree.setMessage(null);

                    if (m_totalDurationInMicros > 0) {
                        adjustForTruncatedTransaction((Transaction) tree.getMessage());
                    }

                    manager.flush(tree);//将消息放入消费队列中
                    return true;
                }
            }

            return false;
        }
........
    public void flush(MessageTree tree) {
        if (tree.getMessageId() == null) {
            tree.setMessageId(nextMessageId());//为消息体生产全局唯一ID,详见snowflate算法
        }

        MessageSender sender = m_transportManager.getSender();

        if (sender != null && isMessageEnabled()) {
            sender.send(tree);

            reset();//ThreadLocal中存储的Context清理
        } else {
            m_throttleTimes++;

            if (m_throttleTimes % 10000 == 0 || m_throttleTimes == 1) {
                m_logger.info("Cat Message is throttled! Times:" + m_throttleTimes);
            }
        }
    }
........
    private Context getContext() {
        if (Cat.isInitialized()) {
            Context ctx = m_context.get();//ThreadLocal存储一个Context对象

            if (ctx != null) {
                return ctx;
            } else {
                if (m_domain != null) {
                    ctx = new Context(m_domain.getId(), m_hostName, m_domain.getIp());
                } else {
                    ctx = new Context("Unknown", m_hostName, "");
                }

                m_context.set(ctx);
                return ctx;
            }
        }

        return null;
    }

//TcpSocketSender.send(MessageTree tree)

    private MessageQueue m_queue = new DefaultMessageQueue(SIZE);

    private MessageQueue m_atomicTrees = new DefaultMessageQueue(SIZE);

    @Override
    public void send(MessageTree tree) {
        if (isAtomicMessage(tree)) {
            boolean result = m_atomicTrees.offer(tree, m_manager.getSample());

            if (!result) {
                logQueueFullInfo(tree);
            }
        } else {
            boolean result = m_queue.offer(tree, m_manager.getSample());

            if (!result) {
                logQueueFullInfo(tree);
            }
        }
    }



至此,构造的消息体放入了阻塞队列中等待上传。

总结: 我们可以看到Cat-SDK通过ThreadLocal对消息进行收集, 

收集进来按照时间以及类型构造为Tree结构,在compele()方法中将这个构造的消息放入一个内存队列中,等待TcpSockekSender这个Daemon线程异步上报给服务端。

 4.1.6 cat-TcpSocketSender

消息上传服务端,会有一个线程cat-TcpSocketSender监听消费队列,并消费(上传服务端)。

通信上报服务端使用了Netty-Client,并且自定义了消息协议。



@Override
    public void run() {
        m_active = true;

        while (m_active) {
            ChannelFuture channel = m_manager.channel();

            if (channel != null && checkWritable(channel)) {
                try {
                    MessageTree tree = m_queue.poll();

                    if (tree != null) {
                        sendInternal(tree);//netty NIO编码后TCP发送到服务端。
                        tree.setMessage(null);
                    }

                } catch (Throwable t) {
                    m_logger.error("Error when sending message over TCP socket!", t);
                }
            } else {
                long current = System.currentTimeMillis();
                long oldTimestamp = current - HOUR;

                while (true) {
                    try {
                        MessageTree tree = m_queue.peek();

                        if (tree != null && tree.getMessage().getTimestamp() < oldTimestamp) {
                            MessageTree discradTree = m_queue.poll();

                            if (discradTree != null) {
                                m_statistics.onOverflowed(discradTree);
                            }
                        } else {
                            break;
                        }
                    } catch (Exception e) {
                        m_logger.error(e.getMessage(), e);
                        break;
                    }
                }

                try {
                    Thread.sleep(5);
                } catch (Exception e) {
                    // ignore it
                    m_active = false;
                }
            }
        }
    }

    private void sendInternal(MessageTree tree) {
        ChannelFuture future = m_manager.channel();
        ByteBuf buf = PooledByteBufAllocator.DEFAULT.buffer(10 * 1024); // 10K

        System.out.println(tree);

        m_codec.encode(tree, buf);//编码后发送

        int size = buf.readableBytes();
        Channel channel = future.channel();

        channel.writeAndFlush(buf);
        if (m_statistics != null) {
            m_statistics.onBytes(size);
        }
    }



 

4.1.7 cat-merge-atomic-task

符合如下逻辑判断的atomicMessage会放入m_atomicTrees消息队列,然后由这个后台线程监听并消费。 

具体代码如下:

TcpSocketSender.java



private MessageQueue m_atomicTrees = new DefaultMessageQueue(SIZE);

......

        private boolean isAtomicMessage(MessageTree tree) {
        Message message = tree.getMessage();//从tree上拿去message

        if (message instanceof Transaction) {//如果这个message实现了Transaction接口,也就是Transaction类型的消息
            String type = message.getType();

            if (type.startsWith("Cache.") || "SQL".equals(type)) {//如果以Cache.,SQL开头的则返回True
                return true;
            } else {
                return false;
            }
        } else {
            return true;
        }
        //看到这里,也就是说,"Cache","SQL"开头的Transaction消息,或者非Transaction消息,认为是atomicMessage.
    }

......

public void send(MessageTree tree) {
        if (isAtomicMessage(tree)) {//如果符合atomicMessage
            boolean result = m_atomicTrees.offer(tree, m_manager.getSample());

            if (!result) {
                logQueueFullInfo(tree);//队列溢出处理
            }
        } else {
            boolean result = m_queue.offer(tree, m_manager.getSample());

            if (!result) {
                logQueueFullInfo(tree);
            }
        }
    }
......



 

 



public class DefaultMessageQueue implements MessageQueue {
    private BlockingQueue<MessageTree> m_queue;

    private AtomicInteger m_count = new AtomicInteger();

    public DefaultMessageQueue(int size) {
        m_queue = new LinkedBlockingQueue<MessageTree>(size);
    }

    @Override
    public boolean offer(MessageTree tree) {
        return m_queue.offer(tree);
    }

    @Override
    public boolean offer(MessageTree tree, double sampleRatio) {
        if (tree.isSample() && sampleRatio < 1.0) {//如果这个消息是sample,并且sampleRation大于1
            if (sampleRatio > 0) {//这段逻辑就是按采样率去剔除一些消息,只选取其中一部分进行后续的消费上传。
                int count = m_count.incrementAndGet();

                if (count % (1 / sampleRatio) == 0) {
                    return offer(tree);
                }
            }
            return false;
        } else {//不做采样过滤,放入队列
            return offer(tree);
        }
    }

    @Override
    public MessageTree peek() {
        return m_queue.peek();
    }

    @Override
    public MessageTree poll() {
        try {
            return m_queue.poll(5, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            return null;
        }
    }

    @Override
    public int size() {
        return m_queue.size();
    }
}



 

这个后台进程的消费动作:



......

private boolean shouldMerge(MessageQueue trees) {
        MessageTree tree = trees.peek();//获取对头元素,非移除

        if (tree != null) {
            long firstTime = tree.getMessage().getTimestamp();
            int maxDuration = 1000 * 30;
            //消息在30s内生成,或者队列挤压消息超过200,则需要merge
            if (System.currentTimeMillis() - firstTime > maxDuration || trees.size() >= MAX_CHILD_NUMBER) {
                return true;
            }
        }
        return false;
    }

......

        @Override
        public void run() {
            while (true) {
                if (shouldMerge(m_atomicTrees)) {
                    MessageTree tree = mergeTree(m_atomicTrees);//把m_atomicTrees队列中的消息merge为一条消息树
                    boolean result = m_queue.offer(tree);//放入m_queue队列,等待cat-TcpSocketSender线程正常消费

                    if (!result) {
                        logQueueFullInfo(tree);
                    }
                } else {
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        break;
                    }
                }
            }
        }

.....

private MessageTree mergeTree(MessageQueue trees) {
        int max = MAX_CHILD_NUMBER;
        DefaultTransaction tran = new DefaultTransaction("_CatMergeTree", "_CatMergeTree", null);//增加merge处理埋点
        MessageTree first = trees.poll();//从队列头部移除

        tran.setStatus(Transaction.SUCCESS);
        tran.setCompleted(true);
        tran.addChild(first.getMessage());
        tran.setTimestamp(first.getMessage().getTimestamp());
        long lastTimestamp = 0;
        long lastDuration = 0;

        //这段逻辑就是不停从这个m_atomicTrees队列头部拿去messsage,并使用同一个messageId,把队列中所有的消息合并为一条Transaction消息。
        while (max >= 0) {
            MessageTree tree = trees.poll();//接着 从队列头部移除

            if (tree == null) {
                tran.setDurationInMillis(lastTimestamp - tran.getTimestamp() + lastDuration);
                break;
            }
            lastTimestamp = tree.getMessage().getTimestamp();
            if(tree.getMessage() instanceof DefaultTransaction){
                lastDuration = ((DefaultTransaction) tree.getMessage()).getDurationInMillis();
            } else {
                lastDuration = 0;
            }
            tran.addChild(tree.getMessage());
            m_factory.reuse(tree.getMessageId());
            max--;
        }
        ((DefaultMessageTree) first).setMessage(tran);
        return first;
    }



4.1.8 TcpSocketSender-ChannelManager 后台线程

这个线程是通过服务端配置的路由ip,10s轮询一次,当满足自旋n(n = m_count % 30)次,去检查路由服务端ip是否变动,并保证连接正常。典型的拉取配置信息机制。

1)客户端跟服务端连接建立,分两步:

  • 初始ChannelMananger的时候 ;
  • ChannelManager异步线程,每隔10秒做一次检查。

初始ChannelMananger的时候

实例化ChannelManager的时候,根据配置的第一个server,从远程服务器读取服务器列表,如果能读取到,则顺序建立连接,直到建立成功为止;如果不能读到,则根据本地配置的列表,逐个建立连接,直到成功为止。

ChannelMananger异步线程,每隔10秒做一次检查

  • 检查Server列表是否变更

每间隔10s,检查当前channelFuture是否活跃,活跃,则300s检查一次,不活跃,则执行检查。检查的逻辑是:比较本地server列表跟远程服务提供的列表是否相等,不相等则根据远程服务提供的server列表顺序的重新建立第一个能用的ChannelFuture

  • 查看当前客户端是否有积压,或者ChannelFuture是否被关闭

如果有积压,或者关闭掉了,则关闭当前连接,将activeIndex=-1,表示当前连接不可用。

  • 重连默认Server

从0到activeIndex中找一个能连接的server,中心建立一个连接。如果activeIndex为-1,则从整个的server列表中顺序的找一个可用的连接建立连接。

2)ChannelManager实例化,建立Netty连接逻辑

客户端实例化DefaultTransportManager对象时,会按照如下流程先实例化m_tcpSocketSender,接着实例化ChannelManager。ChannelManager管理对服务端的netty连接。 实例化流程如下:

开源网络监控系统搭建_开源网络监控系统搭建_15

 ChannelManager通过ChannelHolder把netty的ChannnelFuture封装起来。ChannelHolder结构如下:



public static class ChannelHolder {

        /**
         * 当前活跃的channelFuture
         */
        private ChannelFuture m_activeFuture;

        /**
         * 当前server在m_serverAddresses中的第几个
         */
        private int m_activeIndex = -1;

        /**
         * 当前活跃的ChannelFuture对应的配置
         */
        private String m_activeServerConfig;

        /**
         * 从配置文件中读取的服务端列表
         */
        private List<InetSocketAddress> m_serverAddresses;

        /**
         * 当前活跃的ChannelFutre对应的ip
         */
        private String m_ip; /** * 连接从第一次初始化开始,是否发生过变更 */ private boolean m_connectChanged;
//省略其它的代码



}



3)ChannelManager内部异步线程,动态切换Netty连接逻辑。

ChannelManager内部每隔10秒钟,检查netty连接。这部分代码如下:



public void run() { while (m_active) { /** * make save message id index asyc * 本地存储index,和 时间戳,防止重启,导致本地的消息id重了 */ m_idfactory.saveMark(); /** * 检查本地初始化的服务列表跟远程的服务列表是否有差异,如果有差异,则取远程第一个能建立连接的server,建立一个新的连接,关闭旧的连接 */ checkServerChanged(); ChannelFuture activeFuture = m_activeChannelHolder.getActiveFuture(); List<InetSocketAddress> serverAddresses = m_activeChannelHolder.getServerAddresses(); /** * 检查当前channelFuture是否有消息积压(本地队列长度超过4990),或者 channelFuture不是开的 */ doubleCheckActiveServer(activeFuture); /** * 从serverAddresses列表里面,重新顺序选一个,重新连接 */ reconnectDefaultServer(activeFuture, serverAddresses); try { Thread.sleep(10 * 1000L); // check every 10 seconds } catch (InterruptedException e) { // ignore } } }



 总结:服务端没有做到负载均衡,连接会慢慢连接到server列表里面第一个可用的server上。

4.1.9 StatusUpdateTask

CatClientModule在加载过程中会从StatusUpdateTask中启动一个线程来每隔一段时间发送一个HeartBeatMessage,其中包括了客户端能拿到的各种信息,包括CPU,Memory,Disk等等,开发者也可以通过实现StatusExtension接口的方式来实现对于HeartBeatMessage发送内容的扩展。

这个线程很简单,类似传统的agent,每分钟上报关于应用的各种信息(OS、MXBean信息等等)。而且,在每次线程启动时上报一个Reboot消息表示重启动。

其中比较重要的实现信息收集的是这行代码



StatusInfoCollector statusInfoCollector = new StatusInfoCollector(m_statistics, m_jars); status.accept(statusInfoCollector.setDumpLocked(m_manager.isDumpLocked()));



m_statistics包含的是已经发送过信息的容量,m_jars是通过classLoader加载的jar包名称,StatusInfoCollector通过大量访问者模式的代码实现了将各种指标set到status中的功能,之后将status封装到HeartBeatMessage中,按照一般对于message的处理流程,flush到消息传输层中

4.1.10 MessageId的设计

CAT消息的Message-ID格式applicationName-0a010680-375030-2,CAT消息一共分为四段: 

第一段是应用名applicationName。 

第二段是当前这台机器的IP的16进制格式:



if (m_ipAddress == null) {
            String ip = NetworkInterfaceManager.INSTANCE.getLocalHostAddress();
            List<String> items = Splitters.by(".").noEmptyItem().split(ip);
            byte[] bytes = new byte[4];

            for (int i = 0; i < 4; i++) {
                bytes[i] = (byte) Integer.parseInt(items.get(i));
            }

            StringBuilder sb = new StringBuilder(bytes.length / 2);

            for (byte b : bytes) {
                //1.一个byte 8位
                //2.先获取高4位的16进制字符
                //3.在获取低4位的16进制数            
                sb.append(Integer.toHexString((b >> 4) & 0x0F));//通常使用0x0f来与一个整数进行&运算,来获取该整数的最低4个bit位
                sb.append(Integer.toHexString(b & 0x0F));
            }

            m_ipAddress = sb.toString();



第三段的375030,是系统当前时间除以小时得到的整点数。 
第四段的2,是表示当前这个客户端在当前小时的顺序递增号(AtomicInteger自增,每小时结束后重置)。



public String getNextId() {
        String id = m_reusedIds.poll();

        if (id != null) {
            return id;
        } else {
            long timestamp = getTimestamp();

            if (timestamp != m_timestamp) {
                m_index = new AtomicInteger(0);
                m_timestamp = timestamp;
            }

            int index = m_index.getAndIncrement();

            StringBuilder sb = new StringBuilder(m_domain.length() + 32);

            sb.append(m_domain);
            sb.append('-');
            sb.append(m_ipAddress);
            sb.append('-');
            sb.append(timestamp);
            sb.append('-');
            sb.append(index);

            return sb.toString();
        }



 

总之,同一个小时内、同一个domain、同一个ip , messageId的唯一性需要 AtomicInteger保证。

4.2 cat-home设计

4.2.1 服务端初始化

1)Servlet容器加载、启动

CAT目前是使用war包放入Servlet容器(如:tomcat或者jetty,以下假设使用tomcat容器)中的方式部署启动。 

熟悉servlet容器的同学应该知道,容器启动时会读取每个Context(可理解为web工程)中的web.xml然后启动Servlet等其他组件。

在cat-home模块中的web.xml中可以看到,除了容器默认的Servlet之外,tomcat启动时会启动CatServlet、MVC这两个Servlet(因为load-on-startup>0,也就是会调用init方法初始化):



<web-app>

<filter>...</filter>

<servlet>
        <servlet-name>cat-servlet</servlet-name>
        <servlet-class>com.dianping.cat.servlet.CatServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet>
        <servlet-name>mvc-servlet</servlet-name>
        <servlet-class>org.unidal.web.MVC</servlet-class>
        <init-param>
            <param-name>cat-client-xml</param-name>
            <param-value>client.xml</param-value>
        </init-param>
        <init-param>
            <param-name>init-modules</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>2</load-on-startup>
    </servlet>

<filter-mapping>...</filter-mapping>
<servlet-mapping>...</servlet-mapping>
<jsp-config>...</jsp-config>

</web-app>



 

2)com.dianping.cat.servlet.CatServlet

按照web.xml中Servlet的加载顺序CatServlet会优先于MVC完成初始化。 

CatServlet的逻辑基本可以概括为如下两条线:



CatServlet.init——>CatServlet.initComponents——>DefaultModuleInitializer.execute(...) 
            ——>com.dianping.cat.CatHomeModule.setup(ModuleContext ctx)
                ——>TCPSocketReceiver(netty服务器)

CatServlet.init——>CatServlet.initComponents——>DefaultModuleInitializer.execute(...) 
        ——>com.dianping.cat.***Module.execute(ModuleContext ctx)(完成各个模块的初始化)



 

com.dianping.cat.servlet.CatServlet.init(ServletConfig servletConfig)



public void init(ServletConfig config) throws ServletException {
        super.init(config);

        try {//1.plexus IOC容器初始化(根据components.xml的设定完成IOC初始化)
            if (m_container == null) {
                m_container = ContainerLoader.getDefaultContainer();
            }
            //2.用来打印日志的m_logger对象实例化(根据plexus.xml设定完成实例化)
            m_logger = ((DefaultPlexusContainer) m_container).getLoggerManager().getLoggerForComponent(
                  getClass().getName());
            //3.初始化CAT-Server必备的组件模块:cat-home\cat-consumer\cat-core
            initComponents(config);
        } catch (Exception e) {
            if (m_logger != null) {
                m_logger.error("Servlet initializing failed. " + e, e);
            } else {
                System.out.println("Servlet initializing failed. " + e);
                e.printStackTrace(System.out);
            }

            throw new ServletException("Servlet initializing failed. " + e, e);
        }
    }



进入initComponents(config); 我们继续看下为了启动server服务,各个cat-*模块如何初始化。

com.dianping.cat.servlet.CatServlet.initComponents(ServletConfig servletConfig)



@Override
    protected void initComponents(ServletConfig servletConfig) throws ServletException {
        try {
        //ModuleContext ctx这个对象里主要作用:
        //1.持有 plexus IOC 容器的引用;
        //2.持有 logger对象引用,用来打日志。
        //3.持有 需要使用到的配置文件路径。
        //比如:cat-server-config-file=\data\appdatas\cat\server.xml 
        //cat-client-config-file=\data\appdatas\cat\client.xml

            ModuleContext ctx = new DefaultModuleContext(getContainer());
            ModuleInitializer initializer = ctx.lookup(ModuleInitializer.class);
            File clientXmlFile = getConfigFile(servletConfig, "cat-client-xml", "client.xml");
            File serverXmlFile = getConfigFile(servletConfig, "cat-server-xml", "server.xml");

            ctx.setAttribute("cat-client-config-file", clientXmlFile);
            ctx.setAttribute("cat-server-config-file", serverXmlFile);
            //通过查找启动cat-home必要的模块,然后依次初始化各个模块。
            initializer.execute(ctx);
        } catch (Exception e) {
            m_exception = e;
            System.err.println(e);
            throw new ServletException(e);
        }
    }



org.unidal.initialization.DefaultModuleInitializer.execute(…). 执行各个模块的初始化



@Override
   public void execute(ModuleContext ctx) {

   //我们的topLevelModule是cat-home模块,通过这个模块去查找需要依赖的其他模块并初始化他们。
      Module[] modules = m_manager.getTopLevelModules();
      execute(ctx, modules);
      }


   @Override
   public void execute(ModuleContext ctx, Module... modules) {
      Set<Module> all = new LinkedHashSet<Module>();

      info(ctx, "Initializing top level modules:");

      for (Module module : modules) {
         info(ctx, "   " + module.getClass().getName());
      }

      try {
      //1.根据顶层Module获取到下层所有依赖到的modules,并分别调用他们的setup方法
         expandAll(ctx, modules, all);
      //2.依次调用module实现类的execute方法
         for (Module module : all) {
            if (!module.isInitialized()) {
               executeModule(ctx, module, m_index++);
            }
         }
      } catch (Exception e) {
         throw new RuntimeException("Error when initializing modules! Exception: " + e, e);
      }
   }

   private void expandAll(ModuleContext ctx, Module[] modules, Set<Module> all) throws Exception {
      if (modules != null) {
         for (Module module : modules) {
            expandAll(ctx, module.getDependencies(ctx), all);

            if (!all.contains(module)) {
               if (module instanceof AbstractModule) {
                  ((AbstractModule) module).setup(ctx);//调用各个module实现类的setup
               }
    //all 最终元素以及顺序:
    //CatClientModule\CatCoreModule\CatConsumerModule\CatHomeModule

               all.add(module);
            }
         }
      }
   }



我们看到cat-home模块是一个顶层模块,接着根据这个模块找到其他依赖模块 (CatClientModule\CatConsumerModule\CatCoreModule),并且依次调用setup方法,解析依次调用模块的execute方法完成初始化。

Modules之间的设计使用了典型的模板模式。 

开源网络监控系统搭建_客户端_16

模块依赖关系: 
null<——CatClientModule<——CatClientModule<——CatCoreModule<——CatConsumerModule<——CatHomeModule

接着着重看一下子类 CatHomeModule的setup的实现。注意除了这个子类,Module的子类steup()方法为空 。

com.dianping.cat.CatHomeModule.setup(ModuleContext ctx)



@Override
    protected void setup(ModuleContext ctx) throws Exception {
        File serverConfigFile = ctx.getAttribute("cat-server-config-file");//获取server.xml文件的路径
        //通过 plexus IOC 初始化一个 ServerConfigManager bean
        ServerConfigManager serverConfigManager = ctx.lookup(ServerConfigManager.class);
        //通过 plexus IOC 初始化一个 TcpSocketReceiver bean
        final TcpSocketReceiver messageReceiver = ctx.lookup(TcpSocketReceiver.class);
        //加载\...\server.xml中的配置
        serverConfigManager.initialize(serverConfigFile);
        //启动TCPSocketReceiver,就是一个典型的 netty 事件驱动服务器,用来接收客户端的TCP长连接请求
        messageReceiver.init();
        //增加一个进程观察者,在这个JVM关闭时回调
        Runtime.getRuntime().addShutdownHook(new Thread() {

            @Override
            public void run() {
                messageReceiver.destory();
            }
        });
    }



 

各个模块的启动,executeModule 

各个模块setup就说到这里,setup完成后,会依次调用module.execute(…)用来完成各个模块的启动。

依次调用: 

CatClientModule\CatCoreModule\CatConsumerModule\CatHomeModule.其中只有CatClientModule、CatHomeModule实现了有效的execute方法。

com.dianping.cat.CatClientModule.execute(ModuleContext ctx) 

注意:这里的客户端是用来监控服务端的,具体client的解析可以参考cat-client设计



@Override
    protected void execute(final ModuleContext ctx) throws Exception {
        ctx.info("Current working directory is " + System.getProperty("user.dir"));

        // initialize milli-second resolution level timer
        MilliSecondTimer.initialize();

        // tracking thread start/stop
        // Threads用来对线程做管理的类。这里默认给每个新建的线程加上监听器或者说是观察者
        Threads.addListener(new CatThreadListener(ctx));

        // warm up Cat: setContainer
        Cat.getInstance().setContainer(((DefaultModuleContext) ctx).getContainer());

        // bring up TransportManager:实例化这个类
        ctx.lookup(TransportManager.class);

        //ClientConfigManager对象是加载了client.xml的客户端配置管理对象。
        //客户端的解析不进行展开,请看之前写的《分布式监控CAT源码解析——cat-client》
        ClientConfigManager clientConfigManager = ctx.lookup(ClientConfigManager.class);
        if (clientConfigManager.isCatEnabled()) {
            StatusUpdateTask statusUpdateTask = ctx.lookup(StatusUpdateTask.class);
            Threads.forGroup("cat").start(statusUpdateTask);
            LockSupport.parkNanos(10 * 1000 * 1000L); // wait 10 ms

        }
    }



com.dianping.cat.CatHomeModule.execute(ModuleContext ctx) 
CatHomeModule涉及很多可说的,此处暂时不做展开,继续按照Servlet启动的流程讲解。



@Override
    protected void execute(ModuleContext ctx) throws Exception {
        ServerConfigManager serverConfigManager = ctx.lookup(ServerConfigManager.class);
        //初始化MessageConsumer子类RealtimeConsumer,不仅实例化这个类MessageConsumer对象,还会把这个类中的成员全部实例化
        //      <plexus>
        //      <components>
        //          <component>
        //              <role>com.dianping.cat.analysis.MessageConsumer</role>
        //              <implementation>com.dianping.cat.analysis.RealtimeConsumer</implementation>
        //              <requirements>
        //                  <requirement>
        //                      <role>com.dianping.cat.analysis.MessageAnalyzerManager</role>
        //                  </requirement>
        //                  <requirement>
        //                      <role>com.dianping.cat.statistic.ServerStatisticManager</role>
        //                  </requirement>
        //                  <requirement>
        //                      <role>com.dianping.cat.config.server.BlackListManager</role>
        //                  </requirement>
        //              </requirements>
        //          </component>

        ctx.lookup(MessageConsumer.class);

        ConfigReloadTask configReloadTask = ctx.lookup(ConfigReloadTask.class);
        Threads.forGroup("cat").start(configReloadTask);

        if (serverConfigManager.isJobMachine()) {
            DefaultTaskConsumer taskConsumer = ctx.lookup(DefaultTaskConsumer.class);

            Threads.forGroup("cat").start(taskConsumer);
        }

        if (serverConfigManager.isAlertMachine()) {//如果当前结点开启了告警功能,则对每种报表启动一个daemon线程。1分钟检查一次
            BusinessAlert metricAlert = ctx.lookup(BusinessAlert.class);
            NetworkAlert networkAlert = ctx.lookup(NetworkAlert.class);
            DatabaseAlert databaseAlert = ctx.lookup(DatabaseAlert.class);
            SystemAlert systemAlert = ctx.lookup(SystemAlert.class);
            ExceptionAlert exceptionAlert = ctx.lookup(ExceptionAlert.class);
            FrontEndExceptionAlert frontEndExceptionAlert = ctx.lookup(FrontEndExceptionAlert.class);
            HeartbeatAlert heartbeatAlert = ctx.lookup(HeartbeatAlert.class);
            ThirdPartyAlert thirdPartyAlert = ctx.lookup(ThirdPartyAlert.class);
            ThirdPartyAlertBuilder alertBuildingTask = ctx.lookup(ThirdPartyAlertBuilder.class);
            AppAlert appAlert = ctx.lookup(AppAlert.class);
            WebAlert webAlert = ctx.lookup(WebAlert.class);
            TransactionAlert transactionAlert = ctx.lookup(TransactionAlert.class);
            EventAlert eventAlert = ctx.lookup(EventAlert.class);
            StorageSQLAlert storageDatabaseAlert = ctx.lookup(StorageSQLAlert.class);
            StorageCacheAlert storageCacheAlert = ctx.lookup(StorageCacheAlert.class);

            Threads.forGroup("cat").start(networkAlert);
            Threads.forGroup("cat").start(databaseAlert);
            Threads.forGroup("cat").start(systemAlert);
            Threads.forGroup("cat").start(metricAlert);
            Threads.forGroup("cat").start(exceptionAlert);
            Threads.forGroup("cat").start(frontEndExceptionAlert);
            Threads.forGroup("cat").start(heartbeatAlert);
            Threads.forGroup("cat").start(thirdPartyAlert);
            Threads.forGroup("cat").start(alertBuildingTask);
            Threads.forGroup("cat").start(appAlert);
            Threads.forGroup("cat").start(webAlert);
            Threads.forGroup("cat").start(transactionAlert);
            Threads.forGroup("cat").start(eventAlert);
            Threads.forGroup("cat").start(storageDatabaseAlert);
            Threads.forGroup("cat").start(storageCacheAlert);
        }

        final MessageConsumer consumer = ctx.lookup(MessageConsumer.class);
        Runtime.getRuntime().addShutdownHook(new Thread() {

            @Override
            public void run() {
                consumer.doCheckpoint();
            }
        });
    }



至此,CatServlet初始化完成了,接下来会初始化org.unidal.web.MVC这个Servlet。 
我们接着看一下另外一个Servlet:mvc-servlet

3)org.unidal.web.MVC

MVC这个Servlet继承了AbstractContainerServlet,与CatServlet非常类似,均是AbstractContainerServlet 的实现类。这个Servlet顾名思义就是用来处理请求的,类似Spring中的DispatcherServlet,集中分配进入的请求到对应的Controller。

public void init(ServletConfig config) throws ServletException {…} 

与CatServelet一样,均继承自父类:



public void init(ServletConfig config) throws ServletException {
        super.init(config);

        try {
            if (m_container == null) {
            //DefaultPlexusContainer m_container 是单例对象,在CATServlet中已经完成初始化了
                m_container = ContainerLoader.getDefaultContainer();
            }

            m_logger = ((DefaultPlexusContainer) m_container).getLoggerManager().getLoggerForComponent(
                  getClass().getName());

            initComponents(config);
        } ......



 

org.unidal.web.MVC.initComponents(ServletConfig config) throws Exception



@Override
   protected void initComponents(ServletConfig config) throws Exception {
      // /cat
      String contextPath = config.getServletContext().getContextPath();
      // /cat
      String path = contextPath == null || contextPath.length() == 0 ? "/" : contextPath;

      getLogger().info("MVC is starting at " + path);
//使用client.xml初始化代表CATClient的com.dianping.cat.Cat对象(如果CAT未被初始化)。
      initializeCat(config);
      initializeModules(config);

      m_handler = lookup(RequestLifecycle.class, "mvc");
      m_handler.setServletContext(config.getServletContext());

      config.getServletContext().setAttribute(ID, this);
      getLogger().info("MVC started at " + path);
   }



至此,容器启动成功,http://localhost:2281/cat/r 进入页面。

4.2.2 报表展示

对于实时报表,直接通过HTTP请求分发到相应消费机,待结果返回后聚合展示(对分区数据做聚合);历史报表则直接取数据库并展示。

 

4.3 cat-core设计

Server的主要入口是cat-core包中的RealTimeConsumer类

RealTimeConsumer类以及RealTimeConsumer的依赖类层级结构如下:

开源网络监控系统搭建_服务端_17

Cat Server功能为解码消息,解码后按照固定时间间隔分片,将消息分发到各个Analyzer的消费队列中,然后由各自的Analyzer进行消费。

TCPSocketReceiver,DefaultMessageHandler

TCPSocketReceiver主要负责使用netty建立server端,接受到tcp请求后将其解码,通过DefaultMessageHandler将Message交由RealTimeConsumer消费

RealTimeConsumer

在内部初始化PeriodManager,并启动periodManager的线程,该线程会不断根据时间间隔生成新的Period对象,并启动Period对象内的多个PeriodTask线程,PeriodTask线程会根据持有的Anaylyzer和MessageQueue进行消费

当RealTimeConsumer终止时会调用doCheckPoint方法

PeriodManager,PeriodStrategy

PeriodManager主要是以时间切片作为策略来拆分整体数据的,所以PeriodManager中包含的List类型是根据PeriodStrategy中的时间策略获得的。PeriodManager实现Task接口,他的主要任务是在规定的存活期内,每隔一段固定的时间都会创建新的Period对象,并启动Period对象内的多个消费线程

Period

Period中主要包含了一个类型为Map < String, List < PeriodTask > >的属性,该属性根据MessageAnalyzerManager构建。Map < String, List < PeriodTask > >属性是一个在该Period时间片内,不同类型的Analyzer与各个PeriodTask之间的对应关系,因为偶尔有同一个Analyzer会有多个PeriodTask一同消费,根据Hash进行分配的情况,所以value的类型为List。

PeriodTask是消费Message的消费单元,每个PeriodTask中包含了一个queue,一个analyzer,PeriodTask会一直从queue中取出Message让analyzer进行消费

distribute方法实现了将Message分发到该Period中所有PeriodTask中的功能

start方法启动各个PeriodTask线程,对各个PeriodTask的queue中的Message开始消费

finish方法调用各个PeriodTask的finish方法

MessageAnalyzerManager

持有Map < Long, Map < String, List < MessageAnalyzer > > >,该属性包含了各个Analyzer的实例,每个实例可以通过——时间片——analyzer类型/名字来获得,analyzer的数量由各个MessageAnalyzer中getAnalyzerCount获得

PeriodTask

PeriodTask实现Task接口,每个PeriodTask会持有自己专属的analyzer和queue,在线程启动后会调用analyzer的consume方法来消费queue。在调用finish时会调用checkPoint方法,执行analyzer实现的检查点方法

4.3.1 CAT服务端接收MessageTree

消息通过netty发送到服务端,经过MessageDecoder将字节流转换成文本,PlainTextMessageCode将文本消息转换成一棵消息树,DefaultMessageHandler调用RealtimeConsumer实时消费消息树,RealtimeConsumer调用Period(没有就生成),将消息树分发对应的PeriodTask的队列里面,供对应的Analyzer处理。

开源网络监控系统搭建_服务端_18

4.3.2 EventAnalyzer介绍

Cat server中,以PeriodTask为消费单元,使用MessageAnalyzer进行消息消费,本篇介绍一下EventAnalyzer的功能,并捎带介绍一下MessageAnalyzer的实现

MessageAnalyzer接口实现结构如下

开源网络监控系统搭建_消息队列_19

4.3.3 DumpAnalyzer介绍

 CatServer中,可以定时把消息存储到hdfs中,dumpAnalyzer就是用来支持这种功能的

开源网络监控系统搭建_消息队列_20

LocalMessageBucketManager

ConcurrentHashMap <String, LocalMessageBucket > m_buckets主要根据持久化的日志路径保存LocalMessageBucket对象

BlockingQueue < MessageBlock > m_messageBlocks 保存MessageItem经过gzip压缩的block

ConcurrentHashMap < Integer, LinkedBlockingQueue < MessageItem > > m_messageQueues 在内存中持有各个gzip执行线程压缩队列对象,根据线程的index作为索引

BlockDumper负责将gzip压缩过的block持久化到本地文件

MessageGzip负责定时压缩MessageItem

LogviewUploader负责上传logview到hdfs

  • archive 把传入时间范围内的,将bucket已经压缩到block,但是没有flush的MessageBlock放入m_messageBlocks消费队列中,供BlockDumper,LogviewUploader消费
  • initialize 启动blockDumper线程,启动LogviewUploader线程,启动若干个MessageGzip线程,各自干活
  • loadMessage 从文件中将消息加载出来
  • storeMessage 根据domain,ip等的hash将MessageItem放入MessageGzip线程的消费队列,供其压缩生成MessageBlock

LocalMessageBucket

对Message进行读写,压缩,解压缩的处理单元,LocalMessageBucket使用basePath/{date,yyyyMMdd}/{date,HH}/{name}的路径生成压缩文件,对消息的各种读写压缩操作大多都与该文件有关。

  • storeMessage 通过传入的MessageItem的ByteBuf生成压缩过的MessageBlock对象
  • findById 根据MessageId加载该Bucket对应文件中的MessageTree对象

MessageBlock

压缩后的消息信息的持有者,index是Message的id,size是对应message的size

MessageGzip

MessageItem的消费者,消费存储在ConcurrentHashMap <Integer, LinkedBlockingQueue > m_messageQueues中的MessageItem,将MessageItem压缩成MessageBlock,每个MessageGzip线程都有一个自己的Queue,Integer是每个线程对应queue的索引

BlockDumper

MessageBlock的消费者,消费存储在BlockingQueue m_messageBlocks中的MessageBlock,将其通过LocalMessageBucket持久化到本地

##LogViewUploader
上传logView到hdfs

4.3.4 TaskConsumer介绍

后台的Analyzer在归档时会生成Task的记录到数据库中,server在CatConsumerModule中初始化过程中启动了TaskConsumer线程来处理数据库中的记录

开源网络监控系统搭建_消息队列_21

ReportFacade

此类会在初始化时,将注册在plexus的所有builder加载到m_reportBuilders中,在执行builderReport时,根据传入的task的reportName查找对应的TaskBuilder,根据时间片,domain以及reportName查询已经入库的report基本记录,再将report的基本记录合并,并将合并后的report,生成的graph入库。

在生成聚合report的过程中,会根据层级树自上至下归并递归生成,比如月的根据周,周根据日,日根据小时生成。

EventReportBuilder

基于时间片,调用EventService实现入库聚合报表,入库聚合graph的功能

EventService

基于domain实现报表插入,报表查询等功能,是业务执行的基本单元

4.3.5 TcpSocketReceiver

在CAT-Server启动时会启动Netty的Nio 多线程Reactor模块来接收客户端的请求:

  • 一个Accept线程池(Main Reactor Thread Pool )用来处理连接操作(通常还可以在这各Accept中加入权限验证、名单过滤逻辑);
  • 接着Accept连接成功的socket请求被转发到 专门处理IO操作的线程池(Sub Reactor Thread Pool ,实现异步);在这里做了消息的解码处理;
  • 再接着,解码处理后,将消息发送到每个报表解析器内置的内存队列中。消息将被异步分发给各个解析器单独处理(不存在数据竞争)。

消息的接受是在这个类TcpSocketReceiver.java完成的:



// 在CatHomeModule启动时被调用
    public void init() {
        try {
            startServer(m_port);
        } catch (Throwable e) {
            m_logger.error(e.getMessage(), e);
        }
    }
    /**
     * 启动一个netty服务端
     * @param port
     * @throws InterruptedException
     */
    public synchronized void startServer(int port) throws InterruptedException {
        boolean linux = getOSMatches("Linux") || getOSMatches("LINUX");
        int threads = 24;
        ServerBootstrap bootstrap = new ServerBootstrap();
        //linux走epoll的事件驱动模型
        m_bossGroup = linux ? new EpollEventLoopGroup(threads) : new NioEventLoopGroup(threads);//用来做为接受请求的线程池 master线程
        m_workerGroup = linux ? new EpollEventLoopGroup(threads) : new NioEventLoopGroup(threads);//用来做为处理请求的线程池 slave线程
        bootstrap.group(m_bossGroup, m_workerGroup);
        bootstrap.channel(linux ? EpollServerSocketChannel.class : NioServerSocketChannel.class);

        bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {//channel初始化设置
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ChannelPipeline pipeline = ch.pipeline();

                pipeline.addLast("decode", new MessageDecoder());//增加消息解码器
            }
        });
        // 设置channel的参数
        bootstrap.childOption(ChannelOption.SO_REUSEADDR, true);
        bootstrap.childOption(ChannelOption.TCP_NODELAY, true);
        bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
        bootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

        try {
            m_future = bootstrap.bind(port).sync();//绑定监听端口,并同步等待启动完成
            m_logger.info("start netty server!");
        } catch (Exception e) {
            m_logger.error("Started Netty Server Failed:" + port, e);
        }
    }



启动netty,对每个客户端上报的消息都会做解码处理,从字节流转换为消息树MessageTree tree,接着交给DefaultMessageHandler处理。



public class DefaultMessageHandler extends ContainerHolder implements MessageHandler, LogEnabled {

    /*
     * MessageConsumer按每个period(整小时一个period)组合了多个解析器,用来解析生产多个报表(如:Transaction、
     * Event、Problem等等)。一个解析器对象-一个有界队列-一个整小时时间组合了一个PeriodTask,轮询的处理这个有界队列中的消息
     */
    @Inject
    private MessageConsumer m_consumer;

    private Logger m_logger;

    @Override
    public void enableLogging(Logger logger) {
        m_logger = logger;
    }

    @Override
    public void handle(MessageTree tree) {
        if (m_consumer == null) {
            m_consumer = lookup(MessageConsumer.class);//从容器中加载MessageConsumer实例
        }

        try {
            m_consumer.consume(tree);//消息消费
        } catch (Throwable e) {
            m_logger.error("Error when consuming message in " + m_consumer + "! tree: " + tree, e);
        }
    }
}



 

OMS设计是按照每小时去汇总数据,为什么要使用一个小时的粒度呢? 

这个是一个trade-off,实时内存数据处理的复杂度与内存的开销方面的折中方案。 

在这个小时结束后将生成的Transaction\Event\Problean报表存入Mysql、File(机器根目录侠)。然而为了实时性,当前小时的报表是保存在内存中的。

PeriodManager 用来管理 OMS单位小时内的各种类型的解析器,包括将上报的客户端数据派发给不同的解析器(这种派发可以理解为订阅\发布)。每个解析器,将收到的消息存入内置队列,并且用单独的线程去获取消息并处理。

com.dianping.cat.analysis.PeriodManager



public class PeriodManager implements Task {
     public void init() {
        long startTime = m_strategy.next(System.currentTimeMillis());//当前小时的起始时间

        startPeriod(startTime);
    }

    @Override
    public void run() {
   // 1s检查一下当前小时的Period对象是否需要创建(一般都是新的小时需要创建一个Period代表当前小时)
        while (m_active) {
            try {
                long now = System.currentTimeMillis();
                //value>0表示当前小时的Period不存在,需要创建一个
                //如果当前线小时的Period存在,那么Value==0
                long value = m_strategy.next(now);
                if (value > 0) {
                    startPeriod(value);
                } else if (value < 0) {
                    //  //当这个小时结束后,会异步的调用endPeriod(..),将过期的Period对象移除,help GC
                    Threads.forGroup("cat").start(new EndTaskThread(-value));
                }
            } catch (Throwable e) {
                Cat.logError(e);
            }

            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
    //当这个小时结束后,会异步的调用这个方法,将过期的Period对象移除,help GC
    private void endPeriod(long startTime) {
        int len = m_periods.size();

        for (int i = 0; i < len; i++) {
            Period period = m_periods.get(i);

            if (period.isIn(startTime)) {
                period.finish();
                m_periods.remove(i);
                break;
            }
        }
    }

......
}



 

消息消费是由MessageConsumer的实现类RealtimeConsumer处理

com.dianping.cat.analysis.RealtimeConsumer.consume(MessageTree tree)



@Override
    public void consume(MessageTree tree) {
        String domain = tree.getDomain();
        String ip = tree.getIpAddress();

        if (!m_blackListManager.isBlack(domain, ip)) {// 全局黑名单 按domain-ip
            long timestamp = tree.getMessage().getTimestamp();
            //PeriodManager用来管理、启动periodTask,可以理解为每小时的解析器。
            Period period = m_periodManager.findPeriod(timestamp);//根据消息产生的时间,查找这个小时所属的对应Period

            if (period != null) {
                period.distribute(tree);//将解码后的tree消息依次分发给所有类型解析器
            } else {
                m_serverStateManager.addNetworkTimeError(1);
            }
        } else {
            m_black++;

            if (m_black % CatConstants.SUCCESS_COUNT == 0) {
                Cat.logEvent("Discard", domain);
            }
        }
    }



 

分发消息给各个解析器(类似向订阅者发布消息) 
void com.dianping.cat.analysis.Period.distribute(MessageTree tree)



/**
     * 将解码后的tree消息依次分发给所有类型解析器
     * @param tree
     */
    public void distribute(MessageTree tree) {
        m_serverStateManager.addMessageTotal(tree.getDomain(), 1);// 根据domain,统计消息量
        boolean success = true;
        String domain = tree.getDomain();

        for (Entry<String, List<PeriodTask>> entry : m_tasks.entrySet()) {
            List<PeriodTask> tasks = entry.getValue();//某种类型报表的解析器
            int length = tasks.size();
            int index = 0;
            boolean manyTasks = length > 1;

            if (manyTasks) {
                index = Math.abs(domain.hashCode()) % length;//hashCode的绝对值 % 长度 =0~length-1之间的任一个数
            }
            PeriodTask task = tasks.get(index);
            boolean enqueue = task.enqueue(tree);//注意:这里会把同一个消息依依放入各个报表解析中的队列中

            if (enqueue == false) {
                if (manyTasks) {
                    task = tasks.get((index + 1) % length);
                    enqueue = task.enqueue(tree);//放入队列,异步消费

                    if (enqueue == false) {
                        success = false;
                    }
                } else {
                    success = false;
                }
            }
        }

        if (!success) {
            m_serverStateManager.addMessageTotalLoss(tree.getDomain(), 1);
        }
    }



 

PeriodTask 
每个periodTask对应一个线程,m_analyzer对应解析器处理m_queue中的消息



public class PeriodTask implements Task, LogEnabled {
    @Override
    public void run() {//每个periodTask对应一个线程,m_analyzer对应解析器处理m_queue中的消息
        try {
            m_analyzer.analyze(m_queue);
        } catch (Exception e) {
            Cat.logError(e);
        }
    }



 

4.3.6 AbstractMessageAnalyzer

开源网络监控系统搭建_服务端_22



@Override
    public void analyze(MessageQueue queue) {// 解析器在当前小时内自旋,不停从队列中拿取消息,然后处理
        while (!isTimeout() && isActive()) {// timeOut:当前时间>小时的开始时间+一小时+三分钟;
                                            // isActive默认为true,调用shutdown后为false
            MessageTree tree = queue.poll();// 非阻塞式获取消息

            if (tree != null) {
                try {
                    process(tree);// 解析器实现类 override
                } catch (Throwable e) {
                    m_errors++;

                    if (m_errors == 1 || m_errors % 10000 == 0) {
                        Cat.logError(e);
                    }
                }
            }
        }
        // 如果当前解析器以及超时,那么处理完对应队列内的消息后返回。
        while (true) {
            MessageTree tree = queue.poll();

            if (tree != null) {
                try {
                    process(tree);
                } catch (Throwable e) {
                    m_errors++;

                    if (m_errors == 1 || m_errors % 10000 == 0) {
                        Cat.logError(e);
                    }
                }
            } else {
                break;
            }
        }
    }



 消费流程图:

 

开源网络监控系统搭建_消息队列_23

总结:

消息发送到服务端,服务端解码为 MessageTree准备消费。期间存在一个demon线程,1s检查一下当前小时的Period对象是否需要创建(一般都是新的小时需要创建一个Period代表当前小时)。

如果当前小时的Period存在,那么我们的MessageTree会被分发给各个PeriodTask,这里其实就是把消息发送到每个PeriodTask中的内存队列里,然后每个Task异步去消费。就是通过使用Queue实现了解耦与延迟异步消费。

每个PeriodTask持有MessageAnalyzer analyzer(Transaction\Event\Problean…每种报表都对应一个解析器的实现类)、MessageQueue queue对象,PeriodTask会不停地解析被分发进来的MessageTree,形成这个解析器所代表的报表。

当前时间进入下个小时,会创建一个新的当前小时的Period,并且异步的remove之前的Period。

注意,这里有个比较坑的地方是,作者没有使用线程池,每小时各个解析器的线程并没有池化,而是直接销毁后再次创建!

 

4.4 cat-consumer设计

4.4.1 报表部分数值计算公式

1)数据结构

TRANSACTIONREPORT报表的数据结构如下:

开源网络监控系统搭建_客户端_24

 

CROSSREPORT内存报表数据结构:

开源网络监控系统搭建_客户端_25

 

数据示例:



<?xml version="1.0" encoding="utf-8"?>
<cross-report domain="monitor-cat" startTime="2017-12-28 18:00:00" endTime="2017-12-28 18:59:59">
   <domain>monitor-cat</domain>
   <domain>monitor-dubbo</domain>
   <ip>10.15.83.181</ip>
   <local id="10.15.83.181">


      <remote id="10.15.83.181(monitor-cat):Pigeon.Client" role="Pigeon.Client" app="monitor-cat" ip="10.15.83.181(monitor-cat)">
         <type id="PigeonService" totalCount="19" failCount="0" failPercent="0.00" avg="0.00" sum="28.89" tps="0.00">
            <name id="DubboProviderService.getProviderServiceName" totalCount="19" failCount="0" failPercent="0.00" avg="0.00" sum="28.89" tps="0.00"/>
         </type>
      </remote>


      <remote id="10.15.83.181(monitor-dubbo):Pigeon.Server" role="Pigeon.Server" app="monitor-dubbo" ip="10.15.83.181(monitor-dubbo)">
         <type id="PigeonCall" totalCount="19" failCount="0" failPercent="0.00" avg="0.00" sum="461.26" tps="0.00">
            <name id="DubboProviderService.getProviderServiceName" totalCount="19" failCount="0" failPercent="0.00" avg="0.00" sum="461.26" tps="0.00"/>
         </type>
      </remote>


   </local>
</cross-report>



 

2)计算95线、99线



/**
     * 统计95线,99.9线。
     * 思路:
     *    求请求的总数,假设是100,求线余下的数目,如果是95线,那余下是5,
     *    将之前统计的durations放进treeMap里面,倒序
     *    从前向后遍历,找到第5个元素,拿到对应的值
     *
     *    基本上是按照95线的定义来取数的
     * @param durations
     * @param percent
     * @return
     */
    private double computeLineValue(Map<Integer, AllDuration> durations, double percent) {
        int totalCount = 0;
        Map<Integer, AllDuration> sorted = new TreeMap<Integer, AllDuration>(TransactionComparator.DESC);

        sorted.putAll(durations);

        for (AllDuration duration : durations.values()) {
            totalCount += duration.getCount();
        }

        int remaining = (int) (totalCount * (100 - percent) / 100);

        for (Entry<Integer, AllDuration> entry : sorted.entrySet()) {
            remaining -= entry.getValue().getCount();

            if (remaining <= 0) {
                return entry.getKey();
            }
        }

        return 0.0;
    }



3)求方差



/**
     * 求方差 value = 求和(xi * xi)/count  - 均值*均值
     *      value = 开方(value)
     * @param count
     * @param avg
     * @param sum2
     * @param max
     * @return
     */
    double std(long count, double avg, double sum2, double max) {
        double value = sum2 / count - avg * avg;

        if (value <= 0 || count <= 1) {
            return 0;
        } else if (count == 2) {
            return max - avg;
        } else {
            return Math.sqrt(value);
        }
    }



 4.5 存储设计(cat-hadoop)

存储主要分成两类:一个是 报表(Transaction、Event、Problem….),一个是logview,也是就是原始的MessageTree。

所有原始消息会先存储在本地文件系统,然后上传到HDFS中保存;而对于报表,因其远比原始日志小,则以K/V的方式保存在MySQL中。

报表存储:在每个小时结束后,将内存中的各个XML报表 保存到MySQL、File(/data/appdatas/cat/bucket/report…)中

开源网络监控系统搭建_服务端_26

logView的保存有后台线程(默认20个,Daemon Thread [cat-Message-Gzip-n])轮询处理:会间隔一段时间后从消息队列中拿取MessageTree,并进行编码压缩,保存到\data\appdatas\cat\bucket\dump\年月\日\domain-ip1-ip2-ipn目录下。

com.dianping.cat.consumer.dump.LocalMessageBucketManager.MessageGzip.run()



@Override
        public void run() {
            try {
                while (true) {
                    MessageItem item = m_messageQueue.poll(5, TimeUnit.MILLISECONDS);

                    if (item != null) {
                        m_count++;
                        if (m_count % (10000) == 0) {
                            gzipMessageWithMonitor(item);//数量达到10000的整数倍,通过上报埋点记录监控一下
                        } else {
                            gzipMessage(item);
                        }
                    }
                }
            } catch (InterruptedException e) {
                // ignore it
            }
        }


private void gzipMessage(MessageItem item) {
            try {
                MessageId id = item.getMessageId();
                String name = id.getDomain() + '-' + id.getIpAddress() + '-' + m_localIp;
                String path = m_pathBuilder.getLogviewPath(new Date(id.getTimestamp()), name);
                LocalMessageBucket bucket = m_buckets.get(path);

                if (bucket == null) {
                    synchronized (m_buckets) {
                        bucket = m_buckets.get(path);
                        if (bucket == null) {
                            bucket = (LocalMessageBucket) lookup(MessageBucket.class, LocalMessageBucket.ID);
                            bucket.setBaseDir(m_baseDir);
                            bucket.initialize(path);

                            m_buckets.put(path, bucket);
                        }
                    }
                }

                DefaultMessageTree tree = (DefaultMessageTree) item.getTree();
                ByteBuf buf = tree.getBuffer();
                MessageBlock block = bucket.storeMessage(buf, id);

                if (block != null) {
                    if (!m_messageBlocks.offer(block)) {
                        m_serverStateManager.addBlockLoss(1);
                        Cat.logEvent("DumpError", tree.getDomain());
                    }
                }
            } catch (Throwable e) {
                Cat.logError(e);
            }
        }

    public MessageBlock storeMessage(final ByteBuf buf, final MessageId id) throws IOException {
        synchronized (this) {
            int size = buf.readableBytes();

            m_dirty.set(true);
            m_lastAccessTime = System.currentTimeMillis();
            m_blockSize += size;
            m_block.addIndex(id.getIndex(), size);
            buf.getBytes(0, m_out, size); // write buffer and compress it

            if (m_blockSize >= MAX_BLOCK_SIZE) {
                return flushBlock();
            } else {
                return null;
            }
        }
    }



 

4.5.1 logView的文件存储设计

开源网络监控系统搭建_服务端_27

6. 客户端接入 

6.1 mybatis接入

使用ORM插件-MybatisPlugin

效果:

开源网络监控系统搭建_开源网络监控系统搭建_28

6.2 log4j日志接入

Throwable(及其子类)异常上报,使用log日志框架的appender机制



<root level="INFO">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="CatAppender" />
 </root>



 

效果:

开源网络监控系统搭建_客户端_29

 

6.3 URL请求监控

使用Filter机制实现,并实现URL聚合等其他功能。

开源网络监控系统搭建_开源网络监控系统搭建_30

6.4 代码级别监控

aop 做监控,内部封装,将aop-expression暴露出来给用户配置(填写需要监控的实现类范围)。字节码织入技术。

 

开源网络监控系统搭建_开源网络监控系统搭建_31

6.5 接口抽象\静态绑定

为了向后兼容,轻松替换埋点的实现。比如切换为zipkin或者其他产品的API。从而减少对业务线的影响。 

仿照slf4j的设计,使用了静态绑定。

6.6 分布式调用链

CatCrossHttpClient作为 httpClient的代理类暴露给用户使用。



@Component
public class CatCrossHttpClient
  extends HttpClientProxy
{
  private String serverToCall;

  public void setServerToCall(String serverToCall)
  {
    this.serverToCall = serverToCall;
  }

  public String execute(HttpRequestBase request, int socketTimeout, int connectTimeout)
    throws Exception
  {
    Transaction t = Cat.newTransaction("PigeonCall", request.getURI().getPath());
    createCrossReport(request, this.serverToCall);
    Cat.Context context = new CatContext();
    Cat.logRemoteCallClient(context);
    request.setHeader("_catRootMessageId", context.getProperty("_catRootMessageId"));
    request.setHeader("_catParentMessageId", context.getProperty("_catParentMessageId"));
    request.setHeader("_catChildMessageId", context.getProperty("_catChildMessageId"));
    request.setHeader("ClientApplication.Name", Cat.getManager().getDomain());
    try
    {
      String ret = super.execute(request, socketTimeout, connectTimeout);
      t.setStatus("0");
      return ret;
    }
    catch (Exception e)
    {
      Cat.logEvent("HTTP_REST_CAT_ERROR", request.getURI().toString(), e.getMessage(), " ");
      t.setStatus(e);
      throw e;
    }
    finally
    {
      t.complete();
    }
  }

  private void createCrossReport(HttpRequestBase request, String serverToCall)
    throws Exception
  {
    Cat.logEvent("PigeonCall.app", serverToCall, "0", " ");

    Cat.logEvent("PigeonCall.server", request.getURI().getHost(), "0", " ");

    Cat.logEvent("PigeonCall.port", String.valueOf(request.getURI().getPort()), "0", " ");
    MessageTree tree = Cat.getManager().getThreadLocalMessageTree();
    ((DefaultMessageTree)tree).setDomain(Cat.getManager().getDomain());
    ((DefaultMessageTree)tree).setIpAddress(InetAddress.getLocalHost().getHostAddress());
  }
}



 

当有外部请求进来,通过如下的Filter判断请求中是否带有分布式调用链埋点,如果有就继续构造这个链条。当然分布式调用链得以使用的前提是被监控的应用中有引入这两个功能!



public class HttpCatCrossFilter
  implements Filter
{
  private static final Logger logger = LoggerFactory.getLogger(HttpCatCrossFilter.class);

  public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain)
    throws IOException, ServletException
  {
    HttpServletRequest request = (HttpServletRequest)req;
    if (isCatTracing(request))
    {
      String requestURI = request.getRequestURI();

      Transaction t = Cat.newTransaction("PigeonService", requestURI);
      try
      {
        Cat.Context context = new CatContext();
        context.addProperty("_catRootMessageId", request.getHeader("_catRootMessageId"));
        context.addProperty("_catParentMessageId", request.getHeader("_catParentMessageId"));
        context.addProperty("_catChildMessageId", request.getHeader("_catChildMessageId"));
        Cat.logRemoteCallServer(context);

        createCrossReport(request, t);

        filterChain.doFilter(req, resp);

        t.setStatus("0");
      }
      catch (Exception e)
      {
        logger.error("Get cat msgtree error :" + e);
        Cat.logEvent("HTTP_REST_CAT_ERROR", request.getRequestURL().toString(), e.getMessage(), " ");
        t.setStatus(e);
      }
      finally
      {
        t.complete();
      }
    }
    else
    {
      filterChain.doFilter(req, resp);
    }
  }

  private void createCrossReport(HttpServletRequest request, Transaction t)
    throws Exception
  {
    Cat.logEvent("PigeonService.app", request.getHeader("ClientApplication.Name"), "0", " ");

    Cat.logEvent("PigeonService.client", request.getRemoteAddr(), "0", " ");

    MessageTree tree = Cat.getManager().getThreadLocalMessageTree();

    ((DefaultMessageTree)tree).setDomain(Cat.getManager().getDomain());
    ((DefaultMessageTree)tree).setIpAddress(InetAddress.getLocalHost().getHostName());
  }

  public void init(FilterConfig arg0)
    throws ServletException
  {}

  public void destroy() {}

  private boolean isCatTracing(HttpServletRequest request)
  {
    return (null != request.getHeader("_catRootMessageId")) && (null != request.getHeader("_catParentMessageId")) && (null != request.getHeader("_catChildMessageId"));
  }
}



 

效果:

开源网络监控系统搭建_服务端_32

7. 各开源监控对比

zipkin:

优点:分布式调用链理论的实现系统。最大的特点是分布式调用链。Spring Cloud Sleuth 可以方便的对zipkin元数据进行采集。 

缺点:功能单一,监控维度、监控信息不够丰富。没有告警功能。

pinpoint:

优点:使用字节码织入技术,对用户完全透明,实现自动埋点。可展示代码级别监控。 

缺点:  功能不足够丰富。对于其他非java程序,实现客户端难度大。

Cat:  

优点:功能丰富,多模型报表展示。可展示代码级别监控。以及特殊业务数据监控。支持多语言客户端。多数情况可以替代日志的查看。 

缺点:  手动埋点,需要改造才能减少埋点的侵入性。

 

8. 技术代码赏析

8.1 MilliSecondTimer



/**
 * This timer provides milli-second precise system time.
 */
public class MilliSecondTimer {
    private static long m_baseTime;

    private static long m_startNanoTime;

    private static boolean m_isWindows = false;

    public static long currentTimeMillis() {
        if (m_isWindows) {
            if (m_baseTime == 0) {
                initialize();
            }

            long elipsed = (long) ((System.nanoTime() - m_startNanoTime) / 1e6);

            return m_baseTime + elipsed;
        } else {
            return System.currentTimeMillis();
        }
    }

    public static void initialize() {
        String os = System.getProperty("os.name");

        if (os.startsWith("Windows")) {
            m_isWindows = true;
            m_baseTime = System.currentTimeMillis();

            while (true) {
                LockSupport.parkNanos(100000); // 0.1 ms

                long millis = System.currentTimeMillis();

                if (millis != m_baseTime) {
                    m_baseTime = millis;
                    m_startNanoTime = System.nanoTime();
                    break;
                }
            }
        } else {
            m_baseTime = System.currentTimeMillis();
            m_startNanoTime = System.nanoTime();
        }
    }
}



  System.currentTimeMillis()返回的毫秒,这个毫秒其实就是自1970年1月1日0时起的毫秒数。

  System.nanoTime()返回的是纳秒,nanoTime而返回的可能是任意时间,甚至可能是负数。

  System.currentTimeMillis调用的是native方法,使用的是系统的时间,每个JVM对应的应该是相同的,但因为具体的取值依赖于操作系统的实现,不同JVM间可能会有略微的差异。

  System.nanoTime每个JVM维护一份,和系统时间无关,可用于计算时间间隔,比System.currentTimeMillis的精度要高。

  修改了系统时间会对System.currentTimeMillis造成影响,而对System.nanoTime没有影响。修改系统时间后会有如下效果:Timmer有影响,Thread.sleep有影响,ScheduledThreadPoolExecutor无影响,可以查看方法的实现调用的是System.currentTimeMillis还是System.nanoTime。

java修改系统时间:

  • windows环境下:
Runtime.getRuntime().exec("cmd /c date 2013-05-06");//Windows 系统

  Runtime.getRuntime().exec("cmd /c time 22:35:00");//Windows 系统



  • linux环境下:
Runtime.getRuntime().exec(" sudo date -s 2013-05-06")//linux 系统为tomcat用户分配了权限

  Runtime.getRuntime().exec(" sudo date -s 22:25:00")//linux 系统为tomcat用户分配了权限



  Linux上获取的时间不正确,总是相差几小时考虑时差的问题,修改/etc/sysconfig/clock。

 

9. Q&A

9.1 为什么基于ThreadLocal收集消息?

CAT客户端在收集端数据方面使用ThreadLocal(线程局部变量),是线程本地变量。保证了线程安全。

业务方在处理业务逻辑时基本都是在一个线程内部调用后端服务、数据库、缓存等,将这些数据拿回来再进行业务逻辑封装,最后将结果展示给用户。所以将监控请求作为一个监控上下文存入线程变量就非常合适。

9.2 为什么要使用TCP协议?

AT使用了TCP协议上报消息(引入了netty框架)。那么为什么不适用http协议上报呢?

选择TCP的理由:对于客户端的数据采集尽量降低性能损耗,TCP协议比HTTP协议更加轻量级(比如TCP不需要header等额外的损耗),在高qps的场景下具备明显的性能优势。

另外,CAT的设计也不需要保留一个 Http链接供外部调用,这样的埋点方式效率低下,并不考虑。

 

10. 自己实现小工具

10.1 采集阿里鹰眼的数据,转换成CAT消息树并展示

需求来源:

由于某种原因,现有采用HSF+淘宝TDDL+Diamond+ONS消息+Tair+Search的技术选型,但是缺乏阿里鹰眼的监控系统。

解决方案:

上述技术选型,会进行阿里鹰眼需要的数据跟踪链进行打点,记录日志。

把相关日志通过Flume或者ELK进行采集,投递到Kafka中,实现一个eagleeye-over-cat的springboot应用,并监听kafka消息。

可参考Spring Cloud Sleuth兼容方案。

10.2  自定义小工具,解析应用服务器的CAT dump文件

需求来源:

本地dump文件为压缩文件,无法直观查看,排查问题时对用户为黑盒。

解决方案:

使用MessageBlockReader进行文件读取,使用PlainTextMessageCodec把字节解码为MessageTree。