开源的APM系统的实现
基于Java语言的APM工具,用于大规模分布式服务化系统或实施了微服务架构的系统。
特性
一个分布式服务的调用链跟踪系统,能够收集服务调用的时序数据,解决在微服务架构中定位超时等性能问题。
通过在应用程序中挂载字节码增强库来将实时数据汇报给Zipkin,Zipkin UI可以通过图形的方式显示调用链中有多少请求经过系统的某个节点,并构造和显示系统的拓扑结构。
如果在定位微服务系统间交互的超时问题,则可以根据服务节点,调用链长度,时间戳等信息过滤和查找想找到的调用链,一点找到一个调用链,则能清晰地看到调用链是否有问题及问题在哪里。
美团点评开源的一款实时的应用和性能监控系统。系统原型和理念来源于eBay的CAL系统,并增强了CAL系统的核心模型,添加了丰富的报表功能。
聚焦于对Java应用的全链路监控方面,开源实时处理有时间价值的日志数据,并全量采集应用产生的数据,具有高可用,容错性,高吞吐量和可横向扩展等特性。
CAT不保证消息的可靠收集,允许发送的消息丢失,是在设计过程中对准确性与稳定性的一个权衡,CAT服务端号称可以做到4个9可靠性。
用户请求调前端服务,前端服务分发请求到内部各个服务,每次调用涉及跨系统一次请求和一次响应。
树形结构:
重点关注两个服务之间的通信
一次调用需要保存的调用信息:
一次远程调用过程4个阶段,每种类型的远程调用信息包含:调用端或者被调用端的IP,系统ID,本次请求的TraceID,SpanID,ParentSpanID,时间戳,调用的方法名称及远程调用信息的类型等
第3 4阶段进一步分为成功响应和异常响应,为主子线程间调用增加了一种调用信息类型
Vesta是一款原创的多场景的互联网发号器,此发号器可以作为全局唯一的流水号,即TrackID
当系统故障:
SpanID是一个64位的整型值,有多种策略产生SpanID
一个业务流程的完成由用户的多次请求组成,这些请求之间是有关联的,在串联调用链之后,会根据业务的属性,将不同的调用链聚合在一起形成业务链。
需要在多次请求之间建立联系,可以通过业务系统的订单号来串联业务链,调用链是一个简单的树形结构,而业务链式一个森林结构。
主要解决Java应用内传递、服务间传递、多线程传递、应用与消息队列传递、缓存和数据库间传递TraceID和SpanID的问题。
1.Java进程内传递
在Java应用系统内部通过ThreadLocal传递TraceID 和SpanID,这样在需要调用外部系统时,TraceID和SpanID是可以得到的
2.服务间传递
需要在应用层的网络通信协议中传递TraceID和SpanID,
3.主子线程间传递
服务化架构里,非核心链路上的逻辑抽象成异步处理,通常会在异步线程中执行,跟踪这部分调用链,在创建新的线程或子线程时,将TraceID和SpanID一并传递过去,并放在子线程的TreadLocal中,可以把这个过程封装在一个可以独立使用的线程池的AsyncThreadPoolExecutor子类中。
伪代码
public class AsyncThreadPoolExecutor extends ThreadPoolExecutor
{
public AsyncThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public AsyncThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}
public AsyncThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}
public AsyncThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
@Override
public void execute(Runnable runnable){
AsyncTaskHolder asyncTaskHolder = new AsyncTaskHolder(runnable);
try{
//首先处理TraceID 和 SpanID
asyncTaskHolder.beforeInvoke();
} catch (Exception e) {
//打印异常日志,吃掉异常,不能让新增功能影响原有功能
}
super.execute(asyncTaskHolder);
}
}
public abstract class AsyncTaskHolder {
//用于发送调用信息的Kakfa客户端
private CallInfoSender callInfoSender = new CallInfoKafkaSender(...);
//当前线程的TraceID SpanID 和parentSpanID
private long traceID = -1;
private long spanID = -1;
private long parentSpanID=-1;
//临时保存前面线程TraceID 和 SpanID
private long prevTraceID = -1;
private long preSpanID=-1;
private long prevParentSpanID = -1;
//原生异步线程任务
private volatile Runnable task;
public AsyncTaskHolder(Runnable runnable){
this.task = runnable;
}
@Override
final public void run(){
try{
//进入异步线程处理,将SpanID进一步传递下去,并保存原来的SpanID
beforeRun();
} catch (Exception e) {
//打印异常日志
}
try{
task.run();
}finally {
try{
//退出异常线程处理,恢复原来的SpanID
afterRun()
} catch (Exception e) {
//打印日志
}
}
}
public void beforeSubmit(){
//将当前线程的TraceID SpanID 和parentSpanID传递给即将要执行的任务对象
traceID = getTraceID();
spanID = getSpanID();
parentSpanID = getParentSpanID();
}
public void beforeRun(){
//在子线程执行任务前,在任务对象中保存父线程的TraceID SpanID和parentSpanID
prevTraceID = traceID;
preSpanID = spanID;
prevParentSpanID = parentSpanID;
//在子线程执行任务前,生成新的TraceID,spanID 和parentSpanID 其中traceID不变,parentSpanID为父线程的spanID,生成新的spanID
setTraceID(traceID);
setSpanID(getNewSpanID(spanID));
setParentSpanID(spanID);
long ip = getHostIp();
callInfoSender.send(Thread.currentThread().getId(), RPCPhase.SIB, traceID,spanID,parentSpanID,ip);
}
public void afterRun(){
traceID = prevTraceID;
spanID = preSpanID;
parentSpanID = prevParentSpanID;
}
public Runnable getTask(){
return task;
}
public void setTask(Runnable task){
this.task = task;
}
}
4.消息队列的传递
5缓存、数据库访问
职责:解决采集TraceID 和SpanID 数据及推送数据的问题
4种方法:应用层主动推送,AOP推送,JavaAgent字节码增强,大数据日志推送
1.应用层主动推送
应用层通过编写代码,把相关数据推送到调用链处理器
2. AOP推送
在应用的业务层代码中使用AOP拦截目标服务调用,把请求和响应的调用信息收集后,推送到调用链处理器。
3. JavaAgent 字节码增强
构建一个独立应用程序的代理程序,用来检测和协助运行在JVM上的程序,甚至能替换和修改某些类的定义,增加定制化的监控代码,实现更为灵活的运行时虚拟机监控和Java字节码操作。
4. 代理推送
与大数据日志系统的建设方式类似,日志文件通过应用程序打印相关调用日志,随着日志一起推送到日志中心,再从日志中心提取相关的调用日志,组成调用链。
将日志推送到大数据日志中心一般采用Fluentd,Flume,Scrib,Logstash等框架来实现。
1.Kafka消息队列
在采集器和处理器中间增加了Kafka消息队列,对于高峰时的日志处理起到了有效的削峰作用,可以把产生的大量日志缓冲到Kafka消息队列的Broker服务器上,处理器可以后续慢慢处理,不影响业务。
2.UDP推送
UDP传输数据效率比TCP或者任何上层协议的效率都要高得多
Kafka 和UDP 都是用异步线程池发送调用信息,异步线程池与业务线程之间必须采用有界队列,防止收集器出现问题,出现数据积压后导致应用内存耗光而产生OutOfMemoryError问题。
调用链处理器对调用信息进行组合和聚合,并进行一定的附加处理,然后存储到大数据存储系统中,或者通过Spark等流式处理后发送给监控和报警系统。
对收集的信息进行处理,使用Java开发一个处理器,对得到的日志聚合,存入调用链的大数据存储系统中。
生产实践中,在处理器中除了对调用链进行聚合,还需要从调用链中发现调用的问题,例如异常,超时等。需使用类似Storm,Spark等流式计算系统对恢复的调用链进行分析,然后发送给报警和监控系统。
HBase比较适合存储系统,基于HBase的TSDB也适合存储基于时序的数据。
设计存储的数据结构为宽表,宽表设计:
列簇:列簇中的每一列是一次远程调用的每个阶段的信息,每个阶段做一个编号
两个调用链实例
调用链宽表设计
高表用于通过业务系统的ID确定在某个业务系统中调用链的TraceID
假设交易系统出现问题,业务系统的运营只能提供订单号,则需要使用订单号查询到相应的TraceID,通过调用链的恢复找到问题的所在
问题调用超时