Whys-the-design 笔记

这是笔者在偶然间看到一位大佬写的为什么这么系列文章后,感觉受益颇多,故记录下来与大家共享。

致谢

感谢德莱文大佬的为什么这么设计系列文章专题文章。

这启发了我学习技术应有的视角,感谢他。

正文

首先,我们说TCP是面向连接的,那么什么是连接(connection)?

RFC 793 - Transmission Control Protocol 文档中非常清楚地定义了 TCP 中的连接是什么。

The reliability and flow control mechanisms described above require that TCPs initialize and maintain certain status information for each data stream. The combination of this information, including sockets, sequence numbers, and window sizes, is called a connection.

Request For Comments(RFC),是一系列以编号排定的文件。文件收集了有关互联网相关信息,以及UNIX和互联网社区的软件文件。RFC文件是由Internet Society(ISOC)赞助发行。基本的互联网通信协议都有在RFC文件内详细说明。RFC文件还额外加入许多在标准内的论题,例如对于互联网新开发的协议及发展中所有的记录。因此几乎所有的互联网标准都有收录在RFC文件之中。

上述可靠性和流控制机制要求TCP初始化并维护每个数据流的某些状态信息。 此信息的组合,包括套接字,序列号和窗口大小,称为连接。

所以,TCP三次握手是为了在连接三元素上达成共识。

RFC 793 - Transmission Control Protocol 其实就指出了 TCP 连接使用三次握手的首要原因 —— 为了阻止历史的重复连接初始化造成的混乱问题,防止使用 TCP 协议通信的双方建立了错误的连接。

The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.

在网络情况差的情况下,发送方会多次请求接收方,此时,接收方无法保证接收到的握手包是最新的。而三次握手机制使用ack=seq+1的方式,把检查历史连接的任务转交给发送方,若为历史连接则发送RST终止连接。

TCP 最初的实现 Tahoe 和 Reno 就使用了慢启动拥塞避免两个机制实现拥塞控制。

拥塞控制流程:

  1. 在 TCP 三次握手期间,通信双方会通过 ACK 消息通知对方自己的窗口大小。

  2. 当拥塞窗口大小小于慢启动阈值慢启动阈值(Slow start threshold, ssthresh)时,使用慢启动算法。

  3. 每当有1个报文段被确认,拥塞窗口就增大1。

  4. 当拥塞窗口大小大于慢启动阈值时,使用拥塞避免算法。

  5. 每当一整个“窗口”(1个RTT)中的报文段都被确认后,拥塞窗口大小才增加1。

  6. 超时重传计时器超时,ssthresh设为当前的窗口一半,进入慢启动算法。

  7. 收到3个重复的ACK(快重传机制),ssthresh设为当前的窗口一半,重传丢失的数据报文段,进入拥塞避免算法。

快重传机制解决的问题

TCP 中的 ACK 消息表示该消息之前的全部消息都已经被成功接收和处理,但是假设在1-10号数据中丢失了1号时,接收端无法发送ACK,由于发送方没有收到 ACK,所有数据段对应的计时器就会超时并重新传输数据,会造成大量的带宽浪费。

而快速重传机制使得当接收端收到比期望序号大的报文段时,便会重复发送最近一次确认的报文段的确认信号,我们称之为冗余ACK(duplicate ACK)。

发送方接收到3个duplicate ACK,便会立即重新发送丢失的包。

IP 协议是 TCP 和 UDP 的底层协议,使用 IP 协议时,不同设备之间传输数据前,需要先确定一个 IP 数据片的大小上限,即最大传输单元(Maximum transmission unit,即 MTU),MTU 是 IP 数据片能够传输的数据上限。

以太网对数据帧的限制一般都是 1500 字节,在一般情况下,IP 主机的路径 MTU 都是 1500 字节,去掉 IP 首部的 20 字节,如果待传输的数据大于 1480 节,那么该 IP 协议就会将数据包分片传输

IP 协议的 MTU 是物理设备上的限制,它限制了路径能够发送数据包的上限,而 TCP 协议的 MSS 是操作系统内核层面的限制,通信双方会在三次握手时确定这次连接的 MSS。一旦确定了 MSS,TCP 协议就会对应用层交给 TCP 协议发送的数据进行分包,构成多个数据段。

