gRPC不是银弹:为内网极致性能,如何设计自己的RPC协议?

简介: 自研RPC协议针对内网高并发场景,通过精简帧头、长度前缀解决TCP拆包粘包,支持灵活扩展与高效序列化,显著提升性能与资源利用率,适用于对延迟敏感的分布式系统。

自研RPC协议:为性能而生的赛道利器
尽管gRPC凭借其标准化、跨语言和基于HTTP/2的强大特性,在公网和云原生环境中大放异彩,但在某些特定的内网环境中,对性能、延迟和资源占用的要求可能更为苛刻。HTTP/2虽然高效,但其帧结构和头部处理机制相较于专为内网设计的极简协议,仍可能引入不必要的开销。
相比之下,内网环境的网络特性包括更短的请求链路、更低的丢包率和更可靠的硬件环境。在实际生产环境中,公网环境通常会通过nginx等反向代理服务进行优化。经过反向代理后,服务间的请求链路实际上已经转移到了内网环境。
对于内网环境,自行实现的RPC协议相较于gRPC协议具有以下优势。
1)灵活性:根据业务需求和技术栈定制协议特性,如支持特定的调用模式、元数据传递、流控策略等。
2)轻量级:协议头部和消息结构可以做到极致精简,仅包含必要字段,减少网络传输的字节数和解析开销。
3)性能优化:可以选择或定制最高效的序列化/反序列化方案;可以实现更激进的内存管理和对象复用策略;可以针对特定的硬件特性进行微调。

TCP拆包粘包
RPC协议是建立在传输层协议之上的应用层协议,其中传输层协议包括TCP、UDP等。TCP协议因其高可靠性和全双工的特点,成为许多应用层协议的选择,包括gRPC所使用的HTTP/2协议。
然而,TCP协议传输的是一串无边界的二进制流。由于底层网络并不了解应用层数据的具体含义,它会根据TCP缓冲区(Buffer Cache)的情况进行数据包的划分。这就可能导致一个完整的应用层数据包被TCP拆分为多个小包进行发送,或者将多个小包封装成一个大的数据包进行发送。这种现象通常被称为TCP拆包(Packet splitting)和粘包(Packet sticking)问题。

image.png

TCP拆包和粘包问题可能会导致接收端无法正确解析和处理数据,从而影响应用层的正常运行。为了解决这个问题,通常需要在应用层进行数据的边界划分和处理。常见有如下的解决方案。
1)固定长度(Fixed-Length):每个消息包长度固定。简单但可能浪费空间(若数据小于固定长度)或无法处理大数据(若数据大于固定长度)。
2)分隔符(Delimiter-Based):在消息末尾添加特殊字符序列(如 \r\n)。适用于文本协议,但处理二进制数据或数据本身包含分隔符时较麻烦。
3)长度前缀(Length-Prefixed):在每个消息包前附加一个字段(通常是2或4字节整数)来指明该消息包的长度。接收方先读取长度字段,再根据长度读取完整的消息数据。这是RPC框架(包括HTTP/2的DATA帧内的消息和gRPC的消息封装)最常用的方式,因为它精确、高效且适用于任何类型的数据。
对于RPC框架,每个数据包都不应该传输多余的数据(所以补齐的方式不可取),因此长度前缀更适合这样的场景。

帧头设计
一个典型的自研RPC协议通常包含一个固定长度的帧头 (Frame Header) 和一个可变长度的协议体 (Protocol Body)。协议体又可以进一步划分为包头 (Message Header / Metadata) 和包体 (Message Body / Payload)。
首先,一个最简单的协议包含两部份,比如用4字节的帧头来保存协议体的大小,这样接收端首先读取帧头的里面的值,接着再根据值的大小来读取协议体的数据。

image.png

然而接收端接收到协议体是一串二进制数据,需知道序列化编码方式。因此在帧头增加1字节来保存当前数据的序列化编码方式。

image.png

接下来在帧头增加1字节,用来保存当前数据类型。比如请求、响应、单向调用、流式调用等。这样接收端,可以根据数据类型,来处理不同的逻辑。

image.png

如上实现一个简单的数据接收和解析功能,但这样不足以完整描述一个RPC协议。以gRPC为例,一次Request请求包括请求头,请求体和EOS。请求头和请求体都属于不固定长度的数据,这些数据无法放到帧头中。因为帧头是固定长度,一旦对帧头增加新的功能,将会导致协议解析失败引发线上故障。为了能够平滑地升级改造前后的协议,有必要设计一种支持可扩展的协议。其关键在于让协议体支持可扩展,对于协议体的数据主要包括四部份:
1)当前RPC远程调用的信息,如服务名、接口名、方法名、版本。
2)RPC框架定义的透传元数据,如rpc-version、rpc-env。
3)业务自定义的透传元数据,如usr-traceid、usr-logid。
4)客户端和服务端发送的数据,如请求参数、返回值。
前三部分认为是协议体的扩展部分,用于保存当前RPC远程调用的上下文,称为包头。第四部分用于保存当前RPC发送的数据,称为包体。
将协议体拆分成包头和包体以后,需在帧头再增加2字节来保存包头的长度,这样接收端可根据协议体总长度和包头长度来合理读取包头和包体数据。

