自研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)问题。

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字节的帧头来保存协议体的大小,这样接收端首先读取帧头的里面的值,接着再根据值的大小来读取协议体的数据。

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

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

如上实现一个简单的数据接收和解析功能,但这样不足以完整描述一个RPC协议。以gRPC为例,一次Request请求包括请求头,请求体和EOS。请求头和请求体都属于不固定长度的数据,这些数据无法放到帧头中。因为帧头是固定长度,一旦对帧头增加新的功能,将会导致协议解析失败引发线上故障。为了能够平滑地升级改造前后的协议,有必要设计一种支持可扩展的协议。其关键在于让协议体支持可扩展,对于协议体的数据主要包括四部份:

1)当前RPC远程调用的信息,如服务名、接口名、方法名、版本。

2)RPC框架定义的透传元数据,如rpc-version、rpc-env。

3)业务自定义的透传元数据,如usr-traceid、usr-logid。

4)客户端和服务端发送的数据,如请求参数、返回值。

前三部分认为是协议体的扩展部分,用于保存当前RPC远程调用的上下文,称为包头。第四部分用于保存当前RPC发送的数据,称为包体。

将协议体拆分成包头和包体以后,需在帧头再增加2字节来保存包头的长度,这样接收端可根据协议体总长度和包头长度来合理读取包头和包体数据。

一个完整的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协议也面临着一些挑战,其中最主要的是兼容性问题。如果协议仅支持特定的编程语言或平台,那么在其他环境中的应用就可能会遇到困难。此外,开发和维护成本、生态系统支持、安全性和稳定性等因素也需要开发人员在设计阶段进行深入考虑。

未完待续

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

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

  1. 使用Holer外网SSH访问内网(局域网)Linux系统

    1. Holer工具简介 Holer exposes local servers behind NATs and firewalls to the public internet over secur ...

  2. 使用Holer远程桌面登录家里电脑和公司内网电脑

    1. Holer工具简介 Holer exposes local servers behind NATs and firewalls to the public internet over secur ...

  3. Vue实战041:获取当前客户端IP地址详解(内网和外网)

    前言 我们经常会有需求,希望能获取的到当前用户的IP地址,而IP又分为公网ip(也称外网)和私网IP(也称内网IP),IP地址是IP协议提供的一种统一的地址格式,每台设备都设定了一个唯一的IP地址”, ...

  4. 使用内网映射工具Holer将本地的Web应用映射到公网上访问

    Holer exposes local servers behind NATs and firewalls to the public internet over secure tunnels. Su ...

  5. Holer一款局域网服务器代理到公网的内网映射工具

    Holer简介 Holer是一个将局域网服务器代理到公网的内网映射工具,支持转发基于TCP协议的报文. 相关链接 开源地址:https://github.com/Wisdom-Projects/hol ...

  6. 内网安全---隐藏通信隧道基础&&网络通信隧道之一ICMP隧道

    一,隐藏通信隧道基础知识 在完成信息收集之后,我们要判断流量是否出的去.进的来.隐藏通信隧道技术常用于在受限的网络环境中追踪数据流向和在非受信任的网络中实现安全的数据传输. 1.常见的隧道: .网络层 ...

  7. Windows用户如何安装cpolar内网穿透

    概述 本教程适合于Windows用户,安装并使用cpolar工具. 什么是cpolar? cpolar是一个非常强大的内网穿透工具,开发调试的必备利器 它可以将本地内网服务器的HTTP.HTTPS.T ...

  8. 用SSH访问内网主机的方法

    如今的互联网公司通常不会直接自己直接配主机搭建服务器了,而是采用了类似阿里云的这种云主机,当应用变得越来越大了之后,就不可避免地增加主机,而出于成本考虑,不可能给每一台主机都分配公网带宽,所以实际的情 ...

  9. 内网穿透神器ngrok

    相信做Web开发的同学们,经常会遇到需要将本地部署的Web应用能够让公网环境直接访问到的情况,例如微信应用调试.支付宝接口调试等.这个时候,一个叫ngrok的神器可能会帮到你,它提供了一个能够在公网安 ...

  10. 内网劫持渗透新姿势:MITMf简要指南

    声明:本文具有一定攻击性,仅作为技术交流和安全教学之用,不要用在除了搭建环境之外的环境. 0×01 题记 又是一年十月一,想到小伙伴们都纷纷出门旅游,皆有美酒佳人相伴,想到这里,不禁潸然泪下.子曰:& ...

