Vert.x 技術內幕|Event Bus源碼分析(Local 模式)

Event Bus是Vert.x的“神經系統”,是最爲關鍵的幾個部分之一。今天我們就來探索一下Event Bus的實現原理。本篇分析的是Local模式的Event Bus,對應的Vert.x版本爲3.3.2

本文假定讀者有一定的併發編程基礎以及Vert.x使用基礎,並且對Vert.x的線程模型以及back-pressure有所瞭解。

Local Event Bus的創建

一般情況下,我們通過VertxeventBus方法來創建或獲取一個EventBus實例:

1
2
Vertx vertx = Vertx.vertx();
EventBus eventBus = vertx.eventBus();

eventBus方法定義於Vertx接口中,我們來看一下其位於VertxImpl類中的實現:

1
2
3
4
5
6
7
8
9
10
public EventBus eventBus() {
if (eventBus == null) {
// If reading from different thread possibility that it's been set but not visible - so provide
// memory barrier
synchronized (this) {
return eventBus;
}
}
return eventBus;
}

可以看到此方法返回VertxImpl實例中的eventBus成員,同時需要注意併發可見性問題。那麼eventBus成員是何時初始化的呢?答案在VertxImpl類的構造函數中。這裏截取相關邏輯:

1
2
3
4
5
6
if (options.isClustered()) {
// 集羣模式相關邏輯
} else {
this.clusterManager = null;
createAndStartEventBus(options, resultHandler);
}

可以看到VertxImpl內部是通過createAndStartEventBus方法來初始化eventBus的。我們來看一下其邏輯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void createAndStartEventBus(VertxOptions options, Handler<AsyncResult<Vertx>> resultHandler) {
if (options.isClustered()) {
eventBus = new ClusteredEventBus(this, options, clusterManager, haManager);
} else {
eventBus = new EventBusImpl(this);
}
eventBus.start(ar2 -> {
if (ar2.succeeded()) {
// If the metric provider wants to use the event bus, it cannot use it in its constructor as the event bus
// may not be initialized yet. We invokes the eventBusInitialized so it can starts using the event bus.
metrics.eventBusInitialized(eventBus);
if (resultHandler != null) {
resultHandler.handle(Future.succeededFuture(this));
}
} else {
log.error("Failed to start event bus", ar2.cause());
}
});
}

可以看到此方法通過eventBus = new EventBusImpl(this)eventBus進行了初始化(Local模式爲EventBusImpl),並且調用eventBusstart方法對其進行一些額外的初始化工作。我們來看一下EventBusImpl類的start方法:

1
2
3
4
5
6
7
public synchronized void start(Handler<AsyncResult<Void>> completionHandler) {
if (started) {
throw new IllegalStateException("Already started");
}
started = true;
completionHandler.handle(Future.succeededFuture());
}

首先初始化過程需要防止race condition,因此方法爲synchronized的。該方法僅僅將EventBusImpl類中的一個started標誌位設爲true來代表Event Bus已啓動。注意started標誌位爲volatile的,這樣可以保證其可見性,確保其它線程通過checkStarted方法讀到的started結果總是最新的。設置完started標誌位後,Vert.x會接着調用傳入的completionHandler處理函數,也就是上面我們在createAndStartEventBus方法中看到的 —— 調用metrics成員的eventBusInitialized方法以便Metrics類可以在Event Bus初始化完畢後使用它(不過默認情況下此方法的邏輯爲空)。

可以看到初始化過程還是比較簡單的,我們接下來先來看看訂閱消息 —— consumer方法的邏輯。

consume

我們來看一下consumer方法的邏輯,其原型位於EventBus接口中:

1
2
<T> MessageConsumer<T> consumer(String address);
<T> MessageConsumer<T> consumer(String address, Handler<Message<T>> handler);

其實現位於EventBusImpl類中:

1
2
3
4
5
6
7
@Override
public <T> MessageConsumer<T> consumer(String address, Handler<Message<T>> handler) {
Objects.requireNonNull(handler, "handler");
MessageConsumer<T> consumer = consumer(address);
consumer.handler(handler);
return consumer;
}

首先要確保傳入的handler不爲空,然後Vert.x會調用只接受一個address參數的consumer方法獲取對應的MessageConsumer,最後給獲取到的MessageConsumer綁定上傳入的handler。我們首先來看一下另一個consumer方法的實現:

1
2
3
4
5
6
@Override
public <T> MessageConsumer<T> consumer(String address) {
checkStarted();
Objects.requireNonNull(address, "address");
return new HandlerRegistration<>(vertx, metrics, this, address, null, false, null, -1);
}

首先Vert.x會檢查Event Bus是否已經啓動,並且確保傳入的地址不爲空。然後Vert.x會傳入一大堆參數創建一個新的HandlerRegistration類型的實例,並返回。可以推測HandlerRegistrationMessageConsumer接口的具體實現,它一定非常重要。所以我們來看一看HandlerRegistration類是個啥玩意。首先看一下HandlerRegistration的類體系結構:

可以看到HandlerRegistration類同時繼承了MessageConsumer<T>以及Handler<Message<T>>接口,從其類名可以看出它相當於一個”Handler註冊記錄”,是非常重要的一個類。它有一堆的成員變量,構造函數對vertxmetricseventBusaddress(發送地址), repliedAddress(回覆地址), localOnly(是否在集羣內傳播), asyncResultHandler等幾個成員變量進行初始化,並且檢查超時時間timeout,如果設定了超時時間那麼設定並保存超時計時器(僅用於reply handler中),如果計時器時間到,代表回覆超時。因爲有一些函數還沒介紹,超時的邏輯我們後邊再講。

Note: 由於MessageConsumer接口繼承了ReadStream接口,因此它支持back-pressure,其實現就在HandlerRegistration類中。我們將稍後解析back-pressure的實現。

