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 {
publicBusinessImpl() throws RemoteException {}
@Override
public String echo(String message) throws RemoteException {
return "hello,"+message;
}
}
public class RpcServer {
publicstaticvoidmain(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對於一些分散式的開源框架,都有很好的整合,比如dubbo,有興趣的可以檢視它在script中的文件,結合上面的例子可以更好地理解。
轉:http://www.cnblogs.com/xing901022/p/6237874.html