高并发架构的TCP知识介绍

作为一个有追求的程序员,不能只知足增删改查,咱们要对系统全方面无死角掌控。掌握了这些基本的网络知识后,相信一方面平常排错中会事半功倍,另外一方面平常架构中不得不考虑的高并发问题,理解了这些底层协议也是会如虎添翼。linux

本文不会单纯给你们讲讲TCP三次握手、四次挥手就完事了。若是只是哪样的话,我直接贴几个链接就完事了。我但愿把实际工做中的不少点可以串起来说给你们。固然为了文章完整,我依然会从 三次握手 起头。nginx

再说TCP状态变动过程

无论是三次握手、仍是四次挥手,他们都是完成了TCP不一样状态的切换。进而影响各类数据的传输状况。下面从三次握手开始分析。程序员

本文图片有部分来自网络,如有侵权,告知即焚面试

三次握手

来看看三次握手的图,估计你们看这图都快看吐了,不过为何每次面试、回忆的时候仍是想不起呢?我再来抄抄这锅剩饭吧! 数组

tcp-1st

首先当服务端处于 listen 状态的时候,咱们就能够再客户端发起监听了,此时客户端会处于 SYN_SENT 状态。服务端收到这个消息会返回一个 SYN 而且同时 ACK 客户端的请求,以后服务端便处于 SYN_RCVD 状态。这个时候客户端收到了服务端的 SYN&ACK,就会发送对服务端的 ACK,以后便处于 ESTABLISHED 状态。服务端收到了对本身的 ACK 后也会处于 ESTABLISHED 状态。bash

常常在面试中可能有人提问:为何握手要3次,不是2次或者4次呢?服务器

首先说4次握手,其实为了保证可靠性,这个握手次数能够一直循环下去;可是这没有一个终止就没有意义了。因此3次,保证了各方消息有来有回就足够了。固然这里可能有一种状况是,客户端发送的 ACK 在网络中被丢了。那怎么办?markdown

  1. 其实大部分时候,咱们链接创建完成就会马上发送数据,因此若是服务端没有收到 ACK 不要紧,当收到数据就会认为链接已经创建;
  2. 若是链接创建后不立马传输数据,那么服务端认为链接没有创建成功会周期性重发 SYN&ACK 直到客户端确认成功。

再说为何2次握手不行呢?2次握手咱们能够想象是没有三次握手最后的 ACK, 在实际中确实会出现客户端发送 ACK 服务端没有收到的状况(上面的状况一),那么这是否说明两次握手也是可行的呢? 看下状况二,2次握手当服务端发送消息后,就认为创建成功,而恰巧此时又没有数据传输。这就会带来一种资源浪费的状况。好比:客户端可能因为延时发送了多个链接状况,当服务端每收到一个请求回复后就认为链接创建成功,可是这其中不少求情都是延时产生的重复链接,浪费了不少宝贵的资源。网络

所以综上所述,从资源节省、效率3次握手都是最合适的。话又回来三次握手的真实意义其实就是协商传输数据用的:序列号与窗口大小多线程

下面咱们经过抓包再来看一下真实的状况是否如上所述。

20:33:26.583598 IP 192.168.0.102.58165 > 103.235.46.39.80: Flags [S], seq 621839080, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 1050275400 ecr 0,sackOK,eol], length 0
20:33:26.660754 IP 103.235.46.39.80 > 192.168.0.102.58165: Flags [S.], seq 1754967387, ack 621839081, win 8192, options [mss 1452,nop,wscale 5,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,sackOK,eol], length 0
20:33:26.660819 IP 192.168.0.102.58165 > 103.235.46.39.80: Flags [.], ack 1754967388, win 4096, length 0
复制代码

抓包: sudo tcpdump -n host www.baidu.com -S

  • S 表示 SYN
  • . 表示 ACK
  • P 表示 传输数据
  • F 表示 FIN

四次挥手

挥手,就是说数据传完了,同志们再见!

tcp-3th

这里有个问题须要注意下,其实客户端、服务端都可以主动发起关闭操做,谁调用 close() 就先发送关闭的请求。固然通常的流程,发起创建链接的一方会主动发起关闭请求(http中)。

