Powered by:NEFU AB-IN
文章目录
- 深入浅出RPC框架 | 青训营
- RPC 框架分层设计
- 远程函数调用(RPC)介绍
- 名词解释
- 一次RPC过程
- RPC好处和弊端
- 分层设计
- 编解码层
- 协议层
- 网络通信层
- RPC 关键指标分析与企业实践
- 稳定性
- 保障策略
- 请求成功率
- 长尾请求
- 注册中间件(middleware、拦截器...)
- 易用性
- 拓展性
- 观测性
- 高性能
- RPC企业实践
- 架构
- 自研网络库-Netpoll
- 拓展性设计
- 性能优化-网络库优化
- 性能优化-编解码优化
- 合并部署
- 既有HTTP,为什么用RPC?
- RPC 只是一种设计而已
- RPC框架功能更齐全
深入浅出RPC框架 | 青训营
-
RPC 框架分层设计
-
RPC 关键指标分析与企业实践
RPC 框架分层设计
远程函数调用(RPC)介绍
RPC框架是一种用于实现远程过程调用(Remote Procedure Call,简称RPC)的软件工具或框架。它提供了一种方式,使不同计算机上的程序能够通过网络相互调用函数或方法,就像调用本地函数一样。RPC框架的主要目的是简化分布式系统中的通信和远程调用过程,使开发人员能够更轻松地构建分布式应用程序。
以下是RPC框架的一些常见用途和使用方法:
1. 分布式服务调用: RPC框架允许在不同的服务器上运行的程序相互调用函数或方法。这对于构建分布式应用程序和微服务体系结构非常有用,因为它可以将不同服务之间的通信抽象化。
2. 跨语言通信: RPC框架通常支持多种编程语言,这意味着您可以使用不同编程语言编写的程序之间进行通信。这在构建多语言系统时非常有用。
3. 代码生成: 大多数RPC框架会生成客户端和服务器端的代码,这样开发人员就不需要手动编写网络通信代码。这减少了错误和提高了开发效率。
4. 序列化和反序列化: RPC框架处理数据的序列化(将数据转换为可在网络上传输的格式)和反序列化(将接收到的数据转换回本地格式),从而使远程调用变得更加透明。
5. 异常处理: RPC框架通常具有异常处理机制,允许在分布式系统中处理各种错误情况。
6. 安全性: 许多RPC框架提供安全性功能,如身份验证和数据加密,以保护通信数据的安全性。
下面是使用RPC框架的一般步骤:
1. 定义接口: 首先,您需要定义远程过程调用的接口,包括要调用的函数或方法名称、参数和返回值。
2. 生成代码: 使用RPC框架工具,您可以生成客户端和服务器端的代码。这通常涉及在接口定义上运行代码生成器,以生成与您选择的编程语言相对应的代码。
3. 实现服务器: 在服务器端,您需要实现定义的接口,以处理来自客户端的远程调用请求。
4. 实现客户端: 在客户端,您可以使用生成的客户端代码来远程调用服务器上的函数或方法。
5. 部署和运行: 部署服务器和客户端应用程序,然后通过网络进行通信,进行远程调用。
常见的RPC框架包括gRPC、Apache Thrift、Java RMI(远程方法调用)、JSON-RPC、XML-RPC等。不同的框架具有不同的特点和适用场景,您可以根据项目的需求选择合适的RPC框架。
名词解释
当涉及到RPC框架和分布式系统时,以下是与您提到的术语相关的解释:
-
IDL (Interface Description Language) 文件: IDL是一种特定语法的语言,用于定义接口和数据结构。它提供了一种独立于编程语言的方式来描述接口的方法、参数和返回值,以及数据的结构。IDL文件定义了客户端和服务器之间的通信协议的规范。
-
生成代码: RPC框架通常能够根据IDL文件生成客户端和服务器端的代码。这些代码用于处理序列化、反序列化、网络通信和远程过程调用。生成的代码使开发人员无需手动编写底层通信代码,从而提高了开发效率。
-
编解码: 在RPC中,数据在网络上传输之前需要被序列化(编码)为一种可传输的格式,然后在接收方被反序列化(解码)回本地格式。编解码是为了在网络上传输数据,并确保数据能够正确地在不同机器上传递和解释。
-
通信协议: 通信协议是定义在网络上传输数据时使用的规则和约定。它决定了数据的结构、序列化方法、错误处理等。在RPC中,通信协议用于确保客户端和服务器之间的正确通信。
-
网络传输: RPC框架使用网络传输来在不同计算机之间传递数据。网络传输包括将数据从一个计算机发送到另一个计算机的过程,通常涉及数据分割、传输控制、数据校验等。
在一个典型的RPC框架中,这些步骤按以下方式运作:
- 使用IDL文件定义接口、方法、参数和数据结构。
- 使用RPC框架的工具生成客户端和服务器端的代码,这些代码包括编解码和通信协议的实现。
- 在服务器端实现IDL文件中定义的接口方法,用于处理远程调用请求。
- 在客户端使用生成的客户端代码,通过远程调用请求调用服务器上的方法。
- 数据在客户端和服务器之间经过编码和解码,使用通信协议在网络上传输。
- 网络传输将编码后的数据从客户端发送到服务器,然后将结果返回给客户端。
整个过程的目标是使远程调用和分布式通信尽可能地透明,使开发人员能够将注意力集中在业务逻辑而不是底层通信细节上。
一次RPC过程
RPC好处和弊端
好处
- 单一职责,有利于分工协作和运维开发。开发(使用的语言)、部署以及运维(上线时间)都是独立的。
- 可扩展性强,能够扩缩资源,复用资源,使资源使用率更优。
- 故障隔离,服务整体可靠性更高。
弊端
- 服务宕机,对方应如何处理?
- 调用过程中发生网络异常,如何保证消息的可达性?
- 请求突增导致服务无法及时处理,有哪些应对措施?
分层设计
Apache Thrift(简称Thrift)是一个开源的跨语言的远程过程调用(RPC)框架和服务化的软件堆栈,最初由Facebook开发并于2007年开源。它旨在解决不同编程语言之间进行高效通信的问题,特别是在分布式系统中。
分层主要是三层
- 编解码层
- 协议层
- 网络通信层
编解码层
生成代码也可看做编解码层的一部分,因为内部包含了编解码的逻辑。用户和服务器依赖同一份IDL生成codegen(可以是不同语言的)
IDL的数据格式如何选择?
- 语言特定的格式:许多编程语言都内建了将内存对象编码为字节序列的支持,例如
Java
有Java.io.Serializable
。好处是非常方便,可以用很少的额外代码实现内存对象的保存与恢复。但是这类编码通常与特定的编程语言深度绑定,其他语言很难读取这种数据。此外还有安全性和兼容性的问题。 - 文本格式:JSON、XML、CSV等文本格式具有人类可读性。但数字编码多有歧义之处,如XML和CSV不能区分数字和字符串;JSON不能区分整数和浮点数,且不能指定精度。处理大数据时存在问题。没有强制模型约束使得实际操作中往往以文档方式进行约定,可能会给调试带来不便。由于JSON在一些语言中的序列化和反序列化需要采用反射机制,所以性能也比较差。
- 二进制编码:有TLV、Varint等多种编码方式,具备跨语言和高性能等优点,常见的有Thrift的BinaryProtocol,Protobuf等。
选择依据
- 兼容性(支持新增字段):支持自动增加新的字段,而不影响老的服务,这将提高系统的灵活度
- 通用性(编码大小解码时长):支持跨平台、跨语言
- 性能(平台语言):从空间和时间两个维度来考虑,也就是编码后数据大小和编码耗费时长
协议层
现在把请求的数据转换成字节流了,需要通信协议和元数据,传给另一边
协议是双方确定的交流语义
常见的两种协议
多路复用:同一个连接内可以有多个请求流
网络通信层
Sockets API (套接字应用程序接口)介于应用层和传输层中间,是一组函数和数据结构,用于在网络上进行进程间通信。它提供了一种编程接口,使得开发者可以创建网络连接、发送和接收数据,并处理网络通信的各种细节,使用套接字,应用程序可以建立网络连接并进行双向通信。
套接字在网络通信中扮演重要的角色,它提供了以下功能:
-
建立连接:套接字允许应用程序建立与远程主机的连接。通过指定主机地址和端口,应用程序可以使用套接字来发起连接请求。
-
数据传输:一旦建立了连接,套接字可用于在应用程序之间传输数据。发送方可以将数据写入套接字,而接收方可以从套接字中读取数据。
-
协议支持:套接字库支持多种协议,如TCP(传输控制协议)和UDP(用户数据报协议)。这使得开发人员可以选择适合特定需求的协议,并根据需要进行配置。
下面是一个简单的示例,使用Python的socket模块来创建一个基本的客户端-服务器应用程序:
# 服务器端代码
import socket
# 创建套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定地 址和端口
server_address = ('localhost', 8080)
server_socket.bind(server_address)
# 监听连接
server_socket.listen(1)
# 等待客户端连接
print('等待客户端连接...')
client_socket, client_address = server_socket.accept()
print('客户端已连接:', client_address)
# 接收数据
data = client_socket.recv(1024)
print('接收到的数据:', data.decode())
# 发送响应
response = 'Hello, client!'
client_socket.sendall(response.encode())
# 关闭连接
client_socket.close()
server_socket.close()
# 客户端代码
import socket
# 创建套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接服务器
server_address = ('localhost', 8080)
client_socket.connect(server_address)
# 发送数据
message = 'Hello, server!'
client_socket.sendall(message.encode())
# 接收响应
response = client_socket.recv(1024)
print('收到的响应:', response.decode())
# 关闭连接
client_socket.close()
在这个例子中,通过使用socket模块创建了一个服务器和一个客户端。服务器绑定在本地主机的8080端口上,并监听来自客户端的连接请求。客户端连接到服务器的指定地址和端口。
一旦建立连接,服务器接收到来自客户端的消息,并发送一个简单的响应。客户端发送一个消息给服务器,并等待响应。最后,他们关闭连接。
这只是一个简单的示例,展示了套接字在建立连接、发送和接收数据方面的作用。套接字API提供了更多功能和选项,以满足不同类型的网络通信需求。
服务器会监听连接。由于服务器不止负责连接,而且要负责处理其它事物,于是会先将连接放入消息队列。队列大小(back log)一般设为128。
如果队列未满,客户端直接将状态设为已连接,若队列已满,则客户端阻塞,等待队列更新。
客户端与服务器连接完成后将进行数据读写,直到一方关闭连接。
采用封装好的网络库,作为通信层
- 提供易用API:封装底层Socket API。连接管理和事件分发
- 功能:支持tcp、udp和uds等协议。能够优雅的退出、进行异常处理等
- 性能:应用层buffer减少copy,高性能定时器、对象池等
RPC 关键指标分析与企业实践
稳定性
保障策略
-
熔断: 一个服务 A 调用服务 B 时,服务 B 的业务逻辑又调用了服务 C,而这时服务 C 响应超时了,由于服务 B 依赖服务 C,C 超时直接导致 B 的业务逻辑一直等待,而这个时候服务 A 继续频繁地调用服务 B,服务 B 就可能会因为堆积大量的请求而导致服务宕机,由此就导致了服务雪崩的问题。
-
限流: 当调用端发送请求过来时,服务端在执行业务逻辑之前先执行检查限流逻辑,如果发现访问量过大并且超出了限流条件,就让服务端直接降级处理或者返回给调用方一个限流异常。
-
超时: 当下游的服务因为某种原因响应过慢,下游服务主动停掉一些不太重要的业务,释放出服务器资源,避免浪费资源
三种保障策略都算是降级手段(服务器的降级手段是在服务器面临压力或故障时采取的策略,旨在减轻负载、保障稳定性。常见方法包括请求限流、关闭不必要的服务、降低服务质量、分流流量至备用服务器等。这有助于防止服务器过载,确保系统仍然可用,尽管在较低的性能水平下。)
请求成功率
提高方法:
- 负载均衡:若A调用B,可多个A调用,多个B被调用,减少压力
- 重试
长尾请求
长尾请求的一般标准为用时超过99%其它请求的请求。长尾请求总是会存在,业界关于延迟有一个常用的P99标准,也就是99%的请求延迟要满足在一定耗时以内,1%的请求会大于这个耗时,而这1%就可以认为是长尾请求。造成长尾请求的原因包括网络抖动、GC、系统调度等。
解决:backup request(备份请求):我们先预设一个阈值t3(比超时时间小,通常建议是RPC请求实验的pct99),当req1发出后超过t3没有返回,我们就直接发起重试请求req2。此时相当于有两个请求同时运行,任何一个成功返回后就可以立即结束这次请求。这样整体耗时就是t4,它表示从第一个请求出发到第一个结果返回的时间。相比与超时后再请求,大大减少了时延。
注册中间件(middleware、拦截器…)
框架通过注册中间件的方式使用上述功能。用户或服务器在创建时可选是否加入上述功能,这种灵活的方式保障了整体的稳定性。
易用性
- 开箱即用:合理的默认参数选项,丰富的文档
- 周边工具:框架提供一系列工具辅助用户来更好的使用框架,如生成代码工具,脚手架工具等。尽可能支持多个编解码协议
拓展性
观测性
- Log
- Metric(指标)是在监控和评估系统性能时使用的度量标准
- QPS(每秒查询率,Queries Per Second)
- 延迟(请求处理时间)
- Tracing(链路跟踪)
- 内置状态观测性服务
线程(Thread)和协程(Coroutine)都是用于并发执行任务的编程概念,但它们有一些区别和联系。
区别:
- 并发模型: 线程是操作系统层面的并发执行单元,每个线程都有自己的上下文,可以由操作系统进行调度。而协程是程序层面的并发模型,由程序员显式地控制执行的切换,不需要操作系统的干预。
- 开销: 线程通常有较高的创建和切换开销,因为涉及操作系统的资源分配和上下文切换。协程的切换开销较低,因为切换是在用户空间内进行的。
- 并发性: 线程可以在多个处理器核心上同时运行,因此适合CPU密集型任务。协程通常运行在单个线程内,适合I/O密集型任务。
联系:
- 并发性目标: 线程和协程都旨在实现并发执行,提高程序的效率和响应性。
- 多任务处理: 两者都可以用于处理多个任务,但线程可能需要更多的系统资源。
- 状态管理: 线程和协程都需要考虑状态同步和共享资源的问题,以避免竞争条件和数据不一致。
总之,线程适用于需要并行处理和多核利用的场景,而协程适用于处理大量I/O操作和轻量级并发的场景,通过避免昂贵的线程切换开销来提供更好的性能。
高性能
连接池,多路复用:减少连接的复用率,减少重复创建和销毁的开销
多路复用:调用端向服务端的一个节点发送请求,并发场景下,如果是非连接多路复用,每个请求都会持有一个连接,直到请求结束连接才会被关闭或者放入连接池复用,并发量与连接数是对等的关系。而使用连接多路复用,所有请求都可以在一个连接上完成,大家可以明显看到连接资源利用上的差异
RPC企业实践
Kitex 企业内部大范围使用 go 语言进行开发,而 kitex 是内部多年最佳实践沉淀出来的一个高性能高可扩展性的 go RPC 框架。
架构
kitex分为三个部分
- 核心组件core
- 与字节公司内部基础设施集成的部分Byted
- 代码生成工具tool
核心部分除了用户端和服务端之外,还包括注册中心(register)、服务发现(discovery)、负载均衡(loadbalance)、熔断(circuitbreak)、重试(retry)、限流(limit)等。关键的数据结构包括结点(endpoint)、rpc元数据(rpcinfo)等。remote是与远端交互的一层,transport可以和网络库交互,codec负责编解码。
代码生成工具提供命令行工具(cmd)、解析器(parser)、插件机制(plugin)、生成器(generator)、自更新(selfupdate)。
自研网络库-Netpoll
为什么要自研网络库?
- 原生库(go net)(如
net/http
用于处理 HTTP 请求和响应,net/tcp
用于处理 TCP 连接,net/udp
用于处理 UDP 数据等) 无法感知连接状态,使用连接池时,池中存在失效连接,影响连接池的复用。 - 原生库存在goroutine暴涨的风险:使用一个连接对应一个goroutine的模式,利用率低下,存在大量goroutine占用调度开销,影响性能。
Netpoll 解决了什么问题:
- 解决无法感知状态连接问题:引入epoll主动监听机制,感知连接状态
- 解决goroutine池:建立goroutine池,复用goroutine
- 提升性能:引入Nocopy Buffer,向上层提供NoCopy接口,编解码层面零拷贝
拓展性设计
支持多协议,自定义协议拓展
性能优化-网络库优化
调度优化:
- epoll wait在调度上的控制
- gopool重用goroutine降低同时运行协程数
LinkBuffer:
- 读写并行无锁,支持nocopy地流式读写
- 高效扩缩容
- Nocopy Buffer池化,减少GC
Pool:
- 引入内存池和对象池,减少GC开销
性能优化-编解码优化
编解码逻辑往往在生成代码里面
Codegen(代码生成):
- 预计算并预分配内存,减少内存操作次数,包括内存分配和拷贝
- inline减少函数调用次数和避免不必要得反射操作等
- 自研基于go的thrift IDL解析和代码生成器,支持完善的Thrift IDL语法和语义检查,支持插件机制-Thriftgo
JIT(just in time,即时编译,当某段代码即将第一次被执行时编译):
- 使用JIT编译技术改善用户体验的同时带来更强的编解码性能,减轻用户维护生成代码的负担
- 基于JIT编译技术的高性能动态Thrift编解码器-frugal。
合并部署
但是不同微服务不能直接合并
既有HTTP,为什么用RPC?
RPC 只是一种设计而已
RPC 只是一种概念、一种设计,就是为了解决 不同服务之间的调用问题, 它一般会包含有 传输协议 和 序列化协议 这两个。
但是,HTTP 是一种协议,RPC框架可以使用 HTTP协议作为传输协议或者直接使用TCP作为传输协议,使用不同的协议一般也是为了适应不同的场景。
RPC框架功能更齐全
成熟的 RPC框架还提供好了“服务自动注册与发现”、“智能负载均衡”、“可视化的服务治理和运维”、“运行期流量调度”等等功能,这些也算是选择 RPC 进行服务注册和发现的一方面原因吧!