系列文章
- 騰訊 tRPC-Go 教學——(1)搭建服務
- 騰訊 tRPC-Go 教學——(2)trpc HTTP 能力
- 騰訊 tRPC-Go 教學——(3)微服務間調用
- 騰訊 tRPC-Go 教學——(4)tRPC 組件生態和使用
- 騰訊 tRPC-Go 教學——(5)filter、context 和日誌組件
- 騰訊 tRPC-Go 教學——(6)服務發現
- 騰訊 tRPC-Go 教學——(7)服務配置和指標上報
- 騰訊 tRPC-Go 教學——(8)通過泛 HTTP 能力實現和觀測 MCP 服務
配置,是一個服務的重要組成部份。一般來説,業務的邏輯寫在代碼中,而與系統架構、運維等等偏運維的功能,通過配置來處理。tRPC 框架的配置,可以分為兩類:冷配置和熱配置。
冷配置
簡述
在 之前的文章 中,我們其實已經接觸到一個配置了,那就是服務啓動時的 trpc_go.yaml 文件。這個文件,我們可以將它稱為 tRPC 服務的 “冷配置”,冷配置的意思就是説服務一旦啓動了之後,就不再發生變更,除非服務重啓。
開發者也可以在 trpc_go.yaml 中加入自己的自定義配置項,從而定製化自己服務的行為。在 tRPC 中,支持自定義的冷配置,是通過 plugin 來實現的,業務配置只允許被配置在 yaml 文件的 plugin: 層級下。
獲取方法
讀者可以查閲 tRPC 針對 plugins 的官方 文檔。簡單而言,開發者需要實現 plugin 工廠類型,然後註冊到 tRPC 框架中。當 trpc_go.yaml 文件中包含了這一類型時,框架就會調用自定義的這一工廠類型,開發者可以解析其中的 yaml 配置,並且執行自己的插件邏輯。
説實話,這段實現的過程太麻煩了。tRPC 框架推出的時候,距離 Go 推出泛型還早呢。現在 Go 泛型 推出好久了,讀者可以參考我實現的一個 tRPC 小工具,工具自動解析配置到指定的結構體類型中,然後通過回調函數的方式通知業務邏輯。
如果你只是簡單地作為配置來看待的話,那更簡單,直接用我提供的 plugins.Bind() 函數,工具還自動將解析好的數據 new 到指定位置中。
針對 Bind 函數的例子,讀者可以參閲我在 trpc-go-demo 中,user 服務 讀取 trpc_go.yaml 的 config.client_yaml 配置項的代碼可以看到。下面我簡單説下 Bind 函數的用法:
- 開發者首先要定義一個自己的 struct 類型,指定各字段的
yamltag,比如我例子中,直接定義了一個匿名 struct 類型(反正只使用一次,無需命名):
import "github.com/Andrew-M-C/trpc-go-utils/plugin"
// ......
clientYamlConf := struct {
Key string `yaml:"key"`
}{}
- 然後,利用 Go 泛型的類型推測機制,開發者只需要將上面這個 struct 類型的地址作為 Bind 的參數即可:
plugin.Bind("config", "client_yaml", &clientYamlConf)
- 調完
trpc.NewServer() - Bind 函數會根據泛型類型,自動聲明一個 tRPC plugin 功能的工廠類型,然後調用 tRPC 的
plugin.Register函數 - 當 trpc_go.yaml 文件中包含了指定的配置(如例子中的
config.client_yaml項)存在時,tRPC 框架就會自動調用上一步中Bind函數註冊的內部工廠類型 Bind函數調用yaml.Unmarshal函數解析配置到指定類型的 struct 中- 如果配置文件解析錯誤,tRPC 框架會直接 panic 退出
- 當
trpc.NewServer()函數正常返回,就説明配置不存在或者是解析成功。此時開發者就可以檢查相應的配置情況了
可以看到,這個泛型函數完成了許多邏輯,可以大大減少開發者的代碼量,提高代碼的可讀性。這也是我們團隊在內網使用 tRPC 的主要思路:框架邏輯儘量封裝,將主要精力放在業務開發中,而不是框架適配上。這裏我將外網版的邏輯也給出來,歡迎讀者參考使用。
此外,我還提供了另一個 Register 函數,這個函數與 Bind 的區別是:完成了 unmarshal 動作之後,還會再調用回調函數通知開發者。事實上,Bind 就是通過封裝 Register 函數實現的。
熱配置
簡述
所謂的熱配置,指的就是服務框架啓動不必要的、可以保存在遠端配置中心的、在服務運行過程中可能隨時變化,並且需要服務實時或準實時加載的配置數據。
在騰訊內,我們一般是使用一個名為 “七彩石”(Rainbow)的配置系統來實現配置的編輯、審核、灰度和發佈。但不像北極星,七彩石並沒有開源(不過七彩石提供了商業化私有化部署服務,感興趣的團隊可以考慮)。因此我們對外的配置,為了圖方便,經常是使用 etcd 來實現的。
針對熱配置,可以查閲 tRPC 官方的 config 文檔。不過這個文檔 blah-blah 説了很多,實際上這麼多能力,我們團隊在實際業務中,大部份是用不上的,特別是文檔中的 GetBool、GetInt 等等一系列方法。
配置的獲取和監聽
在實際邏輯中,我們的配置是結構化的 JSON 或者是 YAML 配置文件,保存於配置系統中。對我們而言,配置系統的 Get 和 Watch 能力才是核心。正如前文所述我們在內部使用的七彩石配置中心在外網中無法使用,因此我基於開源的 tRPC etcd 封裝,實現了一個非常接近與我們團隊所使用的配置功能封裝。
其實配置的獲取與前文 plugin 的封裝思路很類似,就是將遠端的配置數據,與本地代碼/服務中的一個 struct 指針相互綁定。我們的框架功能封裝中,主要是利用 Go 在存取指針值時是協程安全的這一特性,當監聽到遠端配置更新時,在邏輯內部完成數據獲取、反序列化、通知等邏輯。主要邏輯是以下兩個倉庫:
- github.com/Andrew-M-C/trpc-go-utils/config
- github.com/Andrew-M-C/trpc-go-utils/config/etcd
我們回到前幾篇文章中的 demo。在之前的文章中提到的 http-auth-server 中,我們留意一下 main 包中的 initDependency() 函數,對 repo 層的初始化邏輯:
r, err := repo.NewRepo(repo.Dependency{
GeneralConfigKeyName: "/http-auth-server/general.json",
})
看 NewRepo 函數的實現,GeneralConfigKeyName 參數是用來初始化 http-auth-server 的配置模塊,在 InitializeGeneralConfigGetter 方法中,調用了我的 config 包中的 config.Bind 函數——是不是很眼熟?是的,這個函數與前文的 plugin.Bind 函數的邏輯非常類似,但是,更進一步的是,在邏輯內部,是調用了 tRPC 的 config 接口的 Watch 函數,實時監聽配置的變化。
這樣一來,開發者只需要一個非常簡單的泛型調用,就可以實現與遠端配置熱更新 / 同步的功能。還是我們團隊的思路:儘量減少框架代碼,將精力專注在業務邏輯的開發中。
客户端尋址熱更新
簡介
騰訊內部的七彩石配置中心,除了業務邏輯配置本身,作為內部 tRPC 生態的一部份,還提供了一個功能:客户端尋址熱更新。這是什麼意思呢?我來詳細解釋一下吧——
我們回顧一下之前的一篇文章:騰訊 tRPC-Go 教學——(3)微服務間調用,如果要調用一個 tRPC 下游服務,我們需要在 trpc_go.yaml 中配置諸如以下信息:
client:
service:
- name: demo.account.User
target: ip://127.0.0.1:8002
network: tcp
protocol: trpc
timeout: 1000
當發起 RPC 時,框架才知道如何尋址。從前文我們知道,trpc_go.yaml 是 tRPC 服務的 “冷配置”。但是,我們在內網使用時,實際上這部分內容,是放在七彩石配置中心作為 “熱配置” 的。將尋址信息作為熱配置,有很多好處:
- 冷熱配置整合在一起,能夠更統一地管理配置的版本、內容和發佈
- 通過熱配置,可以實時修改下游的路由、尋址方式、超時時間等參數,配合 filter 功能,實現更加靈活的 RPC 控制
這個功能,我們團隊一般直接稱為 “client.yaml”,與冷配置的 trpc_go.yaml 相對應。
其中 2 可能有點抽象,我舉一下我本人在業務中的例子吧:
業務遷移
如果讀者學習過各種 騰訊雲認證 的話,在雲架構工程師相關的考試中,肯定會有一個經典的題目就是如何將業務數據從私有 IDC 環境逐步切換到雲上。如果你使用的是 tRPC,那麼這道題實在是太簡單了——改一下配置中心的 client 配置就完成遷移咯。
緊急熔斷
我之前有一篇 文章 提到過,我們有一個服務擁有非常大的 QPS,在那篇文章中我們把 CPU 從 18,000 核降低到 1000 左右。這個項目在實際應用中曾經有一個 bug 導致 Redis 請求量比設計規格大了三個數量級,作為緩存的 Redis 瞬間雪崩。我們要想辦法停止 Redis 的請求,但又不能停止線上服務(因為正在業務一天中的高峯期)。
理論上,我們可以將後端的 Redis 網絡斷開,但是在當時的架構下,無法做到;或者我們直接銷燬 Redis 實例,以後再重新申請(流程很麻煩,誰都不想再來一次)。
我們選擇的方案為:當時我們用的是七彩石的 client.yaml 熱更新功能,我們直接將對應的 Redis 的尋址改為 ip://127.0.0.1:12345,這樣一來,所有針對該 Redis 的調用均會失敗,變相熔斷了 Redis 服務。直到進入業務低谷期,我們滾動更新服務修復 bug 之後,再將正確的 Redis 尋址恢復回來。
實現方案
前文提過,七彩石是不對外開源的,外部開源的配置系統中,我們比較常用的就是簡單的 etcd 配置。但是開源的 etcd 中並不支持這一小節所提及的 client.yaml 熱更新,因此我基於 etcd, 自己實現了一份,調用 etcd.RegisterClientProvider 函數,告訴工具需要監聽的 etcd key,這樣,工具就會自動監聽對應的 client.yaml 並解析和更新到 tRPC 框架中。
讀者可以實驗一下。我們還是按照以前的 http-auth-server 調用 user 的套路,將兩個服務啓動。從 user 服務的 plugin 相關 代碼 可以看到,通過前文所述的 clientYamlConf 變量,獲取了冷配置中 client.yaml 需要監聽的 etcd key。這個配置,在 demo 的 trpc_go.yaml 中配置為 /user/client.yaml
接着,在獲取了 tRPC server 變量之後,user 的 main 包調用:
etcdutil.RegisterClientProvider(context.Background(), s, clientYamlConf.Key)
將 client.yaml 的 key 註冊到框架中。最開始,我的 client.yaml 配置如下:
client:
filter:
- tracelog
service:
- name: db.mysql.userAccount
namespace: dev
target: ip://root:123456@tcp(host.docker.internal:3306)/db_test?charset=utf8mb4&parseTime=true&loc=Local&timeout=1s
timeout: 1000
這是個可觸達的 MySQL 地址。正如前面我們的測試方法一樣,我們像 http-auth-server 發起一個請求:
curl '172.17.0.7:8001/demo/auth/Login?username=amc'
獲得響應:
{"err_code":10001,"err_msg":"密碼錯誤"}
這個錯誤表示服務連上了 DB,查到了用户 amc,但是密碼錯誤(當然了,我都沒傳密碼參數)。接着,我們保持服務正常運行,但是將 client.yaml 的地址修改一下:
client:
filter:
- tracelog
service:
- name: db.mysql.userAccount
namespace: dev
target: ip://root:123456@tcp(127.0.0.1:3306)/db_test?charset=utf8mb4&parseTime=true&loc=Local&timeout=1s
timeout: 1
這個時候,觀察日誌,會發現服務打出了一段 DEBUG 信息:
2024-05-18 14:40:31.008 DEBUG config/config.go:74 [amc.util.config] 配置 '/user/client.yaml' 更新, 原始數據: '["client:\n filter:\n - tracelog\n service:\n - name: db.mysql.userAccount\n namespace: dev\n target: ip://root:123456@tcp(127.0.0.1:3306)/db_test?charset=utf8mb4\u0026parseTime=true\u0026loc=Local\u0026timeout=1s\n timeout: 1"]'
etcd client.yaml 工具監聽到了 etcd 配置的更新。這個時候我們再試一下前文的 curl 命令,這一次,我們得到的響應是這樣的:
{"err_code":-1,"err_msg":"獲取 DB 失敗 (dial tcp 127.0.0.1:3306: connect: connection refused)"}
很明顯,DB 查詢目標變了,並且很清楚地告訴調用方錯誤的原因。這也從側面説明了 client.yaml 配置更新生效了。
trpc-database
雖然我們做到了 client.yaml 的熱更新,但實際上 tRPC 生態內各個 RPC 組件其實並不都支持。首先,tRPC 原生的下游調用框架(也就是 tRPC client),是支持熱更新的,因為它發起 RPC 時,是先從 client.yaml 中獲取尋址信息之後再從 network transport 池中獲取連接再發起請求。
其次,tRPC-database 封裝的 MySQL 也支持 client.yaml 熱更新,它的調用方式與 tRPC client 類似。
但是,tRPC-database 的 Gorm 和 Redis 不支持熱更新,因為他們的實現方式均為發起一個針對遠端 server 的連接之後,就維持一套連接池,不再變化了。
讀者可能就疑惑了:前文 你不是説過 Redis 可以熱切換嗎,怎麼這會兒又不行了?稍安勿躁,雖然官方不支持,但我們又可以封裝呀~~
這就要講一下我實現的,基於 tRPC-database 的另一層封裝了,目前我實現了 Gorm 和 Redis,這兩個工具對外都提供了一個 ClientGetter 函數,返回一個 client 的獲取器,業務請不要直接使用 client 實例,而是在每次發起一次事務時,使用 getter 函數獲取一個 client 實例執行邏輯。而在 getter 的 內部,在每次獲取 client 實例之前,都會簡單檢查一下 client.yaml 是否發生變更,如果變更了的話,則會重新生成 client 實例並返回給業務方。
各位開發者在使用其他 tRPC-database 組件時,也可以簡單實驗一下,針對不支持熱更新的組件,也可以參照我的這個思路進行封裝。
話説,我在內網版的實現是與七彩石強綁定的;而我在開源版的實現更為優雅。因此筆者打算依照開源版的實現思路優化內網版哈哈。
指標監控
指標監控嘛,這個是筆者的弱項,因此只能簡單提一嘴了。在騰訊內網中,我們團隊幾位開發者一致同意:針對 trpc 生態的最好的監控系統,是內部一個名叫 “伽利略” 的可觀測系統、監控和治理中心。但是很可惜的是,這個系統目前沒有對外開源,所以外網開發者只能選擇其他替代。目前官方支持的有 Prometheus 和 OpenTelemetry,有些複雜,筆者還沒時間詳細學習——還是伽利略好,只需要花一個上午,就可以學習明白並且完整接入。
因此筆者這裏只能介紹一下我們團隊在日常開發中,所使用到的自定義屬性上報功能吧。我們主要是用 metrics.IncrCounter 和 metrics.SetGauge 兩個函數,前者主要是上報一個事件的次數,後者是用來上報事件的口徑值用於統計 average、min、max、分佈等。
最典型的例子是:當發起一次調用之後,我們使用 counter 上報一次調用次數;調用成功,則 count 一次 succ,如果是失敗則 count 一次 fail;同時,本次調用的耗時,則 gauge 一次微秒數。
我個人也實現了一個基於日誌的極為簡單的自定義屬性日誌記錄插件,讀者們可以拿來作為指標的補充調試用工具使用。
階段性總結
我為什麼要寫 tRPC 系列文章
講完這篇文章後,tRPC 作為微服務架構基本生態的內容就結束了。回顧了一下,這麼多篇文章下來,我覺得我寫的並不好。最初打算寫這一系列文章時,是因為我看了 tRPC 對外開源的文檔之後,我腦子裏滿是:地鐵、老人、手機。説實話,作為使用者,我最想知道的是一個框架到底怎麼用,有沒有例子,有沒有推薦的架構,而不是上
來就尬吹框架的能力有多少,架構多豐富,但是一點兒接地氣的教程都沒有。我就舉個例子吧,tRPC 的 plugin 功能,試問官方 文檔 説了些啥?我不信有初學者能夠一眼看明白。而作為趟過 tRPC 不少水的使用者,知道 plugin 怎麼用的情況下來看這篇官方 README,才能搞明白它羅列了些什麼。問題是 README 應該是一個入門式的文檔,初學者都看不明白的 README 完全不合格。
我還是挺認可 tRPC 的理念和設計方向的(儘管部份細節不敢苟同,但是不妨礙我對整體的認可),這麼好的東西,不能因為不成功的運營而沉沒。但是面對這 tRPC 文檔的現狀,在搞不明白 tRPC 團隊開源和宣傳計劃(嗯對,我在內網交流後,依然搞不明白)的情況下,我也只好盡我個人的綿薄之力,盡力宣傳。我也只是開個頭、結合我們團隊在內網的實際使用姿勢,介紹一下相應的 tRPC 的內容。一方面我也希望這一系列文章可以成為我們團隊新員工的引導文,另一方面是對自己的技術文檔沉澱,也希望吸引更多的開發者關注和使用 tRPC。
接下來寫什麼
之前的文章中,有讀者在 評論 中提到希望瞭解我們的代碼風格和組織樣式。正好我們當時做 單體服務 的時候,就是在我們的 tRPC 大倉中改造的,我也可以藉此介紹下我們團隊的幾種代碼組織方式,給讀者一些參考吧。此外,對於我個人所使用過的幾種服務框架,也一併做一些對比和推薦吧。
本文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。
原作者: amc,原文發佈於騰訊雲開發者社區,也是本人的博客。歡迎轉載,但請註明出處。
原文標題:《騰訊 tRPC-Go 教學——(7)服務配置和指標上報》
發佈日期:2024-05-19
原文鏈接:https://cloud.tencent.com/developer/article/2418601。