这是我参与「第五届青训营 」笔记创作活动的第14天
一、本堂课重点内容:
基本概念
RPC
IPC(进程间通信,Interprocess communication)是在多任务操作系统或者联网的计算机之间运行的程序和进程所用的通信技术,分为LPC(本地过程调用,Local Procedure Call)和RPC(远程过程调用,Remote Procedure Call)两种类型的进程间通信技术。
说白了LPC就是本地函数调用,RPC就是远程函数调用。
远程调用往往用在网络上,如何保证在网络上高效稳定地传输数据?需要解决:
- 函数映射
- 数据转换成字节流
- 网络传输
RPC概念模型
一次完整PRC过程
-
IDL (Interface description language) 文件,IDL 通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信
-
生成代码(GenCode),通过编译器工具把 IDL 文件转换成语言对应的静态库
-
编解码(Encoder,Decoder),从内存中表示到字节序列的转换称为编码,反之为解码,也常叫做序列化和反序列化(字节流)
-
通信协议,规范了数据在网络中的传输内容和格式。除必须的请求/响应数据外,通常还会包含额外的元数据
-
网络传输,通常基于成熟的网络库走 TCP/UDP 传输
RPC优缺点
-
优点:
-
1.单一职责,有利于分工协作和运维开发。开发(采用不同的语言)、部署以及运维(上线独立)都是独立的
-
2.可扩展性强,资源使用率更优。例如压力过大的时候可以独立扩充资源,底层基础服务可以复用,节省资源
-
3.故障隔离,服务的整体可靠性更高
-
-
缺点:
-
1.服务宿机,对方应该如何处理?
-
2.在调用过程中发生网络异常,如何保证消息的可达性?
-
3.请求量突增导致服务无法及时处理,有哪些应对措施?
-
RPC框架-分层设计
RPC框架采用三层的分层设计:编解码层、协议层、网络通信层
其分层结构以 Apache Thrift 为例:
编解码层
生成代码
数据格式
-
语言特定的格式 :许多编程语言都内建了将内存对象编码为字节序列的支持,例如 Java 有 java.io.Serializable
- 这种编码形式好处是非常方便,可以用很少的额外代码实现内存对象的保存与恢复,这类编码通常与特定的编程语言深度绑定,其他语言很难读取这种数据
-
文本格式 :JSON、XML、CSV 等文本格式,具有人类可读性
- 文本格式具有人类可读性,数字的编码多有歧义之处。
- 比如XML和CSV不能区分数字和字符串
- JSON虽然区分字符串和数字,但是不区分整数和浮点数,而且不能指定精度,处理大量数据时,这个问题更严重了;由于JSON在一些语言中的序列化和反序列化需要采用反射机制,所以在性能比较差
- 没有强制模型约束,实际操作中往往只能采用文档方式来进行约定,这可能会给调试带来一些不便。
-
二进制编码 :具备跨语言和高性能等优点,常见有 Thrift 的 BinaryProtocol,Protobuf 等
- 实现可以有很多种,TLV 编码和 Varint 编码
二进制编码
- TLV编码
- Tag :标签,可以理解为类型
- Lenght: 长度
- Value: 值,Value 也可以是个TLV结构
以IDL文件的结构体为例,采用二进制TLV编码成字节流:
缺点是冗余信息过多。
数据格式选型
应从以下方面考虑:
- 兼容性
- 支持自动增加新的字段,而不影响老的服务,这将提高系统的灵活度
- 通用性
- 支持跨平台、跨语言
- 性能
- 从空间和时间两个维度来考虑,也就是编码后数据大小和编码耗费时长
协议层
概念
根据数据包长度的划分不同分为以下两类:
-
特殊结束符
-
一个特殊字符作为每个协议单元结束的标示
-
过于简单,对于一个协议单元必须要全部读入才能够进行处理,除此之外必须要防止用户传输的数据不能同结束符相同,否则就会出现紊乱
-
HTTP 协议头就是以回车(CR)加换行(LF)符号序列结尾
-
-
变长协议
-
以定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度
-
一般都是自定义协议,由 header 和 payload 组成,会以定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度,使用比较广泛
-
协议构造
以 Thrift 的 THeader 协议为例
-
LENGTH:数据包大小,不包含自身
-
HEADER MAGIC: 标识版本信息,协议解析时候快速校验
-
SEQUENCE NUMBER: 表示数据包的 segID可用于多路复用,单连接内递增
-
HEADER SIZE: 头部长度,从第14个字节开始计算一直到 PAYLOAD前
-
PROTOCOL ID:编解码方式,有 Binary 和Compact 两种
-
TRANSFORM ID: 压缩方式,如 zlib 和snappy
-
INFO ID: 传递一些定制的 meta 信息
-
PAYLOAD: 消息体
网络通信层
Sockets API
套接字编程中的客户端必须知道两个信息:服务器的 IP 地址,以及端口号。
-
服务端socket函数创建一个套接字,bind 将一个套接字绑定到一个地址上。listen 监听进来的连接。
-
connect 客户端向服务器发起连接,accept 接收一个连接请求,如果没有连接则会一直阻塞直到有连接进来。得到客户端的fd之后,就可以调用read, write函数和客户端通讯,读写方式和其他I/O类似
网络库
对底层Socket通信的进一步封装。
- 提供易用 API
- 封装底层 Socket API
- 连接管理和事件分发
- 功能
- 协议支持: tcp、udp 和 uds 等
- 优雅退出、异常处理等
- 性能
- 应用层 buffer 减少 copy
- 高性能定时器、对象池等
关键指标
稳定性
保障策略
从某种程度上讲超时、限流和熔断也是一种服务降级的手段 。
-
熔断: 保护调用方,防止被调用的服务出现问题而影响到整个链路
- 一个服务 A 调用服务 B 时,服务 B 的业务逻辑又调用了服务 C,而这时服务 C 响应超时了,由于服务 B 依赖服务 C,C 超时直接导致 B 的业务逻辑一直等待,而这个时候服务 A 继续频繁地调用服务 B,服务 B 就可能会因为堆积大量的请求而导致服务宕机,由此就导致了服务雪崩的问题
-
限流: 保护被调用方,防止大流量把服务压垮
- 当调用端发送请求过来时,服务端在执行业务逻辑之前先执行检查限流逻辑,如果发现访问量过大并且超出了限流条件,就让服务端直接降级处理或者返回给调用方一个限流异常
-
超时控制: 避免浪费资源在不可用节点上
- 当下游的服务因为某种原因响应过慢,下游服务主动停掉一些不太重要的业务,释放出服务器资源,避免浪费资源
请求成功率
使用以下两种方式提高请求成功率:
- 负载均衡
- 重试
长尾请求
- 长尾请求 :一般是指明显高于均值的那部分占比较小的请求。 业界关于延迟有一个常用的P99标准, P99 单个请求响应耗时从小到大排列,顺序处于99%位置的值即为P99 值,那后面这 1%就可以认为是长尾请求。在较复杂的系统中,长尾延时总是会存在。造成这个的原因非常多,常见的有网络抖动,GC,系统调度。
使用Backup Request策略提高长尾请求的成功率(t3则是P99值):
注册中间件
通过注册中间件来实施以上提高稳定性的措施。Kitex Client 和 Server 的创建接口均采用 Option 模式,提供了极大的灵活性,很方便就能注入这些稳定性策略。
易用性
- 开箱即用
-
合理的默认参数选项、丰富的文档
-
Kitex 使用 Suite 来打包自定义的功能(熔断、限流、降级),提供「一键配置基础依赖」的体验
-
- 周边工具
- 生成代码工具(根据IDL生成代码)、脚手架工具(生成比较重复的代码,比如生成访问数据库的代码,只需要填充参数即可)
扩展性
- Middleware
- Option
- 编解码层
- 协议层
- 网络传输层
- 代码生成工具插件扩展
观测性
-
传统的 Log(日志)、Metric(监控面板:查看QPS、延迟等)、Tracing(链路跟踪:排查错误) 三件套
-
除此之外,对于框架来说可能还不够,还有些框架自身状态需要暴露出来(内置观测性服务),例如当前的环境变量、配置、Client/Server初始化参数、缓存信息等
高性能
分两个维度:高性能意味着高吞吐和低延迟,两者都很重要,甚至大部分场景下低延迟更重要。
根据不同请求的场景需求(或不同框架)提高性能的手段:
- 连接池和多路复用:复用连接,减少频繁建联带来的开销
- 高性能编解码协议:Thrift、Protobuf、Flatbuffer 和 Cap'n Proto 等
- 高性能网络库:Netpoll 和 Netty 等
企业实践(Kitex)
整体架构
- Kitex Core :核心组件
- Kitex Byted:与公司内部基础设施集成
- Kitex Tool:代码生成工具
自研网络库-Netpoll
背景
-
原生库无法感知连接状态
- 在使用连接池时,池中存在失效连接,影响连接池的复用。
-
原生库存在 goroutine 暴涨的风险
- 一个连接一个 goroutine 的模式,由于连接利用率低下,存在大量 goroutine 占用调度开销,影响性能。
Netpoll
-
解决无法感知连接状态问题
- 引入 epoll 主动监听机制,感知连接状态
-
解决 goroutine 暴涨的风险
- 建立 goroutine 池,复用 goroutine
-
提升性能
- 引入 Nocopy Buffer,向上层提供 NoCopy 的调用接口,编解码层面零拷贝
扩展性设计
kitex支持多协议的并且也是可扩展的,交互方式上前面已经说过支持ping-pong、streaming、oneway
性能优化
网络库优化
- 调度优化
- epoll wait 在调度上的控制
- gopool 重用 goroutine 降低同时运行协程数
- LinkBuffer
- 读写并行无锁,支持 nocopy 地流式读写
- 高效扩缩容
- Nocopy Buffer 池化,减少 GC
- Pool
- 引入内存池和对象池,减少 GC 开销
编解码优化
- Codegen
-
预计算并预分配内存,减少内存操作次数,包括内存分配和拷贝Inline 减少函数调用次数和避免不必要的反射操作等
-
自研了 Go 语言实现的 Thrift IDL 解析和代码生成器,支持完善的 Thrift IDL 语法和语义检查,并支持了插件机制 - Thriftgo
-
- JIT
- 使用JT 编译技术改善用户体验的同时带来更强的编解码性能,减轻用户维护生成代码的负担
- 基于JIT 编译技术的高性能动态 Thrift 编解码器 - Frugal
合并部署
- 问题 :微服务过微,传输和序列化开销越来越大
- 方案 :将亲和性强的服务实例尽可能调度到同一个物理机,远程 RPC 调用优化为本地IPC 调用
为了实现合并部署需要对框架进行改造(左侧为开源版,右侧为改造的未开源版):
改造内容:
- 中心化的部署调度和流量控制
- 基于共享内存的通信协议
- 定制化的服务发现和连接池实现
- 定制化的服务启动和监听逻辑