您当前的位置:首页 > 电脑百科 > 程序开发 > 架构

微服务不香了?单体化改造为我们节省上万核CPU

时间:2023-12-08 15:09:15  来源:  作者:dbaplus社群

微服务一直以来是服务治理的基本盘之一,落地到云原生上,往往是每个 K8s pods 部署一个服务,独立迭代、独立运维。

但是在快速部署的时候,有时候,我们可能需要一些宏服务的优势。有没有一种方法,能够 “既要又要” 呢?本文基于 tRPC-Go 服务,提出并最终实践了一种经验证可行的方法。

一、微服务的优劣

微服务是云原生的大潮流,它的优势非常明显:

  • 微服务大大降低了模块间的耦合。当某个模块 / 微服务需要变更时,只需要调整这个微服务即可,其他服务无感知;
  • 微服务化使得模块的更新能够平滑过渡,避免了停机更新的问题,也适合大型团队或多个团队间合作构建;
  • 微服务模块的输入 / 输出定义很明确,非常适合融合 DDD 理念进行设计
  • 问题排查时,能够快速定位出现问题的模块,对运维也很友好。

然而微服务也存在劣势:

  • 当系统趋向复杂时,随着微服务的拆分、功能的繁杂和细化,微服务越来越多,一窥系统全貌的难度越来越大;
  • 模块间通信通过 RPC 实现,RPC 带来了时间和网络流量的开销;
  • 依赖于完备的服务治理体系,对小团队而言,部署成本较高;
  • 多租户隔离部署时,运维难度也成倍增加。

二、遇到的问题

我们是心悦俱乐部首页 Feeds 流推荐系统的开发团队。但我们推荐系统也接入了其他业务,比如我们在接入游戏知几项目的一个功能后,全量发布前的压测中发现 CPU 开销大到难以接受。

1.分析

我们的系统是简单按照 “业务 → 分流 → 重排 → 精排 → 召回” 的推荐系统微服务化部署,没有做编排化:

微服务不香了?单体化改造为我们节省上万核CPU

观察压测数据,我们会发现,在分流层前后的服务,网络开销非常大:

微服务不香了?单体化改造为我们节省上万核CPU

分流服务是推荐系统的总入口,它没有很强的业务属性,而是在整个推荐系统的前面、在业务数据的基础上,加入 A/B Test 参数,供整个推荐系统使用。所以它对于业务负载基本是透传的。

很明显,业务服务发给推荐系统的数据流量非常大,而作为透明传输业务数据的分流服务,入参需要反序列化,出参需要重新序列化,这些都是无谓的算力消耗。

从分流服务的火焰图上也可见一斑——作为主要逻辑的查询实验参数,仅占了不到 10% 的 CPU,剩下的 CPU 都花在 gc、序列化反序列化、RPC 上面:

微服务不香了?单体化改造为我们节省上万核CPU

三、解决方案

从代码上看,占流量大头的数据结构,在整个调用链路上都是一致的,我们自然想到,省去网络开销,直接在内存里存取该多好啊。

其他内部团队其实也曾经提出一个 “单体大应用融合落地方案”,给了我们很大启发。不过,文档里面只是提出了将所有的微服务合并在一个 pod 中进行部署,服务间调用依然是 RPC 而不是内存调用。

实际上我们观察一下 tRPC 的 RPC 调用方法,可以看到所有的 RPC 调用,对 Go 业务代码来看是以一个 Go interface 的形式给出的;而实现方实现对外提供服务的方式,从业务层面也只是实现相应的 server interface 就可以。也就是说,服务的 client 端和 server 端,看到和实现的,都只是普通的 Go 函数。在此思路上,我们团队的同学在该文档的基础上,提出了一个将 RPC "mock" 成本地函数调用的方案,并由我落地验证了。

本文旨在向读者详细说明基于 tRPC 的微服务单体化方案的一种实现方法。代码改造还是有必要的,但我们的目标是尽可能减少代码改造量,避免入侵业务。

1.RPC 背景

以我们的重排服务为例,重排服务需要实现这样的一个 PB:

service FeedsRerank {

rpc GetFeedList (GetFeedRequest) returns (GetFeedReply) {}

}

通过 tRPC 命令行工具 build 之后,会生成一个xxx.trpc.go文件,其中包含 service 接口:

type FeedsRerankService interface {

GetFeedList(ctx context.Context, req *GetFeedRequest) (*GetFeedReply, error)

}

作为服务端,需要实现这个接口,并在 mAIn 函数中调用 RegisterFeedsRerankService 注册实现, tRPC 会自动对接框架和代码实现。

同时还会生成另外一个 client 接口:

type FeedsRerankClientProxy interface {

GetFeedList(ctx context.Context, req *GetFeedRequest, opts ...client.Option) (*GetFeedReply, error)

}

