之前我花了三篇文章來介紹 tRPC 怎麼用。而 tRPC 給開發者帶來的便利, 在整整三篇文章中,我也只是介紹了它可以方便服務在 HTTP、trpc、grpc 三種協議之間靈活切換。誠然, tRPC 作為能夠統一騰訊內開發框架的一個生態級產品,它的能力顯然不止這些。這一篇文章,我們們來一起初窺 tRPC 的周邊生態有哪些, 以及其中的第三方元件使用方法。
系列文章
- 騰訊 tRPC-Go 教學——(1)搭建服務
- 騰訊 tRPC-Go 教學——(2)trpc HTTP 能力
- 騰訊 tRPC-Go 教學——(3)微服務間呼叫
- 騰訊 tRPC-Go 教學——(4)tRPC 元件生態和使用
- ……還有更多,敬請期待……
tRPC-ecosystem 介紹
tRPC 的主倉庫是 trpc-group,在這之外,tRPC 的周邊生態系統則放在 trpc-echosystem。這個組主要包含的事 tRPC 服務之外的周邊生態程式碼, 從分類上,主要包括以下這些方面:
- 名字服務(naming)和定址器(selector)
- 配置(config)
- 日誌(log)
- 指標上報(metrics)
- 攔截器(filter)
- 第三方軟體元件
本文所說的 “第三方軟體元件”,指的是諸如 MySQL、Redis、ElasticSearch 等軟體元件客戶端 Go 實現。在 tRPC 生態系統組下的 go-database 倉庫,我們開啟倉庫,就可以看到這些熟悉的名稱:
資料庫 | 開源庫封裝 |
---|---|
bigcache | bigcache |
clickhouse | clickhouse-go |
cos | 騰訊雲物件儲存 |
goredis | go-redis |
gorm | gorm |
hbase | gohbase |
kafka | sarama |
mysql | sqlx |
timer | 本地/分散式定時器 |
tRPC 把各種元件整合到生態中來,主要目的是為了在維持這些開源庫的使用習慣的基礎上,同時 複用 tRPC 的各種能力,比如路由定址、監控上報等等功能。
本文我們就從 上一篇文章 定義的 user 服務來看, 如何引入 trpc-database 的 MySQL。
邏輯設計
在前文中,我們給 user 服務只設計了一個介面 GetAccountByUserName
,這個介面的功能,簡單而言就是根據入參,從資料庫中撈取指定的使用者資訊。
實體和介面設計
我們採用自頂向下的設計模式,從上層所需的介面往下設計。tRPC 業務程式碼 GetAccountByUserName
所在的層,我們稱為 service 層(有些框架稱為 handler 層、介面層、服務層等)。
這個介面的邏輯很簡單,由於太簡單了,因此我們不需要常規的多一個 logic 層封裝, 而是直接提供一個 repo
層實現就可以,從這個實現中直接根據 username 獲取帳戶資訊(repo
層有些框架稱為 infrastructure
層)。我們簡單設計一下需要傳輸的實體定義:
// Account 表示一個帳戶資訊
type Account struct {
Username string
PasswordHash string
}
至於這個 repo 層依賴, 我們以一個 Dependency
型別定義出來:
// Dependency 表示使用者服務初始化依賴
type Dependency interface {
// QueryAccountByUsername 透過使用者名稱查詢帳戶資訊, 如果帳戶不存在則返回 (nil, nil)
QueryAccountByUsername(ctx context.Context, username string) (*entity.Account, error)
}
type userImpl struct {
dep Dependency
}
可以看到,我把 Dependency 定義為一個 interface, 當然也可以定義為其他的型別,總之是任意方便用於注入測試的一個型別。在以後的文章中,我會說明注入模式的好處。
然後,我們使用這個 Dependency 作為引數, 用於初始化 user 主服務:
// RegisterUserService 註冊使用者服務
func RegisterUserService(s server.Service, d Dependency) error {
impl := &userImpl{dep: d}
user.RegisterUserService(s, impl)
return nil
}
至於 impl 的 GetAccountByUserName
方法實現, 太顯而易見了,就不列在本文裡了,讀者可以自行查閱程式碼細節。
MySQL 表結構
接下來我們實現在 service 層中依賴的 repo 介面。我們先設計一下 MySQL 的表結構:
CREATE TABLE IF NOT EXISTS `t_trpc_demo_user_account` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵 ID',
`username` varchar(128) NOT NULL COMMENT '使用者名稱稱',
`password_hash` varchar(64) NOT NULL COMMENT '使用者密碼雜湊值',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
`update_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
`delete_at_ms` bigint(11) NOT NULL DEFAULT 0 COMMENT '刪除時間戳, 毫秒',
PRIMARY KEY (`id`),
KEY `i_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='使用者賬戶表';
在 Go 裡面,我們並不需要定義所有的欄位,而是我們所需要的欄位就可以了:
type userAccountItem struct {
ID int64 `db:"id"`
Username string `db:"username"`
PasswordHash string `db:"password_hash"`
}
repo 層實現
在 repo/account 的程式碼中,可以寫得與 tRPC 一點關係也沒有,我們使用 sqlx 來實現對資料的存取, 然而,我使用注入的方式, 我們讓呼叫方傳入一個 sqlx DB 的 getter:
// UserAccountRepository 使用者賬戶倉庫
type UserAccountRepository struct {
dep Dependency
}
// Dependency 表示使用者賬戶倉庫初始化依賴
type Dependency struct {
DBGetter func(context.Context) (mysql.Client, error)
}
// InitializeUserAccountRepository 初始化使用者賬戶倉庫
func (r *UserAccountRepository) InitializeUserAccountRepository(d Dependency) error {
r.dep = d
return nil
}
這個倉庫的真正實現中,我們並沒有直接使用 sqlx 型別,而是使用了 trpc,主要的就是對 *sqlx.DB
追加了一層 context
封裝。
服務部署
好了,現在兩個服務的程式碼都寫好了。這個時候我們就要開始啟動這個由兩個服務的 “叢集” 了。
MySQL
咳咳,其實 MySQL 不屬於 trpc 框架裡的。生產環境中,我們的 MySQL 一般是雲上購買的資料庫。從學習的角度,我用的是 Mac 進行開發,Docker 用來開發除錯,我的 MySQL 也是部署在 Mac Docker 下,3306 埠,使用者名稱 root
,密碼 123456
,資料庫名稱 db_test
。
user 服務
user 服務對外提供一個 trpc 協議的介面;同時,它也依賴一個 MySQL。一如既往地,程式碼的邏輯其實很簡單,最主要的、我們來看看這個 MySQL 服務的 啟動配置:
server:
service:
- name: demo.account.User
ip: 127.0.0.1
# nic: eth0
port: 8002
network: tcp
protocol: trpc
timeout: 1800
client:
service:
- name: db.mysql.userAccount
target: ip://root:123456@tcp(host.docker.internal:3306)/db_test?charset=utf8mb4&parseTime=true&loc=Local&timeout=1s
timeout: 1000
先看 server 部份,為了便於除錯,我直接監聽環回地址 127.0.0.1
,協議配置的也是 trpc,而不再是之前文章中慣用的 http。
然後我們們來看新的一個配置項—— client:client 配置和 server 有點像,也是一個 service 陣列。這個配置中定義了服務對下游各種依賴的定址方法和相關配置。上面的配置中,最顯眼的就是 target
引數了。這個引數規定了如何定址指定的下游服務,以及相關的引數。
可以看到 DB 的地址是: host.docker.internal
,因為我的服務執行在 Docker 容器中,得使用 host.docker.internal
才可以訪問主機的埠。
在 tRPC 的 selector(定址器)邏輯中,我們之前提過,框架預設註冊了 ip
這個 selector,因此我們這裡複用了這個功能。此外,selector 配置中剩餘的引數,則會被傳遞到下游元件實現中。同樣的邏輯,我們無需修改業務程式碼的實現,就可以透過 client 配置修改下游的依賴。
http 服務
有了前面我們的描述,針對 http 服務,我們就可以輕車熟路了。http 服務(全名為 http-auth-server
)的 啟動配置 如下:
server:
service:
- name: demo.httpauth.Auth
nic: eth0
port: 8001
network: tcp
protocol: http
timeout: 1800
client:
service:
- name: demo.account.User
target: ip://127.0.0.1:8002
network: tcp
protocol: trpc
timeout: 1000
除錯
我們啟動兩個終端,分別進入兩個程式碼目錄中,分別啟動兩個服務:
cd app/user/; go run . -conf conf/trpc_go.yaml
cd app/http-auth-server/; go run . -conf conf/trpc_go.yaml
然後我們再開啟一個終端,使用命令除錯一下我們的介面:
curl '172.17.0.6:8001/demo/auth/Login?username=amc'
可以獲得返回:
{"err_code":404,"err_msg":"使用者不存在","data":null}
這就說明邏輯透過,這個 404 是我在 程式碼中 寫的當查詢不到使用者名稱的返回資訊。
OK,那我們往資料庫中插入一個條目吧(正常情況下應該是透過頁面建立的)
INSERT INTO t_trpc_demo_user_account (`username`, `password_hash`) VALUES ('amc', '75c498407830cb766fb20d619f3e08280ad7c5b9')
其中 75c498407830cb766fb20d619f3e08280ad7c5b9
就是 123456
的 sha256 值。這個時候我們再執行一下 curl 命令,則可以得到返回:
{"err_code":404,"err_msg":"密碼錯誤","data":null}
哎,使用者找到了,但是密碼錯誤。所以我們最後,再將程式碼帶上:
curl '172.17.0.6:8001/demo/auth/Login?username=amc&password_hash=8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92'
此時的返回就是:
{"err_code":0,"err_msg":"success","data":null}
成功了~邏輯也算是自測 OK 啦~~
小結
至此,我們使用四篇簡短的小文章,介紹瞭如何搭建一個最基本的 tRPC 微服務叢集,這個叢集包含了以下內容:
- 一個對前端的 HTTP API 服務
- 一個純後端服務
- 可配置化的服務配置和服務發現
讀者看完這四篇文章之後,其實就已經掌握了所有使用 tRPC 提供服務的最基本功能了。至少,筆者自己搭建的私人 web 服務的 API,也就只用到了這些知識點。
然而,要部署一個真正完整的、擁有良好可觀測性的服務叢集,我們還需要學習和使用更多 tRPC 的知識。下一步,我們來介紹一下 tRPC 日誌功能的實現吧。
本文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。
原作者: amc,原文釋出於騰訊雲開發者社群,也是本人的部落格。歡迎轉載,但請註明出處。
原文標題:《手把手騰訊 tRPC-Go 教學——(4)tRPC 元件生態和使用》
釋出日期:2024-02-05