基本介绍:
什么是Metric?
metric就是Druid运行过程中产生的一些指标,如查询时间、查询成功数量、JVM参数、任务成功数等。
Metric有什么用?
对Druid进行异常监控报警,对指标数据进行分析等。
Metric发送到哪?
发送位置可以配置,包括日志、http等。
send metric流程(以查询为例):
- 查询并得到查询结果
- 生成QueryMetrics,里面包含全部要发送信息
- 通过QueryMetrics的bulid得到metric event
- 将event加入batch
- batch装满了就包装起来(seal)
- 将batch加入队列并唤醒EmittingThread线程发送
源码分析:
1.查询结束后调用QueryLifecycle的emitLogsAndMetrics函数发送metrics
QueryResource是查询的入口,查询会调用其中的doPost函数
在doPost函数中会首先创建QueryLifecycle用于管理查询的生命周期:
1 | final QueryLifecycle queryLifecycle = queryLifecycleFactory.factorize(); |
通过QueryLifecycle的execute函数得到查询结果:
1 2 | final QueryLifecycle.QueryResponse queryResponse = queryLifecycle.execute(); final Sequence<?> results = queryResponse.getResults(); |
通过QueryLifecycle的emitLogsAndMetrics函数发送metrics:
1 | queryLifecycle.emitLogsAndMetrics( null , req.getRemoteAddr(), - 1 ); |
接下来我们来看下QueryLifecycle的emitLogsAndMetrics函数。
2.生成QueryMetrics
首先生成QueryMetrics,其包含全部要发送的信息,生成QueryMetrics的代码如下:
1 2 3 4 5 6 7 8 | QueryMetrics queryMetrics = DruidMetrics.makeRequestMetrics( queryMetricsFactory, toolChest, baseQuery, StringUtils.nullToEmptyNonDruidDataString(remoteAddress) ); queryMetrics.success(success); queryMetrics.reportQueryTime(queryTimeNs); |
接下来我们看一下DruidMetrics的makeRequestMetrics函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public static <T> QueryMetrics<?> makeRequestMetrics( final GenericQueryMetricsFactory queryMetricsFactory, final QueryToolChest<T, Query<T>> toolChest, final Query<T> query, final String remoteAddr ) { QueryMetrics<? super Query<T>> queryMetrics; if (toolChest != null ) { queryMetrics = toolChest.makeMetrics(query); } else { queryMetrics = queryMetricsFactory.makeMetrics(query); } queryMetrics.context(query); queryMetrics.remoteAddress(remoteAddr); return queryMetrics; } |
在makeRequestMetrics函数中,根据toolChest是否存在决定使用何种方式初始化一个queryMetrics,如果toolChest存在,调用toolChest的makeMetrics函数,我们首先看一下这个函数:
1 2 3 4 5 6 7 | @Override public TimeseriesQueryMetrics makeMetrics(TimeseriesQuery query) { TimeseriesQueryMetrics queryMetrics = queryMetricsFactory.makeMetrics(); queryMetrics.query(query); return queryMetrics; } |
如果toolChest不存在,则调用queryMetricsFactory的makeMetrics函数初始化,接下来看一下这个函数:
1 2 3 4 5 6 7 | @Override public QueryMetrics<Query<?>> makeMetrics(Query<?> query) { DefaultQueryMetrics<Query<?>> queryMetrics = new DefaultQueryMetrics<>(jsonMapper); queryMetrics.query(query); return queryMetrics; } |
通过观察,我们发现这两种方法都初始化一个queryMetrics,然后调用query方法,接下来我们看一下query方法如果将查询所带有的信息存储在queryMetrics中
1 2 3 4 5 6 7 8 9 10 11 | @Override public void query(QueryType query) { dataSource(query); queryType(query); interval(query); hasFilters(query); duration(query); queryId(query); sqlQueryId(query); } |
在query函数中,通过上面所列函数将query所带的dataSource、queryType、interval等信息放入queryMetrics的bulider属性中,以dataSource为例:
1 2 3 4 5 6 7 8 9 10 | @Override public void dataSource(QueryType query) { setDimension(DruidMetrics.DATASOURCE, DataSourceUtil.getMetricName(query.getDataSource())); } protected void setDimension(String dimension, String value) { checkModifiedFromOwnerThread(); builder.setDimension(dimension, value); } |
我们现在再回到QueryLifecycle的emitLogsAndMetrics函数,下面语句会将查询时间信息存储在queryMetrics的metrics属性里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | queryMetrics.reportQueryTime(queryTimeNs); @Override public QueryMetrics<QueryType> reportQueryTime( long timeNs) { return reportMillisTimeMetric( "query/time" , timeNs); } private QueryMetrics<QueryType> reportMillisTimeMetric(String metricName, long timeNs) { return reportMetric(metricName, TimeUnit.NANOSECONDS.toMillis(timeNs)); } protected QueryMetrics<QueryType> reportMetric(String metricName, Number value) { checkModifiedFromOwnerThread(); metrics.put(metricName, value); return this ; } |
现在我们就创建好了queryMetrics,其属性bulider和metrics中含有一条metrics所需要的全部信息。
3.queryMetrics通过bulid函数得到metric event(event表示一条metric数据)
我们回到QueryLifecycle的emitLogsAndMetrics函数,我们现在构建好了queryMetrics,将使用queryMetrics的emit函数发射metrics
1 | queryMetrics.emit(emitter); |
我们查看一下emit这个函数:
1 2 3 4 5 6 7 8 9 10 11 | @Override public void emit(ServiceEmitter emitter) { checkModifiedFromOwnerThread(); for (Map.Entry<String, Number> metric : metrics.entrySet()) { //builder.build(metric.getKey(), metric.getValue()) = ServiceEventBuilder //ServiceEventBuilder里有bulid方法返回一个ServiceMetricEvent emitter.emit(builder.build(metric.getKey(), metric.getValue())); } metrics.clear(); } |
在这个函数中,对于metrics中的每一个metric,取出它的key和value作为builder的bulid函数的参数,builder的bulid函数会返回一个ServiceEventBuilder,ServiceEventBuilder有一个函数bulid函数会返回一个ServiceMetricEvent,ServiceMetricEvent表示一条metric数据。我们可以i看一下相关代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | public ServiceEventBuilder<ServiceMetricEvent> build( final String metric, final Number value ) { return build( null , metric, value); } public ServiceEventBuilder<ServiceMetricEvent> build( final DateTime createdTime, final String metric, final Number value ) { if (Double.isNaN(value.doubleValue())) { throw new ISE( "Value of NaN is not allowed!" ); } if (Double.isInfinite(value.doubleValue())) { throw new ISE( "Value of Infinite is not allowed!" ); } return new ServiceEventBuilder<ServiceMetricEvent>() { @Override public ServiceMetricEvent build(ImmutableMap<String, String> serviceDimensions) { return new ServiceMetricEvent( createdTime, serviceDimensions, userDims, feed, metric, value ); } }; } |
我们现在再回到emit函数,其中会调用ServiceEmitter的emit函数,其参数是我们刚才得到的ServiceEventBuilder,我们看一下ServiceEmitter的emit函数:
1 2 3 4 5 | public void emit(ServiceEventBuilder builder) { //builder.build(serviceDimensions) = ServiceMetricEvent emit(builder.build(serviceDimensions)); } |
首先我们会使用builder.build(serviceDimensions)得到ServiceMetricEvent,然后将其作为参数,调用emit函数:
1 2 3 4 5 | @Override public void emit(Event event) { emitter.emit(event); } |
4.将event加入batch
这里会调用Emitter的emit函数,Emitter是一个接口,因此我们会以HttpPostEmitter为例来代替Emitter进行介绍(HttpPostEmitter是Emitter的子类)。
1 2 3 4 5 | @Override public void emit(Event event) { emitAndReturnBatch(event); } |
HttpPostEmitter的emit函数会调用emitAndReturnBatch函数,这个函数会将metric event放到batch中,batch是一个缓存区,会在装满后加入队列,并通过http发送。将event加入到batch的逻辑在如下语句中:
1 2 3 | if (batch.tryAddEvent(eventBytes)) { return batch; } |
接下来看一下tryAddEvent函数中的部分代码,如下,如果batch中还没有event,则调用tryAddFirstEvent函数;如果batch中已经有event,但是还没装满,则调用tryAddNonFirstEvent函数;否则,也就是batch被装满了,我们将调用seal函数进行发送:
1 2 3 4 5 6 7 8 9 10 11 12 | if (bufferWatermark == 0 ) { if (tryAddFirstEvent(event)) { return true ; } } else if (newBufferWatermark(bufferWatermark, event) <= emitter.maxBufferWatermark) { if (tryAddNonFirstEvent(state, event)) { return true ; } } else { seal(); return false ; } |
这里以tryAddFirstEvent函数为例,介绍如何将event加入batch,tryAddNonFirstEvent函数原理类似,这里不在过多介绍。
在tryAddFirstEvent函数中有如下语句,第一句将根据配置,向batch的buffer中写入"[",第二句调用函数writeEvent将event写入batch的buffer。
1 2 | int bufferOffset = emitter.batchingStrategy.writeBatchStart(buffer); writeEvent(event, bufferOffset); |
在writeEvent中,使用如下语句将event写入buffer:
1 | System.arraycopy(event, 0 , buffer, bufferOffset, event.length); |
5.将batch加入队列并唤醒EmittingThread线程发送
在上一步骤,我们学习了如何将event放入batch,接下来介绍如何将已经装满的batch包装起来,加入队列,并唤醒EmittingThread线程发送
首先看seal函数:
1 2 3 4 | void seal() { releaseShared(SEAL_TAG); } |
seal函数会调用AQS的releaseShared函数,然后会调用tryReleaseShared函数,在tryReleaseShared函数中可能需要处理三种情况,分别是解锁、解锁并打包和打包,这里主要介绍打包的情况,也就是tag == SEAL_TAG的情况,其主要逻辑是如下语句,它会调用emitter的onSealExclusive函数,其中第一个函数是当前batch,第二个参数与超时时间有关:
1 2 3 4 | emitter.onSealExclusive( this , firstEventTimestamp > 0 ? System.currentTimeMillis() - firstEventTimestamp : - 1 ); |
在onSealExclusive函数中,会调用doOnSealExclusive函数,这个函数的主要逻辑是如下两个语句,第一行将将batch加入发射队列,第二行唤醒EmittingThread发射batch:
1 2 3 4 | //将batch加入队列 addBatchToEmitQueue(batch); //唤醒emit线程进行发射 wakeUpEmittingThread(); |
6.EmittingThread线程
接下来介绍一下EmittingThread如何工作,即如何将batch通过http方式发送。
如下代码是EmittingThread的run函数,其会在HttpPostEmitter启动的时候被调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | @Override public void run() { //一直执行,执行到最后会被阻塞,等待唤醒 while ( true ) { boolean needsToShutdown = needsToShutdown(); try { emitLargeEvents(); emitBatches(); tryEmitOneFailedBuffer(); if (needsToShutdown) { tryEmitAndDrainAllFailedBuffers(); // Make GC life easier drainBuffersToReuse(); return ; } } catch (Throwable t) { log.error(t, "Uncaught exception in EmittingThread.run()" ); } if (failedBuffers.isEmpty()) { // Waiting for 1/2 of config.getFlushMillis() in order to flush events not more than 50% later than specified. // If nanos=0 parkNanos() doesn't wait at all, then we don't want. long waitNanos = Math.max(TimeUnit.MILLISECONDS.toNanos(config.getFlushMillis()) / 2 , 1 ); //阻塞,等待唤醒 LockSupport.parkNanos(HttpPostEmitter. this , waitNanos); } } } |
在run函数中,最外层是一个while循环,且没有终止条件,所以函数里的逻辑会一直循环执行,而循环体的最后一行代码是LockSupport.parkNanos(HttpPostEmitter.this, waitNanos);这将使改线程堵塞,等待被唤醒。也就是说,循环体中的逻辑启动后会执行一次,然后被阻塞,直到我们在上一步骤中提到的wakeUpEmittingThread函数来欢迎该线程。即每次唤醒操作会执行一次上述函数中循环体中的代码一次,接下来我们看一下循环体中的代码,其中最重要的是如下三行:
1 2 3 | emitLargeEvents(); emitBatches(); tryEmitOneFailedBuffer(); |
第一行将发射大的event(不能装入batch的event),第二行是最重要的发射batch,第三行会尝试发射失败的buffer,它们三个的发射逻辑相同,这里以emitBatches为例进行介绍。
1 2 3 4 5 6 | private void emitBatches() { for (Batch batch; (batch = pollBatchFromEmitQueue()) != null ; ) { emit(batch); } } |
emitBatches函数中会从发射队列中取出所有batch逐个进行发射,在emit函数中有如下代码。
1 2 3 4 5 6 7 8 | if (sendWithRetries(batch.buffer, bufferEndOffset, eventCount, true )) { buffersToReuse.add(batch.buffer); approximateBuffersToReuseCount.incrementAndGet(); } else { limitFailedBuffersSize(); failedBuffers.addLast( new FailedBuffer(batch.buffer, bufferEndOffset, eventCount)); approximateFailedBuffersCount.incrementAndGet(); } |
上述代码会使用sendWithRetries函数尝试发送,如果发送失败还会进行重试,如果发射成功,会讲该batch的buffer回收利用,当重试次数用尽后依然失败的,会将buffer加入失败队列等待过一会后重试。
在sendWithRetries函数中,会调用RetryUtils的retry函数,该函数有三个参数,第一个参数是一个Task,其perform函数是主要执行逻辑;第二个参数为一个Predicate,用来判断是否满足条件;第三个参数为最大重试次数。
在第一个参数Task的perform函数中,最重要的逻辑是send函数。
在send函数中,会生成一个request,其body为Batch的buffer中的内容,url从配置中取得,然后执行如下语句通过http发送Batch的buffer中的内容到指定位置:
1 | ListenableFuture<Response> future = client.executeRequest(request); |