一般而言,任意一个 client 要调用重排服务的话,只需要 client := pb.NewFeedsRerankClientProxy(),然后就可以直接调用 GetFeedList 方法了,tRPC 帮调用方隐藏了底层 RPC 细节。对调用方而言,这就只是一个函数而已。对,函数!!!

2.代码改造

1)Client 侧

我们的思路是:作为 rerank 这个微服务,要将自己的入口映射到某处;而 client 方不要自行 new 下游的 proxy,而是从这个地方统一取(我们把这个叫做 proxy API),这样我们就可以实现了。用 Go 的语言来描述, 调用方看到的只是一个 interface, 那我们就在内存把被调用方的代码按照这个 interface 进行实现, 然后想办法让 client 端直接用上这个实现,就可以了!

考虑到绝大部分的 trpc proxy 都只是使用默认参数进行初始化即开箱即用,因此我们就将这些都统一收拢起来,构建了一个获取各种 client proxy 的 repo 仓库(比如就简单命名为 "api"),clent 方从这个仓库的 getter 函数中获取自己需要的 client,如:

rerank := api.FeedsRerank()

rsp, err := rerank.GetFeedList(ctx, req)

// .....

2)Server 侧

Server 是提供服务的一侧,每个微服务,首先要把自己的业务代码完全抽出来,不要放在 main 包中——这个改造并不难。各微服务的业务逻辑,可以抽取出来称为 service 包,对外暴露一个 Register 函数,这个函数的入参中包含 trpc-go/service.Server 类型,用于调用 tRPC 服务注册函数,如重排服务:

pb.RegisterFeedsRerankService(server, rerankImpl)

这是原本就有的常规操作。但是除此之外,还需要调用前文的 proxy API,将自己的实现 mock 一下。需要注意的是,tRPC 的 client proxy 函数参数,相比 server 侧实现的方法,多了一个 opts ...client.Option 参数。不过绝大多数情况下,我们忽略这些参数就好了。

还是以重排为例,简单用以下代码 mock 一下自身:

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 调用;但是在单体场景下,不知不觉地就只是一个内存的调用了。

3)main 包

我们在原有逻辑中,每一个微服务的逻辑都写在 main 包中。支持单体化的改造之后,每一个微服务的逻辑都应挪到一个非 main 包中,并且微服务依赖的各种组件尽量使用注入,而不是由微服务内部初始化。包括微服务所依赖的 client proxy 接口。

4)Proxy API 实现

前文提到的 Proxy API 的实现原理很简单,各 client proxy 只需要默认调用 NewXxx 函数初始化即可(比如对应前文的 NewFeedsRerankClientProxy),得益于 tRPC 的懒初始化机制,这些 Proxy 创建了之后,只要不去调用它,那么即便配置里不包含相关的 client 配置,就不会报错。因此,虽然在 Proxy API 中初始化了多个 Proxy,也不会对具体到某个微服务造成影响。

至于 mock 动作,则通过 RegisterXxxx 函数(比如前文的 RegisterFeedsRerank)实现。具体落到细节处,也只不过是一个个的私有成员变量而已。

Proxy API 的代码大致框架如下:

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

}

}

// ...

上面的重复逻辑挺多,为了减少无意义的重复代码开发,我们在代码中编写了 shell 脚本,并且通过 go generate 来生成上述代码。

四、部署改造

按照前文所述,我们用一个单体大应用,包含了五个微服务。那么我们在部署的时候,要如何配置呢?

1.服务配置

首先,我们要决定这个单体应用对外暴露的微服务接口有哪些。如果你需要暴露多个微服务入口,那么就需要在启动时传入的 trpc_go.yaml 中配置对应的多个微服务注册和监听地址。

我们的场景比较简单,因为整个推荐系统是一个单链式调用,所以我们只需要对外暴露业务层的服务即可。注册也直接注册到原有的业务层对应的北极星节点上。

那么剩下的几个微服务呢?每一个微服务可是都调用了 tRPC 的 RegisterXxxx 函数哦?请读者放心,tRPC register 的时候,如果查不到对应的配置入口,那么 tRPC 也只是什么都不做而已,不会导致进程的 panic。

2.配置配置

在启动时传入的 trpc_go.yaml 文件中,我们还需要添加各微服务所需要的配置入口。这个时候,我们就需要将每一个微服务所需的所有配置,都配置上。需要注意的是,如果之前不同的微服务采用了同样的配置名,却实现了不同的功能,那么在代码改造的时候需要修改一下,要不然在此处会发生冲突。

五、收益

1.降本增效

进行单体化改造之前,推荐系统五个服务,在我们预定的容量下,预估需要接近 18,000 核。经过单体优化之后,在没有修改任何逻辑的前提下,就将这个数字降到 7000,优化掉了足有 61%。可见 RPC 给我们系统带来的开销有多大。

