【云计算基础技术提高班之一】体系化认识RPC

本文主体源自体系化认识RPC,文章整体写得不错,本文稍作细化,同时更正了部分不实之处。

RPC技术在分布式系统中有广泛的使用,因而在云计算平台中也是经常使用的技术,本文体系性地介绍了 RPC 包含的核心概念和技术,希望读者读完文章,一提到 RPC,脑中不是零碎的知识,而是具体的一个脑图般的体系。本文并不会深入到每一个主题剖析,只做提纲挈领的介绍。

RPC 最核心要解决的问题就是在分布式系统间,如何执行另外一个地址空间上的函数、方法,就仿佛在本地调用一样,个人总结的 RPC 最核心的概念和技术包括如下,如图所示:

这里写图片描述

下面依次展开每个部分。

传输(Transport)

TCP 协议是 RPC 的 基石,一般来说通信是建立在 TCP 协议之上的,而且 RPC 往往需要可靠的通信,因此不采用 UDP。

这里重申下 TCP 的关键词:面向连接的,全双工,可靠传输(保序、不重、不丢、容错),流量控制(滑动窗口),拥塞控制(拥塞窗口)。

另外,要理解 RPC 中的嵌套 header+body,协议栈每一层都包含了下一层协议的全部数据,只不过包了一个头而已,如下图所示的 TCP segment 包含了应用层的数据,套了一个头而已。

TCP头格式

这里写图片描述

通过上图TCP头的格式,你需要注意这么几点:

  • TCP的包是没有IP地址的,那是IP层上的事。但是有源端口和目标端口。
  • 一个TCP连接需要四个元组来表示是同一个连接(src_ip, src_port, dst_ip, dst_port)准确说是五元组,还有一个是协议。但因为这里只是说TCP协议,所以,这里我只说四元组。
  • 注意上图中的四个非常重要的东西:
    • Sequence Number是包的序号,用来解决网络包乱序(reordering)问题。
    • Acknowledgement Number就是ACK——用于确认收到,用来解决不丢包的问题。
    • Window又叫Advertised-Window,也就是著名的滑动窗口(Sliding Window),用于解决流控的。
    • TCP Flag ,也就是包的类型,主要是用于操控TCP的状态机的。
TCP的状态机(面向连接与全双工)

其实,网络上的传输是没有连接的,包括TCP也是一样的。而TCP所谓的“连接”,其实只不过是在通讯的双方维护一个“连接状态”,让它看上去好像有连接一样。所以,TCP的状态变换是非常重要的。

下面是:“TCP协议的状态机”(图片来源) 和 “TCP建链接”、“TCP断链接”、“传数据” 的对照图,我把两个图并排放在一起,这样方便在你对照着看。另外,下面这两个图非常非常的重要,你一定要记牢。(吐个槽:看到这样复杂的状态机,就知道这个协议有多复杂,复杂的东西总是有很多坑爹的事情,所以TCP协议其实也挺坑爹的)

这里写图片描述

这里写图片描述

很多人会问,为什么建链接要3次握手,断链接需要4次挥手?

  • 对于建链接的3次握手,主要是要初始化Sequence Number 的初始值。通信的双方要互相通知对方自己的初始化的Sequence Number(缩写为ISN:Inital Sequence Number)——所以叫SYN,全称Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。

  • 对于4次挥手,其实你仔细看是2次,因为TCP是全双工的,所以,发送方和接收方都需要Fin和Ack。只不过,有一方是被动的,所以看上去就成了所谓的4次挥手。如果两边同时断连接,那就会就进入到CLOSING状态,然后到达TIME_WAIT状态。下图是双方同时断连接的示意图(你同样可以对照着TCP状态机看):

这里写图片描述

这里有个问题:如果对端宕机,TCP另一端能否及时感知?
答案是不会,原因是TCP是一种有连接的协议,但是这个连接并不是指有一条实际的电路,而是一种虚拟的电路。TCP的建立连接和断开连接都是通过发送数据实现的,也就是我们常说的三次握手、四次挥手。TCP两端保存了一种数据的状态,就代表这种连接,TCP两端之间的路由设备只是将数据转发到目的地,并不知道这些数据实际代表了什么含义,也并没有在其中保存任何的状态信息,也就是说中间的路由设备没有什么连接的概念,只是将数据转发到目的地,只有数据的发送者和接受者两端真正的知道传输的数据代表着一条连接。

如何能及时感知到TCP对端掉线呢?保持连接并不是毫无代价的,如果这种异常断开的连接有很多,那么势必会耗费大量的资源,必须要想办法检测出这种异常连接。

