天天看點

基于Cat的分布式調用追蹤

使用Cat斷斷續續将近兩周的時間,感覺它還算是很輕量級的。文檔相對來說薄弱一些,沒有太全面的官方文檔(官方文檔大多是介紹每個名詞是什麼意思,界面是什麼意思,部署方面比較欠缺);但是好在有一個非常活躍的群,群裡有很多經驗豐富的高手,不會的問題基本都能得到解答。

下面就開始步入正題吧,本篇主要講述一下如何利用Cat進行分布式的調用鍊追蹤。

分布式開發基礎

在最開始網站基本都是單節點的,由于業務逐漸發展,使用者開始增多,單節點已經無法支撐了。于是開始切分系統,把系統拆分成幾個獨立的子產品,子產品之間采用遠端調用的方式進行通信。

那麼遠端調用是如何做到的呢?下面就用最古老的RMI的方式來舉個例子吧!

RMI(Remote method invocation)是java從1.1就開始支援的功能,它支援跨程序間的方法調用。

大體上的原理可以了解為,服務端會持續監聽一個端口。用戶端通過proxy代理的方式遠端調用服務端。即用戶端會把方法的參數以字元串的的方式序列化傳給服務端。服務端反序列化後調用本地的方法執行,執行結果再序列化傳回給用戶端。

服務端的代碼可以參考如下:

  1. interface IBusiness extends Remote{

  2. String echo(String message) throws RemoteException;

  3. }

  4. class BusinessImpl extends UnicastRemoteObject implements IBusiness {

  5. public BusinessImpl() throws RemoteException {}

  6. @Override

  7. public String echo(String message) throws RemoteException {

  8. return "hello,"+message;

  9. }

  10. }

  11. public class RpcServer {

  12. public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {

  13. IBusiness business = new BusinessImpl();

  14. LocateRegistry.createRegistry(8888);

  15. Naming.bind("rmi://localhost:8888/Business",business);

  16. System.out.println("Hello, RMI Server!");

  17. }

  18. }

用戶端的代碼如下:

  1. IBusiness business = (IBusiness) Naming.lookup("rmi://localhost:8888/Business");
  2. 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,有分析器不斷的來隊列拿消息樹進行分析,分析後按照一定的規則把報表存儲到資料庫,把原始資料存儲到本地檔案中(預設是存儲到本地)。

是以資料在服務端的流程大緻可以了解為:

  1. 網絡流-->decode反序列化-->BlockingQueue-->analyzer分析--->報表存儲在DB
  2. |---->原始資料存儲在本地或hdfs

簡單的Transaction例子

在Cat裡面,消息大緻可以分為幾個類型:

  • Transaction 有可能出錯、需要記錄處理的時間的監控,比如SQL查詢、URL通路等
  • Event 普通的監控,沒有處理時間的要求,比如一次偶然的異常,一些基本的資訊
  • Hearbeat 心跳檢測,常常用于一些基本的名額監控,一般是一分鐘一次
  • Metric 名額,比如有一個值,每次通路都要加一,就可以使用它

Transaction支援嵌套,即可以作為消息樹的根節點,也可以作為葉子節點。但是Event、Heartbeat和Metric隻能作為葉子節點。有了這種樹形結構,就可以描述出下面這種調用鍊的結果了:

Transaction和Event的使用很簡單,比如:

  1. @RequestMapping("t")
  2. public @ResponseBody String test() {
  3. Transaction t = Cat.newTransaction("MY-TRANSACTION","test in TransactionTest");
  4. try{
  5. Cat.logEvent("EVENT-TYPE-1","EVENT-NAME-1");
  6. // ....
  7. }catch(Exception e){
  8. Cat.logError(e);
  9. t.setStatus(e);
  10. }finally {
  11. t.setStatus(Transaction.SUCCESS);
  12. t.complete();
  13. }
  14. 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裡面隻提供了一個内部的接口:

  1. public interface Context {
  2. String ROOT = "_catRootMessageId";
  3. String PARENT = "_catParentMessageId";
  4. String CHILD = "_catChildMessageId";
  5. void addProperty(String var1, String var2);
  6. String getProperty(String var1);

我們需要自己實作這個接口,并存儲相關的ID:

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

由于這個Context需要跨程序網絡傳輸,是以需要實作序列化接口。

在Cat中其實已經給我們實作了兩個方法

logRemoteCallClient

以及

logRemoteCallServer

,可以簡化處理邏輯,有興趣可以看一下Cat中的邏輯實作:

  1. //用戶端需要建立一個Context,然後初始化三個ID
  2. public static void logRemoteCallClient(Cat.Context ctx) {
  3. MessageTree tree = getManager().getThreadLocalMessageTree();
  4. String messageId = tree.getMessageId();//擷取目前的MessageId
  5. if(messageId == null) {
  6. messageId = createMessageId();
  7. tree.setMessageId(messageId);
  8. String childId = createMessageId();//建立子MessageId
  9. logEvent("RemoteCall", "", "0", childId);
  10. String root = tree.getRootMessageId();//擷取全局唯一的MessageId
  11. if(root == null) {
  12. root = messageId;
  13. ctx.addProperty("_catRootMessageId", root);
  14. ctx.addProperty("_catParentMessageId", messageId);//把自己的ID作為ParentId傳給調用的方法
  15. ctx.addProperty("_catChildMessageId", childId);
  16. //服務端需要接受這個context,然後設定到自己的Transaction中
  17. public static void logRemoteCallServer(Cat.Context ctx) {
  18. String messageId = ctx.getProperty("_catChildMessageId");
  19. String rootId = ctx.getProperty("_catRootMessageId");
  20. String parentId = ctx.getProperty("_catParentMessageId");
  21. if(messageId != null) {
  22. tree.setMessageId(messageId);//把傳過來的子ID作為自己的ID
  23. if(parentId != null) {
  24. tree.setParentMessageId(parentId);//把傳過來的parentId作為
  25. if(rootId != null) {
  26. tree.setRootMessageId(rootId);//把傳過來的RootId設定成自己的RootId

這樣,結合前面的RMI調用,整個思路就清晰多了.

用戶端調用者的埋點:

  1. @RequestMapping("t2")
  2. public @ResponseBody String test2() {
  3. Transaction t = Cat.newTransaction("Call","test2");
  4. Cat.logEvent("Call.server","localhost");
  5. Cat.logEvent("Call.app","business");
  6. Cat.logEvent("Call.port","8888");
  7. MyContext ctx = new MyContext();
  8. Cat.logRemoteCallClient(ctx);
  9. return "cross!";

遠端被調用者的埋點:

  1. interface IBusiness extends Remote{
  2. String echo(String message,MyContext ctx) throws RemoteException;
  3. class BusinessImpl extends UnicastRemoteObject implements IBusiness {
  4. public BusinessImpl() throws RemoteException {}
  5. public String echo(String message,MyContext ctx) throws RemoteException {
  6. Transaction t = Cat.newTransaction("Service","echo");
  7. Cat.logEvent("Service.client","localhost");
  8. Cat.logEvent("Service.app","cat-client");
  9. Cat.logRemoteCallServer(ctx);
  10. System.out.println(message);
  11. return "hello,"+message;
  12. public class RpcServer {
  13. public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
  14. IBusiness business = new BusinessImpl();
  15. LocateRegistry.createRegistry(8888);
  16. Naming.bind("rmi://localhost:8888/Business",business);
  17. System.out.println("Hello, RMI Server!");
基于Cat的分布式調用追蹤

需要注意的是,Service的client和app需要和Call的server以及app對應上,要不然圖表是分析不出東西的!

最後

Cat對于一些分布式的開源架構,都有很好的內建,比如dubbo,有興趣的可以檢視它在script中的文檔,結合上面的例子可以更好地了解。

繼續閱讀