此外我们后续又做了不少算法和业务层面的优化,又降到了 1000 核的水平,主要是缓存优化、前置计算和闲时算力的优化。

该方案虽然实现了一个单体化的大服务,但是完全不妨碍其他租户的业务采用微服务化的部署。可以说,我们在开发阶段依然是用微服务模式开发,并且在不同租户下采用了不同的部署模式。可谓是在低改造量前提下实现了 “既要又要”。

2.扩展思考

当然,单体化之后的服务,在运维层面自然会带来宏服务的缺点,比如说运维困难,模块迭代不灵活等等。这个时候就需要我们去权衡利弊、综合各项因素之后,再做出决策了。

本文所实践的方法,其实对于其他 Go 语言框架也都是通用的,包括且不限于 Gin、gRPC。只要开发者在进行微服务开发的时候,遵循以下原则,那么微服务和单体之间的切换就非常方面:

  • 功能和接口在传递时,尽量通过 interface 进行实现细节的隐藏,这也便于微服务和单体架构的无感切换;
  • 模块、组件甚至整个服务逻辑的初始化,尽可能采用依赖注入,尽可能减少使用 init 进行重度的初始化;
  • 每一个 package 的功能尽可能简单、独立、明确,避免一个 package 中耦合了大量复杂逻辑。

作者丨张敏

来源丨公众号:腾讯云开发者(ID:QcloudCommunity)



