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 架構。很多朋友往往是在這一步遇到問題,這裡我整理的一些常見問題,希望能幫到大家。
-
環境變量
目前 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 時需要。
-
代碼注意點
注入服務 : 檢查是否執行以下代碼
# 用戶端
func init() {
config.SetConsumerService(userProvider)
}
# 服務端
func init() {
config.SetProviderService(new(UserProvider))
}
hessian.RegisterJavaEnum(Gender(MAN))
hessian.RegisterJavaEnum(Gender(WOMAN))
hessian.RegisterPOJO(&User{})
-
正确了解配置檔案
references/services 下的 key ,如下面例子的 "UserProvider" 需要和服務 Reference() 傳回值保持一緻,此為辨別改接口的 key。
-
name: "GetUser"
retries: 3
注冊中心如果隻有一個注冊中心叢集,隻需配置一個。多個 IP 用逗号隔開,如下:
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 詢問探讨,我們一定第一時間給出回報。