检测的方法很简单,只要让B端主动通过这个连接向A端继续发送数据即可。上文说过,A端异常断开后,和A端直接连接的路由器是知道的。当B端发送的数据经过转发后到达这个路由器后,必然最终会返回B端一个目的不可达。此时B端立刻就会知道这条连接其实已经异常断开了。

但是B端不可能知道什么时候会出现这种异常,所以B端必须定时发送数据来检测连接是否异常断开。数据的内容无关紧要,任何数据都能达到这个效果。这个数据就是我们经常在TCP编程中所说的心跳。

TCP协议本身就提供了一种这样的机制来探测对端的存活。TCP协议有一个KEEP_LIVE开关,只要打开这个开关就会定时发送一些数据长度为零的探测心跳包,发送的频率和次数都可以设置,具体的方法在网上搜索tcp keepalive即可,网上有很多文章,这里不再赘述。

除了使用TCP协议本身的保活开关机制,还可以在应用层主动发送心跳数据包,那么在应用层主动发送心跳数据包的方式和TCP协议本身的保活机制有什么区别呢?

  • 应用层的心跳数据包会耗费更多的带宽,因为TCP协议的保活机制发送的是数据长度为零心跳包,而应用层的心跳数据包长度则必然会大于0。

  • 应用层的心跳数据包可以带一些应用所需要的数据,随应用自己控制,而TCP协议的保活机制则是对于应用层透明的,无法利用心跳携带数据。

数据传输中的Sequence Number(保序)

下图是Wireshark的截图:
这里写图片描述

你可以看到,SeqNum的增加是和传输的字节数相关的。上图中,三次握手后,来了两个Len:1440的包,而第二个包的SeqNum就成了1441。然后第一个ACK回的是1441,表示第一个1440收到了。

注意:如果你用Wireshark抓包程序看3次握手,你会发现SeqNum总是为0,不是这样的,Wireshark为了显示更友好,使用了Relative SeqNum——相对序号,你只要在右键菜单中的protocol preference 中取消掉就可以看到“Absolute SeqNum”了。

TCP重传机制(可靠性)

TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。

注意,接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到),此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。

TCP滑动窗口(流量控制)

需要说明一下,如果你不了解TCP的滑动窗口这个事,你等于不了解TCP协议。我们都知道,TCP必需要解决的可靠传输以及包乱序(reordering)的问题,所以,TCP必需要知道网络实际的数据处理带宽或是数据处理速度,这样才不会引起网络拥塞,导致丢包。

所以,TCP引入了一些技术和设计来做网络流控,Sliding Window是其中一个技术。 前面我们说过,TCP头里有一个字段叫Window,又叫Advertised-Window,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。 为了说明滑动窗口,我们需要先看一下TCP缓冲区的一些数据结构:

这里写图片描述

上图中,我们可以看到:

  • 接收端LastByteRead指向了TCP缓冲区中读到的位置,NextByteExpected指向的地方是收到的连续包的最后一个位置,LastByteRcved指向的是收到的包的最后一个位置,我们可以看到中间有些数据还没有到达,所以有数据空白区。

  • 发送端的LastByteAcked指向了被接收端Ack过的位置(表示成功发送确认),LastByteSent表示发出去了,但还没有收到成功确认的Ack,LastByteWritten指向的是上层应用正在写的地方。

于是:

  • 接收端在给发送端回ACK中会汇报自己的AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;

  • 而发送方会根据这个窗口来控制发送数据的大小,以保证接收方可以处理。

下面我们来看一下发送方的滑动窗口示意图:
这里写图片描述

上图中分成了四个部分,分别是:(其中那个黑模型就是滑动窗口)

  • 1已收到ack确认的数据。
  • 2发还没收到ack的。
  • 3在窗口中还没有发出的(接收方还有空间)。
  • 4窗口以外的数据(接收方没空间)

下面是个滑动后的示意图(收到36的ack,并发出了46-51的字节):
这里写图片描述

下面我们来看一个接受端控制发送端的图示:

这里写图片描述

Zero Window

上图,我们可以看到一个处理缓慢的Server(接收端)是怎么把Client(发送端)的TCP Sliding Window给降成0的。此时,你一定会问,如果Window变成0了,TCP会怎么样?是不是发送端就不发数据了?是的,发送端就不发数据了,你可以想像成“Window Closed”,那你一定还会问,如果发送端不发数据了,接收方一会儿Window size 可用了,怎么通知发送端呢?