現在回到consumer方法中來。創建了MessageConsumer實例後,我們接着調用它的handler方法綁定上對應的消息處理函數。handler方法的實現位於HandlerRegistration類中:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public synchronized MessageConsumer<T> handler(Handler<Message<T>> handler) {
this.handler = handler;
if (this.handler != null && !registered) {
registered = true;
eventBus.addRegistration(address, this, repliedAddress != null, localOnly);
} else if (this.handler == null && registered) {
// This will set registered to false
this.unregister();
}
return this;
}

首先,handler方法將此HandlerRegistration中的handler成員設置爲傳入的消息處理函數。HandlerRegistration類中有一個registered標誌位代表是否已綁定消息處理函數。handler方法會檢查傳入的handler是否爲空且是否已綁定消息處理函數。如果不爲空且未綁定,Vert.x就會將registered標誌位設爲true並且調用eventBusaddRegistration方法將此consumer註冊至Event Bus上;如果handler爲空且已綁定消息處理函數,我們就調用unregister方法註銷當前的consumer。我們稍後會分析unregister方法的實現。

前面提到過註冊consumer的邏輯位於Event Bus的addRegistration方法中,因此我們來分析一下它的實現:

1
2
3
4
5
6
protected <T> void addRegistration(String address, HandlerRegistration<T> registration,
boolean replyHandler, boolean localOnly) {
Objects.requireNonNull(registration.getHandler(), "handler");
boolean newAddress = addLocalRegistration(address, registration, replyHandler, localOnly);
addRegistration(newAddress, address, replyHandler, localOnly, registration::setResult);
}

addRegistration方法接受四個參數:發送地址address、傳入的consumer registration、代表是否爲reply handler的標誌位replyHandler以及代表是否在集羣範圍內傳播的標誌位localOnly。首先確保傳入的HandlerRegistration不爲空。然後Vert.x會調用addLocalRegistration方法將此consumer註冊至Event Bus上:

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
protected <T> boolean addLocalRegistration(String address, HandlerRegistration<T> registration,
boolean replyHandler, boolean localOnly) {
Objects.requireNonNull(address, "address");
Context context = Vertx.currentContext();
boolean hasContext = context != null;
if (!hasContext) {
// Embedded
context = vertx.getOrCreateContext();
}
registration.setHandlerContext(context);
boolean newAddress = false;
HandlerHolder holder = new HandlerHolder<>(metrics, registration, replyHandler, localOnly, context);
Handlers handlers = handlerMap.get(address);
if (handlers == null) {
handlers = new Handlers();
Handlers prevHandlers = handlerMap.putIfAbsent(address, handlers);
if (prevHandlers != null) {
handlers = prevHandlers;
}
newAddress = true;
}
handlers.list.add(holder);
if (hasContext) {
HandlerEntry entry = new HandlerEntry<>(address, registration);
context.addCloseHook(entry);
}
return newAddress;
}

首先該方法要確保地址address不爲空,接着它會獲取當前線程下對應的Vert.x Context,如果獲取不到則表明當前不在Verticle中(即Embedded),需要調用vertx.getOrCreateContext()來獲取Context;然後將獲取到的Context賦值給registration內部的handlerContext(代表消息處理對應的Vert.x Context)。

下面就要將給定的registration註冊至Event Bus上了。這裏Vert.x用一個HandlerHolder類來包裝registrationcontext。接着Vert.x會從存儲消息處理Handler的哈希表handlerMap中獲取給定地址對應的Handlers,哈希表的類型爲ConcurrentMap<String, Handlers>,key爲地址,value爲對應的HandlerHolder集合。注意這裏的Handlers類代表一些Handler的集合,它內部維護着一個列表list用於存儲每個HandlerHolderHandlers類中只有一個choose函數,此函數根據輪詢算法從HandlerHolder集合中選定一個HandlerHolder,這即是Event Bus發送消息時實現load-balancing的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Handlers {
private final AtomicInteger pos = new AtomicInteger(0);
public final List<HandlerHolder> list = new CopyOnWriteArrayList<>();
public HandlerHolder choose() {
while (true) {
int size = list.size();
if (size == 0) {
return null;
}
int p = pos.getAndIncrement();
if (p >= size - 1) {
pos.set(0);
}
try {
return list.get(p);
} catch (IndexOutOfBoundsException e) {
// Can happen
pos.set(0);
}
}
}
}

獲取到對應的handlers以後,Vert.x首先需要檢查其是否爲空,如果爲空代表此地址還沒有註冊消息處理Handler,Vert.x就會創建一個Handlers並且將其置入handlerMap中,將newAddress標誌位設爲true代表這是一個新註冊的地址,然後將其賦值給handlers。接着我們向handlers中的HandlerHolder列表list中添加剛剛創建的HandlerHolder實例,這樣就將registration註冊至Event Bus中了。

前面判斷當前線程是否在Vert.x Context的標誌位hasContext還有一個用途:如果當前線程在Vert.x Context下(比如在Verticle中),Vert.x會通過addCloseHook方法給當前的context添加一個鉤子函數用於註銷當前綁定的registration。當對應的Verticle被undeploy的時候,此Verticle綁定的所有消息處理Handler都會被unregister。Hook的類型爲HandlerEntry<T>,它繼承了Closeable接口,對應的邏輯在close函數中實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HandlerEntry<T> implements Closeable {
final String address;
final HandlerRegistration<T> handler;
public HandlerEntry(String address, HandlerRegistration<T> handler) {
this.address = address;
this.handler = handler;
}
// ...
// Called by context on undeploy
public void close(Handler<AsyncResult<Void>> completionHandler) {
handler.unregister(completionHandler);
completionHandler.handle(Future.succeededFuture());
}
}

可以看到close函數會將綁定的registration從Event Bus的handlerMap中移除並執行completionHandler中的邏輯,completionHandler可由用戶指定。

