dubbo2.js 是 千米網 貢獻給 dubbo 社區的一款 nodejs dubbo 客戶端,它提供了 nodejs 對原生 dubbo 協議的支持,使得 nodejs 和 java 這兩種異構語言的 rpc 調用變得便捷,高效。
原創出處 https://www.cnkirito.moe/dubbojs-in-qianmi/ 「老徐」
微服務跨語言調用
微服務架構已成爲目前互聯網架構的趨勢,關於微服務的討論,幾乎佔據了各種技術大會的絕大多數版面。國內使用最多的服務治理框架非阿里開源的 dubbo 莫屬,千米網也選擇了 dubbo 作爲微服務治理框架。另一方面,和大多數互聯網公司一樣,千米的開發語言是多樣的,大多數後端業務由 java 支撐,而每個業務線有各自開發語言的選擇權,便出現了 nodejs,python,go 多語言調用的問題。
跨語言調用是一個很大的話題,也是一個很有挑戰的技術活,目前業界經常被提及的解決方案有如下幾種,不妨拿出來老生常談一番:
- spring cloud。spring cloud 提供了一整套微服務開發組件,它主要面向 java 開發,但由於其使用的協議是基於 restful 風格的 http 協議,這使得其天然具備跨語言能力,異構語言只需要提供 http 客戶端,便可以實現跨語言調用。
- service mesh。號稱下一代微服務框架的 service mesh,其解決跨語言問題的核心在於 SideCar ,SideCar 在 service mesh 的發展過程中概念不斷的遷移,但本質都是完成了一件事:處理服務間通信,負責實現請求的可靠傳遞。
- motan。motan 是新浪微博開源的一款跨語言服務治理框架,在其早期版本中僅支持 motan-java,隨着版本演進,在目前最新版本(1.1.0)中,提供了 motan-go,motan-php,motan-openresty 等跨語言特性。類似於 service mesh 中的 SideCar,motan 藉助於 motan-go 作爲 agent 完成協議的轉發,並且依賴於定製協議:motan2,實現跨語言調用。
當我們再聊跨語言調用時我們在聊什麼?縱觀上述幾個較爲通用,成熟的解決方案,可以得出結論:解決跨語言調用的思路無非是兩種:
- 尋找一個通用的協議
-
使用 agent 完成協議的適配
如果一個新型的團隊面臨技術選型,我認爲上述的方案都可以納入參考,可考慮到遺留系統的兼容性問題 - 舊系統的遷移成本
這也關鍵的選型因素。我們做出的第一個嘗試,便是在 RPC 協議上下功夫。
通用協議的跨語言支持
springmvc的美好時代
在沒有實現真正的跨語言調用之前,想要實現“跨語言”大多數方案是使用 http 協議做一層轉換,最常見的手段莫過於藉助 springmvc 提供的 controller/restController,間接調用 dubbo provider。這種方案的優勢和劣勢顯而易見
- 優勢是簡單,是最通俗的解決方案。
- 劣勢是使得調用鏈路變長,tcp 通信之上又多了一層 http 通信;開發體驗差,爲了將 rpc 接口暴露出去,需要額外編寫一份 controller 層的代碼。
通用協議的支持
事實上,大多數服務治理框架都支持多種協議,dubbo 框架除默認的 dubbo 協議之外,還有當當網擴展的 rest 協議和千米網擴展的 json-rpc 協議可供選擇。這兩者都是通用的跨語言協議。
rest 協議爲滿足 JAX-RS 2.0 標準規範,在開發過程中引入了 @Path,@POST,@GET 等註解,習慣於編寫傳統 rpc 接口的人可能不太習慣 rest 風格的 rpc 接口。一方面這樣會影響開發體驗,另一方面,獨樹一幟的接口風格使得它與其他協議不太兼容,舊接口的共生和遷移都無法實現。如果沒有遺留系統,rest 協議無疑是跨語言方案最簡易的實現,絕大多數語言支持 rest 協議。
和 rest 協議類似,json-rpc 的實現也是文本序列化&http 協議。dubbox 在 restful 接口上已經做出了嘗試,但是 rest 架構和 dubbo 原有的 rpc 架構是有區別的,rest 架構需要對資源(Resources)進行定義, 需要用到 http 協議的基本操作 GET、POST、PUT、DELETE。在我們看來,restful 更合適互聯網系統之間的調用,而 rpc 更適合一個系統內的調用。使用 json-rpc 協議使得舊接口得以兼顧,開發習慣仍舊保留,同時獲得了跨語言的能力。
千米網在早期實踐中採用了 json-rpc 作爲 dubbo 的跨語言協議實現,並開源了基於 json-rpc 協議下的 python 客戶端 dubbo-client-py 和 node 客戶端 dubbo-node-client,使用 python 和 nodejs 的小夥伴可以藉助於它們直接調用 dubbo-provider-java 提供的 rpc 服務。系統中大多數 java 服務之間的互相調用還是以 dubbo 協議爲主,考慮到新舊協議的適配,在不影響原有服務的基礎上,我們配置了雙協議。
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:protocol name="jsonrpc" port="8080" />
dubbo 協議主要支持 java 間的相互調用,適配老接口;json-rpc 協議主要支持異構語言的調用。
定製協議的跨語言支持
微服務框架所謂的協議(protocol)可以簡單理解爲:報文格式和序列化方案。服務治理框架一般都提供了衆多的協議配置項供使用者選擇,除去上述兩種通用協議,還存在一些定製化的協議,如 dubbo 框架的默認協議:dubbo 協議以及 motan 框架提供的跨語言協議:motan2。
motan2協議的跨語言支持
motan2 協議被設計用來滿足跨語言的需求主要體現在兩個細節中—MetaData 和 motan-go。在最初的 motan 協議中,協議報文僅由 Header+Body 組成,這樣導致 path,param,group 等存儲在 Body 中的數據需要反序列得到,這對異構語言來說是很不友好的,所以在 motan2 中修改了協議的組成;weibo 開源了 motan-go ,motan-php ,motan-openresty ,並藉助於 motan-go 充當了 agent 這一翻譯官的角色,使用 simple 序列化方案來序列化協議報文的 Body 部分(simple 序列化是一種較弱的序列化方案)。
仔細揣摩下可以發現這麼做和雙協議的配置區別並不是大,只不過這裏的 agent 是隱式存在的,與主服務共生。明顯的區別在於 agent 方案中異構語言並不直接交互。
dubbo協議的跨語言支持
dubbo 協議設計之初只考慮到了常規的 rpc 調用場景,它並不是爲跨語言而設計,但跨語言支持從來不是隻有支持、不支持兩種選擇,而是要按難易程度來劃分。是的,dubbo 協議的跨語言調用可能並不好做,但並非無法實現。千米網便實現了這一點,nodejs 構建的前端業務是異構語言的主戰場,最終實現了 dubbo2.js,打通了 nodejs 和原生 dubbo 協議。作爲本文第二部分的核心內容,重點介紹下我們使用 dubbo2.js 幹了什麼事。
Dubbo協議報文格式
dubbo協議報文消息頭詳解:
- magic:類似java字節碼文件裏的魔數,用來判斷是不是 dubbo 協議的數據包。魔數是常量 0xdabb
- flag:標誌位, 一共8個地址位。低四位用來表示消息體數據用的序列化工具的類型(默認 hessian),高四位中,第一位爲 1 表示是 >* request 請求,第二位爲 1 表示雙向傳輸(即有返回 response),第三位爲 1 表示是心跳 ping 事件。
- status:狀態位, 設置請求響應狀態,dubbo 定義了一些響應的類型。具體類型見com.alibaba.dubbo.remoting.exchange.Response
- invoke id:消息 id, long 類型。每一個請求的唯一識別 id(由於採用異步通訊的方式,用來把請求 request 和返回的 response 對應上)
- body length:消息體 body 長度, int 類型,即記錄 Body Content 有多少個字節
- body content:請求參數,響應參數的抽象序列化之後存儲於此。
協議報文最終都會變成字節,使用 tcp 傳輸,任何語言只要支持網絡模塊,有類似 Socket 之類的封裝,那麼通信就不成問題。那,跨語言難在哪兒?以其他語言調用 java 來說,主要有兩個難點:
- 異構語言如何表示 java 中的數據類型,特別是動態語言,可能不存在嚴格的數據類型
- 序列化方案如何做到跨語言
dubbo2.js解決方案
上面我們分析出了兩個難點,dubbo2.js 解決這兩個問題的關鍵依賴於兩個類庫:js-to-java ,hessian.js 。js-to-java 使得 nodejs 具備 java 對象的表達能力,而 hessian.js 提供了序列化能力。藉助於 nodejs 的 socket ,婦科一套 dubbo 協議的報文格式,最終便實現了 nodejs 對 java-dubbo-provider 的調用。
dubbo2.js快速入門
爲了讓對 dubbo2.js 感興趣的讀者有一個直觀的體驗,本節呈現一個快速入門示例,讓你體會到使用 dubbo2.js 調用 dubbo 服務是一件多麼輕鬆的事。
- 創建 dubbo-java-provider
後端 dubbo 服務使用 java 來提供,這服務大多數的業務場景。首先定義服務接口:public interface DemoProvider { String sayHello(String name); String echo() ; void test(); UserResponse getUserInfo(UserRequest request); }
其次,實現服務:
public class DemoProviderImpl implements DemoProvider { public String sayHello(String name) { System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "] Hello " + name + ", request from consumer: " + RpcContext.getContext().getRemoteAddress()); return "Hello " + name + ", response form provider: " + RpcContext.getContext().getLocalAddress(); } @Override public String echo() { System.out.println("receive...."); return "pang"; } @Override public void test() { System.out.println("test"); } @Override public UserResponse getUserInfo(UserRequest request) { System.out.println(request); UserResponse response = new UserResponse(); response.setStatus("ok"); Map<String, String> map = new HashMap<String, String>(); map.put("id", "1"); map.put("name", "test"); response.setInfo(map); return response; } }
暴露服務:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 提供方應用信息,用於計算依賴關係 -->
<dubbo:application name="demo-provider"/>
<dubbo:registry protocol="zookeeper" address="localhost:2181"/>
<!-- 用dubbo協議在20880端口暴露服務 -->
<dubbo:protocol name="dubbo" port="20880"/>
<!-- 和本地bean一樣實現服務 -->
<bean id="demoProvider" class="com.alibaba.dubbo.demo.provider.DemoProviderImpl"/>
<!-- 聲明需要暴露的服務接口 -->
<dubbo:service interface="com.alibaba.dubbo.demo.DemoProvider" ref="demoProvider" version="1.0.0"/>
</beans>
我們完成了服務端的所有配置,啓動啓動類即可在本地註冊一個 dubbo 服務。
public class Provider {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"META-INF/spring/dubbo-demo-provider.xml"});
context.start();
System.in.read();
}
}
1. 實現 nodejs 的 dubbo 客戶端
安裝 dubbo2.js:
npm install dubbo2.js --save
配置 dubboConfig.ts:
import { Dubbo, java, TDubboCallResult } from 'dubbo2.js'
const dubbo = new Dubbo({
application: {name: 'demo-provider'},
register: 'localhost:2181',
dubboVersion: '2.0.0',
interfaces: [
'com.alibaba.dubbo.demo.DemoProvider',
],
});
interface IDemoService {
sayHello(name: string): TDubboCallResult<string>;
}
export const demoService = dubbo.proxyService<IDemoService>({
dubboInterface: 'com.alibaba.dubbo.demo.DemoProvider',
version: '1.0.0',
methods: {
sayHello(name: string) {
return [java.String(name)];
},
echo() {},
test() {},
getUserInfo() {
return [
java.combine('com.alibaba.dubbo.demo.UserRequest', {
id: 1,
name: 'nodejs',
email: '[email protected]',
}),
];
},
},
});
使用 typescript 可以帶來更好的開發體驗。
編寫調用類 main.ts:
import {demoService} from './dubboConfig'
demoService.sayHello('kirito').then(({res,err})=>{
console.log(res)
});
執行調用
Debug 模式啓動 nodejs 客戶端:
DEBUG=dubbo* ts-node main.ts
查看運行結果:
Hello kirito, response form provider: 172.19.6.151:20880
congratulation!
dubbo2.js特性
- 支持 zookeeper 註冊中心
- 支持原生 dubbo 協議
- 支持服務直連
- 全鏈路跟蹤
- dubbo 接口自動生成
MORE DETAILS
本文中的示例代碼,提供在此處,https://github.com/lexburner/Dubbojs-Learning 。如果你對 dubbo 協議不慎瞭解,想要理解它的工作原理,項目中提供了一個子 moudle — java-socket-consumer,使用面向過程的思路實現了 java-socket-consumer,完成了原生 socket 發送 dubbo 協議報文,完成方法調用,並獲取響應的全流程。