关于4次挥手的过程,我就很少解释了,这里有两个重要的状态我须要解释下,这都是我亲自经历过的线上故障,close_waittime_wait

先给你们一个命令,统计tcp的各类状态状况。下面表格内容就来自这个命令的统计。

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

Tcp状态 链接数
CLOSE_WAIT 505
ESTABLISHED 808
TIME_WAIT 3481
SYN_SENT 1
SYN_RECV 1
LAST_ACK 2
FIN_WAIT2 2
FIN_WAIT1 1

大量的CLOSE_WAIT 这个在我以前的一篇文章 线上大量CLOSE_WAIT缘由分析 已经有过介绍,它会致使大量的socket没法释放。而每一个socket都是一个文件,是会占用资源的。这个问题主要是代码问题。它出如今被动关闭的一方(习惯称为server)。

大量的TIME_WAIT 这个问题在平常中常常看到,流量一高就出现大量的该状况。该状态出如今主动发起关闭的一方。该状态通常等待的时间设为 2MSL后自动关闭,MSL是Maximum Segment Lifetime,报文最大生存时间,若是报文超过这个时间,就会被丢弃。处于该状态下的socket也是不能被回收使用的。线上我就遇到这种状况,每次大流量的时候,每台机器处于该状态的socket就多达10w+,远远比处于 Established 状态的socket多的多,致使不少时候服务响应能力降低。这个一方面能够经过调整内核参数处理,另外一方面避免使用太多的短连接,能够采用链接池来提高性能。另外在代码层面多是因为某些地方没有关闭链接致使的,也须要检查业务代码。

上面两个状态必定要牢记发生在哪一方,这方便咱们快速定位问题。

最后这里仍是放上挥手时的抓包数据:

20:33:26.750607 IP 192.168.0.102.58165 > 103.235.46.39.80: Flags [F.], seq 621839159, ack 1754967720, win 4096, length 0
20:33:26.827472 IP 103.235.46.39.80 > 192.168.0.102.58165: Flags [.], ack 621839160, win 776, length 0
20:33:26.827677 IP 103.235.46.39.80 > 192.168.0.102.58165: Flags [F.], seq 1754967720, ack 621839160, win 776, length 0
20:33:26.827729 IP 192.168.0.102.58165 > 103.235.46.39.80: Flags [.], ack 1754967721, win 4096, length 0
复制代码

很少很多,恰好4次。

TCP状态变动

网络上有一张TCP状态机的图,我以为太复杂了,用本身的方式搞个简单点的容易理解的。我从两个角度来讲明状态的变动。

  • 一个是客户端
  • 一个是服务端

看下面两张图的时候,请必定结合上面三次握手、四次挥手的时序图一块儿看,加深理解。

客户端状态变动

tcp-4th

经过这张图,你们是否可以清晰明了的知道 TCP 在客户端上的变动状况了呢?

服务端状态变动

tcp-5th

这一张图描述了 TCP 状态在服务端的变迁。

TCP的流量控制与拥塞控制

咱们常说TCP是面向链接的,UDP是无链接的。那么TCP这个面向链接主要解决的是什么问题呢?

这里继续把三次握手的抓包数据贴出来分析下:

20:33:26.583598 IP 192.168.0.102.58165 > 103.235.46.39.80: Flags [S], seq 621839080, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 1050275400 ecr 0,sackOK,eol], length 0
20:33:26.660754 IP 103.235.46.39.80 > 192.168.0.102.58165: Flags [S.], seq 1754967387, ack 621839081, win 8192, options [mss 1452,nop,wscale 5,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,sackOK,eol], length 0
20:33:26.660819 IP 192.168.0.102.58165 > 103.235.46.39.80: Flags [.], ack 1754967388, win 4096, length 0
复制代码

上面咱们说到 TCP 的三次握手最重要的就是协商传输数据用的序列号。那这个序列号究竟有些什么用呢?这个序号可以帮助后续两端进行确认数据包是否收到,解决顺序、丢包问题;另外咱们还能够看到有一个 win 字段,这是双方交流的窗口大小,这在每次传输数据过程当中也会携带。主要是告诉对方,我窗口是这么大,别发多了或者别发太少。

