极客时间已完结课程限时免费阅读

10 | 网络通信优化之通信协议:如何优化RPC网络通信?

10 | 网络通信优化之通信协议:如何优化RPC网络通信?-极客时间

10 | 网络通信优化之通信协议:如何优化RPC网络通信?

讲述:李良

时长14:47大小13.52M

你好,我是刘超。今天我将带你了解下服务间的网络通信优化。
上一讲中,我提到了微服务框架,其中 SpringCloud 和 Dubbo 的使用最为广泛,行业内也一直存在着对两者的比较,很多技术人会为这两个框架哪个更好而争辩。
我记得我们部门在搭建微服务框架时,也在技术选型上纠结良久,还曾一度有过激烈的讨论。当前 SpringCloud 炙手可热,具备完整的微服务生态,得到了很多同事的票选,但我们最终的选择却是 Dubbo,这是为什么呢?

RPC 通信是大型服务框架的核心

我们经常讨论微服务,首要应该了解的就是微服务的核心到底是什么,这样我们在做技术选型时,才能更准确地把握需求。
就我个人理解,我认为微服务的核心是远程通信和服务治理。远程通信提供了服务之间通信的桥梁,服务治理则提供了服务的后勤保障。所以,我们在做技术选型时,更多要考虑的是这两个核心的需求。
我们知道服务的拆分增加了通信的成本,特别是在一些抢购或者促销的业务场景中,如果服务之间存在方法调用,比如,抢购成功之后需要调用订单系统、支付系统、券包系统等,这种远程通信就很容易成为系统的瓶颈。所以,在满足一定的服务治理需求的前提下,对远程通信的性能需求就是技术选型的主要影响因素。
目前,很多微服务框架中的服务通信是基于 RPC 通信实现的,在没有进行组件扩展的前提下,SpringCloud 是基于 Feign 组件实现的 RPC 通信(基于 Http+Json 序列化实现),Dubbo 是基于 SPI 扩展了很多 RPC 通信框架,包括 RMI、Dubbo、Hessian 等 RPC 通信框架(默认是 Dubbo+Hessian 序列化)。不同的业务场景下,RPC 通信的选择和优化标准也不同。
例如,开头我提到的我们部门在选择微服务框架时,选择了 Dubbo。当时的选择标准就是 RPC 通信可以支持抢购类的高并发,在这个业务场景中,请求的特点是瞬时高峰、请求量大和传入、传出参数数据包较小。而 Dubbo 中的 Dubbo 协议就很好地支持了这个请求。
以下是基于 Dubbo:2.6.4 版本进行的简单的性能测试。分别测试 Dubbo+Protobuf 序列化以及 Http+Json 序列化的通信性能(这里主要模拟单一 TCP 长连接 +Protobuf 序列化和短连接的 Http+Json 序列化的性能对比)。为了验证在数据量不同的情况下二者的性能表现,我分别准备了小对象和大对象的性能压测,通过这样的方式我们也可以间接地了解下二者在 RPC 通信方面的水平。
这个测试是我之前的积累,基于测试环境比较复杂,这里我就直接给出结果了,如果你感兴趣的话,可以留言和我讨论。
通过以上测试结果可以发现:无论从响应时间还是吞吐量上来看,单一 TCP 长连接 +Protobuf 序列化实现的 RPC 通信框架都有着非常明显的优势。
在高并发场景下,我们选择后端服务框架或者中间件部门自行设计服务框架时,RPC 通信是重点优化的对象。
其实,目前成熟的 RPC 通信框架非常多,如果你们公司没有自己的中间件团队,也可以基于开源的 RPC 通信框架做扩展。在正式进行优化之前,我们不妨简单回顾下 RPC。

什么是 RPC 通信

