腾讯 tRPC-Go 教学——(3)微服务间调用

2024-05-19 11:34:40 浏览数 (2)

前两篇文章(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 一样,我们还是先制订协议。我们先简单设计一下我们要做的一个服务吧:

  1. 一个前端 HTTP 服务,对接前端
    • 提供一个登录接口, 用于用户名密码(哈希)登录,如果登录成功,给前端返回一个 JWT token,作为身份验证票据
    • JWT token 的生成逻辑在该服务中实现
  2. 一个后端服务, 内部调用,提供用户及认证功能
    • 本文中这个服务实际实现用户名密码验证的功能。

HTTP 服务协议

HTTP 协议比较简单,参照之前的文章格式,我们这么定义:

代码语言:proto复制
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 规则:

代码语言:makefile复制
.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

注意其中最长的那一句

代码语言:makefile复制
		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 搜索目录。

后端服务协议

后端的服务协议,目前我们先针对这个简单的登录功能,设计一个获取用户帐户数据的功能吧:

代码语言:proto复制
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 实现代码 吧。在代码中,我列出了一个最简单的方法:

代码语言:go复制
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
}

要说明问题的核心代码,就只有一行:

代码语言:go复制
	uRsp, err := user.NewUserClientProxy().GetAccountByUserName(ctx, uReq)

什么 client 初始化,通通不需要。如果下游是一个 tRPC 服务,那么我们只需要在使用的时候再 new 就可以了,这个开销非常低。

服务部署

读者读到上一小节肯定会非常疑惑:啊?代码怎么寻址下游服务的?这一小节我就先尝试着初步解答你的问题。

我们还是像最开始我们的 hello world 服务一样,看看这个 httpauth 服务启动时所需的 trpc_go.yaml 文件 吧:

可以看到,除了之前 hello world 服务给出的例子之外,yaml 文件中多了这一项:

代码语言: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 的配置有 portnicport 等字段,client 并没有这些,取而代之的是一个 target 字段。目前的例子中,配置的值为:ip://127.0.0.1:8002。这个配置包含两部份,也就是 ip://127.0.0.1:8002

其中前面的 ip 表示告诉 tRPC 框架,client 将使用一个被注册为叫做 ip 的寻址器(在 tRPC 中称作 “selector”),寻址器的参数是 127.0.0.1:8002ip 是 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。

0 人点赞