zoukankan      html  css  js  c++  java
  • 基于Cat的分布式调用追踪

    Cat是美团点评出的一款APM工具,同类的产品也有不少,知名的开源产品如zipkin和pinpoint;国内收费的产品如oneapm。考虑到Cat在互联网公司的应用比较广,因此被纳入选型队列,我也有幸参与技术预言。

    使用Cat断断续续将近两周的时间,感觉它还算是很轻量级的。文档相对来说薄弱一些,没有太全面的官方文档(官方文档大多是介绍每个名词是什么意思,界面是什么意思,部署方面比较欠缺);但是好在有一个非常活跃的群,群里有很多经验丰富的高手,不会的问题基本都能得到解答。

    下面就开始步入正题吧,本篇主要讲述一下如何利用Cat进行分布式的调用链追踪。

    分布式开发基础

    在最开始网站基本都是单节点的,由于业务逐渐发展,使用者开始增多,单节点已经无法支撑了。于是开始切分系统,把系统拆分成几个独立的模块,模块之间采用远程调用的方式进行通信。

    那么远程调用是如何做到的呢?下面就用最古老的RMI的方式来举个例子吧!

    RMI(Remote method invocation)是java从1.1就开始支持的功能,它支持跨进程间的方法调用。

    大体上的原理可以理解为,服务端会持续监听一个端口。客户端通过proxy代理的方式远程调用服务端。即客户端会把方法的参数以字符串的的方式序列化传给服务端。服务端反序列化后调用本地的方法执行,执行结果再序列化返回给客户端。

    服务端的代码可以参考如下:

    
    interface IBusiness extends Remote{
        String echo(String message) throws RemoteException;
    }
    class BusinessImpl extends UnicastRemoteObject implements  IBusiness {
        public BusinessImpl() throws RemoteException {}
        @Override
        public String echo(String message) throws RemoteException {
            return "hello,"+message;
        }
    }
    public class RpcServer {
        public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
            IBusiness business = new BusinessImpl();
            LocateRegistry.createRegistry(8888);
            Naming.bind("rmi://localhost:8888/Business",business);
            System.out.println("Hello, RMI Server!");
        }
    }
    

    客户端的代码如下:

    IBusiness business = (IBusiness) Naming.lookup("rmi://localhost:8888/Business");
                business.echo("xingoo",ctx);
    

    上面的例子就可以实现客户端跨进程调用的例子。

    Cat监控

    Cat的监控跟传统的APM产品差不多,模式都是相似的,需要一个agent在客户端进行埋点,然后把数据发送给服务端,服务端进行解析并存储。只要你埋点足够全,那么它是可以进行全面监控的。监控到的数据会首先按照某种规则进行消息的合并,合并成一个MessageTree,这个MessageTree会被放入BlockingQueue里面,这样就解决了多线程数据存储的问题。

    队列会限制存储的MessageTree的个数,但是如果服务端挂掉,客户端也有可能因为堆积大量的心跳而导致内存溢出(心跳是Cat客户端自动向服务端发出的,里面包含了jvm本地磁盘IO等很多的内容,所以MesssageTree挺大的)。

    因此数据在客户端的流程可以理解为:

    TrasactionEvent-->MessageTree-->BlockingQueue-->netty发出网络流
    

    即Transaction、Event等消息会先合并为消息树,以消息树为单位存储在内存中(并未进行本地持久化),专门有一个TcpSocketSender负责向外发送数据。

    再说说服务端,服务端暂时看的不深,大体上可以理解为专门有一个TcpSocketReciever接收数据,由于数据在传输过程中是需要序列化的。因此接收后首先要进行decode,生成消息树。然后把消息放入BlockingQueue,有分析器不断的来队列拿消息树进行分析,分析后按照一定的规则把报表存储到数据库,把原始数据存储到本地文件中(默认是存储到本地)。

    因此数据在服务端的流程大致可以理解为:

    网络流-->decode反序列化-->BlockingQueue-->analyzer分析--->报表存储在DB
                                                        |---->原始数据存储在本地或hdfs
    

    简单的Transaction例子

    在Cat里面,消息大致可以分为几个类型:

    • Transaction 有可能出错、需要记录处理的时间的监控,比如SQL查询、URL访问等
    • Event 普通的监控,没有处理时间的要求,比如一次偶然的异常,一些基本的信息
    • Hearbeat 心跳检测,常常用于一些基本的指标监控,一般是一分钟一次
    • Metric 指标,比如有一个值,每次访问都要加一,就可以使用它

    Transaction支持嵌套,即可以作为消息树的根节点,也可以作为叶子节点。但是Event、Heartbeat和Metric只能作为叶子节点。有了这种树形结构,就可以描述出下面这种调用链的结果了:

    Transaction和Event的使用很简单,比如:

     @RequestMapping("t")
        public @ResponseBody String test() {
            Transaction t = Cat.newTransaction("MY-TRANSACTION","test in TransactionTest");
            try{
                Cat.logEvent("EVENT-TYPE-1","EVENT-NAME-1");
    
                // ....
    
            }catch(Exception e){
                Cat.logError(e);
                t.setStatus(e);
            }finally {
                t.setStatus(Transaction.SUCCESS);
                t.complete();
            }
            return "trasaction test!";
        }
    

    这是一个最基本的Transaction的例子。

    分布式调用链监控

    在分布式环境中,应用是运行在独立的进程中的,有可能是不同的机器,或者不同的服务器进程。那么他们如果想要彼此联系在一起,形成一个调用链,就需要通过几个ID进行串联。这种串联的模式,基本上都是一样的。

    举个例子,A系统在aaa()中调用了B系统的bbb()方法,如果我们在aaa方法中埋点记录上面例子中的信息,在bbb中也记录信息,但是这两个信息是彼此独立的。因此就需要使用一个全局的id,证明他们是一个调用链中的调用方法。除此之外,还需要一个标识谁在调用它的ID,以及一个标识它调用的方法的ID。

    总结来说,每个Transaction需要三个ID:

    • RootId,用于标识唯一的一个调用链
    • ParentId,父Id是谁?谁在调用我
    • ChildId,我在调用谁?

    其实ParentId和ChildId有点冗余,但是Cat里面还是都加上吧!

    那么问题来了,如何传递这些ID呢?在Cat中需要你自己实现一个Context,因为Cat里面只提供了一个内部的接口:

    public interface Context {
            String ROOT = "_catRootMessageId";
            String PARENT = "_catParentMessageId";
            String CHILD = "_catChildMessageId";
    
            void addProperty(String var1, String var2);
    
            String getProperty(String var1);
        }
    

    我们需要自己实现这个接口,并存储相关的ID:

    public class MyContext implements Cat.Context,Serializable{
    
        private static final long serialVersionUID = 7426007315111778513L;
    
        private Map<String,String> properties = new HashMap<String,String>();
    
        @Override
        public void addProperty(String s, String s1) {
            properties.put(s,s1);
        }
    
        @Override
        public String getProperty(String s) {
            return properties.get(s);
        }
    }
    

    由于这个Context需要跨进程网络传输,因此需要实现序列化接口。

    在Cat中其实已经给我们实现了两个方法logRemoteCallClient以及logRemoteCallServer,可以简化处理逻辑,有兴趣可以看一下Cat中的逻辑实现:

    //客户端需要创建一个Context,然后初始化三个ID
    public static void logRemoteCallClient(Cat.Context ctx) {
            MessageTree tree = getManager().getThreadLocalMessageTree();
            String messageId = tree.getMessageId();//获取当前的MessageId
            if(messageId == null) {
                messageId = createMessageId();
                tree.setMessageId(messageId);
            }
    
            String childId = createMessageId();//创建子MessageId
            logEvent("RemoteCall", "", "0", childId);
            String root = tree.getRootMessageId();//获取全局唯一的MessageId
            if(root == null) {
                root = messageId;
            }
    
            ctx.addProperty("_catRootMessageId", root);
            ctx.addProperty("_catParentMessageId", messageId);//把自己的ID作为ParentId传给调用的方法
            ctx.addProperty("_catChildMessageId", childId);
        }
        
    //服务端需要接受这个context,然后设置到自己的Transaction中
    public static void logRemoteCallServer(Cat.Context ctx) {
            MessageTree tree = getManager().getThreadLocalMessageTree();
            String messageId = ctx.getProperty("_catChildMessageId");
            String rootId = ctx.getProperty("_catRootMessageId");
            String parentId = ctx.getProperty("_catParentMessageId");
            if(messageId != null) {
                tree.setMessageId(messageId);//把传过来的子ID作为自己的ID
            }
    
            if(parentId != null) {
                tree.setParentMessageId(parentId);//把传过来的parentId作为
            }
    
            if(rootId != null) {
                tree.setRootMessageId(rootId);//把传过来的RootId设置成自己的RootId
            }
    
        }
    

    这样,结合前面的RMI调用,整个思路就清晰多了.

    客户端调用者的埋点:

    @RequestMapping("t2")
        public @ResponseBody String test2() {
            Transaction t = Cat.newTransaction("Call","test2");
            try{
                Cat.logEvent("Call.server","localhost");
                Cat.logEvent("Call.app","business");
                Cat.logEvent("Call.port","8888");
    
                MyContext ctx = new MyContext();
                Cat.logRemoteCallClient(ctx);
    
                IBusiness business = (IBusiness) Naming.lookup("rmi://localhost:8888/Business");
                business.echo("xingoo",ctx);
            }catch(Exception e){
                Cat.logError(e);
                t.setStatus(e);
            }finally {
                t.setStatus(Transaction.SUCCESS);
                t.complete();
            }
            return "cross!";
        }
    

    远程被调用者的埋点:

    interface IBusiness extends Remote{
        String echo(String message,MyContext ctx) throws RemoteException;
    }
    class BusinessImpl extends UnicastRemoteObject implements  IBusiness {
        public BusinessImpl() throws RemoteException {}
        @Override
        public String echo(String message,MyContext ctx) throws RemoteException {
            Transaction t = Cat.newTransaction("Service","echo");
            try{
                Cat.logEvent("Service.client","localhost");
                Cat.logEvent("Service.app","cat-client");
                Cat.logRemoteCallServer(ctx);
                System.out.println(message);
            }catch(Exception e){
                Cat.logError(e);
                t.setStatus(e);
            }finally {
                t.setStatus(Transaction.SUCCESS);
                t.complete();
            }
            return "hello,"+message;
        }
    }
    public class RpcServer {
        public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
            IBusiness business = new BusinessImpl();
            LocateRegistry.createRegistry(8888);
            Naming.bind("rmi://localhost:8888/Business",business);
            System.out.println("Hello, RMI Server!");
        }
    }
    

    需要注意的是,Service的client和app需要和Call的server以及app对应上,要不然图表是分析不出东西的!

    最后

    Cat对于一些分布式的开源框架,都有很好的集成,比如dubbo,有兴趣的可以查看它在script中的文档,结合上面的例子可以更好地理解。

  • 相关阅读:
    hdu 3790 最短路径问题
    hdu 2112 HDU Today
    最短路问题 以hdu1874为例
    hdu 1690 Bus System Floyd
    hdu 2066 一个人的旅行
    hdu 2680 Choose the best route
    hdu 1596 find the safest road
    hdu 1869 六度分离
    hdu 3339 In Action
    序列化和反序列化
  • 原文地址:https://www.cnblogs.com/xing901022/p/6237874.html
Copyright © 2011-2022 走看看