赞
踩
一提到协议,最先想到的可能是 TCP 协议、UDP 协议等等,这些网络传输协议的实现以及应用层的HTTP协议。
其实rpc协议和http协议都属于应用层协议
可能你会问:“前面你不是说了 HTTP 协议跟 RPC 都属于应用层协议,那有了现成的 HTTP 协议,为啥不直接用,还要为 RPC 设计私有协议呢?”
这还要从 RPC 的作用说起,相对于 HTTP(1.1/1.0) 的用处,RPC 更多的是负责应用间的通信,所以性能要求相对更高。但 HTTP 协议的数据包大小相对请求数据本身要大很多,又需要加入很多无用的内容,比如换行符号、回车符等;(当然这一点http2.0已经改为二进制流式)
还有一个更重要的原因是,HTTP 协议属于无状态协议,客户端无法对请求和响应进行关联,每次请求都需要重新建立连接,响应完成后再关闭连接。因此,对于要求高性能的 RPC 来说,HTTP 协议基本很难满足需求,所以 RPC 会选择设计更紧凑的私有协议。
协议的作用:
在设计协议前,我们先梳理下要完成 RPC 通信的时候,在协议里面需要放哪些内容。
来看下 市面上的rpc框架采用的协议
协议是 RPC 的核心,它规范了数据在网络中的传输内容和格式。除必须的请求、响应数据外,通常还会包含额外控制数据,如单次请求的序列化方式、超时时间、压缩方式和鉴权信息等。
协议的内容包含三部分
RPC 协议的设计需要考虑以下内容:
比于直接构建于 TCP 传输层的私有 RPC 协议,构建于 HTTP 之上的远程调用解决方案会有更好的通用性,如WebServices 或 REST 架构,使用 HTTP + JSON 可以说是一个事实标准的解决方案。
选择构建在 HTTP 之上,有两个最大的优势:
但也存在比较明显的问题:
dubbo默认RPC协议是使用dubbo协议。dubbo协议分为报文头(也叫做Header)和报文体(也叫做Payload)。协议头占16个字节,协议体是可变长的,协议体是具体的请求/响应数据。
各个字段的详细介绍:
报文头:
Magic - Magic High & Magic Low (2字节)
标识协议版本号,Dubbo 协议:0xdabb,该字段是一个常量值。
Req/Res (1 bit)
标识是请求或响应。请求: 1; 响应: 0。
2 Way (1 bit)
仅在 Req/Res 为1(请求)时才有用,标记是否期望从服务器返回值。如果需要来自服务器的返回值,则设置为1。一般应用发送的请求都是1。
Event (1 bit)
标识是否是事件消息,例如,心跳事件。如果这是一个事件,则设置为1。
Serialization ID (5 bit)
标识序列化类型:比如 fastjson 的值为6。
Status (8 bits) 1字节
仅在 Req/Res 为0(响应)时有用,用于标识响应的状态。
Request ID (64 bits) 8字节
标识唯一请求。类型为long。该值是一个自增值。每申请一次增加1。
Data Length (32 bits) 4字节
序列化后的内容长度(标识协议体的长度),按字节计数。int类型。
报文体:
被特定的序列化类型(由序列化ID标识) 序列化后的内容,每个部分都是一个byte[]或者Byte
如果是请求包 ( Req/Res = 1),则每个部分依次为:
Dubbo version,dubbo协议版本号,比如在2.7.5版本里面,dubbo version是2.0.2
Service name,服务接口名
Service version,服务的group值
Method name,方法名
Method parameter types,参数类型
Method arguments,参数值
Attachments,附录
如果是响应包(Req/Res = 0),则每个部分依次为:
返回值类型(byte),标识从服务器端返回的值类型:
异常:RESPONSE_WITH_EXCEPTION=0
正常响应值: RESPONSE_VALUE=1
返回空值:RESPONSE_NULL_VALUE=2
带附录的异常返回值:RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS=3
带附录的正常响应值:RESPONSE_VALUE_WITH_ATTACHMENTS=4
带附录的空值:RESPONSE_NULL_VALUE_WITH_ATTACHMENTS=5
返回值:从服务端返回的响应bytes,如果返回值类型是2或者5,该字段是空
Attachments:当返回值类型是3、4、5时,则在响应包里面添加附录信息,在2.7.5版本里面,附录值只有dubbo协议的版本号,也就是2.0.2。
从协议设计上可以看出,报文体最大不能超过2^31字节,相当于2G大小。
Dubbo协议特性:
针对协议特性,我想到了下面几个问题,我们来看一下dubbo如何解决的。
在官网上也对这个问题进行了分析。官网是从网络负载上分析的,包越大,网络负载越大,每秒传输的请求就越少。
首先dubbo最大可以传输2G的数据,dubbo没有对报文压缩,如果传输大报文,造成网络传输数据量过大,可能会造成网络拥塞;默认消费端发送请求数据后等待服务端返回,大数据包造成网络传输时间长,消费端长时间等待;dubbo最大只能传输2G的数据,过大的包,dubbo无法处理;dubbo是为在微服务环境下快速响应请求的场景设计的,传输大数据包与此设计相违背。
因为dubbo协议采用 单一长链接,如果每次请求的数据包大小为500KByte,假设网络为千兆网卡,每条连接最大为7MByte,
单个服务提供者的TPS最大为:128MByte / 500KByte = 262
单个消费者调用单个服务提供者的 TPS(每秒处理事务数)最大为:7MByte / 500KByte = 14。
所以,服务端受网卡限制。不受单挑连接限制。单挑连接受带宽限制
前提:单一长链接
分为服务端和消费端进行分析:
服务端受网卡约束,(会接收多条连接(消费端),单个连接不会成为瓶颈)
1024Mbit=128MByte
单个连接为什么最大7MByte?(56Mbps)
Mbps=Mbit/s即兆比特每秒(1,000,000bit/s),Million bits per second的缩写是一种传输速率单位,指每秒传输的位(比特)数量。
传输速率是指设备的的数据交换能力,也叫“带宽”,单位是Mbps(兆位/秒),目前主流的集线器带宽主要有10Mbps、54Mbps/100Mbps自适应型、100Mbps和150Mbps四种。
所以,服务端受网卡限制。不受单条连接限制。单条连接受带宽限制。
通俗讲就是服务端接收多个客户端,不受单个连接传输速率限制。千兆网卡1024Mbit(128MByte).
单条连接传输受带宽限制,如果带宽为54Mbps,那么最大每秒传输为54Mbps/8约等于7MByte。
避免每次握手的时间开销。
在Dubbo 文档中也提到了单连接设计的原因:
因为服务的现状大都是服务提供者少,通常只有几台机器,而服务的消费者多,可能整个网站都在访问该服务,比如 Morgan 的提供者只有 6 台提供者,却有上百台消费者,每天有 1.5 亿次调用,如果采用常规的 hessian 服务,服务提供者很容易就被压垮,通过单一连接,保证单一消费者不会压死提供者,长连接,减少连接握手验证等,并使用异步 IO,复用线程池,防止 C10K 问题。
异步有很多好处:可以使用少量线程处理大量请求,避免客户端等待,减少资源占用。
对于消费端,dubbo使用异步的地方是等待服务端返回值,可以通过参数“async”设置是否异步等待。
对于服务端,Netty收到请求后,将请求交给handler,handler使用异步线程调用最终的服务。异步线程中完成下面几件事:请求报文反序列化,构建Response对象,调用过滤器,访问最终的服务,构造响应报文,响应报文序列化,将返回结果发送到消费端。
gRPC是google开源的高性能跨语言的RPC方案。gRPC的设计目标是在任何环境下运行,支持可插拔的负载均衡,跟踪,运行状况检查和身份验证。它不仅支持数据中心内部和跨数据中心的服务调用,它也适用于分布式计算的最后一公里,将设备,移动应用程序和浏览器连接到后端服务。
下面从一个真实的gRPC SayHello
请求,查看它在HTTP/2上是怎样实现的。用wireshark抓包:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8QcpjcQX-1651302509573)(http://hengyunabc.github.io/img/wireshark-grpc.png)]
可以看到下面这些Header:
Header: :authority: localhost:50051
Header: :path: /helloworld.Greeter/SayHello
Header: :method: POST
Header: :scheme: http
Header: content-type: application/grpc
Header: user-agent: grpc-java-netty/1.11.0
Google本身把这个事情想清楚了,它并没有把内部的Stubby开源,而是选择重新做。现在技术越来越开放,私有协议的空间越来越小。
HTTP/2是先有实践再有标准,这个很重要。很多不成功的标准都是先有一大堆厂商讨论出标准后有实现,导致混乱而不可用,比如CORBA。HTTP/2的前身是Google的SPDY,没有Google的实践和推动,可能都不会有HTTP/2。
实际上先用上HTTP/2的也是手机和手机浏览器。移动互联网推动了HTTP/2的发展和普及。
只讨论协议本身的实现,不考虑序列化。
stream就是http2的一个最小的数据单元 也就是数据包 一个包分为Header frame 和 data frame
在业界,有很多支持stream的方案,比如基于websocket的,或者rsocket。但是这些方案都不是通用的。
HTTP/2里的Stream还可以设置优先级,尽管在rpc里可能用的比较少,但是一些复杂的场景可能会用到。
比如传统的rpc dubbo,需要写一个dubbo filter,还要考虑把鉴权相关的信息通过thread local传递进去。rpc协议本身也需要支持。总之,非常复杂。实际上绝大部分公司里的rpc都是没有鉴权的,可以随便调。
尽管HPAC可以压缩HTTP Header,但是对于rpc来说,确定一个函数调用,可以简化为一个int,只要两端去协商过一次,后面直接查表就可以了,不需要像HPAC那样编码解码。
可以考虑专门对gRPC做一个优化过的HTTP/2解析器,减少一些通用的处理,感觉可以提升性能。
一次是HEADERS frame,一次是DATA frame。
gRPC选择基于HTTP/2,那么它的性能肯定不会是最顶尖的。但是对于rpc来说中庸的qps可以接受,通用和兼容性才是最重要的事情。
设计协议需要考虑的问题:
在本次造轮子项目中设计协议时借鉴并改进了dubbo协议,可以在此dubbo基础上做一些优化或者增加一些其他的特色,我这里举几个例子:
总包体加入了 解决了
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
+-----+-----+------+-----+--------+----+----+----+------+-----------+-------+----- --+-----+-----+-------+
|magic code|version| full length |标识 | RequestId |
+-----------------------+--------+---------------------+-----------+-----------+-----------+------------+
| |
| body |
| |
| ... ... |
+-------------------------------------------------------------------------------------------------------+
2B magic code(魔法数) 1B version(版本) 4B full length(消息长度) 2Bit messageType(消息类型)
2bit compress(压缩类型) 4bit codec(序列化类型) 8B requestId(请求的Id)
编码代码如下:
@Slf4j public class RpcMessageEncoder extends MessageToByteEncoder<RpcMessage> { private static final AtomicLong ATOMIC_LONG = new AtomicLong(0); @Override protected void encode(ChannelHandlerContext ctx, RpcMessage rpcMessage, ByteBuf out) { try { out.writeBytes(RpcConstants.MAGIC_NUMBER); out.writeByte(RpcConstants.VERSION); // leave a place to write the value of full length //这里挺重要的 先留着 总长度的位置 out.writerIndex(out.writerIndex() + 4); byte messageType = rpcMessage.getMessageType(); out.writeByte(messageType); out.writeByte(rpcMessage.getCodec()); out.writeByte(CompressTypeEnum.GZIP.getCode()); out.writeLong(ATOMIC_LONG.getAndIncrement()); // build full length byte[] bodyBytes = null; //总长度 先设置为报头长度 int fullLength = RpcConstants.HEAD_LENGTH; // if messageType is not heartbeat message,fullLength = head length + body length if (messageType != RpcConstants.HEARTBEAT_REQUEST_TYPE && messageType != RpcConstants.HEARTBEAT_RESPONSE_TYPE) { // serialize the object String codecName = SerializationTypeEnum.getName(rpcMessage.getCodec()); log.info("codec name: [{}] ", codecName); Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class) .getExtension(codecName); bodyBytes = serializer.serialize(rpcMessage.getData()); // compress the bytes String compressName = CompressTypeEnum.getName(rpcMessage.getCompress()); Compress compress = ExtensionLoader.getExtensionLoader(Compress.class) .getExtension(compressName); bodyBytes = compress.compress(bodyBytes); fullLength += bodyBytes.length; } if (bodyBytes != null) { out.writeBytes(bodyBytes); } int writeIndex = out.writerIndex(); out.writerIndex(writeIndex - fullLength + RpcConstants.MAGIC_NUMBER.length + 1); out.writeInt(fullLength); out.writerIndex(writeIndex); } catch (Exception e) { log.error("Encode request error!", e); } } }
协议头已经占据 2字节
4B = 32位 最大长度 2 32次方 -
消息类型分为:
ping pong req response
0 1 2 3
参考:
https://blog.csdn.net/hengyunabc/article/details/81120904
https://grpc.io/
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。