OpenTraceing 規範
SOFATracer 對 OpenTraceing 的實現
SOFATracer 就是根據 OpenTracing 規範 衍生出來的分散式 鏈路跟 蹤的解決方案。
概念
OpenTracing
標準中有三個重要的相互關聯的型別,分別是Tracer
, Span
和 SpanContext
。
【下面的概念說明過程中,如不做說明,所使用的案例程式碼均以SOFATracer中的實現為例。】
Tracer
一個 trace
代表一個潛在的,分散式的,存在並行資料或並行執行軌跡(潛在的分散式、並行)的系統。一個trace
可以認為是多個span
的有向無環圖(DAG
)。
Tracer介面用來建立Span,以及處理如何處理Inject(serialize) 和 Extract (deserialize),用於跨程式邊界傳遞。
SOFATracer
中 SofaTracer
這個類實現了 opentracing
的 Tracer
介面,並在此規範介面上做了一些擴充套件。看下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
實現了 Tracer
中 buildSpan
方法:
@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
引數。forma
t引數規定了另一個引數"carrier"
的型別,同時約束了"carrier"
中SpanContext
是如何編碼的。所有的Tracer
實現,都必須支援下面的format
。
Text Map
: 基於字串:字串的map
,對於key
和value
不約束字符集。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/TextMapFormatter 和 HttpHeadersB3Formatter/HttpHeadersFormatter 區別就在於編解碼不同。HttpHeadersB3Formatter
使用的是 URLDecoder.decode
&& URLDecoder.encode
; TextMapB3Formatter
返回的是值本身(如果為空或者null
則返回空字串)。
TextMapFormatter和TextMapB3Formatter區別在於注入或者提取是使用的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
元素是一個鍵值對集合,將這些值設定給給定的Span
,Span
的SpanContext
,以及所有和此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
opentracing
中 SpanContext
介面中只有一個baggageItems
方法,通過這個方法來遍歷所有的baggage
元素。
public interface SpanContext {
Iterable<Map.Entry<String, String>> baggageItems();
}
複製程式碼
相對於OpenTracing
中其他的功能,SpanContext
更多的是一個“概念”。也就是說,OpenTracing
實現中,需要重點考慮,並提供一套自己的API
。
OpenTracing
的使用者僅僅需要,在建立span
、向傳輸協議Inject
(注入)和從傳輸協議中Extract
(提取)時,使用SpanContext
和references
,
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:生成器型別以支援使用正確的
sequencer
和publisher
建立RingBuffer
;列舉型別,SINGLE
、MULTI
兩個項。對應於SingleProducerSequencer
和MultiProducerSequencer
兩種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
的消費處理器。前面已經有個LongEvent
和 EventFactory
; 在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
:提供儲存庫機制,用於將EventHandler
與EventProcessor
關聯起來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
中 生產者的頂級父介面,其直接實現有SingleProducerSequencer
和MultiProducerSequencer
;對應 SINGLE
、MULTI
兩個列舉值。
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種,
這些等待策略不同的核心體現是在如何實現 waitFor
這個方法上。
EventProcessor
事件處理器,實際上可以理解為消費者模型的框架,實現了執行緒Runnable
的run
方法,將迴圈判斷等操作封在了裡面。該介面有三個實現類:
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
。
原理圖
無消費者情況下,生產者保持生產,但是 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);
}
複製程式碼
來解釋下這段程式碼的含義:
假設當前 ringBuffer
的 bufferSize
是 8 ;上次申請到的序列號是 5,其實也就是說已經生產過佔用的序列號是5;假設當前已經消費到的序列號是 3,那麼剩餘的容量為: 8-(5-2) = 5;
因為這裡我們可以確定 bufferSize
和 produced
的值了,那麼 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);
複製程式碼
gatingSequences
是 SingleProducerSequencer
父類 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);
}
複製程式碼
這個方法的呼叫棧向前追溯有這幾個地方呼叫了:
WorkerPool
來管理多個消費者;hangdlerEventsWith
這個方法也是用來設定消費者的。但是在上面的測試案例中我們是想通過不設定消費者 只設定生成者 來觀察 環形佇列的佔用情況,所以gatingSequences
會一直是空的,因此在計算時會把 produced
的值作為 minimum
返回。這樣每次計算就相當於:
return getBufferSize() - (produced - produced) === getBufferSize();
複製程式碼
也就驗證了為何在不設定消費者的情況下,remainingCapacity
的值會一直保持不變。
SOFATracer 中 Disruptor 實踐
SOFATracer
中,AsyncCommonDigestAppenderManager
對 disruptor
進行了封裝,用於處理外部元件的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)
Consumer
是 AsyncCommonDigestAppenderManager
的內部類;實現了 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
的啟動委託給了AsyncCommonDigestAppenderManager
的start
方法來執行。
public void start(final String workerName) {
this.threadFactory.setWorkName(workerName);
this.ringBuffer = this.disruptor.start();
}
複製程式碼
來看下,SOFATracer
中 具體是在哪裡呼叫這個start
的:
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 事件釋出的呼叫邏輯:
追溯呼叫的流程,可以知道當前 span
呼叫 finish
時或者 SOFATracer
中呼叫reportSpan
時 就相當於釋出了一個訊息事件。
小結
本文對 SOFATracer
中使用 Disruptor
來進行日誌輸出的程式碼進行了簡單的分析,更多內部細節原理可以自行看下SOFATracer
的程式碼。SOFATracer
作為一種比較底層的中介軟體元件,在實際的業務開發中基本是無法感知的。但是作為技術來學習,還是有很多點可以挖一挖。
如果有小夥伴對中介軟體感興趣,歡迎加入我們團隊,歡迎來撩;對 SOFA 技術體系有興趣的可以關注我們 ALIPAY SOFA 社群;附團隊鎮樓圖。