微服务不香了?单体化改造为我们节省上万核 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 proxyapitype API interface {FeedsRerank() pb.NewFeedsRerankClientProxyRegisterFeedsRerank(p pb.NewFeedsRerankClientProxy)}func DefaultAPI() API {return defaultAPIImpl}type apiImpl struct {internalFeedsRerankClientProxy pb.FeedsRerankClientProxyinternalXxxxClientProxy 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 中耦合了大量复杂逻辑
第一时间看鹅厂架构设计经验