解决这个问题,TCP使用了Zero Window Probe技术,缩写为ZWP,也就是说,发送端在窗口变成0后,会发ZWP的包给接收方,让接收方来ack他的Window尺寸,一般这个值会设置成3次,第次大约30-60秒(不同的实现可能会不一样)。如果3次过后还是0的话,有的TCP实现就会发RST把链接断了。

TCP的拥塞处理 – Congestion Handling

上面我们知道了,TCP通过Sliding Window来做流控(Flow Control),但是TCP觉得这还不够,因为Sliding Window需要依赖于连接的发送端和接收端,其并不知道网络中间发生了什么。TCP的设计者觉得,一个伟大而牛逼的协议仅仅做到流控并不够,因为流控只是网络模型4层以上的事,TCP的还应该更聪明地知道整个网络上的事。

具体一点,我们知道TCP通过一个timer采样了RTT并计算RTO,但是,如果网络上的延时突然增加,那么,TCP对这个事做出的应对只有重传数据,但是,重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,于是,这个情况就会进入恶性循环被不断地放大。试想一下,如果一个网络内有成千上万的TCP连接都这么行事,那么马上就会形成“网络风暴”,TCP这个协议就会拖垮整个网络。这是一个灾难。

所以,TCP不能忽略网络上发生的事情,而无脑地一个劲地重发数据,对网络造成更大的伤害。对此TCP的设计理念是:TCP不是一个自私的协议,当拥塞发生的时候,要做自我牺牲。就像交通阻塞一样,每个车都应该把路让出来,而不要再去抢路了。

关于拥塞控制的论文请参看《Congestion Avoidance and Control》(PDF)

拥塞控制主要是四个算法:1)慢启动,2)拥塞避免,3)拥塞发生,4)快速恢复。这四个算法不是一天都搞出来的,这个四算法的发展经历了很多时间,到今天都还在优化中。 备注:

1988年,TCP-Tahoe 提出了1)慢启动,2)拥塞避免,3)拥塞发生时的快速重传
1990年,TCP Reno 在Tahoe的基础上增加了4)快速恢复

I/O 模型(I/O Model)

做一个高性能 /scalable 的 RPC,需要能够满足:

  • 第一,服务端尽可能多的处理并发请求
  • 第二,同时尽可能短的处理完毕。

CPU 和 I/O 之间天然存在着差异,网络传输的延时不可控,最简单的模型下,如果有线程或者进程在调用 I/O,I/O 没响应时,CPU 只能选择挂起,线程或者进程也被 I/O 阻塞住。

而 CPU 资源宝贵,要让 CPU 在该忙碌的时候尽量忙碌起来,而不需要频繁地挂起、唤醒做切换,同时很多宝贵的线程和进程占用系统资源也在做无用功。

Socket I/O 可以看做是二者之间的桥梁,如何更好地协调二者,去满足前面说的两点要求,有一些模式(pattern)是可以应用的。

RPC 框架可选择的 I/O 模型严格意义上有 5 种,这里不讨论基于 信号驱动 的 I/O(Signal Driven I/O)。这几种模型在《UNIX 网络编程》中就有提到了,它们分别是:

  • 传统的阻塞 I/O(Blocking I/O)
  • 非阻塞 I/O(Non-blocking I/O)
  • I/O 多路复用(I/O multiplexing)
  • 异步 I/O(Asynchronous I/O)

这里不细说每种 I/O 模型。这里举一个形象的例子,读者就可以领会这四种 I/O 的区别,就用 银行办业务 这个生活的场景描述。

下图是使用 传统的阻塞 I/O 模型。一个柜员服务所有客户,可见当客户填写单据的时候也就是发生网络 I/O 的时候,柜员,也就是宝贵的线程或者进程就会被阻塞,白白浪费了 CPU 资源,无法服务后面的请求。

这里写图片描述

下图是上一个的进化版,如果一个柜员不够,那么就 并发处理,对应采用线程池或者多进程方案,一个客户对应一个柜员,这明显加大了并发度,在并发不高的情况下性能够用,但是仍然存在柜员被 I/O 阻塞的可能。

这里写图片描述

下图是 I/O 多路复用,存在一个大堂经理,相当于代理,它来负责所有的客户,只有当客户写好单据后,才把客户分配一个柜员处理,可以想象柜员不用阻塞在 I/O 读写上,这样柜员效率会非常高,这也就是 I/O 多路复用的精髓。
这里写图片描述

