之前我花了三篇文章来介绍 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