【千米網】從跨語言調用到dubbo2.js

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的美好時代
    【千米網】從跨語言調用到dubbo2.js

在沒有實現真正的跨語言調用之前,想要實現“跨語言”大多數方案是使用 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協議的跨語言支持

【千米網】從跨語言調用到dubbo2.js

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 序列化是一種較弱的序列化方案)。
【千米網】從跨語言調用到dubbo2.js

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

dubbo協議的跨語言支持

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

Dubbo協議報文格式

【千米網】從跨語言調用到dubbo2.js

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

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