SOFATracer 中 Disruptor 實踐

glmapper發表於2018-08-04

OpenTraceing 規範

SOFATracer 對 OpenTraceing 的實現

SOFATracer 就是根據 OpenTracing 規範 衍生出來的分散式 鏈路跟 蹤的解決方案。

概念

OpenTracing 標準中有三個重要的相互關聯的型別,分別是Tracer, SpanSpanContext

【下面的概念說明過程中,如不做說明,所使用的案例程式碼均以SOFATracer中的實現為例。】

Tracer

一個 trace 代表一個潛在的,分散式的,存在並行資料或並行執行軌跡(潛在的分散式、並行)的系統。一個trace可以認為是多個span的有向無環圖(DAG)。

Tracer介面用來建立Span,以及處理如何處理Inject(serialize) 和 Extract (deserialize),用於跨程式邊界傳遞。

SOFATracerSofaTracer這個類實現了 opentracingTracer 介面,並在此規範介面上做了一些擴充套件。看下Tracer 中宣告的方法:

public interface Tracer {
    //啟動一個新的span
    SpanBuilder buildSpan(String operationName);
    //將SpanContext上下文Inject(注入)到carrier
    <C> void inject(SpanContext spanContext, Format<C> format, C carrier);
    //將SpanContext上下文從carrier中Extract(提取)
    <C> SpanContext extract(Format<C> format, C carrier);   
    
    interface SpanBuilder {
    // 省略
    } 
}
複製程式碼

所以從介面定義來看,要實現一個Tracer,必須要實現其以下的幾個能力:

啟動一個新的span

SOFATracer 實現了 TracerbuildSpan 方法:

@Override
public SpanBuilder buildSpan(String operationName) {
    return new SofaTracerSpanBuilder(operationName);
}
複製程式碼

operationName :操作名稱,字串型別,表示由Span完成的工作 (例如,RPC方法名稱、函式名稱或一個較大的計算任務中的階段的名稱)。操作名稱應該用泛化的字串形式標識出一個Span例項。

何為泛化的字串形式,比如現在有一個操作:獲取使用者 ;下面有幾種標識方式:

  • 1、/get
  • 2、/get/user
  • 3、/get/user/123

方式1過於抽象,方式3過於具體。方式2是正確的操作名。

將SpanContext上下文Inject(注入)到carrier

@Override
public <C> void inject(SpanContext spanContext, Format<C> format, C carrier) {
    RegistryExtractorInjector<C> registryInjector = TracerFormatRegistry.getRegistry(format);
    if (registryInjector == null) {
        throw new IllegalArgumentException("Unsupported injector format: " + format);
    }
    registryInjector.inject((SofaTracerSpanContext) spanContext, carrier);
}
複製程式碼
  • SpanContext :例項
  • format(格式化)描述,一般會是一個字串常量,但不做強制要求。通過此描述,通知Tracer實現,如何對SpanContext進行編碼放入到carrier中。 carrier,根據format確定。Tracer實現根據format宣告的格式,將SpanContext序列化到carrier物件中。

RegistryExtractorInjector 見後面

將SpanContext上下文從carrier中Extract(提取)

@Override
public <C> SpanContext extract(Format<C> format, C carrier) {
    RegistryExtractorInjector<C> registryExtractor = TracerFormatRegistry.getRegistry(format);
    if (registryExtractor == null) {
        throw new IllegalArgumentException("Unsupported extractor format: " + format);
    }
    return registryExtractor.extract(carrier);
}
複製程式碼
  • 格式描述符(format descriptor)(通常但不一定是字串常量),告訴Tracer的實現如何在載體物件中對SpanContext進行編碼
  • 載體(carrier),其型別由格式描述符指定。Tracer的實現將根據格式描述對此載體物件中的SpanContext進行編碼

返回一個SpanContext例項,可以使用這個SpanContext例項,通過Tracer建立新的Span

Format

Tracer的注入和提取來看,format都是必須的。

Inject(注入)和Extract(提取)依賴於可擴充套件的format引數。format引數規定了另一個引數"carrier"的型別,同時約束了"carrier"SpanContext是如何編碼的。所有的Tracer實現,都必須支援下面的format

  • Text Map: 基於字串:字串的map,對於keyvalue不約束字符集。
  • HTTP Headers: 適合作為HTTP頭資訊的,基於字串:字串的map。(RFC 7230.在工程實踐中,如何處理HTTP頭具有多樣性,強烈建議tracer的使用者謹慎使用HTTP頭的鍵值空間和轉義符)
  • Binary: 一個簡單的二進位制大物件,記錄SpanContext的資訊。

在上面的注入和提取程式碼中,有如下程式碼片段:

//注入
RegistryExtractorInjector<C> registryInjector  = 
    TracerFormatRegistry.getRegistry(format);