Tags:微服务   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
对于微服务架构监控应该遵守的原则
随着软件交付方式的变革,微服务架构的兴起使得软件开发变得更加快速和灵活。在这种情况下,监控系统成为了微服务控制系统的核心组成部分。随着软件的复杂性不断增加,了解系统的...【详细内容】
2024-04-03  Search: 微服务  点击:(4)  评论:(0)  加入收藏
PHP+Go 开发仿简书,实战高并发高可用微服务架构
来百度APP畅享高清图片//下栽のke:chaoxingit.com/2105/PHP和Go语言结合,可以开发出高效且稳定的仿简书应用。在实现高并发和高可用微服务架构时,我们可以采用一些关键技术。首...【详细内容】
2024-01-14  Search: 微服务  点击:(114)  评论:(0)  加入收藏
九条微服务最佳实践,你学会了哪条?
微服务之间连贯一致的代码库对于可维护性至关重要。保持代码成熟度相似,可确保系统统一演进,防止服务间出现性能、安全性和功能差异。在开发微服务时,我们需要遵循哪些最佳实践...【详细内容】
2024-01-05  Search: 微服务  点击:(97)  评论:(0)  加入收藏
Go微服务入门到容器化实践
Go微服务入门到容器化实践Go 是一门高效、现代化、快速增长的编程语言,非常适合构建 Web 应用程序。而 Docker 是一种轻量级的容器化技术,能够使得您的应用程序在任何地方运行...【详细内容】
2024-01-01  Search: 微服务  点击:(61)  评论:(0)  加入收藏
微服务全做错了!谷歌提出新方法,成本直接降为1/9!
2023,微服务“水逆”之年。长期以来,不管大厂还是小厂,微服务都被认为是云原生服务应用程序架构的事实标准,然而2023,不止那位37signals的DHH决心下云,放弃微服务,就连亚马逊和谷歌...【详细内容】
2023-12-29  Search: 微服务  点击:(117)  评论:(0)  加入收藏
微服务架构中的数据一致性
在微服务中,一个逻辑上原子操作可以经常跨越多个微服务。即使是单片系统也可能使用多个数据库或消息传递解决方案。使用多个独立的数据存储解决方案,如果其中一个分布式流程参...【详细内容】
2023-12-27  Search: 微服务  点击:(141)  评论:(0)  加入收藏
监控 Spring Cloud 微服务的实践方案
一、简介Spring Cloud是一个基于Spring Boot实现的微服务框架,它提供了丰富的微服务功能,如分布式配置、服务注册与发现、服务熔断、负载均衡等。为了更好地管理和监控这样复...【详细内容】
2023-12-19  Search: 微服务  点击:(141)  评论:(0)  加入收藏
聊聊微服务链路服务
微服务架构图片如果有用户反馈某个页面很慢,我们知道这个页面的请求调用链是 A -----> C -----> B -----> D(图片有误),怎么来定位是由哪个服务引起的问题呢? 更进一步,如果...【详细内容】
2023-12-15  Search: 微服务  点击:(123)  评论:(0)  加入收藏
选择适合微服务的编程语言,让你的工作事半功倍!
讨论编程语言就像是一场政治辩论。每个开发者都会过分捍卫他/她所使用的编程语言。然而,编程语言应该被看作是它们真正是的东西,即一种工作工具。每种编程语言都有特定的目的...【详细内容】
2023-12-14  Search: 微服务  点击:(177)  评论:(0)  加入收藏
Eureka: 微服务架构中不可或缺的服务治理工具
Eureka是Netflix开源的一款用于服务治理的工具,它是NetflixOSS(OpenSourceSoftware)项目的一部分,主要用于实现微服务架构中的服务注册与发现。在当今庞大而复杂的微服务系统中,E...【详细内容】
2023-12-14  Search: 微服务  点击:(190)  评论:(0)  加入收藏
▌简易百科推荐
对于微服务架构监控应该遵守的原则
随着软件交付方式的变革,微服务架构的兴起使得软件开发变得更加快速和灵活。在这种情况下,监控系统成为了微服务控制系统的核心组成部分。随着软件的复杂性不断增加,了解系统的...【详细内容】
2024-04-03  步步运维步步坑    Tags:架构   点击:(4)  评论:(0)  加入收藏
大模型应用的 10 种架构模式
作者 | 曹洪伟在塑造新领域的过程中,我们往往依赖于一些经过实践验证的策略、方法和模式。这种观念对于软件工程领域的专业人士来说,已经司空见惯,设计模式已成为程序员们的重...【详细内容】
2024-03-27    InfoQ  Tags:架构模式   点击:(13)  评论:(0)  加入收藏
哈啰云原生架构落地实践
一、弹性伸缩技术实践1.全网容器化后一线研发的使用问题全网容器化后一线研发会面临一系列使用问题,包括时机、容量、效率和成本问题,弹性伸缩是云原生容器化后的必然技术选择...【详细内容】
2024-03-27  哈啰技术  微信公众号  Tags:架构   点击:(10)  评论:(0)  加入收藏
DDD 与 CQRS 才是黄金组合
在日常工作中,你是否也遇到过下面几种情况: 使用一个已有接口进行业务开发,上线后出现严重的性能问题,被老板当众质疑:“你为什么不使用缓存接口,这个接口全部走数据库,这怎么能扛...【详细内容】
2024-03-27  dbaplus社群    Tags:DDD   点击:(11)  评论:(0)  加入收藏
高并发架构设计(三大利器:缓存、限流和降级)
软件系统有三个追求:高性能、高并发、高可用,俗称三高。本篇讨论高并发,从高并发是什么到高并发应对的策略、缓存、限流、降级等。引言1.高并发背景互联网行业迅速发展,用户量剧...【详细内容】
2024-03-13    阿里云开发者  Tags:高并发   点击:(5)  评论:(0)  加入收藏
如何判断架构设计的优劣?
架构设计的基本准则是非常重要的,它们指导着我们如何构建可靠、可维护、可测试的系统。下面是这些准则的转换表达方式:简单即美(KISS):KISS原则的核心思想是保持简单。在设计系统...【详细内容】
2024-02-20  二进制跳动  微信公众号  Tags:架构设计   点击:(36)  评论:(0)  加入收藏
详解基于SpringBoot的WebSocket应用开发
在现代Web应用中,实时交互和数据推送的需求日益增长。WebSocket协议作为一种全双工通信协议,允许服务端与客户端之间建立持久性的连接,实现实时、双向的数据传输,极大地提升了用...【详细内容】
2024-01-30  ijunfu  今日头条  Tags:SpringBoot   点击:(8)  评论:(0)  加入收藏
PHP+Go 开发仿简书,实战高并发高可用微服务架构
来百度APP畅享高清图片//下栽のke:chaoxingit.com/2105/PHP和Go语言结合,可以开发出高效且稳定的仿简书应用。在实现高并发和高可用微服务架构时,我们可以采用一些关键技术。首...【详细内容】
2024-01-14  547蓝色星球    Tags:架构   点击:(114)  评论:(0)  加入收藏
GraalVM与Spring Boot 3.0:加速应用性能的完美融合
在2023年,SpringBoot3.0的发布标志着Spring框架对GraalVM的全面支持,这一支持是对Spring技术栈的重要补充。GraalVM是一个高性能的多语言虚拟机,它提供了Ahead-of-Time(AOT)编...【详细内容】
2024-01-11    王建立  Tags:Spring Boot   点击:(124)  评论:(0)  加入收藏
Spring Boot虚拟线程的性能还不如Webflux?
早上看到一篇关于Spring Boot虚拟线程和Webflux性能对比的文章,觉得还不错。内容较长,抓重点给大家介绍一下这篇文章的核心内容,方便大家快速阅读。测试场景作者采用了一个尽可...【详细内容】
2024-01-10  互联网架构小马哥    Tags:Spring Boot   点击:(115)  评论:(0)  加入收藏
站内最新
站内热门
站内头条