分布式链路追踪系列番外篇一(jaeger异步批量发送span)

Jaeger 提供了一整套的分布式链路追踪方案,也是最早实现Opentracing协议的框架之一。今天我们来简单分析一下java客户端的Span异步发送机制

1.异步发送需求简述

我们知道,在Java应用程序中,如果有一些数据不是特别重要,但是又产生得比较多的时候,我们在为了保证程序性能的情况下就会选择采用异步的方式来保存和发送数据。比如日志,Trace数据就是属于这一类的数据。所以这一类的数据最适合暂时保存在内存中然后异步存储。

那么要实现这类异步的需求有什么具体的要求呢?

我觉得至少应该实现以下几点

  • 占用的资源不能太多。这其中包括线程资源,内存资源等。
  • 可以定时将内存中的数据保存起来。
  • 可以批量保存数据,提升性能。
  • 参数可配置化
  • 最重要的一点,无论如何不能影响主业务。

2.Jaeger 实现原理分析

少废话,先上原理图,所谓一图胜千言。


jaeger异步发送机制

下面我们来简单分析流程
1.在每一个线程中都会产生很多Span数据,当Span结束的时候会调用Tracer对象的reportSpans方法,然后Tracer就会委托给Reporter对象来发送Span数据。

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;
    }
    // 只有需要采样的时候才发送Span
    if (context.isSampled()) {
      // 委托给Tracer发送
      tracer.reportSpan(this);
    }
  }
// other functions
// ....
}
public class JaegerTracer implements Tracer, Closeable{
  void reportSpan(JaegerSpan span) {
     // 委托给Reporter发送
    reporter.report(span);
    metrics.spansFinished.inc(1);
  }
  // other functions
  // ....
}

2.然后呢,Reporter对象比较会骗人,它实际上呢,并没有真正的发送给出去,而是将Span数据包裹一下,以Command的形式加到了线程安全的阻塞队列中。然后Reporter对象又开启了两个异步线程:

  • jaeger.RemoteReporter-QueueProcessor 负责不断地消费阻塞对象的Command对象,然后丢给Sender的缓冲区 。

  • jaeger.RemoteReporter-FlushTimer 定时地发送FlushCommand,然后让Sender Flush自己的缓存区

所以,我们看到队列中有多种Command,如果队列画成图的话,就类似于下面这个样子。


block queue.png

其实还有另外一种Command(CloseCommand),后面我们再讲

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方法。如果超过了队列大小,那么就会丢弃后来的span数据。
    // 而且这里不是简单地把span加入到队列中,而是用Command包装了一下。这就是实现定时flush数据的秘诀。我们下面来分析
    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);
    }
  }
/**
* 阻塞队列消费者,不断地从队列中获取命令,然后执行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实际上就是调用Sender的不同方法

  • AppendCommand调用Sender的append方法
  • FlushCommand调用Sender的flush方法
  1. 下面我们来看一下Sender的两个重要方法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);
    // 单个Span过大就报错,并且丢弃这个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;
    // 如果当前的byteBufferSize 小于等于maxSpanBytes小,则直接加入缓冲区,然后更新一下byteBufferSize 
   // 如果当前的byteBufferSize 大于maxSpanBytes,则批量发送数据
   
    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 {
      // 抽象方法,由具体的协议发送者实现(如udp,Http)
      send(process, spanBuffer);
    } catch (SenderException e) {
      throw new SenderException("Failed to flush spans.", e, n);
    } finally {
      // 发送完之后清空缓存区和重置缓冲区大小
      spanBuffer.clear();
      byteBufferSize = processBytesSize;
    }
    return n;
  }

}

从Sender的框架实现看,如果在发送的过程报错了,也不会重试的。

从Jaeger的实现来看,到处都对Span充斥着冷酷无情啊,能丢就丢,毫不犹豫。其实Span也不能怪别人,因为从它出生就被挂上了可丢的标签了。估计也只有日志这个哥们跟它是难兄难弟了,都是可丢弃的。

3."安全"关闭

前面我们讲到阻塞队列中的Command其实有三种。第三种其实是为平滑停机准备的。第三种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 {
         // 执行命令
      }
    }

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

其实很简单,就是一旦要关闭,就直接不从队列中拿数据了。那队列中的数据怎们办?怎们办,丢啊。

4.总结

从源代码的分析过程中,大家应该感觉到了,各个Tracer组件对Span是多么的无情啊。我们再回过头来,看一下需求

  • 占用的资源不能太多。这其中包括线程资源,内存资源等。
    主要占用两个线程资源,以及一个固定大小的队列内存资源
  • 可以定时将内存中的数据保存起来。
    后台启动Timer定时刷新
  • 可以批量保存数据,提升性能。
    Sender中缓冲区,可以批量发送数据
  • 参数可配置化
    队列大小以及刷新间隔可以配置
  • 最重要的一点,无论如何不能影响主业务。
    采用异步队列,异步线程的方式来保证性能以及不影响业务。甚至可以使用UDPSender来保证即使服务端宕机了也不会影响客户端
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容