一提到 RPC,你是否还想到 MVC、SOA 这些概念呢?如果你没有经历过这些架构的演变,这些概念就很容易混淆。你可以通过下面这张图来了解下这些架构的演变史。
无论是微服务、SOA、还是 RPC 架构,它们都是分布式服务架构,都需要实现服务之间的互相通信,我们通常把这种通信统称为 RPC 通信。
RPC(Remote Process Call),即远程服务调用,是通过网络请求远程计算机程序服务的通信技术。RPC 框架封装好了底层网络通信、序列化等技术,我们只需要在项目中引入各个服务的接口包,就可以实现在代码中调用 RPC 服务同调用本地方法一样。正因为这种方便、透明的远程调用,RPC 被广泛应用于当下企业级以及互联网项目中,是实现分布式系统的核心。
RMI(Remote Method Invocation)是 JDK 中最先实现了 RPC 通信的框架之一,RMI 的实现对建立分布式 Java 应用程序至关重要,是 Java 体系非常重要的底层技术,很多开源的 RPC 通信框架也是基于 RMI 实现原理设计出来的,包括 Dubbo 框架中也接入了 RMI 框架。接下来我们就一起了解下 RMI 的实现原理,看看它存在哪些性能瓶颈有待优化。

RMI:JDK 自带的 RPC 通信框架

目前 RMI 已经很成熟地应用在了 EJB 以及 Spring 框架中,是纯 Java 网络分布式应用系统的核心解决方案。RMI 实现了一台虚拟机应用对远程方法的调用可以同对本地方法的调用一样,RMI 帮我们封装好了其中关于远程通信的内容。

RMI 的实现原理

RMI 远程代理对象是 RMI 中最核心的组件,除了对象本身所在的虚拟机,其它虚拟机也可以调用此对象的方法。而且这些虚拟机可以不在同一个主机上,通过远程代理对象,远程应用可以用网络协议与服务进行通信。
我们可以通过一张图来详细地了解下整个 RMI 的通信过程:

RMI 在高并发场景下的性能瓶颈

Java 默认序列化
RMI 的序列化采用的是 Java 默认的序列化方式,我在 09 讲中详细地介绍过 Java 序列化,我们深知它的性能并不是很好,而且其它语言框架也暂时不支持 Java 序列化。
TCP 短连接
由于 RMI 是基于 TCP 短连接实现,在高并发情况下,大量请求会带来大量连接的创建和销毁,这对于系统来说无疑是非常消耗性能的。
阻塞式网络 I/O
在 08 讲中,我提到了网络通信存在 I/O 瓶颈,如果在 Socket 编程中使用传统的 I/O 模型,在高并发场景下基于短连接实现的网络通信就很容易产生 I/O 阻塞,性能将会大打折扣。

一个高并发场景下的 RPC 通信优化路径

SpringCloud 的 RPC 通信和 RMI 通信的性能瓶颈就非常相似。SpringCloud 是基于 Http 通信协议(短连接)和 Json 序列化实现的,在高并发场景下并没有优势。 那么,在瞬时高并发的场景下,我们又该如何去优化一个 RPC 通信呢?
RPC 通信包括了建立通信、实现报文、传输协议以及传输数据编解码等操作,接下来我们就从每一层的优化出发,逐步实现整体的性能优化。

1. 选择合适的通信协议

要实现不同机器间的网络通信,我们先要了解计算机系统网络通信的基本原理。网络通信是两台设备之间实现数据流交换的过程,是基于网络传输协议和传输数据的编解码来实现的。其中网络传输协议有 TCP、UDP 协议,这两个协议都是基于 Socket 编程接口之上,为某类应用场景而扩展出的传输协议。通过以下两张图,我们可以大概了解到基于 TCP 和 UDP 协议实现的 Socket 网络通信是怎样的一个流程。
基于 TCP 协议实现的 Socket 通信是有连接的,而传输数据是要通过三次握手来实现数据传输的可靠性,且传输数据是没有边界的,采用的是字节流模式。
基于 UDP 协议实现的 Socket 通信,客户端不需要建立连接,只需要创建一个套接字发送数据报给服务端,这样就不能保证数据报一定会达到服务端,所以在传输数据方面,基于 UDP 协议实现的 Socket 通信具有不可靠性。UDP 发送的数据采用的是数据报模式,每个 UDP 的数据报都有一个长度,该长度将与数据一起发送到服务端。
通过对比,我们可以得出优化方法:为了保证数据传输的可靠性,通常情况下我们会采用 TCP 协议。如果在局域网且对数据传输的可靠性没有要求的情况下,我们也可以考虑使用 UDP 协议,毕竟这种协议的效率要比 TCP 协议高。

2. 使用单一长连接

