前兩篇文章(1、2),我構建了一個簡單的 HTTP 服務。 HTTP 服務是前後端分離架構中,後端最靠近前端的業務服務。不過純後台 RPC 之間,出於效率、性能、韻味等等考慮,HTTP 不是我們的首選。本文我們就來看看騰訊是怎麼使用 tRPG-Go 構建後台微服務集羣的。
本文我們將開始涉及 tRPC 的核心關鍵點之一:
- tRPC 服務之間如何互相調用
系列文章
- 騰訊 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 服務
制訂協議
與 HTTP 一樣,我們還是先制訂協議。我們先簡單設計一下我們要做的一個服務吧:
-
一個前端 HTTP 服務,對接前端
- 提供一個登錄接口, 用於用户名密碼(哈希)登錄,如果登錄成功,給前端返回一個 JWT token,作為身份驗證票據
- JWT token 的生成邏輯在該服務中實現
-
一個後端服務, 內部調用,提供用户及認證功能
- 本文中這個服務實際實現用户名密碼驗證的功能。
HTTP 服務協議
HTTP 協議比較簡單,參照之前的文章格式,我們這麼定義:
import "common/metadata.proto";
message LoginRequest {
common.Metadata metadata = 1;
string username = 2;
string password_hash = 3;
}
message LoginResponse {
int32 err_code = 1;
string err_msg = 2;
Data data = 3;
message Data {
string id_ticket = 1;
}
}
// Auth 提供 HTTP 認證接口
service Auth {
rpc Login(LoginRequest) returns (LoginResponse); // @alias=/demo/auth/Login
}
這裏我使用到了 protoc 的跨目錄 import 特性。這就需要在 trpc create 命令中追加參數指定 import 的搜索路徑。各位可以看一下我的 Makefile 中的 pb 規則:
.PHONY: $(PB_DIR_TGTS)
$(PB_DIR_TGTS):
@for dir in $(subst _PB,, $@); do \
echo Now Build proto in directory: $$dir; \
cd $$dir; rm -rf mock; \
export PATH=$(PATH); \
rm -f *.pb.go; rm -f *.trpc.go; \
find . -name '*.proto' | xargs -I DD \
trpc create -f --protofile=DD --protocol=trpc --rpconly --nogomod --alias --mock=false --protodir=$(WORK_DIR)/proto; \
ls *.trpc.go | xargs -I DD mockgen -source=DD -destination=mock/DD -package=mock ; \
find `pwd` -name '*.pb.go'; \
done
注意其中最長的那一句
find . -name '*.proto' | xargs -I DD \
trpc create -f --protofile=DD --protocol=trpc --rpconly --nogomod --alias --mock=false --protodir=$(WORK_DIR)/proto; \
這裏通過 --protodir 指定了在 protoc 時的 import 搜索目錄。
後端服務協議
後端的服務協議,目前我們先針對這個簡單的登錄功能,設計一個獲取用户帳户數據的功能吧:
import "common/metadata.proto";
message GetAccountByUserNameRequest {
common.Metadata metadata = 1;
string username = 2;
}
message GetAccountByUserNameResponse {
int32 err_code = 1;
string err_msg = 2;
string user_id = 3;
string username = 4;
string password_hash = 5;
int64 create_ts_sec = 6;
}
// User 提供用户信息服務
service User {
rpc GetAccountByUserName(GetAccountByUserNameRequest) returns (GetAccountByUserNameResponse);
}
邏輯很簡單,就是根據用户名稱,獲取一個用户信息。我們也可以約定一下,如果沒有用户信息,那麼就在 err_msg 中返回一個錯誤信息。
邏輯開發
tRPC 服務間調用
還記得前面説到的兩個關鍵點嗎?我們先來講第一個:tRPC 服務間調用
前面我們規劃了兩個服務,一個主要對外提供 HTTP 接口,直接對接前端;另外一個服務不對前端開放,這種情況下我們可以使用 trpc 協議。這個協議其實與 grpc 非常相似,也使用了 HTTP/2 的各種機制。
這兩個服務互相調用的場景下,HTTP(httpauth 服務)是上游主調方,另一個微服務(user 服務)則是下游被調方。作為被調方,服務的撰寫方式與我們最早介紹的 tRPC 服務創建沒什麼差異,因為在 tRPC 框架下,我們撰寫服務邏輯的時候可以無需關注編碼格式。
作為主調方的服務,如何獲取入參、輸出出參,在之前的文章中我們已經知道該怎麼做了。接下來我們要關注的是如何調用下游。
我們先看看 httpauth 服務的 Login 實現代碼 吧。在代碼中,我列出了一個最簡單的方法:
func (authServiceImpl) Login(
ctx context.Context, req *httpauth.LoginRequest,
) (rsp *httpauth.LoginResponse, err error) {
rsp = &httpauth.LoginResponse{}
uReq := &user.GetAccountByUserNameRequest{
Metadata: req.GetMetadata(),
Username: req.GetUsername(),
}
uRsp, err := user.NewUserClientProxy().GetAccountByUserName(ctx, uReq)
if err != nil {
log.ErrorContextf(ctx, "調用 user 服務失敗: %v", err)
return nil, err
}
// 用户存在與否
if uRsp.GetErrCode() != 0 {
rsp.ErrCode, rsp.ErrMsg = uRsp.GetErrCode(), uRsp.GetErrMsg()
return
}
// 密碼檢查
if uRsp.GetPasswordHash() != req.PasswordHash {
rsp.ErrCode, rsp.ErrMsg = 404, "密碼錯誤"
return
}
return
}
要説明問題的核心代碼,就只有一行:
uRsp, err := user.NewUserClientProxy().GetAccountByUserName(ctx, uReq)
什麼 client 初始化,通通不需要。如果下游是一個 tRPC 服務,那麼我們只需要在使用的時候再 new 就可以了,這個開銷非常低。
服務部署
讀者讀到上一小節肯定會非常疑惑:啊?代碼怎麼尋址下游服務的?這一小節我就先嚐試着初步解答你的問題。
我們還是像最開始我們的 hello world 服務一樣,看看這個 httpauth 服務啓動時所需的 trpc_go.yaml 文件 吧:
可以看到,除了之前 hello world 服務給出的例子之外,yaml 文件中多了這一項:
client:
service:
- name: demo.account.User
target: ip://127.0.0.1:8002
network: tcp
protocol: trpc
timeout: 1000
這一部份規定了在服務中的各種 tRPC 下游依賴的尋址方式。跟服務側一樣,我我這裏也建議讀者參照 pb 中定義的服務名來給 name 字段賦值(demo.account.User)。
protocol 字段的值是 trpc,這表示我們使用 trpc 協議來調用下游。這一點我們需要與下游協商好,因為即便同是 tRPC 服務,如果 server 和 client 側沒有指定好相同的 protocol 字段,那麼雙方的通信將會失敗。
相比起 server 的配置有 port、nic、port 等字段,client 並沒有這些,取而代之的是一個 target 字段。目前的例子中,配置的值為:ip://127.0.0.1:8002。這個配置包含兩部份,也就是 ip:// 和 127.0.0.1:8002。
其中前面的 ip 表示告訴 tRPC 框架,client 將使用一個被註冊為叫做 ip 的尋址器(在 tRPC 中稱作 “selector”),尋址器的參數是 127.0.0.1:8002。ip 是 tRPC 內置的尋址器,邏輯也很簡單,根據後面的 IP + 端口進行尋址。此外,tRPC 還支持 dns 尋址,在這個尋址器下,如果 port 部份是 443,並且 protocol 為 http,那麼tRPC 會自動使用 https 調用。
當然,在正式生產環境下,我們的服務間很少直接使用 ip 尋址器進行服務發現。在後文我會介紹一下我們實際使用的 “北極星” 名字服務系統。此處讀者先知道尋址器功能即可,咱們先把服務打通,然後再來講更進階的事情。
下一步
本文我們説明了從一個 tRPC 服務,如何調用另一個 tRPC 服務。下一篇文章我們從那個被調用的 tRPC 服務來介紹,如何把諸如 MySQL、Redis、Kafka 等組件也接入 tRPC 框架中。
本文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。
原作者: amc,原文發佈於騰訊雲開發者社區,也是本人的博客。歡迎轉載,但請註明出處。
原文標題:《手把手 tRPC-Go 教學——(3)微服務間調用》
發佈日期:2024-01-29
原文鏈接:https://cloud.tencent.com/developer/article/2384591。