那麼在哪裏調用這些綁定的hook呢?答案是在DeploymentManager類中的doUndeploy方法中,通過contextrunCloseHooks方法執行綁定的hook函數。相關代碼如下(只截取相關邏輯):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public synchronized void doUndeploy(ContextImpl undeployingContext, Handler<AsyncResult<Void>> completionHandler) {
// 前面代碼略
context.runCloseHooks(ar2 -> {
if (ar2.failed()) {
// Log error but we report success anyway
log.error("Failed to run close hook", ar2.cause());
}
if (ar.succeeded() && undeployCount.incrementAndGet() == numToUndeploy) {
reportSuccess(null, undeployingContext, completionHandler);
} else if (ar.failed() && !failureReported.get()) {
failureReported.set(true);
reportFailure(ar.cause(), undeployingContext, completionHandler);
}
});
// 後面代碼略
}

再回到addRegistration方法中。剛纔addLocalRegistration方法的返回值newAddress代表對應的地址是否爲新註冊的。接着我們調用另一個版本的addRegistration方法,傳入了一大堆參數:

1
2
3
4
5
protected <T> void addRegistration(boolean newAddress, String address,
boolean replyHandler, boolean localOnly,
Handler<AsyncResult<Void>> completionHandler) {
completionHandler.handle(Future.succeededFuture());
}

好吧,傳入的前幾個參數沒用到。。。最後一個參數completionHandler傳入的是registration::setResult方法引用,也就是說這個方法調用了對應registrationsetResult方法。其實現位於HandlerRegistration類中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public synchronized void setResult(AsyncResult<Void> result) {
this.result = result;
if (completionHandler != null) {
if (result.succeeded()) {
metric = metrics.handlerRegistered(address, repliedAddress);
}
Handler<AsyncResult<Void>> callback = completionHandler;
vertx.runOnContext(v -> callback.handle(result));
} else if (result.failed()) {
log.error("Failed to propagate registration for handler " + handler + " and address " + address);
} else {
metric = metrics.handlerRegistered(address, repliedAddress);
}
}

首先先設置registration內部的result成員(正常情況下爲Future.succeededFuture())。接着Vert.x會判斷registration是否綁定了completionHandler(與之前的completionHandler不同,這裏的completionHandlerMessageConsumer註冊成功時調用的Handler),若綁定則記錄Metrics信息(handlerRegistered)並在Vert.x Context內調用completionHandler的邏輯;若未綁定completionHandler則僅記錄Metrics信息。

到此爲止,consumer方法的邏輯就分析完了。在分析sendpublish方法的邏輯之前,我們先來看一下如何註銷綁定的MessageConsumer

unregister

我們通過調用MessageConsumerunregister方法實現註銷操作。Vert.x提供了兩個版本的unregister方法:

1
2
3
void unregister();
void unregister(Handler<AsyncResult<Void>> completionHandler);

其中第二個版本的unregister方法會在註銷操作完成時調用傳入的completionHandler。比如在cluster範圍內註銷consumer需要消耗一定的時間在集羣內傳播,因此第二個版本的方法就會派上用場。我們來看一下其實現,它們最後都是調用了HandlerRegistration類的doUnregister方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void doUnregister(Handler<AsyncResult<Void>> completionHandler, boolean callEndHandler) {
if (timeoutID != -1) {
vertx.cancelTimer(timeoutID);
}
if (endHandler != null && callEndHandler) {
Handler<Void> theEndHandler = endHandler;
Handler<AsyncResult<Void>> handler = completionHandler;
completionHandler = ar -> {
theEndHandler.handle(null);
if (handler != null) {
handler.handle(ar);
}
};
}
if (registered) {
registered = false;
eventBus.removeRegistration(address, this, completionHandler);
} else {
callCompletionHandlerAsync(completionHandler);
}
registered = false;
}

如果設定了超時定時器(timeoutID合法),那麼Vert.x會首先將定時器關閉。接着Vert.x會判斷是否需要調用endHandler。那麼endHandler又是什麼呢?前面我們提到過MessageConsumer接口繼承了ReadStream接口,而ReadStream接口定義了一個endHandler方法用於綁定一個endHandler,當stream中的數據讀取完畢時會調用。而在Event Bus中,消息源源不斷地從一處發送至另一處,因此只有在某個consumer
被unregister的時候,其對應的stream纔可以叫“讀取完畢”,因此Vert.x選擇在doUnregister方法中調用endHandler

接着Vert.x會判斷此consumer是否已註冊消息處理函數Handler(通過檢查registered標誌位),若已註冊則將對應的Handler從Event Bus中的handlerMap中移除並將registered設爲false;若還未註冊Handler且提供了註銷結束時的回調completionHandler(注意不是HandlerRegistration類的成員變量completionHandler,而是之前第二個版本的unregister中傳入的Handler,用同樣的名字很容易混。。。),則通過callCompletionHandlerAsync方法調用回調函數。

從Event Bus中移除Handler的邏輯位於EventBusImpl類的removeRegistration方法中:

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
protected <T> void removeRegistration(String address, HandlerRegistration<T> handler, Handler<AsyncResult<Void>> completionHandler) {
HandlerHolder holder = removeLocalRegistration(address, handler);
removeRegistration(holder, address, completionHandler);
}
protected <T> void removeRegistration(HandlerHolder handlerHolder, String address,
Handler<AsyncResult<Void>> completionHandler) {
callCompletionHandlerAsync(completionHandler);
}
protected <T> HandlerHolder removeLocalRegistration(String address, HandlerRegistration<T> handler) {
Handlers handlers = handlerMap.get(address);
HandlerHolder lastHolder = null;
if (handlers != null) {
synchronized (handlers) {
int size = handlers.list.size();
// Requires a list traversal. This is tricky to optimise since we can't use a set since
// we need fast ordered traversal for the round robin
for (int i = 0; i < size; i++) {
HandlerHolder holder = handlers.list.get(i);
if (holder.getHandler() == handler) {
handlers.list.remove(i);
holder.setRemoved();
if (handlers.list.isEmpty()) {
handlerMap.remove(address);
lastHolder = holder;
}
holder.getContext().removeCloseHook(new HandlerEntry<>(address, holder.getHandler()));
break;
}
}
}
}
return lastHolder;
}