TCP 协议为了保证可靠性,会通过 IP 协议的 MTU 计算出 MSS 并根据 MSS 分段避免 IP 协议对数据包进行分片。因为 IP 协议对数据包的分片对上层是透明的,如果协议不根据 MTU 做一些限制,那么 IP 协议的分片会导致部分数据包失去传输层协议头,一旦数据包发生丢失就只能丢弃全部数据。

IP 协议拆分数据是因为物理设备的限制,通过 ICMP 协议可以得到 MTU最大传输单元(Maximum transmission unit),以太网对数据帧的限制一般都是 1500 字节,在一般情况下,IP 主机的路径 MTU 都是 1500,去掉 IP 首部的 20 字节,如果待传输的数据大于 1480 节,那么该 IP 协议就会将数据包分片传输。

TCP 协议拆分数据是因为操作系统内核层面的限制,通信双方会在三次握手时确定这次连接的 MSS 最大分段大小(Maximum segment size),在正常情况下,TCP 连接的 MSS 是 MTU - 40 字节,即 1460 字节;不过如果通信双方没有指定 MSS 的话,在默认情况下 MSS 的大小是 536 字节。

MSS 是通过 MTU 以及 TCP 首部、IP 首部计算的,目的是避免 TCP 协议包在 IP 协议层面为碎片 TCP 包添加 IP 头而浪费资源。

RFC 6691

The rest of this document just expounds on that statement, and the goal is to avoid IP-level fragmentation of TCP packets.

拓展:为什么说TCP是面向字节流的?

我们都知道,传输层的两大协议 TCP 和 UDP 分别是面向字节流和面向报文段的。因为 TCP 在传输时会维护一个缓冲区,用于储存待发送的的字节数据,而 TCP 在网络上下层的关系在于保证有序且可靠地抵达目的主机,那么 TCP 看起来就像是一个字节发送器,上层传输字节过来,TCP 从缓存区选择字节并拼接报文头并交由下层网络层,这种字节缓缓流入流出的传输机制就像水流一样,故称为字节流。

而 UDP 并没有维护一个缓冲区,而是直接从应用层拿取报文,并加上传输层的报文头,直接交给网络层,这样看来 UDP 就是面向报文段的。

而在 IP 层传输 UDP 协议报文段的时候,会出现 UDP报文段长度大于 MTU 情况,那么此时 IP 就需要进行分片处理,使用 IP 协议头部的位偏移量进行分片传输以及重组。

粘包问题指的是应用层的不同边界的数据出现在了同一个 TCP 包中,此时需要对 TCP 包进行拆分才能读取每个边界所需要的数据。

出现这样的问题源于 TCP 是面向字节流传输的,并不会对上层的数据进行自动分包。而需要在应用层协议中,定义消息的边界,而最常见的两种解决方案就是基于长度以及基于终结符(Delimiter)。

使用B+树的是InnoDB和MyISAM。

哈希的CRUD性能为O(1),但是使用哈希无法完成高效的范围查询以及排序。

B树拥有和B+树差不多的结构,并且在做查询时无需每次都扫描到叶子结点。但是,在非叶子结点中储存数据会使得节点占用内存大于只存储索引,导致B树需要更多的磁盘读写消耗。

在内存数据未被命中时,CPU会将目标数据从磁盘读取到内存中,而磁盘需要寻道、旋转、传输等大量的时间资源消耗。

作为非关系型的数据库,MongoDB 对于遍历数据的需求没有关系型数据库那么强,它追求的是读写单个记录的性能。

命中索引的查询会受益于 MySQL B+ 树相互连接的叶子节点,因为它能减少磁盘的随机 IO 次数。

而MongoDB 作为非关系型的数据库,它从集合的设计上就使用了完全不同的方法,MongoDB 中推荐的做法其实是使用嵌入文档,存储对应的数据,这样就不会存在需要遍历的情况。

那么不使用哈希的做法是因为哈希这种性能表现较为极端的数据结构往往只能在简单、极端的场景下使用。

引用

本作品采用知识共享署名 4.0 国际许可协议(CC BY-NC-SA 4.0)进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,可适当缩放并在引用处附上图片所在的文章链接。

最后更新于