分布式鏈路追蹤系列番外篇一(jaeger異步批量發(fā)送span)

Jaeger 提供了一整套的分布式鏈路追蹤方案,也是最早實(shí)現(xiàn)Opentracing協(xié)議的框架之一勤篮。今天我們來簡(jiǎn)單分析一下java客戶端的Span異步發(fā)送機(jī)制

1.異步發(fā)送需求簡(jiǎn)述

我們知道辑鲤,在Java應(yīng)用程序中鞭盟,如果有一些數(shù)據(jù)不是特別重要涤妒,但是又產(chǎn)生得比較多的時(shí)候搔扁,我們?cè)跒榱吮WC程序性能的情況下就會(huì)選擇采用異步的方式來保存和發(fā)送數(shù)據(jù)渣触。比如日志羡棵,Trace數(shù)據(jù)就是屬于這一類的數(shù)據(jù)。所以這一類的數(shù)據(jù)最適合暫時(shí)保存在內(nèi)存中然后異步存儲(chǔ)昵观。

那么要實(shí)現(xiàn)這類異步的需求有什么具體的要求呢晾腔?

我覺得至少應(yīng)該實(shí)現(xiàn)以下幾點(diǎn)

  • 占用的資源不能太多。這其中包括線程資源啊犬,內(nèi)存資源等灼擂。
  • 可以定時(shí)將內(nèi)存中的數(shù)據(jù)保存起來。
  • 可以批量保存數(shù)據(jù)觉至,提升性能剔应。
  • 參數(shù)可配置化
  • 最重要的一點(diǎn),無(wú)論如何不能影響主業(yè)務(wù)语御。

2.Jaeger 實(shí)現(xiàn)原理分析

少?gòu)U話峻贮,先上原理圖,所謂一圖勝千言应闯。


jaeger異步發(fā)送機(jī)制

下面我們來簡(jiǎn)單分析流程
1.在每一個(gè)線程中都會(huì)產(chǎn)生很多Span數(shù)據(jù)纤控,當(dāng)Span結(jié)束的時(shí)候會(huì)調(diào)用Tracer對(duì)象的reportSpans方法,然后Tracer就會(huì)委托給Reporter對(duì)象來發(fā)送Span數(shù)據(jù)碉纺。

public class JaegerSpan implements Span{
  @Override
  public void finish() {
    if (computeDurationViaNanoTicks) {
      long nanoDuration = tracer.clock().currentNanoTicks() - startTimeNanoTicks;
      finishWithDuration(nanoDuration / 1000);
    } else {
      finish(tracer.clock().currentTimeMicros());
    }
  }

  @Override
  public void finish(long finishMicros) {
    finishWithDuration(finishMicros - startTimeMicroseconds);
  }

  private void finishWithDuration(long durationMicros) {
    synchronized (this) {
      if (finished) {
        log.warn("Span has already been finished; will not be reported again.");
        return;
      }
      finished = true;

      this.durationMicroseconds = durationMicros;
    }
    // 只有需要采樣的時(shí)候才發(fā)送Span
    if (context.isSampled()) {
      // 委托給Tracer發(fā)送
      tracer.reportSpan(this);
    }
  }
// other functions
// ....
}
public class JaegerTracer implements Tracer, Closeable{
  void reportSpan(JaegerSpan span) {
     // 委托給Reporter發(fā)送
    reporter.report(span);
    metrics.spansFinished.inc(1);
  }
  // other functions
  // ....
}

2.然后呢船万,Reporter對(duì)象比較會(huì)騙人,它實(shí)際上呢骨田,并沒有真正的發(fā)送給出去耿导,而是將Span數(shù)據(jù)包裹一下,以Command的形式加到了線程安全的阻塞隊(duì)列中态贤。然后Reporter對(duì)象又開啟了兩個(gè)異步線程:

  • jaeger.RemoteReporter-QueueProcessor 負(fù)責(zé)不斷地消費(fèi)阻塞對(duì)象的Command對(duì)象舱呻,然后丟給Sender的緩沖區(qū) 。

  • jaeger.RemoteReporter-FlushTimer 定時(shí)地發(fā)送FlushCommand悠汽,然后讓Sender Flush自己的緩存區(qū)

所以箱吕,我們看到隊(duì)列中有多種Command,如果隊(duì)列畫成圖的話柿冲,就類似于下面這個(gè)樣子殖氏。


block queue.png

其實(shí)還有另外一種Command(CloseCommand),后面我們?cè)僦v