下图是 异步 I/O,完全不存在大堂经理,银行有一个天然的“高级的分配机器”,柜员注册自己负责的业务类型,例如 I/O 可读,那么由这个“高级的机器”负责 I/O 读,当可读时候,通过 回调机制,把客户已经填写完毕的单据主动交给柜员,回调其函数完成操作。
这里写图片描述

重点说下高性能,并且工业界普遍使用的方案,也就是后两种。
I/O 多路复用

基于内核,建立在 epoll 或者 kqueue 上实现,I/O 多路复用最大的优势是用户可以在一个线程内同时处理多个 Socket 的 I/O 请求。用户可以订阅事件,包括文件描述符或者 I/O 可读、可写、可连接事件等。
通过一个线程监听全部的 TCP 连接,有任何事件发生就通知用户态处理即可,这么做的目的就是 假设 I/O 是慢的,CPU 是快的,那么要让用户态尽可能的忙碌起来去,也就是最大化 CPU 利用率,避免传统的 I/O 阻塞。

异步 I/O
这里重点说下同步 I/O 和异步 I/O,理论上前三种模型都叫做同步 I/O,同步是指用户线程发起 I/O 请求后需要等待或者轮询内核 I/O 完成后再继续,而异步是指用户线程发起 I/O 请求直接退出,当内核 I/O 操作完成后会通知用户线程来调用其回调函数。

进程 / 线程模型(Thread/Process Model)

进程 / 线程模型往往和 I/O 模型有联系,当 Socket I/O 可以很高效的工作时候,真正的业务逻辑如何利用 CPU 更快地处理请求,也是有 pattern 可寻的。这里主要说 Scalable I/O 一般是如何做的,它的 I/O 需要经历 5 个环节:

Read -> Decode -> Compute -> Encode -> Send

使用传统的阻塞 I/O + 线程池的方案(Multitasks)会遇 C10k问题。

https://en.wikipedia.org/wiki/C10k_problem

但是业界有很多实现都是这个方式,比如 Java web 容器 Tomcat/Jetty 的默认配置就采用这个方案,可以工作得很好。

但是从 I/O 模型可以看出 I/O Blocking is killer to performance,它会让工作线程卡在 I/O 上,而一个系统内部可使用的线程数量是有限的(本文暂时不谈协程、纤程的概念),所以才有了 I/O 多路复用和异步 I/O。

I/O 多路复用往往对应 Reactor 模式,异步 I/O 往往对应 Proactor。

Reactor 一般使用 epoll+ 事件驱动 的经典模式,通过 分治 的手段,把耗时的网络连接、安全认证、编码等工作交给专门的线程池或者进程去完成,然后再去调用真正的核心业务逻辑层,这在 *nix 系统中被广泛使用。

著名的 Redis、Nginx、Node.js 的 Socket I/O 都用的这个,而 Java 的 NIO 框架 Netty 也是,Spark 2.0 RPC 所依赖的同样采用了 Reactor 模式。

Schema 和序列化(Schema & Data Serialization)

当 I/O 完成后,数据可以由程序处理,那么如何识别这些二进制的数据,是下一步要做的。序列化和反序列化,是做对象到二进制数据的转换,程序是可以理解对象的,对象一般含有 schema 或者结构,基于这些语义来做特定的业务逻辑处理。

考察一个序列化框架一般会关注以下几点:

  • Encoding format。是 human readable 还是 binary。
  • Schema declaration。也叫作契约声明,基于 IDL,比如 Protocol Buffers/Thrift,还是自描述的,比如 JSON、XML。另外还需要看是否是强类型的。
  • 语言平台的中立性。比如 Java 的 Native Serialization 就只能自己玩,而 Protocol Buffers 可以跨各种语言和平台。
  • 新老契约的兼容性。比如 IDL 加了一个字段,老数据是否还可以反序列化成功。
  • 和压缩算法的契合度。跑 benchmark 和实际应用都会结合各种压缩算法,例如 gzip、snappy。
  • 性能。这是最重要的,序列化、反序列化的时间,序列化后数据的字节大小是考察重点。

序列化方式非常多,常见的有 Protocol Buffers, Avro,Thrift,XML,JSON,MessagePack,Kyro,Hessian,Protostuff,Java Native Serialize,FST。

协议结构(Wire Protocol)

Socket 范畴里讨论的包叫做 Frame、Packet、Segment 都没错,但是一般把这些分别映射为数据链路层、IP 层和 TCP 层的数据包,应用层的暂时没有,所以下文不必计较包怎么翻译。