其真正的unregister邏輯位於removeLocalRegistration方法中。首先需要從handlerMap中獲取地址對應的Handlers實例handlers,如果handlers不爲空,爲了防止併發問題,Vert.x需要對其加鎖後再進行操作。Vert.x需要遍歷handlers中的列表,遇到與傳入的HandlerRegistration相匹配的HandlerHolder就將其從列表中移除,然後調用對應holdersetRemoved方法標記其爲已註銷並記錄Metrics數據(handlerUnregistered)。如果移除此HandlerHolderhandlers沒有任何註冊的Handler了,就將該地址對應的Handlers實例從handlerMap中移除並保存剛剛移除的HandlerHolder。另外,由於已經將此consumer註銷,在undeploy verticle的時候不需要再進行unregister,因此這裏還要將之前註冊到context的hook移除。

調用完removeLocalRegistration方法以後,Vert.x會調用另一個版本的removeRegistration方法,調用completionHandler(用戶在第二個版本的unregister方法中傳入的處理函數)對應的邏輯,其它的參數都沒什麼用。。。

這就是MessageConsumer註銷的邏輯實現。下面就到了本文的另一重頭戲了 —— 發送消息相關的函數sendpublish

send & publish

sendpublish的邏輯相近,只不過一個是發送至目標地址的某一消費者,一個是發佈至目標地址的所有消費者。Vert.x使用一個標誌位send來代表是否爲點對點發送模式。

幾個版本的sendpublish最終都歸結於生成消息對象然後調用sendOrPubInternal方法執行邏輯,只不過send標誌位不同:

1
2
3
4
5
6
7
8
9
10
11
@Override
public <T> EventBus send(String address, Object message, DeliveryOptions options, Handler<AsyncResult<Message<T>>> replyHandler) {
sendOrPubInternal(createMessage(true, address, options.getHeaders(), message, options.getCodecName()), options, replyHandler);
return this;
}
@Override
public EventBus publish(String address, Object message, DeliveryOptions options) {
sendOrPubInternal(createMessage(false, address, options.getHeaders(), message, options.getCodecName()), options, null);
return this;
}

兩個方法中都是通過createMessage方法來生成對應的消息對象的:

1
2
3
4
5
6
7
protected MessageImpl createMessage(boolean send, String address, MultiMap headers, Object body, String codecName) {
Objects.requireNonNull(address, "no null address accepted");
MessageCodec codec = codecManager.lookupCodec(body, codecName);
@SuppressWarnings("unchecked")
MessageImpl msg = new MessageImpl(address, null, headers, body, codec, send, this);
return msg;
}

createMessage方法接受5個參數:send即上面提到的標誌位,address爲發送目標地址,headers爲設置的header,body代表發送的對象,codecName代表對應的Codec(消息編碼解碼器)名稱。createMessage方法首先會確保地址不爲空,然後通過codecManager來獲取對應的MessageCodec。如果沒有提供Codec(即codecName爲空),那麼codecManager會根據發送對象body的類型來提供內置的Codec實現(具體邏輯請見此處)。準備好MessageCodec後,createMessage方法就會創建一個MessageImpl實例並且返回。

這裏我們還需要了解一下MessageImpl的構造函數:

1
2
3
4
5
6
7
8
9
10
11
public MessageImpl(String address, String replyAddress, MultiMap headers, U sentBody,
MessageCodec<U, V> messageCodec,
boolean send, EventBusImpl bus) {
this.messageCodec = messageCodec; // Codec
this.address = address; // 發送目標地址
this.replyAddress = replyAddress; // 回覆地址
this.headers = headers; // header
this.sentBody = sentBody; // 發送的對象
this.send = send; // 是否爲點對點模式
this.bus = bus; // 相關的Event Bus實例
}

createMessage方法並沒有設置回覆地址replyAddress。如果用戶指定了replyHandler的話,後邊sendOrPubInternal方法會對此消息實體進行加工,設置replyAddress並生成回覆邏輯對應的HandlerRegistration

我們看一下sendOrPubInternal方法的源碼:

1
2
3
4
5
6
7
private <T> void sendOrPubInternal(MessageImpl message, DeliveryOptions options,
Handler<AsyncResult<Message<T>>> replyHandler) {
checkStarted();
HandlerRegistration<T> replyHandlerRegistration = createReplyHandlerRegistration(message, options, replyHandler);
SendContextImpl<T> sendContext = new SendContextImpl<>(message, options, replyHandlerRegistration);
sendContext.next();
}

它接受三個參數:要發送的消息message,發送配置選項options以及回覆處理函數replyHandler。首先sendOrPubInternal方法要檢查Event Bus是否已啓動,接着如果綁定了回覆處理函數,Vert.x就會調用createReplyHandlerRegistration方法給消息實體message包裝上回復地址,並且生成對應的reply consumer。接着Vert.x創建了一個包裝消息的SendContextImpl實例並調用了其next方法。

我們一步一步來解釋。首先是createReplyHandlerRegistration方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private <T> HandlerRegistration<T> createReplyHandlerRegistration(MessageImpl message, DeliveryOptions options, Handler<AsyncResult<Message<T>>> replyHandler) {
if (replyHandler != null) {
long timeout = options.getSendTimeout();
String replyAddress = generateReplyAddress();
message.setReplyAddress(replyAddress);
Handler<Message<T>> simpleReplyHandler = convertHandler(replyHandler);
HandlerRegistration<T> registration =
new HandlerRegistration<>(vertx, metrics, this, replyAddress, message.address, true, replyHandler, timeout);
registration.handler(simpleReplyHandler);
return registration;
} else {
return null;
}
}

