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

1.png

做者 | 潘天穎,Github ID @pantianying,開源愛好者,就任於塗鴉智能java

dubbo 是一個基於 Java 開發的高性能的輕量級 RPC 框架,dubbo 提供了豐富的服務治理功能和優秀的擴展能力。而 dubbo-go 在 java 與 golang 之間提供統一的服務化能力與標準,是塗鴉智能目前最須要解決的主要問題。本文分爲實踐和快速接入兩部分,分享在塗鴉智能的 dubbo-go 實戰經驗,意在幫助用戶快速接入 dubbo-go RPC 框架,但願能讓你們少走些彎路。另外,文中的測試代碼基於 dubbo-go版本 v1.4.0。git

dubbo-go 網關實踐

2.png

dubbo-go 在塗鴉智能的使用狀況如上圖,接下來會爲你們詳細介紹落地細節,但願這些在生產環境中總結的經驗可以幫助到你們。github

1. 背景

在塗鴉智能,dubbo-go 已經做爲了 golang 服務與原有 dubbo 集羣打通的首選 RPC 框架。其中比較有表明性的 open-gateway 網關係統(下文統一稱 gateway,開源版本見 github.com/dubbogo/dub…)。該 gateway 動態加載內部 dubbo 接口信息,以HTTP API 的形式對外暴露。該網關意在解決上一代網關的如下痛點。golang

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

2. 架構設計

針對如上痛點,隨即着手準備設計新的 gateway 架構。首先就是語言選型,golang 的協程調用模型使得 golang 很是適合構建 IO 密集型的應用,且應用部署上也較 java 簡單。redis

通過調研後咱們敲定使用 golang 做爲 proxy 的編碼語言,並使用 dubbo-go 用於鏈接 dubbo provider 集羣。provider 端的業務應用經過使用 java 的插件,以註解形式配置 API 配置信息,該插件會將配置信息和 dubbo 接口元數據更新到元數據註冊中心(下圖中的 redis )。這樣一來,配置從管理後臺頁面轉移到了程序代碼中。開發人員在編碼時,很是方便地看到 dubbo 接口對外的 API 描述,無需從另一個管理後臺配置 API 的使用方式。apache

3.png

3. 實踐

從上圖能夠看到,網關能動態加載 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 鏈中。其核心邏輯以下:bash

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。session

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

這裏就會用到上文提到的 provider 用於收集元數據的插件。引入插件後,應用在啓動時會掃描須要暴露的 dubbo 接口,將 dubbo 元數據和 HTTP API 關聯。插件使用方法大體以下,這裏調了幾個簡單的配置做爲示例,實際生產時註解內容會更多。

4.png

最終得到的 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 中,容許用戶調用及配置。

阿里巴巴編程之夏第二期正在火熱報名中!

2020 年 5 月 25 日,阿里巴巴編程之夏(Alibaba Summer of Code,如下簡稱 ASoC )第二期正式上線,項目規模再度升級,來自開源社區的 Apache Dubbo、Apache RocketMQ、Dragonfly、Nacos 等明星開源項目多達 20 個;導師陣容配置豪華,來自阿里巴巴集團的技術專家、開源社區核心成員、Apache 孵化器導師等多達 32 位;領域涉及微服務、容器、AI 等多個熱點方向,旨在聯合開源社區打造誠意滿滿、公平公正的開源實習平臺,以阿里巴巴開源技術力量爲「推手」,讓中國開源社區和開發者精英受到世界範圍內的承認。

點擊瞭解詳情:developer.aliyun.com/topic/summe…

阿里巴巴雲原生關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,作最懂雲原生開發者的公衆號。」

相關文章
相關標籤/搜索