协议结构,英文叫做 wire protocol 或者 wire format。TCP 只是 binary stream 通道,是 binary 数据的可靠搬用工,它不懂 RPC 里面包装的是什么。而在一个通道上传输 message,势必涉及 message 的识别。

举个例子,正如下图中的例子,ABC+DEF+GHI 分 3 个 message,也就是分 3 个 Frame 发送出去,而接收端分四次收到 4 个 Frame。
这里写图片描述

Socket I/O 的工作完成得很好,可靠地传输过去,这是 TCP 协议保证的,但是接收到的是 4 个 Frame,不是原本发送的 3 个 message 对应的 3 个 Frame。

这种情况叫做发生了 TCP 粘包和半包 现象,AB、H、I 的情况叫做半包,CDEFG 的情况叫做粘包。虽然顺序是对的,但是分组完全和之前对应不上。

这时候应用层如何做语义级别的 message 识别是个问题,只有做好了协议的结构,才能把一整个数据片段做序列化或者反序列化处理。

一般采用的方式有三种:

方式 1:分隔符。

方式 2:换行符。比如 memcache 由客户端发送的命令使用的是文本行\r\n 做为 mesage 的分隔符,组织成一个有意义的 message。

这里写图片描述

方式 3:固定长度。RPC 经常采用这种方式,使用 header+payload 的方式。
比如 HTTP 协议,建立在 TCP 之上最广泛使用的 RPC,HTTP 头中肯定有一个 body length 告知应用层如何去读懂一个 message,做 HTTP 包的识别。

这里写图片描述

可靠性(Reliability)

RPC 框架不光要处理 Network I/O、序列化、协议栈。还有很多不确定性问题要处理,这里的不确定性就是由 网络的不可靠 带来的麻烦。

例如如何保持长连接心跳?网络闪断怎么办?重连、重传?连接超时?这些都非常的细碎和麻烦,所以说开发好一个稳定的 RPC 类库是一个非常系统和细心的工程。

但是好在工业界有一群人就致力于提供平台似的解决方案,例如 Java 中的 Netty,它是一个强大的异步、事件驱动的网络 I/O 库,使用 I/O 多路复用的模型,做好了上述的麻烦处理。

它是面向对象设计模式的集大成者,使用方只需要会使用 Netty 的各种类,进行扩展、组合、插拔,就可以完成一个高性能、可靠的 RPC 框架。

著名的 gRPC Java 版本、Twitter 的 Finagle 框架、阿里巴巴的 Dubbo、新浪微博的 Motan、Spark 2.0 RPC 的网络层(可以参考 kraps-rpc:https://github.com/neoremind/kraps-rpc)都采用了这个类库。

易用性(Ease of use)

RPC 是需要让上层写业务逻辑来实现功能的,如何优雅地启停一个 server,注入 endpoint,客户端怎么连,重试调用,超时控制,同步异步调用,SDK 是否需要交换等等,都决定了基于 RPC 构建服务,甚至 SOA 的工程效率与生产力高低。这里不做展开,看各种 RPC 的文档就知道他们的易用性如何了。

工业界的 RPC 框架一览

国内
Dubbo。来自阿里巴巴 http://dubbo.I/O/
Motan。新浪微博自用 https://github.com/weibocom/motan
Dubbox。当当基于 dubbo 的 https://github.com/dangdangdotcom/dubbox
rpcx。基于 Golang 的 https://github.com/smallnest/rpcx
Navi & Navi-pbrpc。作者开源的 https://github.com/neoremind/navi https://github.com/neoremind/navi-pbrpc
国外
Thrift from facebook https://thrift.apache.org
Avro from hadoop https://avro.apache.org
Finagle by twitter https://twitter.github.I/O/finagle
gRPC by Google http://www.grpc.I/O (Google inside use Stuppy)
Hessian from cuacho http://hessian.caucho.com
Coral Service inside amazon (not open sourced)

参考文献:
https://mp.weixin.qq.com/s?__biz=MzIwMzg1ODcwMw==&mid=2247486833&idx=1&sn=2e44bf6c3bc1f6aadeba9bfb96e34620&chksm=96c9bb11a1be32079f4d09d4b3077e45d429ddd56a7308bf2200a8586d93e32eced25ec89771&mpshare=1&scene=23&srcid=091436xLR5hyteWkuqMeQ8hn#rd

https://coolshell.cn/articles/11564.html

https://coolshell.cn/articles/11609.html

http://www.infoq.com/cn/articles/netty-reliability/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值