其他
微服务不香了?单体化改造为我们节省上万核 CPU!
# 谈谈我在腾讯的架构设计经验
# 第9期 | 基于腾讯 tRPC-Go 单体化改造
微服务一直以来是服务治理的基本盘之一,落地到云原生上,往往是每个 K8s pods 部署一个服务,独立迭代、独立运维。
但是在快速部署的时候,有时候,我们可能需要一些宏服务的优势。有没有一种方法,能够 “既要又要” 呢?本文基于 tRPC-Go 服务,提出并最终实践了一种经验证可行的方法。
本文原文发布在腾讯内网平台中,随着腾讯 tRPC 框架正式开源,笔者决定将敏感信息脱敏后发布至外网,也助力 tRPC 的推广。
微服务大大降低了模块间的耦合。当某个模块 / 微服务需要变更时,只需要调整这个微服务即可,其他服务无感知; 微服务化使得模块的更新能够平滑过渡,避免了停机更新的问题,也适合大型团队或多个团队间合作构建; 微服务模块的输入 / 输出定义很明确,非常适合融合 DDD 理念进行设计; 问题排查时,能够快速定位出现问题的模块,对运维也很友好。
当系统趋向复杂时,随着微服务的拆分、功能的繁杂和细化,微服务越来越多,一窥系统全貌的难度越来越大; 模块间通信通过 RPC 实现,RPC 带来了时间和网络流量的开销; 依赖于完备的服务治理体系,对小团队而言,部署成本较高; 多租户隔离部署时,运维难度也成倍增加。
2.1 分析
interface
的形式给出的;而实现方实现对外提供服务的方式,从业务层面也只是实现相应的 server interface
就可以。也就是说,服务的 client 端和 server 端,看到和实现的,都只是普通的 Go 函数。在此思路上,我们团队的同学在该文档的基础上,提出了一个将 RPC "mock" 成本地函数调用的方案,并由我落地验证了。3.1 RPC 背景
service FeedsRerank {
rpc GetFeedList (GetFeedRequest) returns (GetFeedReply) {}
}
xxx.trpc.go
文件,其中包含 service 接口:type FeedsRerankService interface {
GetFeedList(ctx context.Context, req *GetFeedRequest) (*GetFeedReply, error)
}
RegisterFeedsRerankService
注册实现, tRPC 会自动对接框架和代码实现。type FeedsRerankClientProxy interface {
GetFeedList(ctx context.Context, req *GetFeedRequest, opts ...client.Option) (*GetFeedReply, error)
}
client := pb.NewFeedsRerankClientProxy()
,然后就可以直接调用 GetFeedList
方法了,tRPC 帮调用方隐藏了底层 RPC 细节。对调用方而言,这就只是一个函数而已。对,函数!!!3.2 代码改造
Client 侧
new
下游的 proxy
,而是从这个地方统一取(我们把这个叫做 proxy API
),这样我们就可以实现了。用 Go 的语言来描述, 调用方看到的只是一个 interface, 那我们就在内存把被调用方的代码按照这个 interface 进行实现, 然后想办法让 client 端直接用上这个实现,就可以了! rerank := api.FeedsRerank()
rsp, err := rerank.GetFeedList(ctx, req)
// .....
Server 侧
service
包,对外暴露一个 Register
函数,这个函数的入参中包含 trpc-go/service.Server
类型,用于调用 tRPC 服务注册函数,如重排服务: pb.RegisterFeedsRerankService(server, rerankImpl)
proxy API
,将自己的实现 mock 一下。需要注意的是,tRPC 的 client proxy 函数参数,相比 server 侧实现的方法,多了一个 opts ...client.Option
参数。不过绝大多数情况下,我们忽略这些参数就好了。type rerankProxy struct {
impl *rerankImpl
}
func (r *rerankProxy) GetFeedList(
ctx context.Context, req *pb.GetFeedRequest, opts ...client.Option,
) (*pb.GetFeedReply, error) {
rsp := &pb.GetFeedRequest{}
err := r.impl.GetFeedList(req, rsp)
return rsp, err
}
func (impl *rerankImpl) mockProxy() {
r := &rerankProxy{impl: impl}
proxyAPI.RegisterFeedsRerank(p)
}
rerankImpl
类型实现了作为 server 端的 FeedsRerankService
接口之外, 也通过 rerankProxy
类型实现了 client 端的 FeedsRerankClientProxy
接口。这样,当上游调用时, 统一从 proxy API
中获取 proxy 接口实现, 在微服务场景下,那么就是一个正常的 RPC 调用;但是在单体场景下,不知不觉地就只是一个内存的调用了。main 包
Proxy API 实现
NewXxx
函数初始化即可(比如对应前文的 NewFeedsRerankClientProxy
),得益于 tRPC 的懒初始化机制,这些 Proxy 创建了之后,只要不去调用它,那么即便配置里不包含相关的 client 配置,就不会报错。因此,虽然在 Proxy API 中初始化了多个 Proxy,也不会对具体到某个微服务造成影响。RegisterXxxx
函数(比如前文的 RegisterFeedsRerank
)实现。具体落到细节处,也只不过是一个个的私有成员变量而已。package proxyapi
type API interface {
FeedsRerank() pb.NewFeedsRerankClientProxy
RegisterFeedsRerank(p pb.NewFeedsRerankClientProxy)
}
func DefaultAPI() API {
return defaultAPIImpl
}
type apiImpl struct {
internalFeedsRerankClientProxy pb.FeedsRerankClientProxy
internalXxxxClientProxy pb.XxxxClientProxy // 作为实例, 其他的微服务模式类似, 下同
// ...
}
var _ API = (*apiImpl)(nil)
var defaultAPIImpl = new()
func new() *apiImpl {
return &apiImpl{
internalFeedsRerankClientProxy: pb.NewFeedsRerankClientProxy(), // trpc 的默认 client 初始化逻辑
internalXxxxClientProxy: pb.NewXxxxClientProxy(),
// ...
}
}
func (a *apiImpl) FeedsRerank() pb.NewFeedsRerankClientProxy {
return a.internalFeedsRerankClientProxy
}
func (a *apiImpl) RegisterFeedsRerank(p pb.NewFeedsRerankClientProxy) {
if p != nil {
a.internalFeedsRerankClientProxy = p
}
}
// ...
go generate
来生成上述代码。4.1 服务配置
trpc_go.yaml
中配置对应的多个微服务注册和监听地址。RegisterXxxx
函数哦?请读者放心,tRPC register 的时候,如果查不到对应的配置入口,那么 tRPC 也只是什么都不做而已,不会导致进程的 panic。4.2 配置配置
trpc_go.yaml
文件中,我们还需要添加各微服务所需要的配置入口。这个时候,我们就需要将每一个微服务所需的所有配置,都配置上。需要注意的是,如果之前不同的微服务采用了同样的配置名,却实现了不同的功能,那么在代码改造的时候需要修改一下,要不然在此处会发生冲突。5.1 降本增效
5.2 扩展思考
功能和接口在传递时,尽量通过 interface
进行实现细节的隐藏,这也便于微服务和单体架构的无感切换模块、组件甚至整个服务逻辑的初始化,尽可能采用依赖注入,尽可能减少使用 init
进行重度的初始化每一个 package 的功能尽可能简单、独立、明确,避免一个 package 中耦合了大量复杂逻辑