曾凡維 杭州開課啦教育科技有限公司高級開發工程師
來源 | 阿里巴巴雲原生公眾號
杭州開課啦教育科技有限公司是一家致力於為中小學生提供學習輔導的在線教育公司,目前公司后端服務基礎設施主要依托於阿里雲原生,其中包含計算、網絡、存儲以及 Kubernetes 服務。
技術選型背景
2020 年是開課啦公司發展壯大的一年,整個公司團隊由原來的幾百人擴充至現在的幾千人,在集中使用的時候基本上會有幾千人同時在運營后台進行操作,公司原有的內部后台運營系統是用 PHP 搭建起來的,性能跟業務上已逐漸不能滿足公司的需求規划,加上目前開課啦公司開發部已經做了微服務拆分,主體對外服務是 java 語言的 Dubbo 集群,后台系統需要無縫對接 java 的 Dubbo 服務,所以 PHP 已經逐漸不能滿足開課啦公司的需求。
當時自己也調研過 PHP 的 Dubbo 項目,由於項目已基本無人更新維護所以 pass 掉,后面自己對簡潔高性能的 go 語言感興趣,然后就關注到了 Dubbo-go 項目,經過一段時間的調研之后發現 Dubbo Go 符合我們的業務需要,並且社區非常的活躍,后面便決定選用 Dubbo-go 作為后台的 pc 業務框架。
可能也有同學會問為什么不使用跨言支持程度更好的 gRPC 呢,因為很多公司最開始的 RPC 服務集群都是基於 Dubbo 生態構建的,如果換框架成本太大,所以基本不會考慮,gRPC 雖然跨語言支持程度更好但是很多東西都需要自己造輪子,比如服務注冊,服務發現,日志監控等。
當時在決定選用 Dubbo-go 的時候開發內部也有一些反對的聲音的,為什么不直接轉 java,轉 java 的話就沒有跨語言通信的問題了,轉 java 的問題在於入門成本高,而且對於整個公司的技術棧來說,保持語言的多樣性,才能更加從容的應對未來的業務變化,Go 本身是一個不弱於 Java 的高性能語言,非常適合微服務架構。
面臨的挑戰
確定了框架選型后,我接到的首要任務便是要搭建出一套可快速創建業務項目的腳手架,開發出基於 HTTP 協議的 RPC 代理服務,部署需要接入公司的容器化部署平台,一切都是從零開始,在網上基本上找不到可以借鑒的資料。
首先是要進行 Dubbo-go 項目的架構的規划,確定項目目錄結構,經過參考 Dubbo-go Demo 以及其它的 Go 項目最終確定了項目的目錄結構,以下目錄結構可作為參考。
為了與 Java 服務注冊中心保持一致,Dubbo-go 在項目選型上選用如下組件:
- 使用 zookeeper 作為注冊中心
- nacos 作為配置中心
- 數據庫 orm 采用 gorm
- 消息隊列使用 RocketMQ
為了增加開發的效率我們在 provider 服務初始化前可以對配置進行精簡只保留最基礎的配置就可以類似下面這種,provider 服務的編碼參考 Dubbo-go demo 就可以了。
下面是服務啟動的 main 方法代碼:
Dubbo-go RPC 服務網關設計
一般使用 Dubbo,provider 端需要暴露出接口和方法,consumer 端要十分明確服務使用的接口定義和方法定義,還有入參返參類型等等信息,還需要基於 provider 端提供的 API,兩端才能正常通信調用。
然而網關的使用場景是並不關心要調用的接口的詳細定義,網關只關注要調用的方法、傳遞的參數、能接收返回結果就可以了,實現網關代理的基礎是 Dubbo/Dubbo-go 的泛化調用特性。
下面是 Dubbo-go 官方給的 demo,泛化服務加載后需要等待 3 秒才能完成調用,然而在實際使用的時候肯定是不能實時加載服務去等待 3 秒,所以在網關應用啟動時就需要加載緩存好需要泛化調的服務。
經過對 Dubbo-go 泛化調用 demo 的研究,發現用該特性設計 dubbo-go 網關是可行的,難點在於我們需要把每一個需要網關代理 RPC 服務方法的參數以及服務的路徑等配置獲取到並緩存起來,這樣才能在調用前初始化好泛化調用服務,一個服務的配置如下。
由於是用 go 語言做的網關代理,所以不能通過 Java 的 jar 包來獲取到 Java RPC 服務配置,如果通過人工維護的話工作量太大,而且易出錯,顯然是不可接受的。經過一段時間的了解,Java 服務可以通過注解來實現配置的獲取,Java 端在方法上加上注解后啟動服務的時候會將配置信息通過消息發送到 MQ,網關消費這些消息來實現獲取 Java RPC 服務的配置。
Dubbo Go 的 RPC 服務由於 go 語言不支持注解,所以我經過思考自己寫了一個掃描代碼的小工具,在每個 RPC 服務方法前加上對應的注釋,通過對注釋的掃描來獲取 RPC 服務的配置,獲取到配置后在項目目錄內生成 RPC 服務配置,啟動應用的時候讀取配置發送到 MQ。
網關代理實現之后還可以在網關的基礎實現更多的功能,比如 token 驗證、白名單、限流、熔斷、日志監控功能,網關代理請求實現效果如下:
容器化部署
公司內部的容器化部署環境為阿里雲的 K8s,部署至 K8s 平台只需要提供鏡像文件,由於 Dubbo-go 編譯后是一個二進制的文件,不需任何額外的第三方庫,能在 Docker 環境下穩定運行。有 docker 鏡像文件如下圖所示,可以用 centos 等任一 linux 發行版作為 base 鏡像。
LABEL maintainer="<xxx@xx.com>"
LABEL version="1.0"
LABEL description="KKL-GO-NKO-BASE"`
ARG envType=stable
#設置環境變量
ENV envType ${envType}
#編譯打包好的壓縮包
ADD ./target/nko-base-${envType}.tar.gz /app/
WORKDIR /app
EXPOSE 20000
鏡像寫好后提供給發布平台,發布平台機器啟動鏡像並解壓打包文件,執行 Dubbo-Go 程序 。
Container entrypoint set to [bash, -c, tar -zxf nko-base-stable.tar.gz && SERVER_ENV=kubernetes && sh ./nko-base/bin/load.sh start -group=stable]
由於開發測試到生產一般是有多個部署環境的,所以我們需要改動的dubbo-go samples demo 里的編譯腳本,讓其支持多環境打包。
另外,Dubbo-go 默認注冊的 IP 是 K8s pod 的虛擬 IP,不同 K8s 集群之間網絡是不能互通的,所以如果需要跨集群調用就需要修改默認注冊 IP,將默認注冊的 pod IP + 端口 修改為 Kubernetes 實體機的 IP 加對應端口,Kubernetes 會在 pod 內寫入實體機的 IP 加對應端口環境變量,應用程序可以通過讀取環境變量獲取實體機的 IP加端口,如果需要實現此功能需要修改 Dubbo-go 的注冊邏輯。例如以 zookeeper 注冊中心為例,我們可以通過擴展 registery/zookeeper/registry.go的 registerTempZookeeperNode 方法來實現修改注冊 IP 跟端口,代碼如下圖,Dubbo-go 官方將在后面的版本以配置的形式支持自定義注冊 IP 跟端口的功能。
func (r *zkRegistry) registerTempZookeeperNode(root string, node string) error {
...
regIp = os.Getenv(constant2.RegistryEnvIP) //實體機的ip
regPort = os.Getenv(constant2.RegistryEnvPort) //實體機的端口
urlNode, _ := common.NewURL(node)
role, _ := strconv.Atoi(urlNode.GetParam(constant.ROLE_KEY, ""))
if role == common.PROVIDER && regIp != "" && regPort != "" {
urlNode.Ip = regIp
urlNode.Port = regPort
node = url.QueryEscape(urlNode.String())
}
zkPath, err = r.client.RegisterTemp(root, node)
...
}
最后的話
如果使用 dubbo-go 過程中遇到一些問題,可以提交 issue 或者進社區釘釘群進行溝通交流。
個人建議:如果你做好了入坑 Dubbo-go 的准備,最好自己解決這些問題並給官方反饋 PR,遇到問題解決問題你才能成長並有所收獲,不是嗎?
歡迎對 apache/dubbo-go 項目有興趣的同學通過搜索釘釘群號 31363295 加入交流群。
作者簡介
曾凡維(github @jack15083), 一個有 9 年服務端業務開發經驗的一線程序員,曾在騰訊閱文等多家公司擔任后端開發工程師,目前就職杭州開課啦教育科技有限公司,從事 go 語言服務基礎架構和中間件及部分業務開發工作。