createReplyHandlerRegistration方法首先檢查傳入的replyHandler是否爲空(是否綁定了replyHandler,回覆處理函數),如果爲空則代表不需要處理回覆,直接返回null;若replyHandler不爲空,createReplyHandlerRegistration方法就會從配置中獲取reply的最大超時時長(默認30s),然後調用generateReplyAddress方法生成對應的回覆地址replyAddress

1
2
3
4
5
private final AtomicLong replySequence = new AtomicLong(0);
protected String generateReplyAddress() {
return Long.toString(replySequence.incrementAndGet());
}

生成回覆地址的邏輯有點簡單。。。。EventBusImpl實例中維護着一個AtomicLong類型的replySequence成員變量代表對應的回覆地址。每次生成的時候都會使其自增,然後轉化爲String。也就是說生成的replyAddress都類似於”1”、”5”這樣,而不是我們想象中的直接回復至sender的地址。。。

生成完畢以後,createReplyHandlerRegistration方法會將生成的replyAddress設定到消息對象message中。接着Vert.x會通過convertHandler方法對replyHandler進行包裝處理並生成類型簡化爲Handler<Message<T>>simpleReplyHandler,它用於綁定至後面創建的reply consumer上。接着Vert.x會創建對應的reply consumer。關於reply操作的實現,我們後邊會詳細講述。下面Vert.x就通過handler方法將生成的回覆處理函數simpleReplyHandler綁定至創建好的reply consumer中,其底層實現我們之前已經分析過了,這裏就不再贅述。最後此方法返回生成的registration,即對應的reply consumer。注意這個reply consumer是一次性的,也就是說Vert.x會在其接收到回覆或超時的時候自動對其進行註銷。

OK,現在回到sendOrPubInternal方法中來。下面Vert.x會創建一個SendContextImpl實例並調用其next方法。SendContextImpl類實現了SendContext接口,它相當於一個消息的封裝體,並且可以與Event Bus中的interceptors(攔截器)結合使用。

SendContext接口定義了三個方法:

  • message: 獲取當前SendContext包裝的消息實體
  • next: 調用下一個消息攔截器
  • send: 代表消息的發送模式是否爲點對點模式

在Event Bus中,消息攔截器本質上是一個Handler<SendContext>類型的處理函數。Event Bus內部存儲着一個interceptors列表用於存儲綁定的消息攔截器。我們可以通過addInterceptorremoveInterceptor方法進行消息攔截器的添加和刪除操作。如果要進行鏈式攔截,則在每個攔截器中都應該調用對應SendContextnext方法,比如:

1
2
3
4
eventBus.addInterceptor(sc -> {
// 一些處理邏輯
sc.next(); // 調用下一個攔截器
});

我們來看一下SendContextImpl類中next方法的實現:

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
38
protected class SendContextImpl<T> implements SendContext<T> {
public final MessageImpl message;
public final DeliveryOptions options;
public final HandlerRegistration<T> handlerRegistration;
public final Iterator<Handler<SendContext>> iter;
public SendContextImpl(MessageImpl message, DeliveryOptions options, HandlerRegistration<T> handlerRegistration) {
this.message = message;
this.options = options;
this.handlerRegistration = handlerRegistration;
this.iter = interceptors.iterator();
}
@Override
public Message<T> message() {
return message;
}
@Override
public void next() {
if (iter.hasNext()) {
Handler<SendContext> handler = iter.next();
try {
handler.handle(this);
} catch (Throwable t) {
log.error("Failure in interceptor", t);
}
} else {
sendOrPub(this);
}
}
@Override
public boolean send() {
return message.send();
}
}

我們可以看到,SendContextImpl類中維護了一個攔截器列表對應的迭代器。每次調用next方法時,如果迭代器中存在攔截器,就將下個攔截器取出並進行相關調用。如果迭代器爲空,則代表攔截器都已經調用完畢,Vert.x就會調用EventBusImpl類下的sendOrPub方法進行消息的發送操作。

sendOrPub方法僅僅在metrics模塊中記錄相關數據(messageSent),最後調用deliverMessageLocally(SendContextImpl<T>)方法執行消息的發送邏輯:

1
2
3
4
5
6
7
8
9
10
protected <T> void deliverMessageLocally(SendContextImpl<T> sendContext) {
if (!deliverMessageLocally(sendContext.message)) {
// no handlers
metrics.replyFailure(sendContext.message.address, ReplyFailure.NO_HANDLERS);
if (sendContext.handlerRegistration != null) {
sendContext.handlerRegistration.sendAsyncResultFailure(ReplyFailure.NO_HANDLERS, "No handlers for address "
+ sendContext.message.address);
}
}
}

這裏面又套了一層。。。它最後其實是調用了deliverMessageLocally(MessageImpl)方法。此方法返回值代表發送消息的目標地址是否註冊有MessageConsumer,如果沒有(false)則記錄錯誤並調用sendContext中保存的回覆處理函數處理錯誤(如果綁定了replyHandler的話)。

deliverMessageLocally(MessageImpl)方法是真正區分sendpublish的地方,我們來看一下其實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected <T> boolean deliverMessageLocally(MessageImpl msg) {
msg.setBus(this);
Handlers handlers = handlerMap.get(msg.address());
if (handlers != null) {
if (msg.send()) {
//Choose one
HandlerHolder holder = handlers.choose();
metrics.messageReceived(msg.address(), !msg.send(), isMessageLocal(msg), holder != null ? 1 : 0);
if (holder != null) {
deliverToHandler(msg, holder);
}
} else {
// Publish
metrics.messageReceived(msg.address(), !msg.send(), isMessageLocal(msg), handlers.list.size());
for (HandlerHolder holder: handlers.list) {
deliverToHandler(msg, holder);
}
}
return true;
} else {
metrics.messageReceived(msg.address(), !msg.send(), isMessageLocal(msg), 0);
return false;
}
}

