这篇文章我们就来讲一讲 tRPC 的配置功能,以及自定义指标监控功能吧。
配置,是一个服务的重要组成部份。一般来说,业务的逻辑写在代码中,而与系统架构、运维等等偏运维的功能,通过配置来处理。tRPC 框架的配置,可以分为两类:冷配置和热配置。
在 之前的文章 中,我们其实已经接触到一个配置了,那就是服务启动时的 trpc_go.yaml
文件。这个文件,我们可以将它称为 tRPC 服务的 “冷配置”,冷配置的意思就是说服务一旦启动了之后,就不再发生变更,除非服务重启。
开发者也可以在 trpc_go.yaml
中加入自己的自定义配置项,从而定制化自己服务的行为。在 tRPC 中,支持自定义的冷配置,是通过 plugin 来实现的,业务配置只允许被配置在 yaml 文件的 plugin:
层级下。
读者可以查阅 tRPC 针对 plugins 的官方 文档。简单而言,开发者需要实现 plugin 工厂类型,然后注册到 tRPC 框架中。当 trpc_go.yaml
文件中包含了这一类型时,框架就会调用自定义的这一工厂类型,开发者可以解析其中的 yaml 配置,并且执行自己的插件逻辑。
说实话,这段实现的过程太麻烦了。tRPC 框架推出的时候,距离 Go 推出泛型还早呢。现在 Go 泛型 推出好久了,读者可以参考我实现的一个 tRPC 小工具,工具自动解析配置到指定的结构体类型中,然后通过回调函数的方式通知业务逻辑。
如果你只是简单地作为配置来看待的话,那更简单,直接用我提供的 plugins.Bind() 函数,工具还自动将解析好的数据 new 到指定位置中。
针对 Bind
函数的例子,读者可以参阅我在 trpc-go-demo 中,user 服务 读取 trpc_go.yaml
的 config.client_yaml 配置项的代码可以看到。下面我简单说下 Bind 函数的用法:
yaml
tag,比如我例子中,直接定义了一个匿名 struct 类型(反正只使用一次,无需命名):import "github.com/Andrew-M-C/trpc-go-utils/plugin"
// ......
clientYamlConf := struct {
Key string `yaml:"key"`
}{}
plugin.Bind("config", "client_yaml", &clientYamlConf)
trpc.NewServer()
plugin.Register
函数config.client_yaml
项)存在时,tRPC 框架就会自动调用上一步中 Bind
函数注册的内部工厂类型Bind
函数调用 yaml.Unmarshal
函数解析配置到指定类型的 struct 中trpc.NewServer()
函数正常返回,就说明配置不存在或者是解析成功。此时开发者就可以检查相应的配置情况了可以看到,这个泛型函数完成了许多逻辑,可以大大减少开发者的代码量,提高代码的可读性。这也是我们团队在内网使用 tRPC 的主要思路:框架逻辑尽量封装,将主要精力放在业务开发中,而不是框架适配上。这里我将外网版的逻辑也给出来,欢迎读者参考使用。
此外,我还提供了另一个 Register 函数,这个函数与 Bind
的区别是:完成了 unmarshal 动作之后,还会再调用回调函数通知开发者。事实上,Bind
就是通过封装 Register
函数实现的。
所谓的热配置,指的就是服务框架启动不必要的、可以保存在远端配置中心的、在服务运行过程中可能随时变化,并且需要服务实时或准实时加载的配置数据。
在腾讯内,我们一般是使用一个名为 “七彩石”(Rainbow)的配置系统来实现配置的编辑、审核、灰度和发布。但不像北极星,七彩石并没有开源(不过七彩石提供了商业化私有化部署服务,感兴趣的团队可以考虑)。因此我们对外的配置,为了图方便,经常是使用 etcd 来实现的。
针对热配置,可以查阅 tRPC 官方的 config 文档。不过这个文档 blah-blah 说了很多,实际上这么多能力,我们团队在实际业务中,大部份是用不上的,特别是文档中的 GetBool
、GetInt
等等一系列方法。
在实际逻辑中,我们的配置是结构化的 JSON 或者是 YAML 配置文件,保存于配置系统中。对我们而言,配置系统的 Get
和 Watch
能力才是核心。正如前文所述我们在内部使用的七彩石配置中心在外网中无法使用,因此我基于开源的 tRPC etcd 封装,实现了一个非常接近与我们团队所使用的配置功能封装。
其实配置的获取与前文 plugin
的封装思路很类似,就是将远端的配置数据,与本地代码/服务中的一个 struct 指针相互绑定。我们的框架功能封装中,主要是利用 Go 在存取指针值时是协程安全的这一特性,当监听到远端配置更新时,在逻辑内部完成数据获取、反序列化、通知等逻辑。主要逻辑是以下两个仓库:
我们回到前几篇文章中的 demo。在之前的文章中提到的 http-auth-server
中,我们留意一下 main 包中的 initDependency() 函数,对 repo 层的初始化逻辑:
r, err := repo.NewRepo(repo.Dependency{
GeneralConfigKeyName: "/http-auth-server/general.json",
})
看 NewRepo
函数的实现,GeneralConfigKeyName
参数是用来初始化 http-auth-server 的配置模块,在 InitializeGeneralConfigGetter 方法中,调用了我的 config 包中的 config.Bind
函数——是不是很眼熟?是的,这个函数与前文的 plugin.Bind
函数的逻辑非常类似,但是,更进一步的是,在逻辑内部,是调用了 tRPC 的 config 接口的 Watch
函数,实时监听配置的变化。
这样一来,开发者只需要一个非常简单的泛型调用,就可以实现与远端配置热更新 / 同步的功能。还是我们团队的思路:尽量减少框架代码,将精力专注在业务逻辑的开发中。
腾讯内部的七彩石配置中心,除了业务逻辑配置本身,作为内部 tRPC 生态的一部份,还提供了一个功能:客户端寻址热更新。这是什么意思呢?我来详细解释一下吧——
我们回顾一下之前的一篇文章:腾讯 tRPC-Go 教学——(3)微服务间调用,如果要调用一个 tRPC 下游服务,我们需要在 trpc_go.yaml
中配置诸如以下信息:
client:
service:
- name: demo.account.User
target: ip://127.0.0.1:8002
network: tcp
protocol: trpc
timeout: 1000
当发起 RPC 时,框架才知道如何寻址。从前文我们知道,trpc_go.yaml
是 tRPC 服务的 “冷配置”。但是,我们在内网使用时,实际上这部分内容,是放在七彩石配置中心作为 “热配置” 的。将寻址信息作为热配置,有很多好处:
这个功能,我们团队一般直接称为 “client.yaml”,与冷配置的 trpc_go.yaml
相对应。
其中 2 可能有点抽象,我举一下我本人在业务中的例子吧:
如果读者学习过各种 腾讯云认证 的话,在云架构工程师相关的考试中,肯定会有一个经典的题目就是如何将业务数据从私有 IDC 环境逐步切换到云上。如果你使用的是 tRPC,那么这道题实在是太简单了——改一下配置中心的 client 配置就完成迁移咯。
我之前有一篇 文章 提到过,我们有一个服务拥有非常大的 QPS,在那篇文章中我们把 CPU 从 18,000 核降低到 1000 左右。这个项目在实际应用中曾经有一个 bug 导致 Redis 请求量比设计规格大了三个数量级,作为缓存的 Redis 瞬间雪崩。我们要想办法停止 Redis 的请求,但又不能停止线上服务(因为正在业务一天中的高峰期)。
理论上,我们可以将后端的 Redis 网络断开,但是在当时的架构下,无法做到;或者我们直接销毁 Redis 实例,以后再重新申请(流程很麻烦,谁都不想再来一次)。
我们选择的方案为:当时我们用的是七彩石的 client.yaml
热更新功能,我们直接将对应的 Redis 的寻址改为 ip://127.0.0.1:12345
,这样一来,所有针对该 Redis 的调用均会失败,变相熔断了 Redis 服务。直到进入业务低谷期,我们滚动更新服务修复 bug 之后,再将正确的 Redis 寻址恢复回来。
前文提过,七彩石是不对外开源的,外部开源的配置系统中,我们比较常用的就是简单的 etcd 配置。但是开源的 etcd 中并不支持这一小节所提及的 client.yaml
热更新,因此我基于 etcd, 自己实现了一份,调用 etcd.RegisterClientProvider
函数,告诉工具需要监听的 etcd key,这样,工具就会自动监听对应的 client.yaml 并解析和更新到 tRPC 框架中。
读者可以实验一下。我们还是按照以前的 http-auth-server
调用 user
的套路,将两个服务启动。从 user 服务的 plugin 相关 代码 可以看到,通过前文所述的 clientYamlConf
变量,获取了冷配置中 client.yaml
需要监听的 etcd key。这个配置,在 demo 的 trpc_go.yaml 中配置为 /user/client.yaml
接着,在获取了 tRPC server 变量之后,user 的 main 包调用:
etcdutil.RegisterClientProvider(context.Background(), s, clientYamlConf.Key)
将 client.yaml 的 key 注册到框架中。最开始,我的 client.yaml 配置如下:
client:
filter:
- tracelog
service:
- name: db.mysql.userAccount
namespace: dev
target: ip://root:123456@tcp(host.docker.internal:3306)/db_test?charset=utf8mb4&parseTime=true&loc=Local&timeout=1s
timeout: 1000
这是个可触达的 MySQL 地址。正如前面我们的测试方法一样,我们像 http-auth-server
发起一个请求:
curl '172.17.0.7:8001/demo/auth/Login?username=amc'
获得响应:
{"err_code":10001,"err_msg":"密码错误"}
这个错误表示服务连上了 DB,查到了用户 amc
,但是密码错误(当然了,我都没传密码参数)。接着,我们保持服务正常运行,但是将 client.yaml
的地址修改一下:
client:
filter:
- tracelog
service:
- name: db.mysql.userAccount
namespace: dev
target: ip://root:123456@tcp(127.0.0.1:3306)/db_test?charset=utf8mb4&parseTime=true&loc=Local&timeout=1s
timeout: 1
这个时候,观察日志,会发现服务打出了一段 DEBUG 信息:
2024-05-18 14:40:31.008 DEBUG config/config.go:74 [amc.util.config] 配置 '/user/client.yaml' 更新, 原始数据: '["client:\n filter:\n - tracelog\n service:\n - name: db.mysql.userAccount\n namespace: dev\n target: ip://root:123456@tcp(127.0.0.1:3306)/db_test?charset=utf8mb4\u0026parseTime=true\u0026loc=Local\u0026timeout=1s\n timeout: 1"]'
etcd client.yaml 工具监听到了 etcd 配置的更新。这个时候我们再试一下前文的 curl 命令,这一次,我们得到的响应是这样的:
{"err_code":-1,"err_msg":"获取 DB 失败 (dial tcp 127.0.0.1:3306: connect: connection refused)"}
很明显,DB 查询目标变了,并且很清楚地告诉调用方错误的原因。这也从侧面说明了 client.yaml 配置更新生效了。
虽然我们做到了 client.yaml 的热更新,但实际上 tRPC 生态内各个 RPC 组件其实并不都支持。首先,tRPC 原生的下游调用框架(也就是 tRPC client),是支持热更新的,因为它发起 RPC 时,是先从 client.yaml 中获取寻址信息之后再从 network transport 池中获取连接再发起请求。
其次,tRPC-database 封装的 MySQL 也支持 client.yaml 热更新,它的调用方式与 tRPC client 类似。
但是,tRPC-database 的 Gorm 和 Redis 不支持热更新,因为他们的实现方式均为发起一个针对远端 server 的连接之后,就维持一套连接池,不再变化了。
读者可能就疑惑了:前文 你不是说过 Redis 可以热切换吗,怎么这会儿又不行了?稍安勿躁,虽然官方不支持,但我们又可以封装呀~~
这就要讲一下我实现的,基于 tRPC-database 的另一层封装了,目前我实现了 Gorm 和 Redis,这两个工具对外都提供了一个 ClientGetter
函数,返回一个 client 的获取器,业务请不要直接使用 client 实例,而是在每次发起一次事务时,使用 getter 函数获取一个 client 实例执行逻辑。而在 getter 的 内部,在每次获取 client 实例之前,都会简单检查一下 client.yaml 是否发生变更,如果变更了的话,则会重新生成 client 实例并返回给业务方。
各位开发者在使用其他 tRPC-database 组件时,也可以简单实验一下,针对不支持热更新的组件,也可以参照我的这个思路进行封装。
话说,我在内网版的实现是与七彩石强绑定的;而我在开源版的实现更为优雅。因此笔者打算依照开源版的实现思路优化内网版哈哈。
指标监控嘛,这个是笔者的弱项,因此只能简单提一嘴了。在腾讯内网中,我们团队几位开发者一致同意:针对 trpc 生态的最好的监控系统,是内部一个名叫 “伽利略” 的可观测系统、监控和治理中心。但是很可惜的是,这个系统目前没有对外开源,所以外网开发者只能选择其他替代。目前官方支持的有 Prometheus 和 OpenTelemetry,有些复杂,笔者还没时间详细学习——还是伽利略好,只需要花一个上午,就可以学习明白并且完整接入。
因此笔者这里只能介绍一下我们团队在日常开发中,所使用到的自定义属性上报功能吧。我们主要是用 metrics.IncrCounter 和 metrics.SetGauge 两个函数,前者主要是上报一个事件的次数,后者是用来上报事件的口径值用于统计 average、min、max、分布等。
最典型的例子是:当发起一次调用之后,我们使用 counter 上报一次调用次数;调用成功,则 count 一次 succ
,如果是失败则 count 一次 fail
;同时,本次调用的耗时,则 gauge 一次微秒数。
我个人也实现了一个基于日志的极为简单的自定义属性日志记录插件,读者们可以拿来作为指标的补充调试用工具使用。
讲完这篇文章后,tRPC 作为微服务架构基本生态的内容就结束了。回顾了一下,这么多篇文章下来,我觉得我写的并不好。最初打算写这一系列文章时,是因为我看了 tRPC 对外开源的文档之后,我脑子里满是:地铁、老人、手机。说实话,作为使用者,我最想知道的是一个框架到底怎么用,有没有例子,有没有推荐的架构,而不是上
来就尬吹框架的能力有多少,架构多丰富,但是一点儿接地气的教程都没有。我就举个例子吧,tRPC 的 plugin 功能,试问官方 文档 说了些啥?我不信有初学者能够一眼看明白。而作为趟过 tRPC 不少水的使用者,知道 plugin 怎么用的情况下来看这篇官方 README,才能搞明白它罗列了些什么。问题是 README 应该是一个入门式的文档,初学者都看不明白的 README 完全不合格。
我还是挺认可 tRPC 的理念和设计方向的(尽管部份细节不敢苟同,但是不妨碍我对整体的认可),这么好的东西,不能因为不成功的运营而沉没。但是面对这 tRPC 文档的现状,在搞不明白 tRPC 团队开源和宣传计划(嗯对,我在内网交流后,依然搞不明白)的情况下,我也只好尽我个人的绵薄之力,尽力宣传。我也只是开个头、结合我们团队在内网的实际使用姿势,介绍一下相应的 tRPC 的内容。一方面我也希望这一系列文章可以成为我们团队新员工的引导文,另一方面是对自己的技术文档沉淀,也希望吸引更多的开发者关注和使用 tRPC。
之前的文章中,有读者在 评论 中提到希望了解我们的代码风格和组织样式。正好我们当时做 单体服务 的时候,就是在我们的 tRPC 大仓中改造的,我也可以藉此介绍下我们团队的几种代码组织方式,给读者一些参考吧。此外,对于我个人所使用过的几种服务框架,也一并做一些对比和推荐吧。
本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
原作者: amc,欢迎转载,但请注明出处。
原文标题:《腾讯 tRPC-Go 教学——(7)服务配置和指标上报》
发布日期:2024-05-19
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。