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

塗鴉智能 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 的參數定義,將首J字母大寫,最後反而無法序列化匹配。

第三步:拓展功能J

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

本文作者:

潘天穎,Github ID @pantianying,開源愛好者,就職於塗鴉智能。

歡迎加入 dubbo-go 社區

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

最新活動

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

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