首先Vert.x需要從handlerMap中獲取目標地址對應的處理函數集合handlers。接着,如果handlers不爲空的話,Vert.x就會判斷消息實體的send標誌位。如果send標誌位爲true則代表以點對點模式發送,Vert.x就會通過handlerschoose方法(之前提到過),按照輪詢算法來獲取其中的某一個HandlerHolder。獲取到HandlerHolder之後,Vert.x會通過deliverToHandler方法將消息分發至HandlerHolder中進行處理;如果send標誌位爲false則代表向所有消費者發佈消息,Vert.x就會對handlers中的每一個HandlerHolder依次調用deliverToHandler方法,以便將消息分發至所有註冊到此地址的Handler中進行處理。

消息處理的真正邏輯就在deliverToHandler方法中。我們來看一下它的源碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private <T> void deliverToHandler(MessageImpl msg, HandlerHolder<T> holder) {
// Each handler gets a fresh copy
@SuppressWarnings("unchecked")
Message<T> copied = msg.copyBeforeReceive();
if (metrics.isEnabled()) {
metrics.scheduleMessage(holder.getHandler().getMetric(), msg.isLocal());
}
holder.getContext().runOnContext((v) -> {
// Need to check handler is still there - the handler might have been removed after the message were sent but
// before it was received
try {
if (!holder.isRemoved()) {
holder.getHandler().handle(copied);
}
} finally {
if (holder.isReplyHandler()) {
holder.getHandler().unregister();
}
}
});
}

首先deliverToHandler方法會複製一份要發送的消息,然後deliverToHandler方法會調用metricsscheduleMessage方法記錄對應的Metrics信息(計劃對消息進行處理。此函數修復了Issue 1480)。接着deliverToHandler方法會從傳入的HandlerHolder中獲取對應的Vert.x Context,然後調用runOnContext方法以便可以讓消息處理邏輯在Vert.x Context中執行。爲防止對應的handler在處理之前被移除,這裏還需要檢查一下holderisRemoved屬性。如果沒有移除,那麼就從holder中獲取對應的handler並調用其handle方法執行消息的處理邏輯。注意這裏獲取的handler實際上是一個HandlerRegistration。前面提到過HandlerRegistration類同時實現了MessageConsumer接口和Handler接口,因此它兼具這兩個接口所期望的功能。另外,之前我們提到過Vert.x會自動註銷接收過回覆的reply consumer,其邏輯就在這個finally塊中。Vert.x會檢查holder中的handler是否爲reply handler(reply consumer),如果是的話就調用其unregister方法將其註銷,來確保reply consumer爲一次性的。

之前我們提到過MessageConsumer繼承了ReadStream接口,因此HandlerRegistration需要實現flow control(back-pressure)的相關邏輯。那麼如何實現呢?我們看到,HandlerRegistration類中有一個paused標誌位代表是否還繼續處理消息。ReadStream接口中定義了兩個函數用於控制stream的通斷:當處理速度小於讀取速度(發生擁塞)的時候我們可以通過pause方法暫停消息的傳遞,將積壓的消息暫存於內部的消息隊列(緩衝區)pending中;當相對速度正常的時候,我們可以通過resume方法恢復消息的傳遞和處理。

我們看一下HandlerRegistrationhandle方法的實現:

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
@Override
public void handle(Message<T> message) {
Handler<Message<T>> theHandler;
synchronized (this) {
if (paused) {
if (pending.size() < maxBufferedMessages) {
pending.add(message);
} else {
if (discardHandler != null) {
discardHandler.handle(message);
} else {
log.warn("Discarding message as more than " + maxBufferedMessages + " buffered in paused consumer");
}
}
return;
} else {
if (pending.size() > 0) {
pending.add(message);
message = pending.poll();
}
theHandler = handler;
}
}
deliver(theHandler, message);
}

果然。。。handle方法在處理消息的基礎上實現了擁塞控制的功能。爲了防止資源爭用,需要對自身進行加鎖;首先handle方法會判斷當前的consumer是否爲paused狀態,如果爲paused狀態,handle方法會檢查當前緩衝區大小是否已經超過給定的最大緩衝區大小maxBufferedMessages,如果沒超過,就將收到的消息push到緩衝區中;如果大於或等於閾值,Vert.x就需要丟棄超出的那部分消息。如果當前的consumer爲正常狀態,則如果緩衝區不爲空,就將收到的消息push到緩衝區中並從緩衝區中pull隊列首端的消息,然後調用deliver方法執行真正的消息處理邏輯。注意這裏是在鎖之外執行deliver方法的,這是爲了保證在multithreaded worker context下可以併發傳遞消息(見Bug 473714 )。由於multithreaded worker context允許在不同線程併發執行邏輯(見官方文檔),如果將deliver方法置於synchronized塊之內,其他線程必須等待當前鎖被釋放才能進行消息的傳遞邏輯,因而不能做到“delivery concurrently”。

deliver方法是真正執行“消息處理”邏輯的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void deliver(Handler<Message<T>> theHandler, Message<T> message) {
checkNextTick();
boolean local = true;
if (message instanceof ClusteredMessage) {
// A bit hacky
ClusteredMessage cmsg = (ClusteredMessage)message;
if (cmsg.isFromWire()) {
local = false;
}
}
String creditsAddress = message.headers().get(MessageProducerImpl.CREDIT_ADDRESS_HEADER_NAME);
if (creditsAddress != null) {
eventBus.send(creditsAddress, 1);
}
try {
metrics.beginHandleMessage(metric, local);
theHandler.handle(message);
metrics.endHandleMessage(metric, null);
} catch (Exception e) {
log.error("Failed to handleMessage", e);
metrics.endHandleMessage(metric, e);
throw e;
}
}

首先Vert.x會調用checkNextTick方法來檢查消息隊列(緩衝區)中是否存在更多的消息等待被處理,如果有的話就取出隊列首端的消息並調用deliver方法將其傳遞給handler進行處理。這裏仍需要注意併發問題,相關實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private synchronized void checkNextTick() {
if (!pending.isEmpty()) {
handlerContext.runOnContext(v -> {
Message<T> message;
Handler<Message<T>> theHandler;
synchronized (HandlerRegistration.this) {
if (paused || (message = pending.poll()) == null) {
return;
}
theHandler = handler;
}
deliver(theHandler, message);
});
}
}

