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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章