随机推荐

  1. Excel中,文本显示的纯数字(超15位)处理建议

    EXCEL中,对于文本显示的纯数字(只能认前15位),vlookup,countif都会出错,但可以用新函数xlookup,filter等处理就没问题.所以在统计文本显示的数字的时候,一定要多注意.

  2. SQL Server 删除重复行,查询重复,多记录匹配保留最小行(如何删除 SQL Server 表中的重复行)

    https://support.microsoft.com/zh-cn/topic/%E5%A6%82%E4%BD%95%E5%88%A0%E9%99%A4-sql-server-%E8%A1%A8% ...

  3. 线性回归原理推导与应用(二):Python一元线性回归实战

    在上一篇文章中我们对一元线性回归模型和方程的原理及公式进行了推导,本篇文章将根据实际数据建立一个一元回归模型,代码基于Python 建模应用 首先导入需要用到的Python库: #一元线性回归 imp ...

  4. 文献阅读 A Subdivision Scheme for Hexahedral Meshes

    简介 本文提出了一种六面体体细分的方案,可以将六面体分成比较光顺的六面体.同时增加六面体的数量且不改变六面体的拓扑. 本文作者 Bajaj =.= 论文方案 主要由两个步骤组成 split & ...

  5. 关于Codigger之软件项目体检Software Project HealthCheck

    项目体检是Codigger推出的智能代码质量检查工具,可以系统地帮助您交付干净的代码.作为我们Codigger解决方案的重要元素 ,代码体检集成到您现有的工作流程中并检测代码中的问题,以帮助您对项目执 ...

  6. SciTech-EECS-Circuits-PCAU(Phase Controlled Array Ultrasound,相控 麦克风/超声波头/雷达/传感器 阵列):精准调制"声场": 实现超声波"悬浮(反重力)" 原理 精准调控 上下两个 或 立体阵列 的 超声波头 组成 "空间超声波场" 使场压抵消物体重力

    SciTech-EECS-Circuits-Ultrasound: 超声波"悬浮(反重力)" 原理 PCAU(Phase Controlled Array Ultrasound,相 ...

  7. P6429 [COCI2008-2009#1] JEZ 题解

    题目传送门:Click. 更好地观感:Click(进入速度玄学) 某蒟蒻看见这道题,想了足足一个晚上,过后茅塞顿开,故作此篇. 感谢神犇的题解,思路基本相同,补充了一些自己的想法或这片题解可能没有注意 ...

  8. PG系列:在 `psql` 客户端中定义参数与动态赋值

    我们的文章会在微信公众号IT民工的龙马人生和博客网站( www.htz.pw )同步更新 ,欢迎关注收藏,也欢迎大家转载,但是请在文章开始地方标注文章出处,谢谢! 由于博客中有大量代码,通过页面浏览效 ...

  9. GROOVY 面向对象

    面向对象: class Student{ private int StudentID; private String StudentName; void setStudentID(int pID){ ...

  10. CloudQuery 1.4.0 线上发布邀请 | 数据工具集成箱 全栈数据管控平台的新起点

    随着企业规模不断扩大.应用业务系统多元化进程不断加快,数据操作已经成为DBA.开发运维人员每日的工作常态.在当前数据上云趋势中,如何便捷地进行数据操作以及权限管控变成了企业内部数据治理的难题:如何实现 ...