檢查完消息隊列以後,Vert.x會接着根據message判斷消息是否僅在本地進行處理並給local標誌位賦值,local標誌位將在記錄Metrics數據時用到。

接下來我們看到Vert.x從消息的headers中獲取了一個地址creditsAddress,如果creditsAddress存在就向此地址發送一條消息,body爲1。那麼這個creditsAddress又是啥呢?其實,它與flow control有關,我們會在下面詳細分析。發送完credit消息以後,接下來就到了調用handler處理消息的時刻了。在處理消息之前需要調用metricsbeginHandleMessage方法記錄消息開始處理的metrics數據,在處理完消息以後需要調用endHandleMessage方法記錄消息處理結束的metrics數據。

嗯。。。到此爲止,消息的發送和處理過程就已經一目瞭然了。下面我們講一講之前代碼中出現的creditsAddress到底是啥玩意~

MessageProducer

之前我們提到過,Vert.x定義了兩個接口作爲 flow control aware object 的規範:WriteStream以及ReadStream。對於ReadStream我們已經不再陌生了,MessageConsumer就繼承了它;那麼大家應該可以想象到,有MessageConsumer就必有MessageProducer。不錯,Vert.x中的MessageProducer接口對應某個address上的消息生產者,同時它繼承了WriteStream接口,因此MessageProducer的實現類MessageProducerImpl同樣具有flow control的能力。我們可以把MessageProducer看做是一個具有flow control功能的增強版的EventBus。我們可以通過EventBus接口的publisher方法創建一個MessageProducer

MessageProducer有了初步瞭解之後,我們就可以解釋前面deliver方法中的creditsAddress了。MessageProducer接口的實現類 —— MessageProducerImpl類的流量控制功能是基於credit的,其內部會維護一個credit值代表“發送消息的能力”,其默認值等於DEFAULT_WRITE_QUEUE_MAX_SIZE

1
2
private int maxSize = DEFAULT_WRITE_QUEUE_MAX_SIZE;
private int credits = DEFAULT_WRITE_QUEUE_MAX_SIZE;

在採用點對點模式發送消息的時候,MessageProducer底層會調用doSend方法進行消息的發送。發送依然利用Event Bus的send方法,只不過doSend方法中添加了flow control的相關邏輯:

1
2
3
4
5
6
7
8
9
10
11
12
private synchronized <R> void doSend(T data, Handler<AsyncResult<Message<R>>> replyHandler) {
if (credits > 0) {
credits--;
if (replyHandler == null) {
bus.send(address, data, options);
} else {
bus.send(address, data, options, replyHandler);
}
} else {
pending.add(data);
}
}

MessageConsumer類似,MessageProducer內部同樣保存着一個消息隊列(緩衝區)用於暫存堆積的消息。當credits大於0的時候代表可以發送消息(沒有出現擁塞),Vert.x就會調用Event Bus的send方法進行消息的發送,同時credits要減1;如果credits小於等於0,則代表此時消息發送的速度太快,出現了擁塞,需要暫緩發送,因此將要發送的對象暫存於緩衝區中。大家可能會問,credits值不斷減小,那麼恢復消息發送能力(增大credits)的邏輯在哪呢?這就要提到creditsAddress了。我們看一下MessageProducerImpl類的構造函數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public MessageProducerImpl(Vertx vertx, String address, boolean send, DeliveryOptions options) {
this.vertx = vertx;
this.bus = vertx.eventBus();
this.address = address;
this.send = send;
this.options = options;
if (send) {
String creditAddress = UUID.randomUUID().toString() + "-credit";
creditConsumer = bus.consumer(creditAddress, msg -> {
doReceiveCredit(msg.body());
});
options.addHeader(CREDIT_ADDRESS_HEADER_NAME, creditAddress);
} else {
creditConsumer = null;
}
}

MessageProducerImpl的構造函數中生成了一個creditAddress,然後給該地址綁定了一個Handler,當收到消息時調用doReceiveCredit方法執行解除擁塞,恢復消息發送的邏輯。MessageProducerImpl會將此MessageConsumer保存,以便在關閉消息生產者流的時候將其註銷。接着構造函數會往optionsheaders中添加一條記錄,保存對應的creditAddress,這也就是上面我們在deliver函數中獲取的creditAddress

1
2
3
4
5
// 位於HandlerRegistration類的deliver函數中
String creditsAddress = message.headers().get(MessageProducerImpl.CREDIT_ADDRESS_HEADER_NAME);
if (creditsAddress != null) {
eventBus.send(creditsAddress, 1);
}

這樣,發送消息到creditsAddress的邏輯也就好理解了。由於deliver函數的邏輯就是處理消息,因此這裏向creditsAddress發送一個 1 其實就是將對應的credits值加1。恢復消息發送的邏輯位於MessageProducerImpl類的doReceiveCredit方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private synchronized void doReceiveCredit(int credit) {
credits += credit;
while (credits > 0) {
T data = pending.poll();
if (data == null) {
break;
} else {
credits--;
bus.send(address, data, options);
}
}
final Handler<Void> theDrainHandler = drainHandler;
if (theDrainHandler != null && credits >= maxSize / 2) {
this.drainHandler = null;
vertx.runOnContext(v -> theDrainHandler.handle(null));
}
}

邏輯一目瞭然。首先給credits加上發送過來的值(正常情況下爲1),然後恢復發送能力,將緩衝區的數據依次取出、發送然後減小credits。同時如果MessageProducer綁定了drainHandler(消息流不擁塞的時候調用的邏輯,詳見官方文檔),並且MessageProducer發送的消息不再擁塞(credits >= maxSize / 2),那麼就在Vert.x Context中執行drainHandler中的邏輯。

