歡迎光臨
每天分享高質量文章

【RPC 專欄】從跨語言呼叫到 dubbo2.js

點選上方“芋道原始碼”,選擇“置頂公眾號”

技術文章第一時間送達!

原始碼精品專欄

 


摘要: 原創出處 https://www.cnkirito.moe/dubbojs-in-qianmi/ 「老徐」歡迎轉載,保留摘要,謝謝!

  • 微服務跨語言呼叫

  • 通用協議的跨語言支援

  • 定製協議的跨語言支援

    • motan2協議的跨語言支援

  • dubbo協議的跨語言支援

    • Dubbo協議報文格式

    • dubbo2.js解決方案

    • dubbo2.js快速入門

  • dubbo2.js特性

  • MORE DETAILS

  • 666. 彩蛋


dubbo2.js 是 千米網 貢獻給 dubbo 社群的一款 nodejs dubbo 客戶端,它提供了 nodejs 對原生 dubbo 協議的支援,使得 nodejs 和 java 這兩種異構語言的 rpc 呼叫變得便捷,高效。

微服務跨語言呼叫

微服務架構已成為目前網際網路架構的趨勢,關於微服務的討論,幾乎佔據了各種技術大會的絕大多數版面。國內使用最多的服務治理框架非阿裡開源的 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的美好時代

springmvc

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

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

仔細揣摩下可以發現這麼做和雙協議的配置區別並不是大,只不過這裡的 agent 是隱式存在的,與主服務共生。明顯的區別在於 agent 方案中異構語言並不直接互動。

dubbo協議的跨語言支援

dubbo 協議設計之初只考慮到了常規的 rpc 呼叫場景,它並不是為跨語言而設計,但跨語言支援從來不是隻有支援、不支援兩種選擇,而是要按難易程度來劃分。是的,dubbo 協議的跨語言呼叫可能並不好做,但並非無法實現。千米網便實現了這一點,nodejs 構建的前端業務是異構語言的主戰場,最終實現了 dubbo2.js,打通了 nodejs 和原生 dubbo 協議。作為本文第二部分的核心內容,重點介紹下我們使用 dubbo2.js 幹了什麼事。

Dubbo協議報文格式

dubbo協議

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 來說,主要有兩個難點:

  1. 異構語言如何表示 java 中的資料型別,特別是動態語言,可能不存在嚴格的資料型別

  2. 序列化方案如何做到跨語言

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 服務是一件多麼輕鬆的事。

  1. 建立 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 map = new HashMap();
        map.put("id""1");
        map.put("name""test");
        response.setInfo(map);
        return response;
    }
}

暴露服務:


<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:protocol name="dubbo" port="20880"/>

    
    <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;
}

export const demoService = dubbo.proxyService({
  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', {
          id1,
          name'nodejs',
          email'node@qianmi.com',
        }),
      ];
    },
  },
});

使用 typescript 可以帶來更好的開發體驗。

編寫呼叫類 main.ts:

import {demoService} from './dubboConfig'

demoService.sayHello('kirito').then(({res,err})=>{
    console.log(res)
});
  1. 執行呼叫

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 協議報文,完成方法呼叫,並獲取響應的全流程。

666. 彩蛋




如果你對 Dubbo 感興趣,歡迎加入我的知識星球一起交流。

知識星球

目前在知識星球(https://t.zsxq.com/2VbiaEu)更新瞭如下 Dubbo 原始碼解析如下:

01. 除錯環境搭建
02. 專案結構一覽
03. 配置 Configuration
04. 核心流程一覽

05. 拓展機制 SPI

06. 執行緒池

07. 服務暴露 Export

08. 服務取用 Refer

09. 註冊中心 Registry

10. 動態編譯 Compile

11. 動態代理 Proxy

12. 服務呼叫 Invoke

13. 呼叫特性 

14. 過濾器 Filter

15. NIO 伺服器

16. P2P 伺服器

17. HTTP 伺服器

18. 序列化 Serialization

19. 叢集容錯 Cluster

20. 優雅停機

21. 日誌適配

22. 狀態檢查

23. 監控中心 Monitor

24. 管理中心 Admin

25. 運維命令 QOS

26. 鏈路追蹤 Tracing


一共 60 篇++

贊(0)

分享創造快樂