总结下,TCP的几个特色是:

  • 顺序问题,依靠序号
  • 丢包问题,依靠序号
  • 流量控制,依靠滑动窗口
  • 拥塞控制,依靠拥塞窗口+滑动窗口
  • 链接维护,三次握手/四次挥手

顺序与丢包问题

这个问题其实应该很好理解。因为数据在传输前咱们已经有序号了,这里注意一下这个序号是随机的,重复的几率极地,避免了程序发生乱入的可能性。

因为咱们每一个数据包有序号,虽然发送与到达可能不是顺序的,可是TCP层收到数据后,能够根据序号进行从新排列;另外在这个排列过程当中,发现有了1,2,3,5,6这几个包,一检查就知道4要么延时未到达,要么丢包了,等待重传。

这里须要重要说明的一点是。为了提高效率,TCP其实并非收到一个包就发一个ack。那是如何ACK的呢?仍是以上面为例,TCP收到了1,2,3,5,6这几个包,它可能会发送一个 ack ,seq=3 的确认包,这样次一次确认了3个包。可是它不会发送 5,6 的ack。由于4没有收到啊!一旦4延时到达或者重发到达,就会发送一个 ack, seq=6,又一次确认了3个包。

流量控制与拥塞控制

这两个概念说实话,让我理解了挺长时间,主要是对它们各自控制的内容以及相互之间是否有做用一直没有闹清楚。

先大概说下:

  • 流量控制:是根据接收方的窗口大小来感知我此次可以传多少数据给对方;———— 滑动窗口
  • 拥塞控制:而拥塞控制主要是避免网络拥塞,它考虑的问题更多。根据综合因素来以为发多少数据给对方;———— 滑动窗口&拥塞窗口

举个例子说下,好比:A给B发送数据,经过握手后,A知道B一次能够收1000的数据(B有这么大的处理能力),那么这个时候滑动窗口就能够设置成1000。那是否是最后真的能够一次发这么多数据给B呢?还不是,这时候得问问拥塞窗口,老兄,如今网络状况怎么样?一次运1000的数据有压力吗?拥塞窗口一通计算说不行,如今是高峰期,最多只能有600的货上路。最终此次传数据的时候就是 600 的标注。你们也能够关注抓包数据的 win 值,一直在动态调整。

固然另一种状况是滑动窗口比拥塞窗口小,虽然运输能力强,可是接收能力有限,这时候就要取滑动窗口的值来实际发生。因此它们两者之间是有关系的。

因此具体到每次可以发送多少数据,有这么一个公式:

LastByteSend - LastByteAcked <= min{cwnd,rwnd}

  • LastByteSend 是最后一个发送的字节的序号
  • LastByteAcked 最后一个被确认的字节的序号

这两个相减获得的是本次可以发送的数据,这个数据必定小于或等于 cwnd 与 rwnd 中最小的一个值。相信你们可以理清楚。

那么这部分知识对于实际工做中有什么做用呢?指导意义就是:若是你的业务很重要、很核心必定不要混布;二是若是你的服务忽快忽慢,而确信依赖服务没有问题,检查下机器对应的网络状况;三是窗口这个速度控制机制,在咱们进行服务设计的时候,很是具备参考意义。是否是有点消息队列的感受?(不少消息队列都是匀速的,咱们是否能够加一个窗口的概念来进行优化呢?)

是什么限制了你的链接

到了最关键的地方了,精华我都是留到最后讲。下面放一张网上找的socket操做步骤图,画的太好了我就直接用了。

tcp-6th

咱们假设个人服务端就是 Nginx ,我来尝试解读一下。当客户端调用 connect() 时候就会发起三次握手,此次握手的时候有几个元素惟一肯定了此次通讯(或者说这个socket),[源IP:源Port, 目的IP:目的Port] ,固然这个socket还不是最终用来传输数据的socket,一旦握手完成后,服务端会在返回一个 socket 专门用来后续的数据传输。这里暂且把第一个socket叫 监听socket,第二个叫 传输socket 方便后文叙述。