怎麼樣,體會到Vert.x中flow control的強大之處了吧!官方文檔中MessageProducer的篇幅幾乎沒有,只在介紹WriteStream的時候提了提,因此這部分也可以作爲MessageProducer的參考。

reply

最後就是消息的回覆邏輯 —— reply方法了。reply方法的實現位於MessageImpl類中,最終調用的是reply(Object, DeliveryOptions, Handler<AsyncResult<Message<R>>>)這個版本:

1
2
3
4
5
6
@Override
public <R> void reply(Object message, DeliveryOptions options, Handler<AsyncResult<Message<R>>> replyHandler) {
if (replyAddress != null) {
sendReply(bus.createMessage(true, replyAddress, options.getHeaders(), message, options.getCodecName()), options, replyHandler);
}
}

這裏reply方法同樣調用EventBuscreateMessage方法創建要回復的消息實體,傳入的replyAddress即爲之前講過的生成的非常簡單的回覆地址。然後再將消息實體、配置以及對應的replyHandler(如果有的話)傳入sendReply方法進行消息的回覆。最後其實是調用了Event Bus中的四個參數的sendReply方法,它的邏輯與之前講過的sendOrPubInternal非常相似:

1
2
3
4
5
6
7
8
9
protected <T> void sendReply(MessageImpl replyMessage, MessageImpl replierMessage, DeliveryOptions options,
Handler<AsyncResult<Message<T>>> replyHandler) {
if (replyMessage.address() == null) {
throw new IllegalStateException("address not specified");
} else {
HandlerRegistration<T> replyHandlerRegistration = createReplyHandlerRegistration(replyMessage, options, replyHandler);
new ReplySendContextImpl<>(replyMessage, options, replyHandlerRegistration, replierMessage).next();
}
}

參數中replyMessage代表回覆消息實體,replierMessage則代表回覆者自身的消息實體(sender)。

如果地址爲空則拋出異常;如果地址不爲空,則先調用createReplyHandlerRegistration方法創建對應的replyHandlerRegistrationcreateReplyHandlerRegistration方法的實現之前已經講過了。注意這裏的createReplyHandlerRegistration其實對應的是此replier的回覆,因爲Vert.x中的 Request-Response 消息模型不限制相互回覆(通信)的次數。當然如果沒有指定此replier的回覆的replyHandler,那麼此處的replyHandlerRegistration就爲空。最後sendReply方法會創建一個ReplySendContextImpl並調用其next方法。

ReplySendContextImpl類同樣是SendContext接口的一個實現(繼承了SendContextImpl類)。ReplySendContextImpl比起其父類就多保存了一個replierMessagenext方法的邏輯與父類邏輯非常相似,只不過將回復的邏輯替換成了另一個版本的sendReply方法:

1
2
3
4
5
6
7
8
9
@Override
public void next() {
if (iter.hasNext()) {
Handler<SendContext> handler = iter.next();
handler.handle(this);
} else {
sendReply(this, replierMessage);
}
}

然而。。。sendReply方法並沒有用到傳入的replierMessage,所以這裏最終還是調用了sendOrPub方法(尼瑪,封裝的ReplySendContextImpl貌似並沒有什麼卵用,可能爲以後的擴展考慮?)。。。之後的邏輯我們都已經分析過了。

這裏再強調一點。當我們發送消息同時指定replyHandler的時候,其內部爲reply創建的reply consumer(類型爲HandlerRegistration)指定了timeout。這個定時器從HandlerRegistration創建的時候就開始計時了。我們回顧一下:

1
2
3
4
5
6
if (timeout != -1) {
timeoutID = vertx.setTimer(timeout, tid -> {
metrics.replyFailure(address, ReplyFailure.TIMEOUT);
sendAsyncResultFailure(ReplyFailure.TIMEOUT, "Timed out after waiting " + timeout + "(ms) for a reply. address: " + address);
});
}

計時器會在超時的時候記錄錯誤並強制註銷當前consumer。由於reply consumer是一次性的,當收到reply的時候,Vert.x會自動對reply consumer調用unregister方法對其進行註銷(實現位於EventBusImpl#deliverToHandler方法中),而在註銷邏輯中會關閉定時器(參見前面對doUnregister方法的解析);如果超時,那麼計時器就會觸發,Vert.x會調用sendAsyncResultFailure方法註銷當前reply consumer並處理錯誤。

synchronized的性能問題

大家可能看到爲了防止race condition,Vert.x底層大量使用了synchronized關鍵字(重量級鎖)。這會不會影響性能呢?其實,如果開發者遵循Vert.x的線程模型和開發規範(使用Verticle)的話,有些地方的synchronized對應的鎖會被優化爲 偏向鎖 或 輕量級鎖(因爲通常都是同一個Event Loop線程獲取對應的鎖),這樣性能總體開銷不會太大。當然如果使用Multi-threaded worker verticles就要格外關注性能問題了。。。

總結

我們來簡略地總結一下Event Bus的工作原理。當我們調用consumer綁定一個MessageConsumer時,Vert.x會將它保存至Event Bus實例內部的Map中;當我們通過sendpublish向對應的地址發送消息的時候,Vert.x會遍歷Event Bus中存儲consumer的Map,獲取與地址相對應的consumer集合,然後根據相應的策略傳遞並處理消息(send通過輪詢策略獲取任意一個consumer並將消息傳遞至consumer中,publish則會將消息傳遞至所有註冊到對應地址的consumer中)。同時,MessageConsumerMessageProducer這兩個接口的實現都具有flow control功能,因此它們也可以用在Pump中。

Event Bus是Vert.x中最爲重要的一部分之一,探索Event Bus的源碼可以加深我們對Event Bus工作原理的理解。作爲開發者,只會使用框架是不夠的,能夠理解內部的實現原理和精華,並對其進行改進纔是更爲重要的。本篇文章分析的是Local模式下的Event Bus,下篇文章我們將來探索一下生產環境中更常用的 Clustered Event Bus 的實現原理,敬請期待!

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