image.png

一个完整的RPC协议设计如上,帧头一共19字节。
1)魔数(Magic):2字节,用于快速识别协议类型和版本。
2)消息类型(Data Type):1字节,消息的类型(如0x01=Request, 0x02=Response, 0x03=Heartbeat)。
2)整体长度(Total Length):4字节,协议体(包头 + 包体)的总长度。
3)包头长度(Body Head Length):2字节,包体长度 = TotalLength - HeaderLength。
5)序列化ID(Serialization ID):1字节,序列化类型(如0x1=Protobuf, 0x2=JSON, 0x3=Kryo)。
6)压缩算法ID(Compress ID):1字节,压缩算法类型(如0x1=Gzip, 0x2=Snappy)。包头通常不压缩或使用轻量压缩。
7)消息ID(Request ID):4字节,唯一标识一次RPC调用,用于异步请求响应的匹配。
8)预留字段(Reserved):4字节,预留字段,用于未来协议扩展,增加兼容性。

协议体设计
协议体的包头用于承载RPC调用的元信息,分为请求包头和响应包头,会被特定的序列化类型序列化(由序列化ID标识),比如使用Protobuf进行序列化。下面用.proto对包头进行定义。

// 请求包头
message RequestBodyHead {
   
  // 协议版本
  uint32 version = 1;
  // 主调服务的名称
  bytes caller = 2;
  // 被调服务的名称
  bytes callee = 3;
  // 调用服务的方法名
  bytes func = 4;
  // 框架透传的信息key-value对,目前分两部分
  // 1是框架层要透传的信息,key的名字要以rpc-开头
  // 2是业务层要透传的信息,业务可以自行设置
  map<string, bytes> trans_info = 5;
  // 其他信息
  ......
}

// 响应包头
message ResponseBodyHead {
   
  // 协议版本
  uint32 version = 1;
  // 请求在框架层的错误状态码
  int32 status = 2;
  // 调用结果信息描述
  // 失败的时候用
  bytes status_msg = 3;
  // 框架透传回来的信息key-value对,
  // 目前分两部分
  // 1是框架层透传回来的信息,key的名字要以rpc-开头
  // 2是业务层透传回来的信息,业务可以自行设置
  map<string, bytes> trans_info = 4;
  // 其他信息
  ......
}

编码解码
以上定义了一个RPC协议的帧头、包头和包体。下面简单用Java Netty框架演示如何编码解码RPC协议体数据。

// 编码数据
encode(ChannelBuffer in) {
   
    // 写入魔数
    in.writeShort(FRAME_MAGIC);
    // 写入数据类型
    in.writeByte(dataType);
    // 写入协议体长度
    in.writeInt(totalSize);
    // 写入协议体包头长度
    in.writeUnsignedShort(headSize);
    // 写入序列化ID
    in.writeByte(serializationID);
    // 写入压缩算法ID
    in.writeByte(compressID);
    // 写入消息ID
    in.writeInt(requestID);
    // 写入预留字段
    in.writeBytes(reserved);
    // 写入包头数据 
    // 根据serializationID和compressID 进行序列化和压缩
    in.writeBytes(headBytes);
    // 写入包体数据
    // 根据serializationID和compressID 进行序列化和压缩
    in.writeBytes(bodyBytes);
}

 // 解码数据
  decode(ChannelBuffer in) {
   
    // 检查帧头长度是否有18个字节
    if (in.readableBytes() < FRAME_SIZE) {
   
        return NOT_ENOUGH_DATA;
    }
    // 判断是否是合法的RPC协议
    if (in.readShort() != FRAME_MAGIC) {
   
        // 不是抛出RPC异常信息
        throw RpcException;
    }
    // 获取数据类型
    byte dataType = in.readByte();
    // 获取协议体长度
    int totalSize = in.readInt();
    // 获取协议体包头长度
    int headSize = in.readUnsignedShort();
    // 获取序列化ID
    byte serializationID = in.readByte();
    // 获取压缩算法ID
    byte compressID = in.readByte();
    // 获取消息ID
    int requestID = in.readInt();
    // 获取预留字段
    in.readBytes(reserved);
    // 获取包头数据
    // 根据serializationID和compressID 进行解压和反序列化
    in.readBytes(headBytes);
    // 获取包体长度
    int bodySize = totalSize - headSize;
    // 获取包体数据
    // 根据serializationID和compressID 进行解压和反序列化
    in.readBytes(bodyBytes);
}

自研RPC协议的主要优势在于其设计的紧凑性,这使得它能够满足特定高并发场景下的数据传输性能需求。由于协议体的数据格式统一,将包头和包体序列化为特定的二进制数据,这使得代码的实现过程变得更为简单。
然而,自研RPC协议也面临着一些挑战,其中最主要的是兼容性问题。如果协议仅支持特定的编程语言或平台,那么在其他环境中的应用就可能会遇到困难。此外,开发和维护成本、生态系统支持、安全性和稳定性等因素也需要开发人员在设计阶段进行深入考虑。

