塗鴉智慧 dubbo-go 億級流量的實踐與探索

阿里巴巴雲原生發表於2020-06-15

塗鴉智慧 dubbo-go 億級流量的實踐與探索

dubbo 是一個基於 Java 開發的高效能的輕量級 RPC 框架,dubbo 提供了豐富的服務治理功能和優秀的擴充套件能力。而 dubbo-go 在 java 與 golang 之間提供統一的服務化能力與標準,是塗鴉智慧目前最需要解決的主要問題。本文分為實踐和快速接入兩部分,分享在塗鴉智慧的 dubbo-go 實戰經驗,意在幫助使用者快速接入 dubbo-go RPC 框架,希望能讓大家少走些彎路。

另外,文中的測試程式碼基於 dubbo-go 版本 v1.4.0

dubbo-go 閘道器實踐

dubbo-go 在塗鴉智慧的使用情況如上圖,接下來會為大家詳細介紹落地細節,希望這些在生產環境中總結的經驗能夠幫助到大家。

背景

在塗鴉智慧,dubbo-go 已經作為了 golang 服務與原有 dubbo 叢集打通的首選 RPC 框架。其中比較有代表性的 open-gateway 閘道器係統(下文統一稱 gateway,開源版本見 https://github.com/dubbogo/dubbo-go-proxy)。該 gateway 動態載入內部 dubbo 介面資訊,以 HTTP API 的形式對外暴露。該閘道器意在解決上一代閘道器的以下痛點。

  • 通過頁面配置 dubbo 介面開放規則,步驟繁瑣,許可權難以把控。
  • 介面非 RESTful 風格,對外部開發者不友好。
  • 依賴繁重,升級風險大。
  • 併發效能問題。

架構設計

針對如上痛點,隨即著手準備設計新的 gateway 架構。首先就是語言選型,golang 的協程呼叫模型使得 golang 非常適合構建 IO 密集型的應用,且應用部署上也較 java 簡單。經過調研後我們敲定使用 golang 作為 proxy 的編碼語言,並使用 dubbo-go 用於連線 dubbo provider 叢集。provider 端的業務應用通過使用 java 的外掛,以註解形式配置 API 配置資訊,該外掛會將配置資訊和 dubbo 介面後設資料更新到後設資料註冊中心(下圖中的 redis )。這樣一來,配置從管理後臺頁面轉移到了程式程式碼中。開發人員在編碼時,非常方便地看到 dubbo 介面對外的 API 描述,無需從另外一個管理後臺配置 API 的使用方式。

實踐

從上圖可以看到,閘道器能動態載入 dubbo 介面資訊,呼叫 dubbo 介面是基於 dubbo 泛化呼叫。泛化呼叫使 client 不需要構建 provider 的 interface 程式碼,在 dubbo-go 中表現為無需呼叫 config.SetConsumerService 和 hessian.RegisterPOJO 方法,而是將請求模型純引數完成,這使得 client 動態新增、修改介面成為可能。在 apache/dubbo-sample/golang/generic/go-client 中的有泛化呼叫的演示程式碼。

func test() {
    var appName = "UserProviderGer"
    var referenceConfig = config.ReferenceConfig{
        InterfaceName: "com.ikurento.user.UserProvider",
        Cluster:       "failover",
        Registry:      "hangzhouzk",
        Protocol:      dubbo.DUBBO,
        Generic:       true,
    }
    referenceConfig.GenericLoad(appName) // appName is the unique identification of RPCService

    time.Sleep(3 * time.Second)

    resp, err := referenceConfig.GetRPCService().(*config.GenericService).
        Invoke([]interface{}{"GetUser", []string{"java.lang.String"}, []interface{}{"A003"}})
    if err != nil {
        panic(err)
    }
}

泛化呼叫的實現其實相當簡單。其功能作用在 dubbo 的 Filter 層中。Generic Filter 已經作為預設開啟的 Filter 加入到 dubbo Filter 鏈中。其核心邏輯如下:

func (ef *GenericFilter) Invoke(ctx context.Context, invoker protocol.Invoker, invocation protocol.Invocation) protocol.Result {
    if invocation.MethodName() == constant.GENERIC && len(invocation.Arguments()) == 3 {
        oldArguments := invocation.Arguments()

        if oldParams, ok := oldArguments[2].([]interface{}); ok {
            newParams := make([]hessian.Object, 0, len(oldParams))
            for i := range oldParams {
                newParams = append(newParams, hessian.Object(struct2MapAll(oldParams[i])))
            }
            newArguments := []interface{}{
                oldArguments[0],
                oldArguments[1],
                newParams,
            }
            newInvocation := invocation2.NewRPCInvocation(invocation.MethodName(), newArguments, invocation.Attachments())
            newInvocation.SetReply(invocation.Reply())
            return invoker.Invoke(ctx, newInvocation)
        }
    }
    return invoker.Invoke(ctx, invocation)
}

Generic Filter 將使用者請求的結構體引數轉化為統一格式的 map(程式碼中的 struct2MapAll ),將類( golang 中為 struct )的正反序列化操作變成 map 的正反序列化操作。這使得無需 POJO 描述通過硬編碼注入 hessain 庫。

從上面程式碼可以看到,泛化呼叫實際需要動態構建的內容有 4 個,ReferenceConfig 中需要的 InterfaceName 、引數中的 method 、ParameterTypes、實際入參 requestParams。

那麼這些引數是如何從 HTTP API 匹配獲取到的呢?

這裡就會用到上文提到的 provider 用於收集後設資料的外掛。引入外掛後,應用在啟動時會掃描需要暴露的 dubbo 介面,將 dubbo 後設資料和 HTTP API 關聯。外掛使用方法大致如下,這裡調了幾個簡單的配置作為示例,實際生產時註解內容會更多。

最終獲得的 dubbo 後設資料如下:

{
    "key": "POST:/hello/{uid}/add",
    "interfaceName": "com.tuya.hello.service.template.IUserServer",
    "methodName": "addUser",
    "parameterTypes": ["com.tuya.gateway.Context", "java.lang.String", "com.tuya.hello.User"],
    "parameterNames": ["context", "uid", "userInfo"],
    "updateTimestamp": "1234567890",
    "permissionDO":{},
    "voMap": {
        "userInfo": {
            "name": "java.lang.String",
            "sex": "java.lang.String",
            "age": "java.lang.Integer"
        }
    },
    "parameterNameHumpToLine": true,
    "resultFiledHumpToLine": false,
    "protocolName": "dubbo",
  .......
}

Gateway 從後設資料配置中心訂閱到以上資訊,就能把一個 API 請求匹配到一個 dubbo 介面。再從 API 請求中抓取引數作為入參。這樣功能就完成了流量閉環。

以上內容,大家應該對此 gateway 的專案拓撲結構有了清晰的認知。我接著分享專案在使用 dubbo-go 過程中遇到的問題和調優經驗。19 年初,當時的 dubbo-go 專案還只是構建初期,沒有什麼使用者落地的經驗。我也是一邊參與社群開發,一邊編碼公司內部閘道器專案。在解決了一堆 hessain 序列化和 zookeeper 註冊中心的問題後,專案最終跑通了閉環。但是,作為一個核心應用,跑通閉環離上生產環境還有很長的路要走,特別是使用了當時穩定性待測試的新框架。整個測試加上功能補全,整整花費了一個季度的時間,直到專案趨於穩定,壓測效果也良好。單臺閘道器機器( 2C 8G )全鏈路模擬真實環境壓測達到 2000 QPS。由於引入了比較重的業務邏輯(單個請求平均呼叫 3 個 dubbo 介面),對於這個壓測結果,是符合甚至超出預期的。

總結了一些 dubbo-go 引數配置調優的經驗,主要是一些網路相關配置。大家在跑 demo 時,應該會看到配置檔案最後有一堆配置,但如果對 dubbo-go 底層網路模型不熟悉,就很難理解這些配置的含義。目前 dubbo-go 網路層以 getty 為底層框架,實現讀寫分離和協程池管理。getty 對外暴露 session 的概念,session 提供一系列網路層方法注入的實現,因為本文不是原始碼解析文件,在這裡不過多論述。讀者可以簡單的認為 dubbo-go 維護了一個 getty session 池,session 又維護了一個 TCP 連線池。對於每個連線,getty 會有讀協程和寫協程伴生,做到讀寫分離。這裡我儘量用通俗的註釋幫大家梳理下對效能影響較大的幾個配置含義:

protocol_conf:
  # 這裡是協議獨立的配置,在dubbo協議下,大多數配置即為getty session相關的配置。
  dubbo:
    # 一個session會始終保證connection_number個tcp連線個數,預設是16,
    # 但這裡建議大家配置相對小的值,一般系統不需要如此多的連線個數。
    # 每隔reconnect_interval時間,檢查連線個數,如果小於connection_number,
    # 就建立連線。填0或不填都為預設值300ms
    reconnect_interval: 0
    connection_number: 2
    # 客戶端傳送心跳的間隔
    heartbeat_period: "30s"
    # OnCron時session的超時時間,超過session_timeout無返回就關閉session
    session_timeout: "30s"
    # 每一個dubbo interface的客戶端,會維護一個最大值為pool_size大小的session池。
    # 每次請求從session池中select一個。所以真實的tcp數量是session數量*connection_number,
    # 而pool_size是session數量的最大值。測試總結下來一般程式4個tcp連線足以。
    pool_size: 4
    # session保活超時時間,也就是超過session_timeout時間沒有使用該session,就會關閉該session
    pool_ttl: 600
    # 處理返回值的協程池大小
    gr_pool_size: 1200
    # 讀資料和協程池中的緩衝佇列長度,目前已經廢棄。不使用緩衝佇列
    queue_len: 64
    queue_number: 60
    getty_session_param:
      compress_encoding: false
      tcp_no_delay: true
      tcp_keep_alive: true
      keep_alive_period: "120s"
      tcp_r_buf_size: 262144
      tcp_w_buf_size: 65536
      pkg_wq_size: 512
      tcp_read_timeout: "1s"  # 每次讀包的超時時間
      tcp_write_timeout: "5s" # 每次寫包的超時時間
      wait_timeout: "1s" 
      max_msg_len: 102400     # 最大資料傳輸長度
      session_name: "client"

dubbo-go 快速接入

前文已經展示過 dubbo-go 在塗鴉智慧的實踐成果,接下來介紹快速接入 dubbo-go 的方式。

第一步:hello world

dubbo-go 使用範例目前和 dubbo 一致,放置在 apache/dubbo-samples 專案中。在 dubbo-sample/golang 目錄下,使用者可以選擇自己感興趣的 feature 目錄,快速測試程式碼效果。

tree dubbo-samples/golang -L 1
dubbo-samples/golang
├── README.md
├── async
├── ci.sh
├── configcenter
├── direct
├── filter
├── general
├── generic
├── go.mod
├── go.sum
├── helloworld
├── multi_registry
└── registry

我們以 hello world 為例,按照 dubbo-samples/golang/README.md 中的步驟,分別啟動 server 和 client 。可以嘗試 golang 呼叫 java 、 java 呼叫 golang 、golang 呼叫 golang 、java 呼叫 java。dubbo-go 在協議上支援和 dubbo 互通。

我們以啟動 go-server 為例,註冊中心預設使用 zookeeper 。首先確認本地的 zookeeper 是否執行正常。然後執行以下命令,緊接著你就可以看到你的服務正常啟動的日誌了。

export ARCH=mac
export ENV=dev
cd dubbo-samples/golang/helloworld/dubbo/go-server
sh ./assembly/$ARCH/$ENV.sh
cd ./target/darwin/user_info_server-2.6.0-20200608-1056-dev/
sh ./bin/load.sh start

第二步:在專案中使用 dubbo-go

上面,我們通過社群維護的測試程式碼和啟動指令碼將用例跑了起來。接下來,我們需要在自己的程式碼中嵌入 dubbo-go 框架。很多朋友往往是在這一步遇到問題,這裡我整理的一些常見問題,希望能幫到大家。

1. 環境變數

目前 dubbo-go 有 3 個環境變數需要配置。

  • CONF_CONSUMER_FILE_PATH : Consumer 端配置檔案路徑,使用 consumer 時必需。
  • CONF_PROVIDER_FILE_PATH:Provider 端配置檔案路徑,使用 provider 時必需。
  • APP_LOG_CONF_FILE :Log 日誌檔案路徑,必需。
  • CONF_ROUTER_FILE_PATH:File Router 規則配置檔案路徑,使用 File Router 時需要。
2. 程式碼注意點
  • 注入服務 : 檢查是否執行以下程式碼
# 客戶端
func init() {
    config.SetConsumerService(userProvider)
}

# 服務端
func init() {
    config.SetProviderService(new(UserProvider))
}
  • 注入序列化描述 :檢查是否執行以下程式碼
hessian.RegisterJavaEnum(Gender(MAN))
hessian.RegisterJavaEnum(Gender(WOMAN))
hessian.RegisterPOJO(&User{})
3. 正確理解配置檔案
  • references/services 下的 key ,如下面例子的 "UserProvider" 需要和服務 Reference() 返回值保持一致,此為標識改介面的 key。

    references:
    "UserProvider":
    registry: "hangzhouzk"
    protocol : "dubbo"
    interface : "com.ikurento.user.UserProvider"
    cluster: "failover"
    methods :
    - name: "GetUser"
      retries: 3
    
  • 註冊中心如果只有一個註冊中心叢集,只需配置一個。多個 IP 用逗號隔開,如下:

    registries :
    "hangzhouzk":
    protocol: "zookeeper"
    timeout    : "3s"
    address: "172.16.120.181:2181,172.16.120.182:2181"
    username: ""
    password: ""
    
4. java 和 go 的問題
  • go 和 java 互動的大小寫 :golang 為了適配 java 的駝峰格式,在呼叫 java 服務時,會自動將 method 和屬性首字母變成小寫。很多同學故意將 java 程式碼寫成適配 golang 的引數定義,將首字母大寫,最後反而無法序列化匹配。

第三步:擴充功能

dubbo-go 和 dubbo 都提供了非常豐富的擴充機制。可以實現自定義模組代替 dubbo-go 預設模組,或者新增某些功能。比如實現 Cluster、Filter 、Router 等來適配業務的需求。這些注入方法暴露在 dubbo-go/common/extension 中,允許使用者呼叫及配置。

本文作者: 潘天穎,Github ID @pantianying,開源愛好者,就職於塗鴉智慧。

歡迎加入 dubbo-go 社群

有任何 dubbo-go 相關的問題,可以加我們的釘釘群 23331795 詢問探討,我們一定第一時間給出反饋。

最新活動

Dubbo-go ASoC 相關題目 ,參加詳情 請點選

更多原創文章乾貨分享,請關注公眾號
  • 塗鴉智慧 dubbo-go 億級流量的實踐與探索
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章