使用 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 的监控跟传统的 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
在 Cat 里面,消息大致可以分为几个类型:
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:
其实 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
,可以简化处理逻辑, 有兴趣可以看一下 Cat 中的逻辑实现:
- logRemoteCallServer
- //客户端需要创建一个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 中的文档,结合上面的例子可以更好地理解。
来源: