前两篇文章(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)服务配置和指标上报
- …… 还会有更多,敬请期待 ……
制订协议
与 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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。