未完待续

很高兴与你相遇!如果你喜欢本文内容,记得关注哦!

目录
相关文章
|
2月前
|
JSON 自然语言处理 API
gRPC凭什么成为微服务通信首选?深度解析RPC进化史
本文深入解析了分布式系统中服务通信的核心机制,重点介绍了 RPC 与 gRPC 的原理、优势及使用场景,并详解 gRPC 所依赖的序列化协议 Protocol Buffers(Protobuf)。内容涵盖 RPC 概念、gRPC 特性、Protobuf 语法及服务定义,适合微服务架构设计与维护人员阅读,助你构建高性能、低耦合的服务通信体系。
382 73
gRPC凭什么成为微服务通信首选?深度解析RPC进化史
|
6月前
|
XML JSON 网络协议
利用HTTP POST协议实现简单的RPC协议:WireShark抓包分析
通过这种方式,我们可以使用HTTP POST实现简单的RPC协议,并使用WireShark进行抓包分析。这不仅可以帮助我们理解RPC协议的工作原理,也可以帮助我们调试和优化我们的代码。
251 30
|
6月前
|
JSON 数据格式
利用HTTP POST协议实现简单的RPC协议,并使用WireShark进行抓包分析
通过这种方式,我们可以利用HTTP POST实现简单的RPC协议,并使用WireShark进行抓包分析。这种方式简单易懂,实用性强,可以应用于各种网络编程场景。
202 16
|
11月前
|
Dubbo 安全 应用服务中间件
Apache Dubbo 正式发布 HTTP/3 版本 RPC 协议,弱网效率提升 6 倍
在 Apache Dubbo 3.3.0 版本之后,官方推出了全新升级的 Triple X 协议,全面支持 HTTP/1、HTTP/2 和 HTTP/3 协议。本文将围绕 Triple 协议对 HTTP/3 的支持进行详细阐述,包括其设计目标、实际应用案例、性能测试结果以及源码架构分析等内容。
594 112
|
Java Android开发
java利用xml-rpc协议操作wordpress博客
java利用xml-rpc协议操作wordpress博客
175 1
|
11月前
|
自然语言处理 负载均衡 API
gRPC 一种现代、开源、高性能的远程过程调用 (RPC) 可以在任何地方运行的框架
gRPC 是一种现代开源高性能远程过程调用(RPC)框架,支持多种编程语言,可在任何环境中运行。它通过高效的连接方式,支持负载平衡、跟踪、健康检查和身份验证,适用于微服务架构、移动设备和浏览器客户端连接后端服务等场景。gRPC 使用 Protocol Buffers 作为接口定义语言,支持四种服务方法:一元 RPC、服务器流式处理、客户端流式处理和双向流式处理。
|
前端开发 C# 开发者
WPF开发者必读:MVVM模式实战,轻松构建可维护的应用程序,让你的代码更上一层楼!
【8月更文挑战第31天】在WPF应用程序开发中,MVVM(Model-View-ViewModel)模式通过分离关注点,提高了代码的可维护性和可扩展性。本文详细介绍了MVVM模式的三个核心组件:Model(数据模型)、View(用户界面)和ViewModel(处理数据绑定与逻辑),并通过示例代码展示了如何在WPF项目中实现MVVM模式。通过这种模式,开发者可以更高效地构建桌面应用程序。希望本文能帮助你在WPF开发中更好地应用MVVM模式。
706 1
|
API 开发者 微服务
RPC和 HTTP协议
【8月更文挑战第8天】RPC(远程过程调用)使程序能像本地调用般请求远程服务,简化网络通信细节。其优点包括高效的数据传输及严格的类型定义,适合微服务间的高效通信。HTTP(超文本传输协议)则是用于万维网数据传输的通用协议,以文本为基础,易于理解和调试,并被广泛支持。两者各有侧重,RPC偏高速服务通信,HTTP则更适用于多样化的网络场景。选择时需根据具体需求决定。
131 2
|
网络协议 编译器 Go
揭秘!TCP、RPC、gRPC、HTTP大PK,谁才是网络通信界的超级巨星?一篇文章带你秒懂!
【8月更文挑战第25天】本文以教程形式深入对比了TCP、RPC、gRPC与HTTP这四种关键通信协议,并通过Go语言中的示例代码展示了各自的实现方法。TCP作为一种可靠的传输层协议,确保了数据的完整性和顺序性;RPC与gRPC作为远程过程调用框架,特别适合于分布式系统的函数调用与数据交换,其中gRPC在性能和跨语言支持方面表现出色;HTTP则是广泛应用于Web浏览器与服务器通信的应用层协议。选择合适的协议需根据具体需求综合考量。
895 0
|
存储 C++
gRPC 四模式之 双向流RPC模式
gRPC 四模式之 双向流RPC模式
856 0