public class RemoteReporter implements Reporter {
  private RemoteReporter(Sender sender, int flushInterval, int maxQueueSize, int closeEnqueueTimeout,
      Metrics metrics) {
    this.sender = sender;
    this.metrics = metrics;
    this.closeEnqueueTimeout = closeEnqueueTimeout;
    commandQueue = new ArrayBlockingQueue<Command>(maxQueueSize);

    // start a thread to append spans
    queueProcessor = new QueueProcessor();
    queueProcessorThread = new Thread(queueProcessor, "jaeger.RemoteReporter-QueueProcessor");
    queueProcessorThread.setDaemon(true);
    queueProcessorThread.start();

    flushTimer = new Timer("jaeger.RemoteReporter-FlushTimer", true /* isDaemon */);
    flushTimer.schedule(
        new TimerTask() {
          @Override
          public void run() {
            flush();
          }
        },
        flushInterval,
        flushInterval);
  }
  @Override
  public void report(JaegerSpan span) {
    // Its better to drop spans, than to block here
    // 注意這里用的是offer方法姻采。如果超過了隊(duì)列大小,那么就會(huì)丟棄后來的span數(shù)據(jù)。
    // 而且這里不是簡(jiǎn)單地把span加入到隊(duì)列中慨亲,而是用Command包裝了一下婚瓜。這就是實(shí)現(xiàn)定時(shí)flush數(shù)據(jù)的秘訣。我們下面來分析
    boolean added = commandQueue.offer(new AppendCommand(span));

    if (!added) {
      metrics.reporterDropped.inc(1);
    }
  }
  public interface Command {
    void execute() throws SenderException;
  }
  class AppendCommand implements Command {
    private final JaegerSpan span;

    public AppendCommand(JaegerSpan span) {
      this.span = span;
    }

    @Override
    public void execute() throws SenderException {
      // 單純地委托給send的append方法
      sender.append(span);
    }
  }
/**
* 刷新命令
**/
  class FlushCommand implements Command {
    @Override
    public void execute() throws SenderException {
      int n = sender.flush();
      metrics.reporterSuccess.inc(n);
    }
  }
/**
* 阻塞隊(duì)列消費(fèi)者刑棵,不斷地從隊(duì)列中獲取命令巴刻,然后執(zhí)行Command
**/
class QueueProcessor implements Runnable {
    private boolean open = true;

    @Override
    public void run() {
      while (open) {
        try {
          RemoteReporter.Command command = commandQueue.take();

          try {
            command.execute();
          } catch (SenderException e) {
            metrics.reporterFailure.inc(e.getDroppedSpanCount());
          }
        } catch (InterruptedException e) {
          log.error("QueueProcessor error:", e);
          // Do nothing, and try again on next span.
        }
      }
    }

    public void close() {
      open = false;
    }
  }
}

我們可以看到,兩種不同的Command實(shí)際上就是調(diào)用Sender的不同方法

  • AppendCommand調(diào)用Sender的append方法
  • FlushCommand調(diào)用Sender的flush方法
  1. 下面我們來看一下Sender的兩個(gè)重要方法append和fush
public abstract class ThriftSender extends ThriftSenderBase implements Sender {
     @Override
  public int append(JaegerSpan span) throws SenderException {
    if (process == null) {
      process = new Process(span.getTracer().getServiceName());
      process.setTags(JaegerThriftSpanConverter.buildTags(span.getTracer().tags()));
      processBytesSize = calculateProcessSize(process);
      byteBufferSize += processBytesSize;
    }

    io.jaegertracing.thriftjava.Span thriftSpan = JaegerThriftSpanConverter.convertSpan(span);
    int spanSize = calculateSpanSize(thriftSpan);
    // 單個(gè)Span過大就報(bào)錯(cuò)蛉签,并且丟棄這個(gè)Span
    if (spanSize > getMaxSpanBytes()) {
      throw new SenderException(String.format("ThriftSender received a span that was too large, size = %d, max = %d",
          spanSize, getMaxSpanBytes()), null, 1);
    }

    byteBufferSize += spanSize;
    // 如果當(dāng)前的byteBufferSize 小于等于maxSpanBytes小胡陪,則直接加入緩沖區(qū),然后更新一下byteBufferSize 
   // 如果當(dāng)前的byteBufferSize 大于maxSpanBytes碍舍,則批量發(fā)送數(shù)據(jù)
   
    if (byteBufferSize <= getMaxSpanBytes()) {
      spanBuffer.add(thriftSpan);
      if (byteBufferSize < getMaxSpanBytes()) {
        return 0;
      }
      return flush();
    }

    int n;
    try {
      n = flush();
    } catch (SenderException e) {
      // +1 for the span not submitted in the buffer above
      throw new SenderException(e.getMessage(), e.getCause(), e.getDroppedSpanCount() + 1);
    }

    spanBuffer.add(thriftSpan);
    byteBufferSize = processBytesSize + spanSize;
    return n;
  }
  @Override
  public int flush() throws SenderException {
    if (spanBuffer.isEmpty()) {
      return 0;
    }

    int n = spanBuffer.size();
    try {
      // 抽象方法柠座,由具體的協(xié)議發(fā)送者實(shí)現(xiàn)(如udp,Http)
      send(process, spanBuffer);
    } catch (SenderException e) {
      throw new SenderException("Failed to flush spans.", e, n);
    } finally {
      // 發(fā)送完之后清空緩存區(qū)和重置緩沖區(qū)大小
      spanBuffer.clear();
      byteBufferSize = processBytesSize;
    }
    return n;
  }

}