如果是基于 TCP 协议实现 Socket 通信,我们还能做哪些优化呢?
服务之间的通信不同于客户端与服务端之间的通信。客户端与服务端由于客户端数量多,基于短连接实现请求可以避免长时间地占用连接,导致系统资源浪费。
但服务之间的通信,连接的消费端不会像客户端那么多,但消费端向服务端请求的数量却一样多,我们基于长连接实现,就可以省去大量的 TCP 建立和关闭连接的操作,从而减少系统的性能消耗,节省时间。

3. 优化 Socket 通信

建立两台机器的网络通信,我们一般使用 Java 的 Socket 编程实现一个 TCP 连接。传统的 Socket 通信主要存在 I/O 阻塞、线程模型缺陷以及内存拷贝等问题。我们可以使用比较成熟的通信框架,比如 Netty。Netty4 对 Socket 通信编程做了很多方面的优化,具体见下方。
实现非阻塞 I/O:在 08 讲中,我们提到了多路复用器 Selector 实现了非阻塞 I/O 通信。
高效的 Reactor 线程模型:Netty 使用了主从 Reactor 多线程模型,服务端接收客户端请求连接是用了一个主线程,这个主线程用于客户端的连接请求操作,一旦连接建立成功,将会监听 I/O 事件,监听到事件后会创建一个链路请求。
链路请求将会注册到负责 I/O 操作的 I/O 工作线程上,由 I/O 工作线程负责后续的 I/O 操作。利用这种线程模型,可以解决在高负载、高并发的情况下,由于单个 NIO 线程无法监听海量客户端和满足大量 I/O 操作造成的问题。
串行设计:服务端在接收消息之后,存在着编码、解码、读取和发送等链路操作。如果这些操作都是基于并行去实现,无疑会导致严重的锁竞争,进而导致系统的性能下降。为了提升性能,Netty 采用了串行无锁化完成链路操作,Netty 提供了 Pipeline 实现链路的各个操作在运行期间不进行线程切换。
零拷贝:在 08 讲中,我们提到了一个数据从内存发送到网络中,存在着两次拷贝动作,先是从用户空间拷贝到内核空间,再是从内核空间拷贝到网络 I/O 中。而 NIO 提供的 ByteBuffer 可以使用 Direct Buffers 模式,直接开辟一个非堆物理内存,不需要进行字节缓冲区的二次拷贝,可以直接将数据写入到内核空间。
除了以上这些优化,我们还可以针对套接字编程提供的一些 TCP 参数配置项,提高网络吞吐量,Netty 可以基于 ChannelOption 来设置这些参数。
TCP_NODELAY:TCP_NODELAY 选项是用来控制是否开启 Nagle 算法。Nagle 算法通过缓存的方式将小的数据包组成一个大的数据包,从而避免大量的小数据包发送阻塞网络,提高网络传输的效率。我们可以关闭该算法,优化对于时延敏感的应用场景。
SO_RCVBUF 和 SO_SNDBUF:可以根据场景调整套接字发送缓冲区和接收缓冲区的大小。
SO_BACKLOG:backlog 参数指定了客户端连接请求缓冲队列的大小。服务端处理客户端连接请求是按顺序处理的,所以同一时间只能处理一个客户端连接,当有多个客户端进来的时候,服务端就会将不能处理的客户端连接请求放在队列中等待处理。
SO_KEEPALIVE:当设置该选项以后,连接会检查长时间没有发送数据的客户端的连接状态,检测到客户端断开连接后,服务端将回收该连接。我们可以将该时间设置得短一些,来提高回收连接的效率。

4. 量身定做报文格式

接下来就是实现报文,我们需要设计一套报文,用于描述具体的校验、操作、传输数据等内容。为了提高传输的效率,我们可以根据自己的业务和架构来考虑设计,尽量实现报体小、满足功能、易解析等特性。我们可以参考下面的数据格式:

5. 编码、解码

在 09 讲中,我们分析过序列化编码和解码的过程,对于实现一个好的网络通信协议来说,兼容优秀的序列化框架是非常重要的。如果只是单纯的数据对象传输,我们可以选择性能相对较好的 Protobuf 序列化,有利于提高网络通信的性能。

6. 调整 Linux 的 TCP 参数设置选项

