在微服務(wù)架構中,一般都是通過(guò) API 網(wǎng)關(guān)統一向外部系統提供 API 服務(wù)。熟悉 Spring Cloud 的同學(xué)知道,Zuul 在 Spring Cloud 中就起到了 API 網(wǎng)關(guān)的作用。同理,在微服務(wù)架構中,API 網(wǎng)關(guān)作為對外暴露服務(wù)的網(wǎng)關(guān)。
菜鳥(niǎo)在面向全球 CP(Cainiao Partner)提供服務(wù)的時(shí)候,遇到了傳統 API 網(wǎng)關(guān)在跨國網(wǎng)絡(luò )上傳輸數據的瓶頸,如下圖,Partner C(國外的 CP)調用 API Gateway 走了跨國的公網(wǎng),該段網(wǎng)絡(luò )的質(zhì)量非常不穩定,嚴重影響了 Partner C 的服務(wù)體驗。我們把這種由于網(wǎng)絡(luò )延遲高、抖動(dòng)嚴重而引起的網(wǎng)絡(luò )質(zhì)量問(wèn)題稱(chēng)為 網(wǎng)絡(luò )桎梏。
API 網(wǎng)關(guān)通過(guò)多域部署解決了這個(gè)問(wèn)題。Partner C 可以接入同域的 API 網(wǎng)關(guān),將 Partner C 的 API 服務(wù)調用轉發(fā)到目標 API 網(wǎng)關(guān),需要引入一種機制來(lái)完成調用轉發(fā),這里使用的就是 跨域調用。而前文提到的網(wǎng)絡(luò )桎梏問(wèn)題下沉到跨域調用解決。本文將會(huì )重點(diǎn)介紹菜鳥(niǎo)的跨域調用解決方案,希望能給你一些啟發(fā)。
很多應用,最開(kāi)始都部署在 IDC(中心機房),通過(guò) SOA(比如 Dubbo)進(jìn)行相互依賴(lài),隨著(zhù)業(yè)務(wù)的發(fā)展,會(huì )有上云和出海的需求,但并非所有系統都會(huì )多域部署,從而存在了跨域依賴(lài)的問(wèn)題,這個(gè)時(shí)候,就可以通過(guò) 跨域調用 解決。
例如:A、B、C 三個(gè)應用都部署在 A 域,A 和 B 都依賴(lài) C,但是只有應用 A 和 B 會(huì )出海,從而 A 和 B 會(huì )跨域依賴(lài)應用 C。
為了讓?xiě)?A、B、C 盡可能少的改動(dòng)甚至不改動(dòng)就完成出海,要求跨域調用具備普通 SOA 框架(比如 Dubbo)的調用形式,具體來(lái)說(shuō),跨域調用要和 Dubbo 一樣,支持基于接口的調用。這樣,在接口不變的情況下,可以很方便的在域內調用和域見(jiàn)調用進(jìn)行切換。
從 API Gateway 和應用出海的實(shí)例可以看出,需要沉淀一種跨域調用的能力。在菜鳥(niǎo)提供了專(zhuān)門(mén)用于跨域調用的項目:
CRPC(CrossDomain Remote Procedure Call)。CRPC 的設計目標總結成一句話(huà):像調用本地服務(wù)一樣解決網(wǎng)絡(luò )桎梏調用跨域服務(wù)。
網(wǎng)絡(luò )桎梏是跨域調用要解決的首要問(wèn)題,解決方式是通過(guò)雙向代理的模式,代理之間通過(guò)專(zhuān)線(xiàn)打通。
如下圖(C 指 Consumer,P 指 Provider):Consumer 和 Provider 在 CRPC 中都是 Client,代理層作為 CRPC 的 Server。對于同域的 Consumer 而言,Proxy 是正向代理;對于同域的 Provider 而言,Proxy 是反向代理。
CRPC 將一次調用鏈路分成了三段:兩段同域調用,一段跨域調用。由于跨域調用僅存在于 Proxy 之間,而 Proxy 之間又是通過(guò)專(zhuān)線(xiàn)打通,從而能夠解決跨域的網(wǎng)絡(luò )桎梏。
當業(yè)務(wù)發(fā)生一次遠程調用時(shí),客戶(hù)端會(huì )將請求發(fā)到同域的 Proxy(稱(chēng)為源 Proxy),源 Proxy 將請求轉發(fā)到 Provider 同域的 Proxy(稱(chēng)為目標 Proxy),目標 Proxy 將請求推送給 Provider 端,Provider 端完成本地調用后,將相應返回給目標 Proxy,然后將該響應原路返回給源 Proxy,源 Proxy 然后將響應返回給 Consumer 端,通過(guò)圖中標注的 6 個(gè)步驟,完成整個(gè)跨域調用鏈路。
由于網(wǎng)絡(luò )被分成了三段,引入了一個(gè)新的問(wèn)題:如何完成服務(wù)發(fā)現? 此外,由于 Proxy 和 Consumer/Provider 的結構是 C/S,從而引入了另一個(gè)問(wèn)題:如果保證 Server 端的性能和可擴展性?最后,所有依賴(lài)跨域調用的域都需要部署 Proxy:如何完成 Proxy 的快速部署? 下面將分別解決這里提出的問(wèn)題。
首先來(lái)解決跨域調用的服務(wù)發(fā)現?缬蛘{用的網(wǎng)絡(luò )被分成了三段,其中 Consumer 到 Proxy,Proxy 到 Provider 這兩段是同域調用,Proxy 到 Proxy 是跨域網(wǎng)絡(luò )。
同域的服務(wù)發(fā)現采用 soa 的服務(wù)發(fā)現,如下,在這個(gè)模型中,服務(wù)提供者首先把服務(wù)注冊到服務(wù)注冊中心,然后消費者向服務(wù)注冊中心獲取服務(wù)元信息(如生產(chǎn)者地址),然后消費者向服務(wù)提供者發(fā)起一次服務(wù)調用。在 Spring Cloud Netflix 中,Eureka 作為服務(wù)注冊中心。
為了實(shí)現跨域網(wǎng)絡(luò )的服務(wù)發(fā)現,引入了一個(gè)新的思想:服務(wù)具有域的概念。CRPC 把域的概念提了出來(lái),服務(wù)有域的概念,語(yǔ)義為某域提供的 xxx 服務(wù)。
舉個(gè)例子,某個(gè)系統擁有自己的配置數據,通過(guò)暴露寫(xiě)配置服務(wù)來(lái)更改配置數據,對于不同域來(lái)說(shuō),比如上海和新加坡,配置數據是不一樣的,所以在調用寫(xiě)配置服務(wù)時(shí),需要明確指明是調用上海的寫(xiě)配置服務(wù)還是調用新加坡的寫(xiě)配置服務(wù)。
在這個(gè)思路下,在調用方調用一個(gè)服務(wù)時(shí)需要指定服務(wù)提供的域,該信息就可以用于跨域網(wǎng)絡(luò )的路由。
從而,跨域調用解決了從 Consumer 到 Provider 的服務(wù)發(fā)現,如下圖:
接下來(lái)解決 Server 端的性能問(wèn)題,主要介紹 CRPC 使用的異步化和長(cháng)連接方案。
異步化:提升 Proxy 吞吐量
由于跨域調用有較長(cháng)的 rt,所以如果每次調用都獨占一個(gè)線(xiàn)程,可能導致線(xiàn)程資源緊張。因此,Proxy 使用了異步化架構,從而達到可觀(guān)的吞吐量。Proxy 的入口和出口分為如下幾種情況:
需要針對每種入口和出口采用異步實(shí)現,Proxy 節點(diǎn)內部的線(xiàn)程情況如下,從而 SOA 和 Servlet 的線(xiàn)程資源不會(huì )受 Proxy 內部處理邏輯阻塞影響,Proxy 內部的線(xiàn)程資源也不會(huì )受調用下游 rt 長(cháng)而影響。
長(cháng)連接:減少 Proxy 資源使用,提升 Proxy 轉發(fā)性能
Proxy 站點(diǎn)間通過(guò) HTTP/2 進(jìn)行長(cháng)連接,HTTP/2 使用二進(jìn)制格式,多路復用以及 HPACK 壓縮技術(shù),性能較 HTTP/1.x 有很大提升,并且長(cháng)連接能夠減少 Socket 資源。
Proxy 節點(diǎn)間長(cháng)連接如下,長(cháng)連接建立在 Proxy 和目標域的 nginx 集群上,而不是建立在源 Proxy 和目標 Proxy 之間。通過(guò)雙向建立長(cháng)連接,完成 Domain A 調用 Domain B 以及反向調用。
由于采用了 C/S 架構,Server 端需要支持水平擴展。Proxy 的入口有兩個(gè),分別是 Consumer 調用 Proxy,以及 Proxy 調用 Proxy。只需要保證兩種調用時(shí)負載均衡的,并且 Server 端是無(wú)狀態(tài)的,就可以很方便的實(shí)現 Server 端的水平擴展。
其中,Consumer 調用 Proxy 是通過(guò)微服務(wù)框架實(shí)現的軟負載均衡實(shí)現的負載均衡策略。而 Proxy 調用 Proxy 是通過(guò) Nginx 將 HTTP/2 協(xié)議降級成 HTTP/1.x 反向代理實(shí)現的負載均衡。這里也解釋了前面講述異步化時(shí)為什么 Proxy 的入口是 HTTP 而不是 HTTP/2,以及介紹長(cháng)連接時(shí) Proxy 為什么是和 Nginx 集群做長(cháng)連接的問(wèn)題。
最后來(lái)解決 Proxy 快速部署的問(wèn)題,為了能夠使 Proxy 能夠快速部署,使用了 Docker 的部署方式,并且多域的 Proxy 沒(méi)有差異,完全對等部署。從而一個(gè)新的域需要生產(chǎn) Proxy 時(shí),可以快速部署。
在服務(wù)路由一節講到,Consumer 調用服務(wù)時(shí)需要指定服務(wù)提供方所在的域,但是 CRPC 要基于接口的調用,這要求指定域的操作不能侵入接口,具體的 CRPC 調用如下。(此處代碼僅做示例,無(wú)法調用)
1. 生成跨域 Provider Bean
// 本地接口實(shí)現通過(guò) CRPCProvider 注解生成跨域服務(wù)的 Provider
@CRPCProvider(serviceVersion = "1.0.0")
public class TestServiceImpl implements TestService{
@Override
public String test(String input) {
return "hello " + input;
}
}
2. 生成跨域 Consumer Bean
@Configuration
public class CRPCConfig {
// 通過(guò)在接口上增加 CRPCConsumer 注解生成跨域服務(wù)的 Consumer,指定服務(wù)提供方的默認域
@CRPCConsumer(version = "1.0.0", defaultDomain = "新加坡")
private TestService testService;
}
3. CRPC 調用
public Class CRPCTest{
@Autowired
private TestService testService;
// 調用時(shí)使用 CRPCConsumer 配置的默認域
@Test
public void test_0() {
String result = testService.test("world");
}
// 調用時(shí)指定服務(wù)提供方的域
@Test
public void test_1() {
String result = ConsumerUtil.forDomain("新加坡", testService).test("world");
}
}
總結思考