更多总结mysql、Redis、网络编程、golang、高并发方案
高并发感觉是检验程序员一个很好的标准。经常写业务的人员,如果写的高并发业务,他底层知识一般也不会差,因为遇到过几次高并发下的事故后,排查后也会逐步被动完善底层知识。
例如在高并发下,发现个别服务器可用性降低。排查业务代码后也没有发现任何异常。最后登录异常实例的机器,才发现有特别大量TIME_WAIT的状态。
TIME_WAIT的危害
在高并发短连接的TCP服务器上,当服务器处理完请求后立刻主动正常关闭连接。这个场景下会出现大量socket处于TIME_WAIT状态。
第一是内存资源占用,这个目前看来不是太严重,基本可以忽略。 第二是对端口资源的占用,一个 TCP 连接至少消耗一个本地端口。要知道,端口资源也是有限的,一般可以开启的端口为 32768~61000 ,也可以通过net.ipv4.ip_local_port_range指定,如果 TIME_WAIT 状态过多,会导致无法创建新连接。这个也是我们在一开始讲到的那个例子。
扩展知识
我服务监听的是80端口啊,服务端在主动关闭后,会占用这么多端口呢?不应该只是80这一个端口吗什么会涉及到端口被占满?
端口作用
互联网上各主机间通过TCP/IP协议发送和接收数据包,各个数据包根据其目的主机的ip地址来进行互联网络中的路由选择。ip解决了主机之间的传输。
但是主机上都会支持多进程/线程,那么这个数据包需要发给哪个线程呢?这个就需要端口了。系统会给每个进程/线程分配一个端口号,这样数据包就能根据端口号找到指定的进程/线程。
socket
socket是7层网络协议的传输层,每一个任务(进程/线程),和对端建立传输时,都是通过socket建立的。
socket 上面包含五元组信息
- 源IP
- 源端口
- 目的IP
- 目的端口
- 类型:TCP or UDP
这个五元组,即标识了一条可用的连接。
基于上面知识,可以回答这个问题了。80端口是映射到外网的一个出口端口,当程序监听80端口,收到一个连接时,会创建一个 socket 传输数据包,创建socket是,系统就会分配一个端口号。
TIME_WAIT有什么用
如果TIME_WAIT的危害这么大,那为什么还需要他呢?
这个就需要从TCP的四次挥手说起了。同时我们也聊聊TCP的三次握手。
之前在提到3次握手,也都感觉仅仅是面试时需要准备下的,其实他在网络通信中,还是比较重要的。
首先简单说下,为什么TCP 需要三次握手和四次挥手,因为TCP协议都是建立在默认网络是不好的情况下,他不相信自己发出的信息对方一定能够收到,这个时候,就需要对方回复确认收到的信息,才能安心。
同时TCP面对网络也有很复杂的拥塞控制,防止丢包等等涉及。
先说说和本文相关的四次挥手,这个懂了,建立连接时的三次握手也就更好理解了。
四次挥手
![74fc191c44738f176a9459f47fba92d3.png](https://img-blog.csdnimg.cn/img_convert/74fc191c44738f176a9459f47fba92d3.png)
这个是四次挥手的流程图:
第一次:
主机1发送关闭并且进入ESTABLISHED 状态变为 FIN_WAIT_1
第二次:
主机2收到结束消息,立刻返回结束的响应,并且改变状态ESTABLISHED 状态变为 CLOSE_WAIT。
主机1收到消息,也只是知道2收到消息的,FIN_WAIT_1 改成 FIN_WAIT_2。
第三次:
主机2在收到消息后,不能立刻结束任务,socket是有缓存的,结束的消息不可能实时处理,当主机2知道应用程序通过系统函数read读到socket的EOF后(这个时候,证明服务以及知道要结束了),会发送一个FIN 的消息给主机一,表示可以真的结束了。
并且状态从CLOSE_WAIT 变为 LAST_ACK。
第四次:
主机1收到主机2真正结束的消息后,也会发生一个反馈告知自己收到了,就从FIN_WAIT_2 变成了 TIME_WAIT(我们这次的主角)。
主机2 收到消息后,就能安心的结束自己的这个socket了。
最后:
主机 1 在 TIME_WAIT 停留持续时间是固定的(Linux默认是60秒),是最长分节生命期 MSL(maximum segment lifetime)的两倍,一般称之为 2MSL。过了这个时间之后,主机 1 就进入 CLOSED 状态。
TIME_WAIT能马上进入CLOSED吗?
- 确保最后的 ACK 能让被动关闭方接收
TCP在设计的时候,有充分的容错性,如果报文出错,是需要可以重传的。如果TIME_WAIT马上进入CLOSED,这个时候,发现最后的一个ack传输失败,它就失去了当前状态的上下文,只能回复一个 RST 操作,从而导致被动关闭方出现错误。如果能够保留TIME_WAIT 这个状态,就能正确的发起重试。 - 连接“化身”和报文迷走
为了让旧连接的重复分节在网络中自然消失。在原连接中断后,又重新创建了一个原连接的“化身”,说是化身其实是因为这个连接和原先的连接四元组完全相同,如果迷失报文经过一段时间也到达,那么这个报文会被误认为是连接“化身”的一个 TCP 分节,这样就会对 TCP 通信产生影响。TCP 就设计出了这么一个机制,经过 2MSL 这个时间,足以让两个方向上的分组都被丢弃,使得原来连接的分组在网络中都自然消失,再出现的分组一定都是新化身所产生的。
TIME_WAIT如何优化
优化的方案比较多,常用的是修改系统文件相关的参数。还有就是利用:net.ipv4.tcp_tw_reuse 到达一种可复用状态。
这种优化其实真实情况下,需要多结合实际的业务去处理。
个人比较建议的优化是,尽量都采用长连接的模式,避免短连接。例如golang的原生http包也都是默认支持长连接的。
长连接需要考虑不可控的情况是服务针对外部设备,比如100W的用户设备(浏览器)同时访问服务端,这个时候,确实是需要在考虑长连接维护这个问题的。
不过,针对这种问题,一般的服务架构,不会把业务模块直接对外暴露,会有一个专门的接入层。
例如百度的bfe,这个接入层会维护管理上百万的长连接关系,有专家级别的人会维护,然后和下游服务连接的也是有限的可控的bfe实例。
所以理论上,如果不是专门维护接入层的同学,其实可以暂时忽略长连接太多的情况。
长连接相关可以参考网络编程_HTTP长连接
三次握手
把比较复杂的四次挥手弄懂了,会发现三次握手其实就比较简单了
![08d379a8ef6643348ac4b05a7913e0eb.png](https://img-blog.csdnimg.cn/img_convert/08d379a8ef6643348ac4b05a7913e0eb.png)
在客户端发起三次握手前,服务端会先创建socket,绑定一个具体的ip,port。然后通过listen指令以阻塞的方式,监听绑定端口的响应。类似于告诉系统我这个socket是用来等待用户请求的。
客服端发起请求时,就会开始三次握手:
第一次:
发送一个消息给服务端,表示我要连接了,服务端收到消息后,开始调用accept,准备和客户端开始通信(这个时候,操作系统就和监听的应用程序建立起了连接)
第二次:
服务端阻塞方式调用accept后,回复一个好的,我接受连接了,客户端就知道服务端简历连接成功,进入ESTABLISHED 状态
第三次:
客户端ESTABLISHED后,告诉服务端,我建立好了,我们可以开始通信了。服务端收到后,服务器端协议栈使得 accept 阻塞调用返回,也进入 ESTABLISHED 状态。
扩展
服务器发现很多 CLOSE_WAIT是什么原因
应用程序没有及时响应对端的关闭。一般是程序bug导致,程序收到对端关闭后,没有正常关闭。