为何要这么设计呢?你们想想,若是监听的socket还要负责数据的收发,请问这个服务端的效率如何提高?什么东西、谁都往这个socket里边丢,太复杂!

提升链接经常使用套路

到了这一步,咱们如今先停下来算算本身的服务器机器可以有多少链接呢?这个极限又是如何一步步被突破呢?

先说 监听socket ,服务器的prot通常都是固定的,服务器的ip固然也是固定的(单机)。那么上面的结构 [源IP:源Port, 目的IP:目的Port] 其实只有客户端的ip与端口能够发生变化。假设客户端用的是IPv4,那么理论链接数是:2^32(ip数) * 2^16(端口数) = 2^48。

看起来这个值蛮大的。可是真的可以有这么多链接吗?不可能的,由于每个socket都须要消耗内存;以及每个进程的文件描述符是有上限的。这些都限制了最终的链接数。

那么如何进行调和呢?我知道的操做有:多进程、多线程、IO多路服用、协程等手段组合使用。

多进程

也就是监听是一个进程,一旦accept后,对于 传输socket 咱们就fork一个新的子进程来处理。可是这种方式过重,fork一个进程、销毁一个进程都是特别费事的。单机对进程的建立上限也是有限制的。

多线程

线程比进程要轻量级的多,它会共享父进程的不少资源,好比:文件描述符、进程空间,它就是多了一个引用。所以它的建立、销毁更加容易。每个 传输socket 在这里就交给了线程来处理。

可是无论是多进程、仍是多线程都存在一个问题,一个链接对应一个进程或者协程。这都很难逃脱 C10K 的问题。那么该怎么办呢?

IO多路复用

IO多路复用是什么意思呢?在上面单纯的多进程、多线程模型中,一个进程或线程只能处理一个链接。用了IO多路复用后,我一个进程或线程就能处理多个链接。

咱们都知道 Nginx 很是高效,它的结构是:master + worker,worker 会在 80、443端口上来监听请求。它的worker通常设置为 cpu 的cores数,那么这么少的子进程是如何解决超多链接的呢?这里其实每一个worker就采用了 epoll 模型(固然IO多路复用还有个select,这里就不说了)。

处于监听状态的worker,会把全部 监听socket 加入到本身的epoll中。当这些socket都在epoll中时,若是某个socket有事件发生就会当即被回调唤醒(这涉及epoll的红黑树,讲不清楚不细说了)。这种模式,大大增长了每一个进程能够管理的socket数量,上限直接能够上升到进程可以操做的最大文件描述符。

通常机器能够设置百万级别文件描述符,因此单机单进程就是百万链接,epoll是解决C10K的利器,不少开源软件用到了它。

这里说下,并非全部的worker都是同时处于监听端口的状态,这涉及到nginx惊群、抢自旋锁的问题,再也不本文范围内很少说。

关于ulimit

在文章的最后,补充一些单机文件描述符设置的问题。咱们常说链接数受限于文件描述符,这是为何?

由于在linux上一切皆文件,故每个socket都是被看成一个文件看待,那么每一个文件就会有一个文件描述符。在linux中每个进程中都有一个数组保存了该进程须要的全部文件描述符。这个文件描述符其实就是这个数组的 key ,它的 value 是一个指针,指向的就是打开的对应文件。

关于文件描述符有两点注意:

  1. 它对应的实际上是一个linux上的文件
  2. 文件描述符自己这个值在不一样进程中是能够重复的

另外补充一点,单机设置的ulimit的上线受限与系统的两个配置:

fs.nr_open,进程级别

fs.file-max,系统级别

fs.nr_open 老是应该小于等于 fs.file-max,这两个值的设置也不是随意能够操做,由于设置的越大,系统资源消耗越多,因此须要根据真实状况来进行设置。


至此,本篇长文就完结了。这跟上篇 高并发架构的CDN知识介绍 属于一个系列,高并发架构须要理解的网络基础知识。

后面还会写一下 HTTP/HTTPS 的知识。而后关于高并发网络相关的东西就算完结。我会开启下一个篇章。

相关文章
相关标签/搜索