從Sender的框架實(shí)現(xiàn)看,如果在發(fā)送的過程報(bào)錯(cuò)了片橡,也不會(huì)重試的妈经。

從Jaeger的實(shí)現(xiàn)來看,到處都對(duì)Span充斥著冷酷無(wú)情啊捧书,能丟就丟吹泡,毫不猶豫。其實(shí)Span也不能怪別人经瓷,因?yàn)閺乃錾捅粧焐狭丝蓙G的標(biāo)簽了爆哑。估計(jì)也只有日志這個(gè)哥們跟它是難兄難弟了,都是可丟棄的舆吮。

3."安全"關(guān)閉

前面我們講到阻塞隊(duì)列中的Command其實(shí)有三種揭朝。第三種其實(shí)是為平滑停機(jī)準(zhǔn)備的。第三種Command代碼如下

  class CloseCommand implements Command {
    @Override
    public void execute() throws SenderException {
      queueProcessor.close();
    }
  }
  class QueueProcessor implements Runnable {
    private boolean open = true;

    @Override
    public void run() {
      while (open) {
        try {
         // 執(zhí)行命令
      }
    }

    public void close() {
      open = false;
    }
  }

其實(shí)很簡(jiǎn)單歪泳,就是一旦要關(guān)閉萝勤,就直接不從隊(duì)列中拿數(shù)據(jù)了。那隊(duì)列中的數(shù)據(jù)怎們辦呐伞?怎們辦敌卓,丟啊。

4.總結(jié)

從源代碼的分析過程中伶氢,大家應(yīng)該感覺到了趟径,各個(gè)Tracer組件對(duì)Span是多么的無(wú)情啊。我們?cè)倩剡^頭來癣防,看一下需求

  • 占用的資源不能太多蜗巧。這其中包括線程資源,內(nèi)存資源等蕾盯。
    主要占用兩個(gè)線程資源幕屹,以及一個(gè)固定大小的隊(duì)列內(nèi)存資源
  • 可以定時(shí)將內(nèi)存中的數(shù)據(jù)保存起來。
    后臺(tái)啟動(dòng)Timer定時(shí)刷新
  • 可以批量保存數(shù)據(jù),提升性能望拖。
    Sender中緩沖區(qū)渺尘,可以批量發(fā)送數(shù)據(jù)
  • 參數(shù)可配置化
    隊(duì)列大小以及刷新間隔可以配置
  • 最重要的一點(diǎn),無(wú)論如何不能影響主業(yè)務(wù)说敏。
    采用異步隊(duì)列鸥跟,異步線程的方式來保證性能以及不影響業(yè)務(wù)。甚至可以使用UDPSender來保證即使服務(wù)端宕機(jī)了也不會(huì)影響客戶端
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末盔沫,一起剝皮案震驚了整個(gè)濱河市医咨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌架诞,老刑警劉巖拟淮,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異侈贷,居然都是意外死亡惩歉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門俏蛮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來撑蚌,“玉大人,你說我怎么就攤上這事搏屑≌浚” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵辣恋,是天一觀的道長(zhǎng)亮垫。 經(jīng)常有香客問我,道長(zhǎng)伟骨,這世上最難降的妖魔是什么饮潦? 我笑而不...
    開封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮携狭,結(jié)果婚禮上继蜡,老公的妹妹穿的比我還像新娘。我一直安慰自己逛腿,他們只是感情好稀并,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著单默,像睡著了一般碘举。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搁廓,一...
    開封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天引颈,我揣著相機(jī)與錄音耕皮,去河邊找鬼。 笑死线欲,一個(gè)胖子當(dāng)著我的面吹牛明场,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播李丰,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼逼泣!你這毒婦竟也來了趴泌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拉庶,失蹤者是張志新(化名)和其女友劉穎嗜憔,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體氏仗,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吉捶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了皆尔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呐舔。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖慷蠕,靈堂內(nèi)的尸體忽然破棺而出珊拼,到底是詐尸還是另有隱情,我是刑警寧澤流炕,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布澎现,位于F島的核電站,受9級(jí)特大地震影響每辟,放射性物質(zhì)發(fā)生泄漏剑辫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一渠欺、第九天 我趴在偏房一處隱蔽的房頂上張望妹蔽。 院中可真熱鬧,春花似錦峻堰、人聲如沸讹开。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)旦万。三九已至,卻和暖如春镶蹋,著一層夾襖步出監(jiān)牢的瞬間成艘,已是汗流浹背赏半。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留淆两,地道東北人断箫。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像秋冰,于是被迫代替她去往敵國(guó)和親仲义。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容