如果 RPC 是基于 TCP 短连接实现的,我们可以通过修改 Linux TCP 配置项来优化网络通信。开始 TCP 配置项的优化之前,我们先来了解下建立 TCP 连接的三次握手和关闭 TCP 连接的四次握手,这样有助后面内容的理解。
三次握手
四次握手
我们可以通过 sysctl -a | grep net.xxx 命令运行查看 Linux 系统默认的的 TCP 参数设置,如果需要修改某项配置,可以通过编辑 vim/etc/sysctl.conf,加入需要修改的配置项, 并通过 sysctl -p 命令运行生效修改后的配置项设置。通常我们会通过修改以下几个配置项来提高网络吞吐量和降低延时。
以上就是我们从不同层次对 RPC 优化的详解,除了最后的 Linux 系统中 TCP 的配置项设置调优,其它的调优更多是从代码编程优化的角度出发,最终实现了一套 RPC 通信框架的优化路径。
弄懂了这些,你就可以根据自己的业务场景去做技术选型了,还能很好地解决过程中出现的一些性能问题。

总结

在现在的分布式系统中,特别是系统走向微服务化的今天,服务间的通信就显得尤为频繁,掌握服务间的通信原理和通信协议优化,是你的一项的必备技能。
在一些并发场景比较多的系统中,我更偏向使用 Dubbo 实现的这一套 RPC 通信协议。Dubbo 协议是建立的单一长连接通信,网络 I/O 为 NIO 非阻塞读写操作,更兼容了 Kryo、FST、Protobuf 等性能出众的序列化框架,在高并发、小对象传输的业务场景中非常实用。
在企业级系统中,业务往往要比普通的互联网产品复杂,服务与服务之间可能不仅仅是数据传输,还有图片以及文件的传输,所以 RPC 的通信协议设计考虑更多是功能性需求,在性能方面不追求极致。其它通信框架在功能性、生态以及易用、易入门等方面更具有优势。

思考题

目前实现 Java RPC 通信的框架有很多,实现 RPC 通信的协议也有很多,除了 Dubbo 协议以外,你还使用过其它 RPC 通信协议吗?通过这讲的学习,你能对比谈谈各自的优缺点了吗?
期待在留言区看到你的见解。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起学习。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 15

提建议

上一篇
09 | 网络通信优化之序列化:避免使用Java序列化
下一篇
11 | 答疑课堂:深入了解NIO的优化实现原理
unpreview
 写留言

