1. 程式人生 > >基於Cat的分布式調用追蹤

基於Cat的分布式調用追蹤

new term ger let 調用方法 全局 try service per

使用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挺大的)。

因此數據在客戶端的流程可以理解為:

Trasaction\Event-->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的分布式調用追蹤