Druid源码分析之send metrics

基本介绍:

什么是Metric?

metric就是Druid运行过程中产生的一些指标,如查询时间、查询成功数量、JVM参数、任务成功数等。

Metric有什么用?

对Druid进行异常监控报警,对指标数据进行分析等。

Metric发送到哪?

发送位置可以配置,包括日志、http等。

send metric流程(以查询为例):

  1. 查询并得到查询结果
  2. 生成QueryMetrics,里面包含全部要发送信息
  3. 通过QueryMetrics的bulid得到metric event
  4. 将event加入batch
  5. batch装满了就包装起来(seal)
  6. 将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()) / 21);

      //阻塞,等待唤醒

      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);

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值