精选留言(28)

  • 夏天39度
    2019-06-11
    老师,能说一下Netty是如何实现串行无锁化完成链路操作吗,怎么做到无锁化的线程切换

    作者回复: Netty中,分为Reactor主线程和Reactor从线程,主线程主要用来监听连接事件,从线程主要用来处理监听I/O事件,以及处理读写I/O操作。 一般有其他的业务操作,我们可以在handler中创建线程池来处理。但为了减少上下文切换,我们可以在ChannelPipeline上注册handler事件,例如解码过程。一般Reactor从线程监听到读操作,会立即调用ChannelPipeline的fireChannelRead方法完成读操作,在调用完读操作之后,会检查是否有其他handler,如果有,则直接调用,不会创建新的线程。 这种方式的好处是,不用创建新的线程,在从线程中串行化完成所有的I/O以及业务操作,减少上下文切换;坏处是,给从线程带来了一定的阻塞。

    共 2 条评论
    25
  • WL
    2019-06-11
    请教老师两个问题: 1. 在RMI的实现原理示意图中客户端的存根和服务端的骨架这两个概念是啥意思, 我感觉不太理解. 2. 在TCP的四次挥手中, 客户端最后的TIME_WAIT状态是不是就是CLOSE的状态, 如果不是那TIME_WAIT状态是在啥时候转换成CLOSE状态的.

    作者回复: 我先回答第二个问题。 是的,TIME_WAIT状态就是主动断开方的最后状态了。主动断开连接方之所以是TIME_WAIT状态,是担心被断开方没有收到最后的ACK,这个TIME_WAIT时间内核默认设置是2MSL(报文最大生存时间),被断开方如果超时没有收到ACK,将重新发送FIN,主动断开方收到之后又会重新发送ACK通知,重置TIME_WAIT时间。 正常情况下,当主动断开方的TIME_WAIT状态到达了定时时间后,内核就会关闭该连接。 第一个问题,这块文章中没有过多的介绍,我在这里再叙述下: Stub是client端的远程对象的代理,负责将远程对象上的方法调用转发到实际远程对象实现所在的服务器,我们的程序要通过远程调用,底层一定是套接字的字节传输,要一个对象序列化成为字节码,传输到服务器或者客户端的对端之后,再把该对象反序列化成为对应的对象,Stub承担着底层序列化、数据组装以及协议封装等工作。 Skeleton则是server端的服务对象的代理,负责将接收解析远程调用分派到实际远程对象实现调用。Stub与Skeleton的关系以及操作是对应的关系,只有实现了java.rmi.Remote接口的类或者继承了java.rmi.Remote接口的接口,才能作为Stub与Skeleton之间通信的远程对象,Stub与Skeleton之间的通信使用Java远程消息交换协议JRMP(Java Remote Messaging Protocol)进行通信,JRMP是专为Java的远程对象制定的协议。Stub和Skeleton之间协作完成客户端与服务器之间的方法调用时的通信。

    19
  • Stalary
    2019-06-11
    老师,如果业务架构已经选择了SpringCloud,该如何优化远程调用呢,目前使用Feign,底层配置了HttpClient,发现qps一直上不去,暂时是对频繁的请求做了本地cache,但是需要订阅更新事件进行刷新

    作者回复: 可以尝试扩展其他RPC框架,例如有同学提到的Google的grpc框架,也是基于Netty通信框架实现,基于protobuf实现的序列化。

    共 3 条评论
    14
  • n88
    2019-10-25
    写http是短连接不太严谨

    作者回复: 这里纠正下,http1.0版本默认是短链接,而在http1.0以后默认是保持连接的,但只是一个单向的长连接,默认情况下保持60s

    共 2 条评论
    9
  • 尔冬橙
    2019-09-14
    这个Dubbo阿里可以吹很多年啊
    共 1 条评论
    5
  • 风轻扬
    2019-08-13
    老师。我看到留言中有同学提到http和tcp的对比。http不是建立在tcp的基础上吗?http和tcp的关系应该怎么定义呢?

    作者回复: http是基于tcp实现的协议。如果做过Socket编程通信,你会发现两个端之间如果要实现接口通信,除了传输我们需要的请求参数和返回参数之外,我们还需要给通信定义一个协议头,单纯的tcp通信是没有协议头的,而http则是在tcp基础上定义了自己的消息头和序列化方式。http通信协议是一种短连接,也就是说通信完成之后会断开连接。 简而言之,http是tcp的一个上层封装协议,http是基于tcp实现的。

    共 2 条评论
    5
  • 晓晨同学
    2019-08-07
    有个问题请教一下老师 1.一直不清楚通信协议和序列化协议的区别是什么,两者都是制定报文协议然后传输,感觉序列化协议更具体到业务属性

    作者回复: 通信协议是指我们传输信息的协议,包括头协议和包体,头协议中可能包含传输的id、包体大小、序列化方式等等信息,序列化则表示我们传输的包体的载体是什么样的格式,例如是将对象转成json格式还是转成xml格式,再转成二进制进行传输。

    3
  • nightmare
    2019-06-13
    能不能讲一下netty的串行无锁化

    作者回复: Netty中,分为Reactor主线程和Reactor从线程,主线程主要用来监听连接事件,从线程主要用来处理监听I/O事件,以及处理读写I/O操作。 一般有其他的业务操作,我们可以在handler中创建线程池来处理。但为了减少上下文切换,我们可以在ChannelPipeline上注册handler事件,例如解码过程。一般Reactor从线程监听到读操作,会立即调用ChannelPipeline的fireChannelRead方法完成读操作,在调用完读操作之后,会检查是否有其他handler,如果有,则直接调用,不会创建新的线程。 这种方式的好处是,不用创建新的线程,在从线程中串行化完成所有的I/O以及业务操作,减少上下文切换;坏处是,给从线程带来了一定的阻塞。

    3
  • Y丶z
    2019-06-11
    老师好,我想问下已经在线上跑的服务,序列化方式是hessian,如果直接换成Protobuf,那么consumer会报错吗?如果报错的话,如何避免这种情况发生呢?

    作者回复: 服务端和消费端重启,会走protobuf序列化

    4
  • 张德
    2019-07-07
    还知道JAVA和Python系统之间互相调用的thrift

    作者回复: thrift框架也很优秀

    2
  • 晓杰
    2019-06-11
    请问老师,对于大文件的传输,用哪种协议比较好

    作者回复: 建议使用hessian协议

    3
  • 假装自己不胖
    2019-06-11
    对于网络编程比较迷茫,请问有没有小白一些的读物或博客推荐一下

    作者回复: 这块知识点比较多,建议可以看一些基础书籍,例如Unix网络编程、TCP/IP网络编程,再看看netty实战,就可以进阶Java网络编程了。

    2
  • Sdylan
    2019-10-15
    Spring Clound 的Feigin更多是路由功能,将注解拼接成地址,Spring Cloud主要是整体完备,负载均衡、熔断啥的。
    2
  • 2019-09-07
    课后思考及问题 1:网上常见有关TCP和HTTP的问题,比如: TCP连接的建立详细过程?TCP的连接断开过程? 三次握手建立连接,四次握手断开连接,感觉有些简单啦!如果面试时问到这个问题,老师建议该怎么回答? 另外,还有问一次HTTP请求经过了几次TCP连接,这个如果面试时遇到了,老师又建议该怎么好好的回答?
    展开

    作者回复: 熟悉连接时为什么是三次握手,断开时为什么需要四次握手,以及粘包拆包的问题、解决方案。我理解的一次HTTP请求只有一次TCP连接。

    共 2 条评论
    1
  • 电光火石
    2019-06-11
    1. 老师线上有用过grpc吗,看文档说好像现在还不是特别的稳定? 2. 文中的性能测试,http是否有打开keep alive?走tcp无疑更快,我只想知道用http会慢多少,因为毕竟http更简单。有看过其他的benchmark,在打开keep alive的情况下,性能也还行,不知道老师这个测试是否打开?另外,从测试结果上看,当单次请求数据量很大的时候,http比tcp好像查不了多少是吗? 谢谢!
    展开

    作者回复: grpc目前很多公司在用,在Github中有源码阅读https://github.com/grpc。 对的,没有打开keep alive,如果开启效果会好一些,减少了网络连接。

    1
  • ZeWe
    2021-09-07
    RPC 远程服务调用,是一种通过网络通信调用远程服务的技术。RPC框架底层封装了网络通信,序列化等技术,提供接口包。本地调用接口可以无感透明的访问远程服务,是分布式系统的核心。 如何优化RPC框架,可从网络通信,报文序列化两方面考虑。 网络通信 1)选择合适的通信协议 TCP, UDP等 2)TCP协议下 使用长链接,减少连接资源消耗 3)高效的IO模型 如Netty NIO, 非阻塞io,Reactor线程模型,零拷贝等 4) OS层 TCP参数设置 file-max, keepalive-time等 序列化 1) 简单高效的报文设计 2) 高效序列化框架 编码解码
    展开
    1
  • 惘 闻
    2020-12-28
    老师netty的reactor监听链接事件的主线程,可以认为是IO多路复用里的多路复用器selector吗?
  • 放下
    2020-05-23
    老师你上面Feign的压测结果都是基于HTTP1.0的短链接吗?如果是HTTP1.1压测效果会不会更好一些

    作者回复: 会的,现在都默认使用http1.1版本了,默认是在超时时间内为长连接。

  • greekw
    2019-10-20
    老师,能讲下netty的rector 线程模型的实例,以及串行无锁化设计的原理

    作者回复: 加餐篇中讲到了

  • lizhibo
    2019-09-28
    老师 我想问的 Dubbo怎么更换成Protobuf 系列化协议?是要扩展Dubbo Serialization 接口吗?另外 Kryo 和 Protobuf 哪个性能高点啊

    作者回复: 是的,目前2.7版本已经加入了Protobuf序列化。Kryo和Protobuf 性能非常接近,Kryo序列化后的空间要比Protobuf小一些,但Protobuf序列化与反序列化的时间要优于Kryo。