塗鴉智能 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)
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:
reconnect_interval: 0
connection_number: 2
heartbeat_period: "30s"
session_timeout: "30s"
pool_size: 4
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 詢問探討,我們一定第一時間給出反饋。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://my.oschina.net/dubbogo/blog/4306343