本文基于dubbo 2.7.5版本代码
详解监控Monitor的实现原理
前一篇文章介绍了MonitorFilter实现,MonitorFilter可以拦截所有的请求和返回值,收集所有的统计信息。之后便将统计信息交给Monitor对象处理。在dubbo2.7.5版本里面,Monitor实现类只有DubboMonitor,因此监控中心只支持dubbo协议。本文将详细介绍DubboMonitor的实现原理。
一、DubboMonitor对象创建
DubboMonitor对象是在MonitorFilter的collect方法里面创建的,因为collect方法只有在服务请求时才会被访问,因此Monitor对象的创建属于懒加载,只有在使用的时候才会创建。创建Monitor的代码如下:
Monitor monitor = monitorFactory.getMonitor(monitorUrl);
monitorFactory使用ExtensionLoader.getAdaptiveExtension方法获得,getMonitor根据monitorUrl获得对应的MonitorFactory实现类。dubbo只提供了实现类DubboMonitorFactory。DubboMonitorFactory的getMonitor方法将Monitor对象的创建委托给DubboMonitorFactory的createMonitor方法。下面介绍createMonitor方法。
1、createMonitor方法
protected Monitor createMonitor(URL url) {
//这里的url是在客户端启动的时候调用loadMonitor方法构造的
//例如:dubbo://127.0.0.1:2181/org.apache.dubbo.monitor.MonitorService?application=consumer&dubbo=2.0.2&interface=org.apache.dubbo.monitor.MonitorService&pid=108660&qos.enable=false®ister.ip=192.168.56.1&release=2.7.5×tamp=1589616815635
//要了解这段url的组成,可以看一下ConfigValidationUtils.loadMonitor方法,url里面的ip地址、端口是使用dubbo.monitor.address设置的
URLBuilder urlBuilder = URLBuilder.from(url);
//设置协议,默认是dubbo
urlBuilder.setProtocol(url.getParameter(PROTOCOL_KEY, DUBBO_PROTOCOL));
if (StringUtils.isEmpty(url.getPath())) {
urlBuilder.setPath(MonitorService.class.getName());
}
//filter表示访问监控中心需要经过的过滤器,默认没有过滤器
String filter = url.getParameter(REFERENCE_FILTER_KEY);
if (StringUtils.isEmpty(filter)) {
filter = "";
} else {
filter = filter + ",";
}
urlBuilder.addParameters(CHECK_KEY, String.valueOf(false),
REFERENCE_FILTER_KEY, filter + "-monitor");
//引用远程服务,也就是远程的监控中心的服务,monitorInvoker相当于客户端,所有访问监控中心都是通过monitorInvoker完成。
//这里也可以看到监控中心暴露的服务是MonitorService,所以入参url的path是org.apache.dubbo.monitor.MonitorService。
//refer方法以后在介绍客户端启动的时候说明
Invoker<MonitorService> monitorInvoker = protocol.refer(MonitorService.class, urlBuilder.build());
//创建monitorInvoker的代理
MonitorService monitorService = proxyFactory.getProxy(monitorInvoker);
//创建DubboMonitor对象
return new DubboMonitor(monitorInvoker, monitorService);
}
2、DubboMonitor的构造方法
上一小节的代码最后调用了DubboMonitor的构造方法。我们看一下构造方法的内容。
//monitorInvoker可以简单理解为监控中心的客户端,这一版dubbo里面使用
//monitorService是monitorInvoker的代理对象
//这两个对象本质是一样,monitorService最终也是访问monitorInvoker。
//在DubboMonitor中访问监控中心使用的是monitorService
public DubboMonitor(Invoker<MonitorService> monitorInvoker, MonitorService monitorService) {
this.monitorInvoker = monitorInvoker;
this.monitorService = monitorService;
//monitorInterval表示monitorInvoker每多长时间访问一次监控中心,默认是1分钟
//该值可以通过dubbo.monitor.interval修改
this.monitorInterval = monitorInvoker.getUrl().getPositiveParameter("interval", 60000);
//创建定时任务执行器,这个定时器没有使用时间轮算法,而是使用java的ScheduledExecutorService
//任务执行间隔是monitorInterval指定的
sendFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> {
try {
// collect data
send();//将统计的数据发送到监控中心
} catch (Throwable t) {
logger.error("Unexpected error occur at send statistic, cause: " + t.getMessage(), t);
}
}, monitorInterval, monitorInterval, TimeUnit.MILLISECONDS);
}
二、MonitorService
监控中心发布的服务是MonitorService,客户端也是访问MonitorService的方法将数据发送至监控中心。DubboMonitor实现了MonitorService接口。
public interface MonitorService {
//。。。一些常量定义删除
//MonitorFilter收集完信息后调用collect方法,
//该方法的作用是,其他对象访问完该方法可以认为统计信息已经发送给了监控中心
void collect(URL statistics);
//查询监控中心数据,dubbo提供了多个维度查询,在dubbo2.7.5版本中没有对该方法的调用
//下面是dubbo里面对该方法的注释,解释了入参query查询规则
/*
* 1. support lookup by day: count://host/interface?application=foo&method=foo&side=provider&view=chart&date=2012-07-03
* 1.1 host,application,interface,group,version,method: query criteria for looking up by host, application, interface, method. When one criterion is not present, it means ALL will be accepted, but 0.0.0.0 is ALL for host
* 1.2 side=consumer,provider: decide the data from which side, both provider and consumer are returned by default
* 1.3 default value is view=summary, to return the summarized data for the whole day. view=chart will return the URL address showing the whole day trend which is convenient for embedding in other web page
* 1.4 date=2012-07-03: specify the date to collect the data, today is the default value
*
*/
List<URL> lookup(URL query);
}
三、重要方法详解
1、collect
从《dubbo解析-详解监控MonitorFilter的实现原理》文章中可以知道,在MonitorFilter的collect方法中会调用Monitor的collect方法。
访问完Monitor的collect方法,便可以认为统计信息已经发送给了监控中心。
//dubbo会将统计信息组装成URL对象作为该方法的入参,组装的内容可以参见
//文章《dubbo解析-详解监控MonitorFilter的实现原理》
public void collect(URL url) {
//从url中获取一些统计信息
int success = url.getParameter(MonitorService.SUCCESS, 0);//服务是否成功,用于统计成功次数
int failure = url.getParameter(MonitorService.FAILURE, 0);//服务是否失败,用于统计失败次数
int input = url.getParameter(MonitorService.INPUT, 0);//服务端收到请求信息的大小,以字节为单位
int output = url.getParameter(MonitorService.OUTPUT, 0);//客户端收到返回信息的大小,以字节为单位
int elapsed = url.getParameter(MonitorService.ELAPSED, 0);//访问服务耗时
int concurrent = url.getParameter(MonitorService.CONCURRENT, 0);//访问服务的总次数
//创建Statistics对象,该对象作为statisticsMap的key
//statisticsMap是DubboMonitor的属性,类型是ConcurrentHashMap<Statistics, AtomicReference<long[]>>
Statistics statistics = new Statistics(url);
//reference保存各个统计信息,使用long数组存储,存储了10项信息
AtomicReference<long[]> reference = statisticsMap.get(statistics);
if (reference == null) {
statisticsMap.putIfAbsent(statistics, new AtomicReference<long[]>());
reference = statisticsMap.get(statistics);
}
long[] current;
long[] update = new long[LENGTH];
//下面的代码为更新reference数组的统计信息,使用循环,直到更新成功为止
do {
current = reference.get();
if (current == null) {
update[0] = success;
update[1] = failure;
update[2] = input;
update[3] = output;
update[4] = elapsed;
update[5] = concurrent;
update[6] = input;
update[7] = output;
update[8] = elapsed;
update[9] = concurrent;
} else {
update[0] = current[0] + success;
update[1] = current[1] + failure;
update[2] = current[2] + input;
update[3] = current[3] + output;
update[4] = current[4] + elapsed;
update[5] = (current[5] + concurrent) / 2;
//下面四项信息,只统计最大值
update[6] = current[6] > input ? current[6] : input;
update[7] = current[7] > output ? current[7] : output;
update[8] = current[8] > elapsed ? current[8] : elapsed;
update[9] = current[9] > concurrent ? current[9] : concurrent;
}
} while (!reference.compareAndSet(current, update));
}
DubboMonitor的collect方法只是将统计信息保存在了statisticsMap中,也就是保存在本地内存中。
statisticsMap的value使用的是AtomicReference对象,使用原子类可以避免并发访问时加锁。
在DubboMonitor的构造方法中创建了定时任务,每过一段时间,会将statisticsMap的内容发送到监控中心。定时任务调用的是send方法。
2、send
send方法做了三件事:
- 将统计信息构造为URL对象;
- 将URL对象发送给监控中心;
- 将部分统计数据清零。
public void send() {
String timestamp = String.valueOf(System.currentTimeMillis());
//遍历statisticsMap
for (Map.Entry<Statistics, AtomicReference<long[]>> entry : statisticsMap.entrySet()) {
Statistics statistics = entry.getKey();
AtomicReference<long[]> reference = entry.getValue();
long[] numbers = reference.get();
long success = numbers[0];
long failure = numbers[1];
long input = numbers[2];
long output = numbers[3];
long elapsed = numbers[4];
long concurrent = numbers[5];
long maxInput = numbers[6];
long maxOutput = numbers[7];
long maxElapsed = numbers[8];
long maxConcurrent = numbers[9];
String protocol = getUrl().getParameter(DEFAULT_PROTOCOL);
//将10项统计信息构造为URL对象
URL url = statistics.getUrl()
.addParameters(MonitorService.TIMESTAMP, timestamp,
MonitorService.SUCCESS, String.valueOf(success),
MonitorService.FAILURE, String.valueOf(failure),
MonitorService.INPUT, String.valueOf(input),
MonitorService.OUTPUT, String.valueOf(output),
MonitorService.ELAPSED, String.valueOf(elapsed),
MonitorService.CONCURRENT, String.valueOf(concurrent),
MonitorService.MAX_INPUT, String.valueOf(maxInput),
MonitorService.MAX_OUTPUT, String.valueOf(maxOutput),
MonitorService.MAX_ELAPSED, String.valueOf(maxElapsed),
MonitorService.MAX_CONCURRENT, String.valueOf(maxConcurrent),
DEFAULT_PROTOCOL, protocol
);
//将统计信息发送到监控中心,monitorService可以认为是监控中心的客户端,
//是监控中心在本地的代理对象
//monitorService内部会访问网络,将数据使用dubbo协议传送给监控中心
monitorService.collect(url);
//重置6项数据,这六项数据的统计时间范围是上次发送给监控中心
//到这次发送给监控中心为止,每发送给监控中心后,这六项数据清零
//6项数据为:1、服务成功次数;2、服务失败次数;3、服务端收到请求信息总大小;
//4、客户端收到返回信息的总大小;5、服务耗时总时间;6、作用未知
long[] current;
long[] update = new long[LENGTH];
do {
current = reference.get();
if (current == null) {
update[0] = 0;
update[1] = 0;
update[2] = 0;
update[3] = 0;
update[4] = 0;
update[5] = 0;
} else {
update[0] = current[0] - success;
update[1] = current[1] - failure;
update[2] = current[2] - input;
update[3] = current[3] - output;
update[4] = current[4] - elapsed;
update[5] = current[5] - concurrent;
}
} while (!reference.compareAndSet(current, update));
}
}
send方法将10项数据发送给监控中心:
- 一段时间内服务成功次数;
- 一段时间内服务失败次数;
- 一段时间内服务端收到请求的总大小
- 一段时间内客户端收到返回值的总大小
- 一段时间内服务的总耗时
- 作用未知,有博客介绍该数据为一次收集周期的平均TPS
- 最大请求信息大小
- 最大响应信息大小
- 最大响应时间
- 一段时间内总调用次数
这里的一段时间指的是上次访问监控中心到这次访问监控中心为止。
四、总结
DubboMonitor的作用总起来说是收集10项统计信息,然后使用定时任务将信息发送给监控中心。
dubbo只提供了DubboMonitor实现,监控中心必须使用dubbo协议。如果收集的信息不符合要求,需要重新实现MonitorFactory和Monitor接口,并且在配置参数dubbo.monitor.address中指定协议。