本節主要對 Apollo 服務端設計原理進行解析。
1. 配置發佈後的實時推送設計
配置中心最重要的一個特性就是實時推送,正因爲有這個特性,我們纔可以依賴配置中心做很多事情。如圖 1 所示。
圖 1 簡要描述了配置發佈的大致過程。
- 用戶在 Portal 中進行配置的編輯和發佈。
- Portal 會調用 Admin Service 提供的接口進行發佈操作。
- Admin Service 收到請求後,發送 ReleaseMessage 給各個 Config Service,通知 Config Service 配置發生變化。
- Config Service 收到 ReleaseMessage 後,通知對應的客戶端,基於 Http 長連接實現。
2. 發送 ReleaseMessage 的實現方式
ReleaseMessage 消息是通過 Mysql 實現了一個簡單的消息隊列。之所以沒有采用消息中間件,是爲了讓 Apollo 在部署的時候儘量簡單,儘可能減少外部依賴,如圖 2 所示。
圖 2 簡要描述了發送 ReleaseMessage 的大致過程:
- Admin Service 在配置發佈後會往 ReleaseMessage 表插入一條消息記錄。
- Config Service 會啓動一個線程定時掃描 ReleaseMessage 表,來查看是否有新的消息記錄。
- Config Service 發現有新的消息記錄,就會通知到所有的消息監聽器。
- 消息監聽器得到配置發佈的信息後,就會通知對應的客戶端。
3. Config Service 通知客戶端的實現方式
通知採用基於 Http 長連接實現,主要分爲下面幾個步驟:
- 客戶端會發起一個 Http 請求到 Config Service 的 notifications/v2 接口。
- notifications/v2 接口通過 Spring DeferredResult 把請求掛起,不會立即返回。
- 如果在 60s 內沒有該客戶端關心的配置發佈,那麼會返回 Http 狀態碼 304 給客戶端。
- 如果發現配置有修改,則會調用 DeferredResult 的 setResult 方法,傳入有配置變化的 namespace 信息,同時該請求會立即返回。
- 客戶端從返回的結果中獲取到配置變化的 namespace 後,會立即請求 Config Service 獲取該 namespace 的最新配置。
4. 源碼解析實時推送設計
Apollo 推送涉及的代碼比較多,本教程就不做詳細分析了,筆者把推送這裏的代碼稍微簡化了下,給大家進行講解,這樣理解起來會更容易。
當然,這些代碼比較簡單,很多細節就不做考慮了,只是爲了能夠讓大家明白 Apollo 推送的核心原理。
發送 ReleaseMessage 的邏輯我們就寫一個簡單的接口,用隊列存儲,測試的時候就調用這個接口模擬配置有更新,發送 ReleaseMessage 消息。具體代碼如下所示。
@RestController
public class NotificationControllerV2 implements ReleaseMessageListener {
// 模擬配置更新, 向其中插入數據表示有更新
public static Queue<String> queue = new LinkedBlockingDeque<>();
@GetMapping("/addMsg")
public String addMsg() {
queue.add("xxx");
return "success";
}
}
消息發送之後,根據前面講過的 Config Service 會啓動一個線程定時掃描 ReleaseMessage 表,查看是否有新的消息記錄,然後取通知客戶端,在這裏我們也會啓動一個線程去掃描,具體代碼如下所示。
@Component
public class ReleaseMessageScanner implements InitializingBean {
@Autowired
private NotificationControllerV2 configController;
@Override
public void afterPropertiesSet() throws Exception {
// 定時任務從數據庫掃描有沒有新的配置發佈
new Thread(() -> {
for (;;) {
String result = NotificationControllerV2.queue.poll();
if (result != null) {
ReleaseMessage message = new ReleaseMessage();
message.setMessage(result);
configController.handleMessage(message);
}
}
}).start();
;
}
}
循環讀取 NotificationControllerV2 中的隊列,如果有消息的話就構造一個 Release-Message 的對象,然後調用 NotificationControllerV2 中的 handleMessage() 方法進行消息的處理。
ReleaseMessage 就一個字段,模擬消息內容,具體代碼如下所示。
public class ReleaseMessage {
private String message;
public void setMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
接下來,我們來看 handleMessage 做了哪些工作。
NotificationControllerV2 實現了 ReleaseMessageListener 接口,ReleaseMessageListener 中定義了 handleMessage() 方法,具體代碼如下所示。
public interface ReleaseMessageListener {
void handleMessage(ReleaseMessage message);
}
handleMessage 就是當配置發生變化的時候,發送通知的消息監聽器。消息監聽器在得到配置發佈的信息後,會通知對應的客戶端,具體代碼如下所示。
@RestController
public class NotificationControllerV2 implements ReleaseMessageListener {
private final Multimap<String, DeferredResultWrapper> deferredResults = Multimaps
.synchronizedSetMultimap(HashMultimap.create());
@Override
public void handleMessage(ReleaseMessage message) {
System.err.println("handleMessage:" + message);
List<DeferredResultWrapper> results = Lists.newArrayList(deferredResults.get("xxxx"));
for (DeferredResultWrapper deferredResultWrapper : results) {
List<ApolloConfigNotification> list = new ArrayList<>();
list.add(new ApolloConfigNotification("application", 1));
deferredResultWrapper.setResult(list);
}
}
}
Apollo 的實時推送是基於 Spring DeferredResult 實現的,在 handleMessage() 方法中可以看到是通過 deferredResults 獲取 DeferredResult,deferredResults 就是第一行的 Multimap,Key 其實就是消息內容,Value 就是 DeferredResult 的業務包裝類 DeferredResultWrapper,我們來看下 DeferredResultWrapper 的代碼,代碼如下所示。
public class DeferredResultWrapper {
private static final long TIMEOUT = 60 * 1000;// 60 seconds
private static final ResponseEntity<List<ApolloConfigNotification>> NOT_MODIFIED_RESPONSE_LIST = new ResponseEntity<>(
HttpStatus.NOT_MODIFIED);
private DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> result;
public DeferredResultWrapper() {
result = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE_LIST);
}
public void onTimeout(Runnable timeoutCallback) {
result.onTimeout(timeoutCallback);
}
public void onCompletion(Runnable completionCallback) {
result.onCompletion(completionCallback);
}
public void setResult(ApolloConfigNotification notification) {
setResult(Lists.newArrayList(notification));
}
public void setResult(List<ApolloConfigNotification> notifications) {
result.setResult(new ResponseEntity<>(notifications, HttpStatus.OK));
}
public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> getResult() {
return result;
}
}
通過 setResult() 方法設置返回結果給客戶端,以上就是當配置發生變化,然後通過消息監聽器通知客戶端的原理,那麼客戶端是在什麼時候接入的呢?具體代碼如下。
@RestController
public class NotificationControllerV2 implements ReleaseMessageListener {
// 模擬配置更新, 向其中插入數據表示有更新
public static Queue<String> queue = new LinkedBlockingDeque<>();
private final Multimap<String, DeferredResultWrapper> deferredResults = Multimaps
.synchronizedSetMultimap(HashMultimap.create());
@GetMapping("/getConfig")
public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> getConfig() {
DeferredResultWrapper deferredResultWrapper = new DeferredResultWrapper();
List<ApolloConfigNotification> newNotifications = getApolloConfigNotifications();
if (!CollectionUtils.isEmpty(newNotifications)) {
deferredResultWrapper.setResult(newNotifications);
} else {
deferredResultWrapper.onTimeout(() -> {
System.err.println("onTimeout");
});
deferredResultWrapper.onCompletion(() -> {
System.err.println("onCompletion");
});
deferredResults.put("xxxx", deferredResultWrapper);
}
return deferredResultWrapper.getResult();
}
private List<ApolloConfigNotification> getApolloConfigNotifications() {
List<ApolloConfigNotification> list = new ArrayList<>();
String result = queue.poll();
if (result != null) {
list.add(new ApolloConfigNotification("application", 1));
}
return list;
}
}
NotificationControllerV2 中提供了一個 /getConfig 的接口,客戶端在啓動的時候會調用這個接口,這個時候會執行 getApolloConfigNotifications() 方法去獲取有沒有配置的變更信息,如果有的話證明配置修改過,直接就通過 deferredResultWrapper.setResult(newNotifications) 返回結果給客戶端,客戶端收到結果後重新拉取配置的信息覆蓋本地的配置。
如果 getApolloConfigNotifications() 方法沒有返回配置修改的信息,則證明配置沒有發生修改,那就將 DeferredResultWrapper 對象添加到 deferredResults 中,等待後續配置發生變化時消息監聽器進行通知。
同時這個請求就會掛起,不會立即返回,掛起是通過 DeferredResultWrapper 中的下面這部分代碼實現的,具體代碼如下所示。
private static final long TIMEOUT = 60 * 1000; // 60 seconds
private static final ResponseEntity<List<ApolloConfigNotification>> NOT_MODIFIED_RESPONSE_LIST = new ResponseEntity<>(
HttpStatus.NOT_MODIFIED);
private DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> result;
public DeferredResultWrapper() {
result = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE_LIST);
}
在創建 DeferredResult 對象的時候指定了超時的時間和超時後返回的響應碼,如果 60s 內沒有消息監聽器進行通知,那麼這個請求就會超時,超時後客戶端收到的響應碼就是 304。
整個 Config Service 的流程就走完了,接下來我們來看一下客戶端是怎麼實現的,我們簡單地寫一個測試類模擬客戶端註冊,具體代碼如下所示。
public class ClientTest {
public static void main(String[] args) {
reg();
}
private static void reg() {
System.err.println("註冊");
String result = request("http://localhost:8081/getConfig");
if (result != null) {
// 配置有更新, 重新拉取配置
// ......
}
// 重新註冊
reg();
}
private static String request(String url) {
HttpURLConnection connection = null;
BufferedReader reader = null;
try {
URL getUrl = new URL(url);
connection = (HttpURLConnection) getUrl.openConnection();
connection.setReadTimeout(90000);
connection.setConnectTimeout(3000);
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept-Charset", "utf-8");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Charset", "UTF-8");
System.out.println(connection.getResponseCode());
if (200 == connection.getResponseCode()) {
reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
StringBuilder result = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
result.append(line);
}
System.out.println("結果 " + result);
return result.toString();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (connection != null) {
connection.disconnect();
}
}
return null;
}
}
首先啓動 /getConfig 接口所在的服務,然後啓動客戶端,然後客戶端就會發起註冊請求,如果有修改直接獲取到結果,則進行配置的更新操作。如果無修改,請求會掛起,這裏客戶端設置的讀取超時時間是 90s,大於服務端的 60s 超時時間。
每次收到結果後,無論是有修改還是無修改,都必須重新進行註冊,通過這樣的方式就可以達到配置實時推送的效果。
我們可以調用之前寫的 /addMsg 接口來模擬配置發生變化,調用之後客戶端就能馬上得到返回結果。