//提取
RegistryExtractorInjector<C> registryExtractor = 
    TracerFormatRegistry.getRegistry(format);
複製程式碼

來通過TracerFormatRegistry這個類來來看下 SOFATracer 中的 Format 的具體實現。

X-B3

在看Format之前,先了解下X-B3

Access-Control-Expose-Headers: 
X-B3-TraceId,X-B3-ParentSpanId,X-B3-SpanId
複製程式碼

HTTP請求時其span引數通過http headers來傳遞追蹤資訊;header中對應的key分別是:

  • X-B3-TraceId: 64 encoded bits(id被encode為hex Strings)
  • X-B3-SpanId : 64 encoded bits
  • X-B3-ParentSpanId: 64 encoded bits
  • X-B3-Sampled:(是否取樣) Boolean (either “1” or “0”)(下面的呼叫是否進行取樣)
  • X-B3-Flags:a Long

SOFATracer 中的 Format

具體程式碼在 tracer-core -> com.alipay.common.tracer.core.registy 包下:

  • TextMapFormatter
  • TextMapB3Formatter
  • HttpHeadersFormatter
  • HttpHeadersB3Formatter
  • BinaryFormater

BinaryFormater:這個的注入和提取實現沒有編解碼一說;本身就是基於二進位制流的操作。

TextMapB3Formatter/TextMapFormatterHttpHeadersB3Formatter/HttpHeadersFormatter 區別就在於編解碼不同。HttpHeadersB3Formatter使用的是 URLDecoder.decode && URLDecoder.encode ; TextMapB3Formatter 返回的是值本身(如果為空或者null則返回空字串)。

TextMapFormatterTextMapB3Formatter區別在於注入或者提取是使用的key不用。TextMapB3Formatter中使用的是 x-b3-{} 的字串作為key

Span

一個span代表系統中具有開始時間和執行時長的邏輯執行單元。span之間通過巢狀或者順序排列建立邏輯因果關係。當Span結束後(span.finish()),除了通過Span獲取SpanContext外,下列其他所有方法都不允許被呼叫。

同樣先來看下opentracing規範api 定義的 span 的定義及方法:

public interface Span extends Closeable {
    SpanContext context();
    void finish();
    void finish(long finishMicros);
    void close();
    Span setTag(String key, String value);
    Span setTag(String key, boolean value);
    Span setTag(String key, Number value);
    Span log(Map<String, ?> fields);
    Span log(long timestampMicroseconds, Map<String, ?> fields);
    Span log(String event);
    Span log(long timestampMicroseconds, String event);
    Span setBaggageItem(String key, String value);
    String getBaggageItem(String key);
    Span setOperationName(String operationName);
    Span log(String eventName, /* @Nullable */ Object payload);
    Span log(long timestampMicroseconds, String eventName, /* @Nullable */ Object payload);
} 
複製程式碼

通過Span獲取SpanContext

//SOFATracerSpan
@Override
public SpanContext context() {
    return this.sofaTracerSpanContext;
}
複製程式碼

返回值,Span構建時傳入的SpanContext。這個返回值在Span結束後(span.finish()),依然可以使用。

複寫操作名

@Override
public Span setOperationName(String operationName) {
    this.operationName = operationName;
    return this;
}
複製程式碼

operationName:新的操作名,覆蓋構建Span時,傳入的操作名。

結束Span

@Override
public void finish() {
    this.finish(System.currentTimeMillis());
}

@Override
public void finish(long endTime) {
    this.setEndTime(endTime);
    //關鍵記錄:report span
    this.sofaTracer.reportSpan(this);
    SpanExtensionFactory.logStoppedSpan(this);
}
複製程式碼

有一個可選引數,如果指定完成時間則使用當前指定的時間;如果省略此引數,使用當前時間作為完成時間。finish方法中會將當前span進行report操作。

為Span設定tag

Tag是一個key:value格式的資料。key必須是String型別,value可以是字串、布林或者數字

  • 字串型別的value 設定tag
@Override
public Span setTag(String key, String value) {
    if (StringUtils.isBlank(key) || StringUtils.isBlank(value)) {
        return this;
    }
    this.tagsWithStr.put(key, value);
    //注意:server 還是 client 在 OpenTracing 標準中是用 tags 標識的,所以在這裡進行判斷
    if (isServer()) {
        Reporter serverReporter = this.sofaTracer.getServerReporter();
        if (serverReporter != null) {
            this.setLogType(serverReporter.getReporterType());
        }
    } else if (isClient()) {
        Reporter clientReporter = this.sofaTracer.getClientReporter();
        if (clientReporter != null) {
            this.setLogType(clientReporter.getReporterType());
        }
    }
    return this;
}
複製程式碼
  • 布林型別的value 設定tag
public Span setTag(String key, boolean value) {
    this.tagsWithBool.put(key, value);
    return this;
}
複製程式碼
  • 數字型別的value 設定tag
