Go语言框架:深入浅出RPC框架(下)| 青训营

75 阅读6分钟

三、关键指标

3.1 稳定性-保障策略

熔断:保护调用方,防止被调用的服务出现问题而影响到整个链路

一个服务A调用服务B时,服务B的业务逻辑又调用了服务C,而这时服务C响应超时了,生于服务B依赖服务C, C超时直接导致B的业务逻辑一直等待,而这个时候服务A继续频繁地调用服务B,服务B就可能会因为准积大量的请求而导致服务宕机,主比就导致了服务雪崩的问题

限流:保护被调用方,防止大流量把服务压垮

当调用端发送请求过来时,服务端在执行业务逻辑之前先执行检查限流逻辑,如果发现访问量过大并三超出了限流条件,就让服务端直接降级处理或者返回给调用方一个限流异常

超时控制:避免浪费资源在不可用节点上

当下游的服务因为某种原因响应过慢,下 游服务主动停掉一些不太重要的业务,释放出服务器资源,避免浪费资源

从某种程度上讲,超时、限流和熔断也是一种服务降级的手段

3.2 稳定性一请求成功率

  • 长尾请求一般是指明显高于均值的那部分占比较小的请求。业界关于延迟有一个常用的P99标准,P99 单个请求响应耗时从小到大排列,顺序处于99%位置的 值即为P99值,那后面这1%就可以认为是长尾请求。在较复杂的系统中,长尾延时总是会存在。造成这个的原因非常多,常见的有网络抖动,GC, 系统调度。
  • 我们预先设定一个阈值 t3 (比超时时间小,通常建议是RPC请求延时的pct99 ),当 Req1发出去后超过t3时间都没有返回,那我们直接发起重试请求Req2 这样相当于同时有两个请求运行。然后等待请求返回,只要Resp1或者Resp2任意一个 返回成功的结果,就可以立即结束这次请求,这样整体的耗时就是t4 它表示从第一个请求发出到第-一个成功结果返回之间的时间,相比于等待超时后再发出请求,这种机制能大大减少整体延时。

3.4 稳定性-注册中间件

Kitex Clent和Server的创建接口均采用Ooption 模式,提供了极大的灵活性,很方便就能注入这些稳定性策略

3.5 易用性

开箱即用

合理的默认参数选项、丰富的文档

周边工具

生成代码工具、脚手架工具 image.png

Kitex使用Suite来打包自定义的功能,提供「一键配置 基础依赖」的体验

3.6 扩展性

image.png

  • Middleware
  • Option
  • 编解码层
  • 协议层
  • 网络传输层
  • 代码生成工具插件扩展

一次请 求发起首先会经过治理层面,治理相关的逻辑被封装在middleware中,这些middleware会被构造成-一个 有序调用链逐个执行,比如服务发现、路由、负载均衡、超时控制等,mw执行后就会进)到remote模块,完成与远端的通信

3.7 观测性

image.png

  • Log、Metric、Tracing
  • 内量观测性服务 除了传统的Log. Metic. Tracing 三件喜之外,对于框架来说可能还不够,还有些框架自身状态需要暴露出来,例如当前的环境变量、 配置、 ClientSene切 始化参数、缓存信息等

3.8 高性能

场景

  • 单机多机
  • 单连接多连接
  • 单/多client 单/多server
  • 不同大小的请求包
  • 不同请求类型:例如pingpong、streaming等

目标

  • 高吞吐
  • 低延迟

手段

  • 连接池
  • 多路复用
  • 高性能编解码协议
  • 高性能网络库

3.9 小结

    1. 框架通过中间件来注入各种服务治理策略,保障服务的稳定性
    1. 通过提供合理的默认配置和方便的命令行工具可以提升框架的易用性
    1. 框架应当提供丰富的扩展点,例如核心的传输层和协议层
    1. 观测性除了传统的Log、Metric 和Tracing 之外,内置状态暴露服务也很有必要
    1. 性能可以从多个层面去优化,例如选择高性能的编解码协议和网络库

四、企业实践

  • 企业内部大范围使用go语言进行开发,而kitex是内部多年最佳实践沉淀出来的一个高性能高可扩展性的go RPC框架。

4.1 整体架构- Kitex

Kitex Core

核心组件

Kitex Byted

与公司内部基础设施集成

Kitex Tool

代码生成工具

  • core是它的的主干逻辑,定义了框架的层次结构、接口,还有接口的默认实现,如中间蓝色部分所示,最上面clentEsever是对用户暴露的,client/server
  • option的配置都是在这两个oackage中提供的,还有client/server的初始化,在第二节介绍kitex gen生成代码时,大家应该注意到单面有client.go和servergo,虽然我们在初始化client时调用的是kitex. gen中的方法,其实大家看下<itex. gen下service package代码就知道,面是对这里的 client/server的封 装。
  • clien/server下面的是框架治理层面的功能模块和交互元信息,remote是与对端交互的模块,包括编解码和网络通信:右边绿色的byted是对字节内部的扩展,集成了内部的二方库还有与字节相关的通用的实现,byed部分是在生成代码中初始化clien和server时通过suite集成进来的,这样实现的好处是与字节的内部特生解精,方便后续开源拆分。 左边的tool则是生活成代码相关的实现,我们的生成代码工具就是编泽这个包得到的,里面包括idl解析、校验。 代码生成、 插件支持、 自更新等, 未来生成代码逻辑还会做一些拆分,便于给用户提供更友好的扩展

image.png

4.2 自研网络库-背景

原生库无法感知连接状态

在使用连接池时,池中存在失效连接,影响连接池的复用。

原生库存在goroutine暴涨的风险

