使用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);
- 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) {
- 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");
- Cat.logEvent("Call.server","localhost");
- Cat.logEvent("Call.app","business");
- Cat.logEvent("Call.port","8888");
- MyContext ctx = new MyContext();
- Cat.logRemoteCallClient(ctx);
- return "cross!";
遠端被調用者的埋點:
- interface IBusiness extends Remote{
- String echo(String message,MyContext ctx) throws RemoteException;
- class BusinessImpl extends UnicastRemoteObject implements IBusiness {
- public BusinessImpl() throws RemoteException {}
- public String echo(String message,MyContext ctx) throws RemoteException {
- Transaction t = Cat.newTransaction("Service","echo");
- Cat.logEvent("Service.client","localhost");
- Cat.logEvent("Service.app","cat-client");
- Cat.logRemoteCallServer(ctx);
- System.out.println(message);
- 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中的文檔,結合上面的例子可以更好地了解。