public Span setTag(String key, Number number) {
    if (number == null) {
        return this;
    }
    this.tagsWithNumber.put(key, number);
    return this;
}
複製程式碼

Log結構化資料

@Override
public Span log(long currentTime, Map<String, ?> map) {
    AssertUtils.isTrue(currentTime >= startTime, "current time must greater than start time");
    this.logs.add(new LogData(currentTime, map));
    return this;
}

@Override
public Span log(Map<String, ?> map) {
    return this.log(System.currentTimeMillis(), map);
}
複製程式碼
  • Map<String, ?> map : 鍵必須是字串型別,值可以是任意型別
  • currentTime : 時間戳。如果指定時間戳,那麼它必須在span的開始和結束時間之內。

設定一個baggage(隨行資料)元素

Baggage元素是一個鍵值對集合,將這些值設定給給定的SpanSpanSpanContext,以及所有和此Span有直接或者間接關係的本地Span。 也就是說,baggage元素隨trace一起保持在帶內傳遞。(譯者注:帶內傳遞,在這裡指,隨應用程式呼叫過程一起傳遞)

Baggage元素為OpenTracing的實現全棧整合,提供了強大的功能 (例如:任意的應用程式資料,可以在移動端建立它,顯然的,它會一直傳遞了系統最底層的儲存系統。由於它如此強大的功能,他也會產生巨大的開銷,請小心使用此特性。

再次強調,請謹慎使用此特性。每一個鍵值都會被拷貝到每一個本地和遠端的下級相關的span中,因此,總體上,他會有明顯的網路和CPU開銷。

@Override
public Span setBaggageItem(String key, String value) {
    this.sofaTracerSpanContext.setBizBaggageItem(key, value);
    return this;
}
複製程式碼

SofaTracerSpan 中的屬性

  • sofaTracer  : 當前 tracer
  • spanReferences : 當前span的關係,ChildOf(引用) or FollowsFrom(跟隨)
  • tagsWithStr : String 型別的tag 集合
  • tagsWithBool : 布林型別的tag集合
  • tagsWithNumber : 數值型別的tag集合
  • logs : log結構化資料列表,通過span.log(map)操作的map,均儲存在logs中。
  • operationName:當前span的操作名
  • sofaTracerSpanContext:當前 spanContext
  • startTime : 當前span 開始時間
  • endTime : 當前span 結束時間,在finish方法中傳入。
  • logType : report時才有意義:摘要日誌型別,日誌能夠正確列印的關鍵資訊;當前 span 的日誌型別,如:客戶端為 rpc-client-digest.log,服務端為 rpc-server-digest.log
  • parentSofaTracerSpan:父親 span,當作為客戶端結束並彈出執行緒上下文時,需要將父親 span 再放入

SpanContext

opentracingSpanContext 介面中只有一個baggageItems方法,通過這個方法來遍歷所有的baggage元素。

public interface SpanContext {
    Iterable<Map.Entry<String, String>> baggageItems();
}
複製程式碼

相對於OpenTracing中其他的功能,SpanContext更多的是一個“概念”。也就是說,OpenTracing實現中,需要重點考慮,並提供一套自己的API

OpenTracing的使用者僅僅需要,在建立span、向傳輸協議Inject(注入)和從傳輸協議中Extract(提取)時,使用SpanContextreferences

OpenTracing要求,SpanContext是不可變的,目的是防止由於Span的結束和相互關係,造成的複雜生命週期問題。

Disruptor 簡介

A High Performance Inter-Thread Messaging Library 高效能的執行緒間訊息傳遞庫

關於 Disruptor 的 一些原理分析可以參考:disruptor

案例

先通過 Disruptor 的一個小例子來有個直觀的認識;先看下它的建構函式:

public Disruptor(
        final EventFactory<T> eventFactory,
        final int ringBufferSize,
        final ThreadFactory threadFactory,
        final ProducerType producerType,
        final WaitStrategy waitStrategy)
{
    this(
        RingBuffer.create(producerType, eventFactory, ringBufferSize, waitStrategy),
        new BasicExecutor(threadFactory));
}
複製程式碼
  • eventFactory : 在環形緩衝區中建立事件的 factory
  • ringBufferSize:環形緩衝區的大小,必須是2的冪。
  • threadFactory:用於為處理器建立執行緒。
  • producerType:生成器型別以支援使用正確的sequencerpublisher建立RingBuffer;列舉型別,SINGLEMULTI兩個項。對應於 SingleProducerSequencerMultiProducerSequencer兩種Sequencer
  • waitStrategy : 等待策略;

如果我們想構造一個disruptor,那麼我們就需要上面的這些元件。從eventFactory來看,還需要一個具體的Event來作為訊息事件的載體。【下面按照官方給的案例進行簡單的修改作為示例】

訊息事件 LongEvent ,能夠被消費的資料載體

public class LongEvent {
    private long value;
    public void set(long value) {
        this.value = value;
    }
    public long getValue() {
        return value;
    }
}
複製程式碼

建立訊息事件的factory

public class LongEventFactory implements EventFactory<LongEvent> {
    @Override
    public LongEvent newInstance() {
        return new LongEvent();
    }
}
複製程式碼

ConsumerThreadFactory

public class ConsumerThreadFactory implements ThreadFactory {
    private final AtomicInteger index = new AtomicInteger(1);
    @Override
    public Thread newThread(Runnable r) {
        return new Thread(r, "disruptor-thread-" + index.getAndIncrement());
    }
}
複製程式碼

OK ,上面的這些可以滿足建立一個disruptor了:

private int ringBufferCapacity = 8;
//訊息事件生產Factory
LongEventFactory longEventFactory = new LongEventFactory();
//執行事件處理器執行緒Factory
ConsumerThreadFactory consumerThreadFactory = new ConsumerThreadFactory();
//用於環形緩衝區的等待策略。
WaitStrategy waitStrategy = new BlockingWaitStrategy();

//構建disruptor
Disruptor<LongEvent> disruptor = new Disruptor<>(
    longEventFactory,
    ringBufferCapacity,
    longEventThreadFactory,
    ProducerType.SINGLE,
    waitStrategy);
複製程式碼

現在是已經有了 disruptor 了,然後通過:start 來啟動:

//啟動 disruptor
 disruptor.start();
複製程式碼

到這裡,已經構建了一個disruptor;但是目前怎麼使用它來發布訊息和消費訊息呢?

釋出訊息

下面在 for 迴圈中 釋出 5 條資料:

RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();
for (long l = 0; l < 5; l++)
{
    long sequence = ringBuffer.next();
    LongEvent event = ringBuffer.get(sequence);
    event.set(100+l);
    System.out.println("publish event :" + l);
    ringBuffer.publish(sequence);
    Thread.sleep(1000);
}
複製程式碼

訊息已經發布,下面需要設定當前disruptor的消費處理器。前面已經有個LongEventEventFactory ; 在disruptor中是通過 EventHandler 來進行訊息消費的。

編寫消費者程式碼

public class LongEventHandler implements EventHandler<LongEvent> {
    @Override
    public void onEvent(LongEvent event, long sequence, boolean endOfBatch) throws Exception {
        System.out.println("Event: " + event.getValue()+" -> " + Thread.currentThread().getName());
        Thread.sleep(2000);
    }
}
複製程式碼

eventHandler 設定到 disruptor 的處理鏈上

//將處理事件的事件處理程式 -> 消費事件的處理程式
LongEventHandler longEventHandler = new LongEventHandler();
disruptor.handleEventsWith(longEventHandler);
複製程式碼

執行結果(這裡):

publish event :0
Event: 0 -> disruptor-thread-1
-------------------------------->
publish event :1
Event: 1 -> disruptor-thread-1
-------------------------------->
publish event :2
Event: 2 -> disruptor-thread-1
-------------------------------->
publish event :3
Event: 3 -> disruptor-thread-1
-------------------------------->
publish event :4
Event: 4 -> disruptor-thread-1
-------------------------------->
複製程式碼

基本概念和原理

Disruptor

整個基於ringBuffer實現的生產者消費者模式的容器。主要屬性

private final RingBuffer<T> ringBuffer;
private final Executor executor;
private final ConsumerRepository<T> consumerRepository = new ConsumerRepository<>();
private final AtomicBoolean started = new AtomicBoolean(false);
private ExceptionHandler<? super T> exceptionHandler = new ExceptionHandlerWrapper<>();
複製程式碼
  • ringBuffer:內部持有一個 RingBuffer 物件,Disruptor 內部的事件釋出都是依賴這個RingBuffer物件完成的。
  • executor:消費事件的執行緒池
  • consumerRepository:提供儲存庫機制,用於將EventHandlerEventProcessor關聯起來
  • started : 用於標誌當前Disruptor是否已經啟動
  • exceptionHandler : 異常處理器,用於處理BatchEventProcessor事件週期中 uncaught exceptions

RingBuffer

環形佇列[實現上是一個陣列],可以類比為BlockingQueue之類的佇列,ringBuffer的使用,使得記憶體被迴圈使用,減少了某些場景的記憶體分配回收擴容等耗時操作。

public final class RingBuffer<E> extends RingBufferFields<E> 
implements Cursored, EventSequencer<E>, EventSink<E> 
複製程式碼
  • E:在事件的交換或並行協調期間儲存用於共享的資料的實現 -> 訊息事件

Sequencer

RingBuffer 中 生產者的頂級父介面,其直接實現有SingleProducerSequencerMultiProducerSequencer;對應 SINGLEMULTI 兩個列舉值。

SOFATracer 中 Disruptor 實踐

EventHandler

事件處置器,改介面用於對外擴充套件來實現具體的消費邏輯。如上面 demo 中的 LongEventHandler ;

//回撥介面,用於處理{@link RingBuffer}中可用的事件
public interface EventHandler<T> {
    void onEvent(T event, long sequence, boolean endOfBatch) throws Exception;
}
複製程式碼
  • event : RingBuffer 已經發布的事件
  • sequence : 正在處理的事件 的序列號
  • endOfBatch : 用來標識否是來自 RingBuffer 的批次中的最後一個事件

SequenceBarrier

消費者路障。規定了消費者如何向下走。事實上,該路障算是變向的鎖。

final class ProcessingSequenceBarrier implements SequenceBarrier {
    //當等待(探測)的需要不可用時,等待的策略
    private final WaitStrategy waitStrategy;
    //依賴的其它Consumer的序號,這個用於依賴的消費的情況,
    //比如A、B兩個消費者,只有A消費完,B才能消費。
    private final Sequence     dependentSequence;
    private volatile boolean   alerted = false;
    //Ringbuffer的寫入指標
    private final Sequence     cursorSequence;
    //RingBuffer對應的Sequencer
    private final Sequencer    sequencer;
    //exclude method
}
複製程式碼

waitStrategy 決定了消費者採用何種等待策略。

WaitStrategy

Strategy employed for making {@link EventProcessor}s wait on a cursor {@link Sequence}.

EventProcessor 的等待策略;具體實現在 disruptor 中有8種,

SOFATracer 中 Disruptor 實踐

這些等待策略不同的核心體現是在如何實現 waitFor 這個方法上。

EventProcessor

事件處理器,實際上可以理解為消費者模型的框架,實現了執行緒Runnablerun方法,將迴圈判斷等操作封在了裡面。該介面有三個實現類:

1、BatchEventProcessor

public final class BatchEventProcessor<T> implements EventProcessor {
    private final AtomicBoolean           running          = new AtomicBoolean(false);
    private ExceptionHandler<? super T>   exceptionHandler = new FatalExceptionHandler();
    private final DataProvider<T>         dataProvider;
    private final SequenceBarrier         sequenceBarrier;
    private final EventHandler<? super T> eventHandler;
    private final Sequence                sequence         = new Sequence(                                      Sequencer.INITIAL_CURSOR_VALUE);
    private final TimeoutHandler          timeoutHandler;
    //exclude method
}
複製程式碼
  • ExceptionHandler:異常處理器
  • DataProvider:資料來源,對應 RingBuffer
  • EventHandler:處理 Event 的回撥物件
  • SequenceBarrier:對應的序號屏障
  • TimeoutHandler:超時處理器,預設情況為空,如果要設定,只需要要將關聯的EventHandler實現TimeOutHandler即可。

如果我們選擇使用 EventHandler 的時候,預設使用的就是 BatchEventProcessor,它與EventHandler是一一對應,並且是單執行緒執行。

如果某個RingBuffer有多個BatchEventProcessor,那麼就會每個BatchEventProcessor對應一個執行緒。

2、WorkProcessor

public final class WorkProcessor<T> implements EventProcessor {
    private final AtomicBoolean running = new AtomicBoolean(false);
    private final Sequence sequence = new Sequence(Sequencer.INITIAL_CURSOR_VALUE);
    private final RingBuffer<T> ringBuffer;
    private final SequenceBarrier  sequenceBarrier;
    private final WorkHandler<? super T> workHandler;
    private final ExceptionHandler<? super T> exceptionHandler;
    private final Sequence workSequence;

    private final EventReleaser eventReleaser = new EventReleaser() {
            @Override
            public void release() {
                sequence.set(Long.MAX_VALUE);
            }
    };
    private final TimeoutHandler timeoutHandler;
}
複製程式碼

基本和 BatchEventProcessor 類似,不同在於,用於處理Event的回撥物件是WorkHandler

原理圖

SOFATracer 中 Disruptor 實踐

無消費者情況下,生產者保持生產,但是 remainingCapacity 保持不變

在寫demo的過程中,本來想通過不設定 消費者 來觀察 RingBuffer 可用容量變化的。但是驗證過程中,一直得不到預期的結果,(注:沒有設定消費者,只有生產者),先看結果:

publish event :0
bufferSie:8
remainingCapacity:8
cursor:0
-------------------------------->
publish event :1
bufferSie:8
remainingCapacity:8
cursor:1
-------------------------------->
publish event :2
bufferSie:8
remainingCapacity:8
cursor:2
-------------------------------->
publish event :3
bufferSie:8
remainingCapacity:8
cursor:3
-------------------------------->
publish event :4
bufferSie:8
remainingCapacity:8
cursor:4
-------------------------------->
publish event :5
bufferSie:8
remainingCapacity:8
cursor:5
-------------------------------->
publish event :6
bufferSie:8
remainingCapacity:8
cursor:6
-------------------------------->
publish event :7
bufferSie:8
remainingCapacity:8
cursor:7
-------------------------------->
publish event :8
bufferSie:8
remainingCapacity:8
cursor:8
-------------------------------->
publish event :9
bufferSie:8
remainingCapacity:8
cursor:9
-------------------------------->
複製程式碼

從結果來看,remainingCapacity 的值應該隨著 釋出的數量 遞減的;但是實際上它並沒有發生任何變化。

來看下ringBuffer.remainingCapacity() 這個方法:

/**
 * Get the remaining capacity for this ringBuffer.
 *
 * @return The number of slots remaining.
 */
public long remainingCapacity()
{
    return sequencer.remainingCapacity();
}
複製程式碼

這裡面又使用 sequencer.remainingCapacity()這個方法來計算的。上面的例子中使用的是ProducerType.SINGLE,那來看SingleProducerSequencer 這個裡面remainingCapacity的實現。

@Override
public long remainingCapacity()
{
    //上次申請完畢的序列值
    long nextValue = this.nextValue;
    //計算當前已經消費到的序列值
    long consumed = Util.getMinimumSequence(gatingSequences, nextValue);
    //當前生產到的序列值
    long produced = nextValue;
    return getBufferSize() - (produced - consumed);
}
複製程式碼

來解釋下這段程式碼的含義:

假設當前 ringBufferbufferSize 是 8 ;上次申請到的序列號是 5,其實也就是說已經生產過佔用的序列號是5;假設當前已經消費到的序列號是 3,那麼剩餘的容量為: 8-(5-2) = 5;

SOFATracer 中 Disruptor 實踐

因為這裡我們可以確定 bufferSizeproduced 的值了,那麼 remainingCapacity 的結果就取決於getMinimumSequence的計算結果了。

public static long getMinimumSequence(final Sequence[] sequences, long minimum)
{
    for (int i = 0, n = sequences.length; i < n; i++)
    {
        long value = sequences[i].get();
        minimum = Math.min(minimum, value);
    }
    return minimum;
}
複製程式碼

這個方法是從 Sequence 陣列中獲取最小序列 。如果sequences 為空,則返回 minimum。回到上一步,看下sequences這個陣列是從哪裡過來的,它的值在哪裡設定的。

long consumed = Util.getMinimumSequence(gatingSequences, nextValue);
複製程式碼

gatingSequencesSingleProducerSequencer父類 AbstractSequencer 中的成員變數:

protected volatile Sequence[] gatingSequences = new Sequence[0];
複製程式碼

gatingSequences 是在下面這個方法裡面來管理的。

/**
 * @see Sequencer#addGatingSequences(Sequence...)
 */
@Override
public final void addGatingSequences(Sequence... gatingSequences)
{
    SequenceGroups.addSequences(this, SEQUENCE_UPDATER, this, gatingSequences);
}
複製程式碼

這個方法的呼叫棧向前追溯有這幾個地方呼叫了:

SOFATracer 中 Disruptor 實踐

WorkerPool來管理多個消費者;hangdlerEventsWith 這個方法也是用來設定消費者的。但是在上面的測試案例中我們是想通過不設定消費者 只設定生成者 來觀察 環形佇列的佔用情況,所以gatingSequences 會一直是空的,因此在計算時會把 produced 的值作為 minimum 返回。這樣每次計算就相當於:

return getBufferSize() - (produced - produced) === getBufferSize();
複製程式碼

也就驗證了為何在不設定消費者的情況下,remainingCapacity 的值會一直保持不變。

SOFATracer 中 Disruptor 實踐

SOFATracer中,AsyncCommonDigestAppenderManagerdisruptor 進行了封裝,用於處理外部元件的Tracer摘要日誌。該部分藉助 AsyncCommonDigestAppenderManager 的原始碼來分析下SOFATracer如何使用disruptor的。

SOFATracer中使用了兩種不同的事件模型,一種是SOFATracer內部使用的 StringEvent , 一種是 外部擴充套件使用的 SofaTacerSpanEvent。這裡以 SofaTacerSpanEvent 這種事件模型來分析。StringEvent 訊息事件模型對應的是 AsyncCommonAppenderManager 類封裝的disruptor

SofaTracerSpanEvent ( -> LongEvent)

定義訊息事件模型,SofaTacerSpanEvent 和 前面 demo 中的 LongEvent 基本結構是一樣的,主要是內部持有的訊息資料不同,LongEvent 中是一個long型別的資料,SofaTacerSpanEvent中持有的是 SofaTracerSpan

public class SofaTracerSpanEvent {
    private volatile SofaTracerSpan sofaTracerSpan;
    public SofaTracerSpan getSofaTracerSpan() {
        return sofaTracerSpan;
    }
    public void setSofaTracerSpan(SofaTracerSpan sofaTracerSpan) {
        this.sofaTracerSpan = sofaTracerSpan;
    }
}
複製程式碼

Consumer ( -> LongEventHandler)

ConsumerAsyncCommonDigestAppenderManager 的內部類;實現了 EventHandler 介面,這個consumer就是作為消費者存在的。

AsyncCommonAppenderManager中也有一個,這個地方個人覺得可以抽出去,這樣可以使得AsyncCommonDigestAppenderManager/AsyncCommonAppenderManager的程式碼看起來更乾淨;

private class Consumer implements EventHandler<SofaTracerSpanEvent> {
       //日誌型別集合,非該集合內的日誌型別將不會被處理
        protected Set<String> logTypes = Collections.synchronizedSet(new HashSet<String>());
        @Override
        public void onEvent(SofaTracerSpanEvent event, long sequence, boolean endOfBatch)
                                throws Exception {
            // 拿到具體的訊息資料 sofaTracerSpan
            SofaTracerSpan sofaTracerSpan = event.getSofaTracerSpan();
            // 如果沒有資料,則不做任何處理
            if (sofaTracerSpan != null) {
                try {
                    String logType = sofaTracerSpan.getLogType();
                    // 驗證當前日誌型別是否可以被當前consumer消費
                    if (logTypes.contains(logType)) {
                        // 獲取編碼型別
                        SpanEncoder encoder = contextEncoders.get(logType);
                        //獲取 appender
                        TraceAppender appender = appenders.get(logType);
                        // 對資料進行編碼處理
                        String encodedStr = encoder.encode(sofaTracerSpan);
                        if (appender instanceof LoadTestAwareAppender) {
                            ((LoadTestAwareAppender) appender).append(encodedStr,
                                TracerUtils.isLoadTest(sofaTracerSpan));
                        } else {
                            appender.append(encodedStr);
                        }
                        // 重新整理緩衝區,日誌輸出
                        appender.flush();
                    }
                } catch (Exception e) {
                   // 異常省略
                }
            }
        }

        public void addLogType(String logType) {
            logTypes.add(logType);
        }
    }
複製程式碼

SofaTracerSpanEventFactory (-> LongEventFactory)

用於產生訊息事件的 Factory

public class SofaTracerSpanEventFactory implements EventFactory<SofaTracerSpanEvent> {
    @Override
    public SofaTracerSpanEvent newInstance() {
        return new SofaTracerSpanEvent();
    }
}
複製程式碼

ConsumerThreadFactory (-> LongEventThreadFactory )

用來產生消費執行緒的 Factory

public class ConsumerThreadFactory implements ThreadFactory {
    private String workName;
    public String getWorkName() {
        return workName;
    }
    public void setWorkName(String workName) {
        this.workName = workName;
    }
    @Override
    public Thread newThread(Runnable runnable) {
        Thread worker = new Thread(runnable, "Tracer-AsyncConsumer-Thread-" + workName);
        worker.setDaemon(true);
        return worker;
    }
}
複製程式碼

構建disruptor

disruptor 的構建是在 AsyncCommonDigestAppenderManager 的建構函式中完成的。

public AsyncCommonDigestAppenderManager(int queueSize, int consumerNumber) {
    // 使用這個計算來保證realQueueSize是2的次冪(返回當前 大於等於queueSize的最小的2的次冪數 )
    int realQueueSize = 1 << (32 - Integer.numberOfLeadingZeros(queueSize - 1));
    //構建disruptor,使用的是 ProducerType.MULTI
    //等待策略是 BlockingWaitStrategy
    disruptor = new Disruptor<SofaTracerSpanEvent>(new SofaTracerSpanEventFactory(),
        realQueueSize, threadFactory, ProducerType.MULTI, new BlockingWaitStrategy());
    //消費者列表
    this.consumers = new ArrayList<Consumer>(consumerNumber);
    
    for (int i = 0; i < consumerNumber; i++) {
        Consumer consumer = new Consumer();
        consumers.add(consumer);
        //設定異常處理程式
        disruptor.setDefaultExceptionHandler(new ConsumerExceptionHandler());
        //繫結消費者
        disruptor.handleEventsWith(consumer);
    }

    //是否允許丟棄,從配置檔案獲取
    this.allowDiscard = Boolean.parseBoolean(SofaTracerConfiguration.getProperty(
        SofaTracerConfiguration.TRACER_ASYNC_APPENDER_ALLOW_DISCARD, DEFAULT_ALLOW_DISCARD));
    
    if (allowDiscard) {
        //是否記錄丟失日誌的數量
        this.isOutDiscardNumber = Boolean.parseBoolean(SofaTracerConfiguration.getProperty(
            SofaTracerConfiguration.TRACER_ASYNC_APPENDER_IS_OUT_DISCARD_NUMBER,
            DEFAULT_IS_OUT_DISCARD_NUMBER));
        //是否記錄丟失日誌的TraceId和RpcId
        this.isOutDiscardId = Boolean.parseBoolean(SofaTracerConfiguration.getProperty(
            SofaTracerConfiguration.TRACER_ASYNC_APPENDER_IS_OUT_DISCARD_ID,
            DEFAULT_IS_OUT_DISCARD_ID));
        //丟失日誌的數量達到該閾值進行一次日誌輸出
        this.discardOutThreshold = Long.parseLong(SofaTracerConfiguration.getProperty(
            SofaTracerConfiguration.TRACER_ASYNC_APPENDER_DISCARD_OUT_THRESHOLD,
            DEFAULT_DISCARD_OUT_THRESHOLD));
        if (isOutDiscardNumber) {
            this.discardCount = new PaddedAtomicLong(0L);
        }
    }
}
複製程式碼

啟動 disruptor

disruptor的啟動委託給了AsyncCommonDigestAppenderManagerstart方法來執行。

public void start(final String workerName) {
    this.threadFactory.setWorkName(workerName);
    this.ringBuffer = this.disruptor.start();
}
複製程式碼

來看下,SOFATracer 中 具體是在哪裡呼叫這個start 的:

SOFATracer 中 Disruptor 實踐

  • CommonTracerManager : 這個裡面持有了AsyncCommonDigestAppenderManager 類的一個單例物件,並且是static 靜態程式碼塊中呼叫了start方法;這個用來輸出普通日誌。
  • SofaTracerDigestReporterAsyncManager:這裡類裡面也是持有了AsyncCommonDigestAppenderManager 類的一個單例對像,並且提供了getSofaTracerDigestReporterAsyncManager方法來獲取該單例,在這個方法中呼叫了start方法;該物件用來輸出摘要日誌。

釋出事件

前面的demo中是通過一個for迴圈來發布事件的,在 SOFATracer 中 的事件釋出無非就是當有Tracer日誌需要輸出時會觸發釋出,那麼對應的就是日誌的 append 操作,將日誌 append 到環形緩衝區。

public boolean append(SofaTracerSpan sofaTracerSpan) {
    long sequence = 0L;
    //是否允許丟棄
    if (allowDiscard) {
        try {
            //允許丟棄就使用tryNext嘗試申請序列,申請不到丟擲異常
            sequence = ringBuffer.tryNext();
        } catch (InsufficientCapacityException e) {
            //是否輸出丟失日誌的TraceId和RpcId
            if (isOutDiscardId) {
                SofaTracerSpanContext sofaTracerSpanContext = sofaTracerSpan
                    .getSofaTracerSpanContext();
                if (sofaTracerSpanContext != null) {
                    SynchronizingSelfLog.warn("discarded tracer: traceId["
                                              + sofaTracerSpanContext.getTraceId()
                                              + "];spanId[" + sofaTracerSpanContext.getSpanId()
                                              + "]");
                }
            }
             //是否輸出丟失日誌的數量
            if ((isOutDiscardNumber) && discardCount.incrementAndGet() == discardOutThreshold) {
                discardCount.set(0);
                if (isOutDiscardNumber) {
                    SynchronizingSelfLog.warn("discarded " + discardOutThreshold + " logs");
                }
            }

            return false;
        }
    } else {
        // 不允許丟棄則使用next方法
        sequence = ringBuffer.next();
    }

    try {
        SofaTracerSpanEvent event = ringBuffer.get(sequence);
        event.setSofaTracerSpan(sofaTracerSpan);
    } catch (Exception e) {
        SynchronizingSelfLog.error("fail to add event");
        return false;
    }
    //釋出
    ringBuffer.publish(sequence);
    return true;
}
複製程式碼

SOFATracer 事件釋出的呼叫邏輯:

SOFATracer 中 Disruptor 實踐

追溯呼叫的流程,可以知道當前 span 呼叫 finish時或者 SOFATracer中呼叫reportSpan時 就相當於釋出了一個訊息事件。

小結

本文對 SOFATracer 中使用 Disruptor 來進行日誌輸出的程式碼進行了簡單的分析,更多內部細節原理可以自行看下SOFATracer的程式碼。SOFATracer 作為一種比較底層的中介軟體元件,在實際的業務開發中基本是無法感知的。但是作為技術來學習,還是有很多點可以挖一挖。

SOFATracer GitHub 傳送門

如果有小夥伴對中介軟體感興趣,歡迎加入我們團隊,歡迎來撩;對 SOFA 技術體系有興趣的可以關注我們 ALIPAY SOFA 社群;附團隊鎮樓圖。

SOFATracer 中 Disruptor 實踐

相關文章