涂鸦智能 dubbo-go 亿级流量的实践与探索
作者 | 潘天穎,Github ID @pantianying,開源愛好者,就職于涂鴉智能
dubbo 是一個(gè)基于 Java 開發(fā)的高性能的輕量級(jí) RPC 框架,dubbo 提供了豐富的服務(wù)治理功能和優(yōu)秀的擴(kuò)展能力。而 dubbo-go 在 java 與 golang 之間提供統(tǒng)一的服務(wù)化能力與標(biāo)準(zhǔn),是涂鴉智能目前最需要解決的主要問題。本文分為實(shí)踐和快速接入兩部分,分享在涂鴉智能的 dubbo-go 實(shí)戰(zhàn)經(jīng)驗(yàn),意在幫助用戶快速接入 dubbo-go RPC 框架,希望能讓大家少走些彎路。另外,文中的測(cè)試代碼基于 dubbo-go版本 v1.4.0。
dubbo-go 網(wǎng)關(guān)實(shí)踐
dubbo-go 在涂鴉智能的使用情況如上圖,接下來會(huì)為大家詳細(xì)介紹落地細(xì)節(jié),希望這些在生產(chǎn)環(huán)境中總結(jié)的經(jīng)驗(yàn)?zāi)軌驇椭酱蠹摇?/p>
1. 背景
在涂鴉智能,dubbo-go 已經(jīng)作為了 golang 服務(wù)與原有 dubbo 集群打通的首選 RPC 框架。其中比較有代表性的 open-gateway 網(wǎng)關(guān)系統(tǒng)(下文統(tǒng)一稱 gateway,開源版本見 https://github.com/dubbogo/dubbo-go-proxy)。該 gateway 動(dòng)態(tài)加載內(nèi)部 dubbo 接口信息,以HTTP API 的形式對(duì)外暴露。該網(wǎng)關(guān)意在解決上一代網(wǎng)關(guān)的以下痛點(diǎn)。
- 通過頁面配置 dubbo 接口開放規(guī)則,步驟繁瑣,權(quán)限難以把控;
- 接口非 RESTful 風(fēng)格,對(duì)外部開發(fā)者不友好;
- 依賴繁重,升級(jí)風(fēng)險(xiǎn)大;
- 并發(fā)性能問題。
2. 架構(gòu)設(shè)計(jì)
針對(duì)如上痛點(diǎn),隨即著手準(zhǔn)備設(shè)計(jì)新的 gateway 架構(gòu)。首先就是語言選型,golang 的協(xié)程調(diào)用模型使得 golang 非常適合構(gòu)建 IO 密集型的應(yīng)用,且應(yīng)用部署上也較 java 簡單。
經(jīng)過調(diào)研后我們敲定使用 golang 作為 proxy 的編碼語言,并使用 dubbo-go 用于連接 dubbo provider 集群。provider 端的業(yè)務(wù)應(yīng)用通過使用 java 的插件,以注解形式配置 API 配置信息,該插件會(huì)將配置信息和 dubbo 接口元數(shù)據(jù)更新到元數(shù)據(jù)注冊(cè)中心(下圖中的 redis )。這樣一來,配置從管理后臺(tái)頁面轉(zhuǎn)移到了程序代碼中。開發(fā)人員在編碼時(shí),非常方便地看到 dubbo 接口對(duì)外的 API 描述,無需從另外一個(gè)管理后臺(tái)配置 API 的使用方式。
3. 實(shí)踐
從上圖可以看到,網(wǎng)關(guān)能動(dòng)態(tài)加載 dubbo 接口信息,調(diào)用 dubbo 接口是基于 dubbo 泛化調(diào)用。泛化調(diào)用使 client 不需要構(gòu)建 provider 的 interface 代碼,在 dubbo-go 中表現(xiàn)為無需調(diào)用 config.SetConsumerService 和 hessian.RegisterPOJO 方法,而是將請(qǐng)求模型純參數(shù)完成,這使得 client 動(dòng)態(tài)新增、修改接口成為可能。在 apache / dubbo-sample / golang / generic / go-client 中的有泛化調(diào)用的演示代碼。
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 RPCServicetime.Sleep(3 * time.Second)resp, err := referenceConfig.GetRPCService().(*config.GenericService).Invoke([]interface{}{"GetUser", []string{"java.lang.String"}, []interface{}{"A003"}})if err != nil {panic(err)} }泛化調(diào)用的實(shí)現(xiàn)其實(shí)相當(dāng)簡單。其功能作用在 dubbo 的 Filter 層中。Generic Filter 已經(jīng)作為默認(rèn)開啟的 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 將用戶請(qǐng)求的結(jié)構(gòu)體參數(shù)轉(zhuǎn)化為統(tǒng)一格式的 map(代碼中的 struct2MapAll ),將類( golang 中為 struct )的正反序列化操作變成 map 的正反序列化操作。這使得無需 POJO 描述通過硬編碼注入 hessain 庫。
從上面代碼可以看到,泛化調(diào)用實(shí)際需要?jiǎng)討B(tài)構(gòu)建的內(nèi)容有 4 個(gè),ReferenceConfig 中需要的 InterfaceName、參數(shù)中的 method、ParameterTypes、實(shí)際入?yún)?requestParams。
那么這些參數(shù)是如何從 HTTP API 匹配獲取到的呢?
這里就會(huì)用到上文提到的 provider 用于收集元數(shù)據(jù)的插件。引入插件后,應(yīng)用在啟動(dòng)時(shí)會(huì)掃描需要暴露的 dubbo 接口,將 dubbo 元數(shù)據(jù)和 HTTP API 關(guān)聯(lián)。插件使用方法大致如下,這里調(diào)了幾個(gè)簡單的配置作為示例,實(shí)際生產(chǎn)時(shí)注解內(nèi)容會(huì)更多。
最終獲得的 dubbo 元數(shù)據(jù)如下:
{"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 從元數(shù)據(jù)配置中心訂閱到以上信息,就能把一個(gè) API 請(qǐng)求匹配到一個(gè) dubbo 接口。再從 API 請(qǐng)求中抓取參數(shù)作為入?yún)?。這樣功能就完成了流量閉環(huán)。
以上內(nèi)容,大家應(yīng)該對(duì)此 gateway 的項(xiàng)目拓?fù)浣Y(jié)構(gòu)有了清晰的認(rèn)知。我接著分享項(xiàng)目在使用 dubbo-go 過程中遇到的問題和調(diào)優(yōu)經(jīng)驗(yàn)。19 年初,當(dāng)時(shí)的 dubbo-go 項(xiàng)目還只是構(gòu)建初期,沒有什么用戶落地的經(jīng)驗(yàn)。我也是一邊參與社區(qū)開發(fā),一邊編碼公司內(nèi)部網(wǎng)關(guān)項(xiàng)目。在解決了一堆 hessain 序列化和 zookeeper 注冊(cè)中心的問題后,項(xiàng)目最終跑通了閉環(huán)。但是,作為一個(gè)核心應(yīng)用,跑通閉環(huán)離上生產(chǎn)環(huán)境還有很長的路要走,特別是使用了當(dāng)時(shí)穩(wěn)定性待測(cè)試的新框架。整個(gè)測(cè)試加上功能補(bǔ)全,整整花費(fèi)了一個(gè)季度的時(shí)間,直到項(xiàng)目趨于穩(wěn)定,壓測(cè)效果也良好。單臺(tái)網(wǎng)關(guān)機(jī)器( 2C 8G )全鏈路模擬真實(shí)環(huán)境壓測(cè)達(dá)到 2000 QPS。由于引入了比較重的業(yè)務(wù)邏輯(單個(gè)請(qǐng)求平均調(diào)用 3 個(gè) dubbo 接口),對(duì)于這個(gè)壓測(cè)結(jié)果,是符合甚至超出預(yù)期的。
總結(jié)了一些 dubbo-go 參數(shù)配置調(diào)優(yōu)的經(jīng)驗(yàn),主要是一些網(wǎng)絡(luò)相關(guān)配置。
大家在跑 demo 時(shí),應(yīng)該會(huì)看到配置文件最后有一堆配置,但如果對(duì) dubbo-go 底層網(wǎng)絡(luò)模型不熟悉,就很難理解這些配置的含義。目前 dubbo-go 網(wǎng)絡(luò)層以 getty 為底層框架,實(shí)現(xiàn)讀寫分離和協(xié)程池管理。getty 對(duì)外暴露 session 的概念,session 提供一系列網(wǎng)絡(luò)層方法注入的實(shí)現(xiàn),因?yàn)楸疚牟皇窃创a解析文檔,在這里不過多論述。讀者可以簡單的認(rèn)為 dubbo-go 維護(hù)了一個(gè) getty session池,session 又維護(hù)了一個(gè) TCP 連接池。對(duì)于每個(gè)連接,getty 會(huì)有讀協(xié)程和寫協(xié)程伴生,做到讀寫分離。這里我盡量用通俗的注釋幫大家梳理下對(duì)性能影響較大的幾個(gè)配置含義:
protocol_conf:# 這里是協(xié)議獨(dú)立的配置,在dubbo協(xié)議下,大多數(shù)配置即為getty session相關(guān)的配置。dubbo:# 一個(gè)session會(huì)始終保證connection_number個(gè)tcp連接個(gè)數(shù),默認(rèn)是16,# 但這里建議大家配置相對(duì)小的值,一般系統(tǒng)不需要如此多的連接個(gè)數(shù)。# 每隔reconnect_interval時(shí)間,檢查連接個(gè)數(shù),如果小于connection_number,# 就建立連接。填0或不填都為默認(rèn)值300msreconnect_interval: 0connection_number: 2# 客戶端發(fā)送心跳的間隔heartbeat_period: "30s"# OnCron時(shí)session的超時(shí)時(shí)間,超過session_timeout無返回就關(guān)閉sessionsession_timeout: "30s"# 每一個(gè)dubbo interface的客戶端,會(huì)維護(hù)一個(gè)最大值為pool_size大小的session池。# 每次請(qǐng)求從session池中select一個(gè)。所以真實(shí)的tcp數(shù)量是session數(shù)量*connection_number,# 而pool_size是session數(shù)量的最大值。測(cè)試總結(jié)下來一般程序4個(gè)tcp連接足以。pool_size: 4# session?;畛瑫r(shí)時(shí)間,也就是超過session_timeout時(shí)間沒有使用該session,就會(huì)關(guān)閉該sessionpool_ttl: 600# 處理返回值的協(xié)程池大小gr_pool_size: 1200# 讀數(shù)據(jù)和協(xié)程池中的緩沖隊(duì)列長度,目前已經(jīng)廢棄。不使用緩沖隊(duì)列queue_len: 64queue_number: 60getty_session_param:compress_encoding: falsetcp_no_delay: truetcp_keep_alive: truekeep_alive_period: "120s"tcp_r_buf_size: 262144tcp_w_buf_size: 65536pkg_wq_size: 512tcp_read_timeout: "1s" # 每次讀包的超時(shí)時(shí)間tcp_write_timeout: "5s" # 每次寫包的超時(shí)時(shí)間wait_timeout: "1s" max_msg_len: 102400 # 最大數(shù)據(jù)傳輸長度session_name: "client"dubbo-go 快速接入
前文已經(jīng)展示過 dubbo-go 在涂鴉智能的實(shí)踐成果,接下來介紹快速接入 dubbo-go 的方式。
第一步:hello world
dubbo-go 使用范例目前和 dubbo 一致,放置在 apache/dubbo-samples 項(xiàng)目中。在 dubbo-sample/golang 目錄下,用戶可以選擇自己感興趣的 feature 目錄,快速測(cè)試代碼效果。
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 中的步驟,分別啟動(dòng) server 和 client ??梢試L試 golang 調(diào)用 java 、 java 調(diào)用 golang 、golang 調(diào)用 golang 、java 調(diào)用 java。dubbo-go 在協(xié)議上支持和 dubbo 互通。
我們以啟動(dòng) go-server 為例,注冊(cè)中心默認(rèn)使用 zookeeper 。首先確認(rèn)本地的 zookeeper 是否運(yùn)行正常。然后執(zhí)行以下命令,緊接著你就可以看到你的服務(wù)正常啟動(dòng)的日志了。
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第二步:在項(xiàng)目中使用 dubbo-go
上面,我們通過社區(qū)維護(hù)的測(cè)試代碼和啟動(dòng)腳本將用例跑了起來。接下來,我們需要在自己的代碼中嵌入 dubbo-go 框架。很多朋友往往是在這一步遇到問題,這里我整理的一些常見問題,希望能幫到大家。
1)環(huán)境變量
目前 dubbo-go 有 3 個(gè)環(huán)境變量需要配置:
- CONF_CONSUMER_FILE_PATH:Consumer 端配置文件路徑,使用 consumer 時(shí)必需;
- CONF_PROVIDER_FILE_PATH:Provider 端配置文件路徑,使用 provider 時(shí)必需;
- APP_LOG_CONF_FILE:Log 日志文件路徑,必需;
- CONF_ROUTER_FILE_PATH:File Router 規(guī)則配置文件路徑,使用 File Router 時(shí)需要。
2)代碼注意點(diǎn)
- 注入服務(wù) : 檢查是否執(zhí)行以下代碼
- 注入序列化描述 :檢查是否執(zhí)行以下代碼
3)正確理解配置文件
- references / services 下的 key ,如下面例子的 “UserProvider” 需要和服務(wù) Reference() 返回值保持一致,此為標(biāo)識(shí)改接口的 key。
- 注冊(cè)中心如果只有一個(gè)注冊(cè)中心集群,只需配置一個(gè)。多個(gè) IP 用逗號(hào)隔開,如下:
4)java 和 go 的問題
go 和 java 交互的大小寫 :golang 為了適配 java 的駝峰格式,在調(diào)用 java 服務(wù)時(shí),會(huì)自動(dòng)將 method 和屬性首字母變成小寫。很多同學(xué)故意將 java 代碼寫成適配 golang 的參數(shù)定義,將首字母大寫,最后反而無法序列化匹配。
第三步:拓展功能
dubbo-go 和 dubbo 都提供了非常豐富的拓展機(jī)制。可以實(shí)現(xiàn)自定義模塊代替 dubbo-go 默認(rèn)模塊,或者新增某些功能。比如實(shí)現(xiàn) Cluster、Filter 、Router 等來適配業(yè)務(wù)的需求。這些注入方法暴露在 dubbo-go/common/extension 中,允許用戶調(diào)用及配置。
阿里巴巴編程之夏第二期正在火熱報(bào)名中!
2020 年 5 月 25 日,阿里巴巴編程之夏(Alibaba Summer of Code,以下簡稱 ASoC )第二期正式上線,項(xiàng)目規(guī)模再度升級(jí),來自開源社區(qū)的 Apache Dubbo、Apache RocketMQ、Dragonfly、Nacos 等明星開源項(xiàng)目多達(dá) 20 個(gè);導(dǎo)師陣容配置豪華,來自阿里巴巴集團(tuán)的技術(shù)專家、開源社區(qū)核心成員、Apache 孵化器導(dǎo)師等多達(dá) 32 位;領(lǐng)域涉及微服務(wù)、容器、AI 等多個(gè)熱點(diǎn)方向,旨在聯(lián)合開源社區(qū)打造誠意滿滿、公平公正的開源實(shí)習(xí)平臺(tái),以阿里巴巴開源技術(shù)力量為“推手”,讓中國開源社區(qū)和開發(fā)者精英受到世界范圍內(nèi)的認(rèn)可。
點(diǎn)擊了解詳情:https://developer.aliyun.com/topic/summerofcode2020
“阿里巴巴云原生關(guān)注微服務(wù)、Serverless、容器、Service Mesh 等技術(shù)領(lǐng)域、聚焦云原生流行技術(shù)趨勢(shì)、云原生大規(guī)模的落地實(shí)踐,做最懂云原生開發(fā)者的公眾號(hào)。”
總結(jié)
以上是生活随笔為你收集整理的涂鸦智能 dubbo-go 亿级流量的实践与探索的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从零入门 Serverless | 一文
- 下一篇: K8s 资源全汇总 | K8s 大咖带