一个连接一个goroutine的模式,由于连接利用率低下,存在大量goroutine占用调度开销,影响性能。

    1. Go Net使用EpollET,Netpoll 使用LT。
    1. Netpoll在大包场景下会占用更多的内存。
    1. Go Net只有一个Epoll事件循环(因为ET模式被唤醒的少,且事件循环内无需负责读写,所以干的活少),而 Netpoll 允许有多个事件循环(循环内需要负 责读写,干的活多,读写越重,越需要开更多Loops)。
    1. Go Net一个连接一个 Goroutine, Netpoll 连接数和Goroutine数量没有关系,和请求数有一定关系, 但是有Gopool重用。
    1. Go Net不支持ZeroCopy, 甚至于如果用户想要实现BufferdConnection这类缓存读取,还会产生二 次拷贝。Netpoll支持管理一 个Buffer池直接交给用 户,且上层用户可以不使用Read(p Obyte)接口而使用特定零拷贝读取接口对Buffer进行管理, 实现零拷贝能力的传递。

4.3 自研网络库一Netpoll

解决无法感知连接状态问题

引入epoll主动监听机制,感知连接状态

解决goroutine暴涨的风险

建立goroutine池,复用goroutine

提升性能

引入Nocopy Buffer,向上层提供NoCopy的调用接口,编解码层面零拷贝

go net无法检测连接对端关团(无法感知连接状态)

    1. 在使用长连接池时,池中存在失效连接,严重影响了连接池的使用和效率:
    1. 希望通过引入epoll主动监听机制,感知连接状态:

go net缺乏对协程数量的管理

    1. Kite 采取一个连接一个goroutine模式,由于连接利用率低,服务存在较多无用的gorouine,占用调度开销,影响性能。
    1. 希望建立协程池,提升性能: netpol基于epoll,同时采用Reactor模型,对于服务端则是主从Reactor模型,如右图所示:服务端的主reactor用于接受调用端的连接,然后将建立好的连接 注册至某个从Reactor上,从Reactor负责监听连接上的读写事件,然后将读写事件分发到协程池里进行处理。

为了提升性能,引入了Nocooy Buffer, 向上层提供NoCopy的调用接口,编解码层面零拷贝

4.4 扩展性设计

支持多协议,也支持灵活的自定义协议扩展

  • kitex支持多协议的并且也是可扩展的,交互方式上前面已经说过支持ping-pong、streaming、oneway
  • 编解码支持thrift、Protoouf
  • 应用层协议支持TTHeader、Ht:p2、 也支持裸的tnrift协议
  • 传输层目前支持TCP,未来考虑支持UDP、kenel-bypass的RDMA
  • 如右图所示,框架内部不强依赖任何协议和网络模块,可以基于接口扩展,在传输层上则可以集成其他库进行扩展。
  • 目前集成的有自研的Netpoll,基于netpoll实现的nt:p2库,用于mesh场 景通过共享内存高效通信的shm-ipc,以后也可以增力对RDMA支持的扩展

4.5 性能优化一网络库优化

调度优化

  • epoll wait在调度.上的控制
  • gopool重用goroutine降低同时运行协程数

LinkBuffer

  • 读写并行无锁,支持nocopy地流式读写
  • 高效扩缩容
  • Nocopy Buffer池化,减少GC

Pool

  • 引入内存池和对象池,减少GC开销

4.6 性能优化-编解码优化

Codegen

  • 预计算并预分配内存,减少内存操作次数,包括内存分配和拷贝Inline减少函数调用次数和避免不必要的反射操作等
  • 自研了Go语言实现的Thrift IDL解析和代码生成器,支持完善的Thrift IDL语法和语义检查,并支持了插件机制一Thriftgo

JIT

  • 使用JIT编译技术改善用户体验的同时带来更强的编解码性能,减轻用户维护生成代码的负担
  • 基于JIT编译技术的高性能动态Thrift 编解码器一Frugal
  • 序列化和反序列的生能优化从大的方面来看可以从时间和空间两个维度进行优化。从兼容已有的Binary协议来看,空间上的优化似乎不太可行,只能从时间 维度进行优化,包括下面的几点:
  • 代码生成code-gen的优点是库开发者实现起来相对简单,缺点是普加业务代码的维护成本和局限性。
  • JIT编译(jus-in-time complation)狭义天说是当某段代码习将第一次被执行时进行编泽, 因币1即时编译“
  • 即时编译JIT则将编译过程移到了程序力载(或首次解析)阶段,可以一次性编译生成对应的codec并亳效执行,目前公司内部正在尝试,压测数摆表明性能收益还是挺不错的,目的是不损失生能的前提下,减轻用户的维护负担生 成代码的负担。

4.7 合并部署

  • 微服务过微,传输和序列化开销越来越大
  • 将亲和性强的服务实例尽可能调度到同一个物理机,远程RPC调用优化为本地IPC调用

image.png

  • 中心化的部署调度和流量控制
  • 基于共享内存的通信协议
  • 定制化的服务发现和连接池实现
  • 定制化的服务启动和监听逻辑

4.8 小结

  • 1.介绍了Kitex 的整体架构
  • 2.介绍了自研网络库Netpoll的背景和优势
  • 3.从扩展性和性能优化两个方面分享了相关实践
  • 4.介绍了内部正在尝试落地的新的微服务形态:合并部署

五、总结

  • 1.从本地函数调用引出RPC的基本概念
  • 2.重点讲解了RPC框架的核心的三层,编解码层、协议层和网络传输层
  • 3.围绕RPC框架的核心指标,例如稳定性、可扩展性和高性能等, 展开讲解相关的知识
  • 4.分享了字节跳动高性能RPC框架Kitex的相关实践