基于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 { 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对于一些分布式的开源框架,都有很好的集成,比如dubbo,有兴趣的可以查看它在script中的文档,结合上面的例子可以更好地理解。
文章转自:http://www.cnblogs.com/xing901022/p/6237874.html
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
kubernetes在腾讯游戏的应用实践[转]
腾讯在线游戏的容器化应用场景 2014年,我们开启了容器化探索之路,先回顾一下之前遇到的一些问题。 在物理机时代,资源的交付时间较长,资源的利用率较低,也不能做到隔离。到了xen\kvm虚拟机时代,问题得到了初步的解决,但在弹性伸缩方面仍有不足。随着Docker技术的兴起,我们开始调研Docker在游戏容器化方面的应用。我们的目标有两个,一是提高资源利用率,二是通过Docker镜像来标准化部署流程。 选择Docker技术之后,我们开始了容器调度平台的选型。我们当时也调研了其它的一些组件,比如Shipyard、Fig等,但这些组件无法支撑海量游戏容器调度。而自建调度平台的话,时间成本非常的高。就在那时,Google开源了kubernetes(当时的版本是kubernetes v0.4),我们基于这个版本进行了定制和开发,使其成为我们游戏容器的调度管理平台。 在2015年初的时候,TDocker平台上线。之后,我们开始逐步接入业务。一开始的模式非常简单,就是把Docker当成虚拟机来使用,但这不意味着游戏全容器化的实现。 大家知道,对于一项新技术来说,大家都很谨慎,会通过不断的灰度上线,由...
- 下一篇
如何将已有项目迁移到Spring Boot
1、创建可部署的war文件 生成可部署war文件的第一步是创建一个 SpringBootServletInitializer的子类并重写它的configure方法。这样就可以利用Spring Servlet 3.0的支持,并允许在servlet容器启动时配置你的应用程序。通常,main方法所在的类需要继承SpringBootServletInitializer: @SpringBootApplication publicclassApplicationextendsSpringBootServletInitializer{ @Override protectedSpringApplicationBuilderconfigure(SpringApplicationBuilderapplication){ returnapplication.sources(Application.class); } publicstaticvoidmain(String[]args)throwsException{ SpringApplication.run(Application.class,args)...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS7设置SWAP分区,小内存服务器的救世主
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题