十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
当TIME_WAIT超过linux系统tw数量的阀值(可用数量不会大于65535),系统会把多余的time-wait socket 删除掉,并且显示警告信息,如果是NAT网络环境又存在大量访问,会产生各种连接不稳定断开的情况,从而影响了服务的稳定性。
创新互联建站长期为上1000+客户提供的网站建设服务,团队从业经验10年,关注不同地域、不同群体,并针对不同对象提供差异化的产品和服务;打造开放共赢平台,与合作伙伴共同营造健康的互联网生态环境。为重庆企业提供专业的网站建设、成都网站建设,重庆网站改版等技术服务。拥有十多年丰富建站经验和众多成功案例,为您定制开发。
一、状态的产生
要解决TIME_WAIT状态过多的问题,先来研究下TIME_WAIT状态的产生,下面是TCP连接断开时的四次挥手状态转换图,说明一点,途中显示的是客户端主动断开连接,tcp连接也可以由服务器端主动断开连接。我们先来描述一下断开的状态:
1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2MSL(最长报文段寿命,RFC规定一个MSL为2min,linux中一般设置为30s)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
可以看到TIME_WAIT状态产生是在tcp连接主动关闭的一端产生的正常tcp状态,超过两个MSL之后,就会关闭,释放占用的端口。基于以上的分析我们可以推断,在我们的应用中产生大量TIME_WAIT状态的根本原因是频繁创建断开连接TCP连接。要解决TIME_WATIT状态过多的问题,就要分析我们的应用把频繁创建的短连接改为长连接。
二、常见的短连接产生的场景
1.服务连接服务
后台业务服务器,通常需要调用redis、mysql以及其他http服务和grpc服务,在服务相互调用中,如果使用的是短连接,高并发时就会产生大量TIME_WAIT,如何解决呢?一般情况下,redis等客户端会有连接池,我们要做的是设置好相关的连接服用参数,一般会有连接数、连接重用时间、连接空闲数等。所以在应用中通过设置合理的连接池参数可以避免TIME_WAIT状态过多的问题:
1.检查http连接池
2.检查grpc连接池
3.检查redis连接池
4.检查mysql连接池
...
我们来查看一个mysql连接池配置信息,最大连接数100,最大空闲连接数10,测试的并发数50,产生的效果如下:
可以看到TIME_WAIT状态快速上升,我们查看redis客户端的连接情况:
{MaxOpenConnections:100 OpenConnections:1 InUse:0 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:17 InUse:15 Idle:2 WaitCount:0 WaitDuration:0s MaxIdleClosed:48 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:44 Idle:7 WaitCount:0 WaitDuration:0s MaxIdleClosed:82 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:50 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:90 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:50 InUse:49 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:126 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:49 Idle:2 WaitCount:0 WaitDuration:0s MaxIdleClosed:131 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:50 InUse:49 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:181 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:51 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:233 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:51 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:240 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:46 InUse:38 Idle:8 WaitCount:0 WaitDuration:0s MaxIdleClosed:296 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:50 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:313 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:50 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:363 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:50 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:409 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:50 InUse:48 Idle:2 WaitCount:0 WaitDuration:0s MaxIdleClosed:438 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:49 InUse:49 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:494 MaxLifetimeClosed:0}
分析发现MaxIdleClosed数据持续上升,此为mysql客户端连接池配置不合理产生大量TIME_WAIT状态的例子
2.网络抖动
网络情况不好时,如果主动方无TIME_WAIT等待,关闭前个连接后,主动方与被动方又建立起新的TCP连接,这时被动方重传或延时过来的FIN包过来后会直接影响新的TCP连接。同样网络情况不好并且无TIME_WAIT等待,关闭连接后无新连接,当接收到被动方重传或延迟的FIN包后,会给被动方回一个RST包,可能会影响被动方其它的服务连接。
网络抖动问题比较好排查,直接使用ping命令可以观察到。
1)需要使用protobuf定义接口,即.proto文件
2)然后使用compile工具生成特定语言的执行代码,比如JAVA、C/C++、Python等。类似于thrift,为了解决跨语言问题。
3)启动一个Server端,server端通过侦听指定的port,来等待Client链接请求,通常使用Netty来构建,GRPC内置了Netty的支持。
4)启动一个或者多个Client端,Client也是基于Netty,Client通过与Server建立TCP长链接,并发送请求;Request与Response均被封装成HTTP2的stream Frame,通过Netty Channel进行交互。
对于GRPC的“鼓吹”,本文不多表述,截止到今日,GRPC仍然处于开发阶段,尚没有release版本,而且特性也很多需要补充;GRPC基于protobuf 3.x,但是protobuf 3.x也没有release版本;虽然HTTP2协议已成定局,但尚未被主流web容器包括代理服务器支持,这意味着GRPC在HTTP负载均衡方面尚有欠缺;最终,在短期内我们还不能在production环境中实施,可以做技术储备。不过GRPC的缺点,在将来将会成为它的优点,我们需要时间等待它的成熟。
1)GRPC尚未提供连接池
2)尚未提供“服务发现”、“负载均衡”机制
3)因为基于HTTP2,绝大部多数HTTP Server、Nginx都尚不支持,即Nginx不能将GRPC请求作为HTTP请求来负载均衡,而是作为普通的TCP请求。(nginx将会在1.9版本支持)
4)GRPC尚不成熟,易用性还不是很理想;就本人而言,我还是希望GRPC能够像hessian一样:无IDL文件,无需代码生成,接口通过HTTP表达。
5)Spring容器尚未提供整合。
在实际应用中,GRPC尚未完全提供连接池、服务自动发现、进程内负载均衡等高级特性,需要开发人员额外的封装;最大的问题,就是GRPC生成的接口,调用方式实在是不太便捷(JAVA),最起码与thrift相比还有差距,希望未来能够有所改进。
IDL(proto buffer) + RPC
netty:异步/事件驱动的 网络应用程序服务器框架(高性能)
Http2:流式、双向
protobuf:序列化(节省网络带宽)
IDL定义示例:
Starting from a service definition in a .proto file, gRPC provides protocol buffer compiler plugins that generate client- and server-side code.
Http:如果没有API文档就不知道细节,
GRPC:IDL就相当于API文档,可以同时
开发顺序:
Http:先服务端后客户端
GRPC:可以同时
跨语言:都支持多语言
性能:GRPC远差于Thrift,因为用了Http2,各大server目前支持不完善
易用性:GRPC尚未完全提供连接池、服务自动发现、进程内负载均衡等高级特性,需要开发人员额外的封装;
多语言
Http2特性:如stream
易用性:GRPC尚未完全提供连接池、服务自动发现、进程内负载均衡等高级特性,需要开发人员额外的封装
内网穿透即是使用公网服务器作为代理,转发内网(如办公室、家里)的网络请求使其能够在外网中被访问到。
server端监听两个端口,一个用来和接收用户的http请求,一个监听gRPC客户端,和内网服务器进行通信;
client启动时连接server端;
当User请求server http端口时,将http进行阻塞,并将User请求内容通过gRPC发给client;
client将从server收到的请求发往本地的http服务;
client将从本地程序收到的http response通过gRPC发送给server;
server结束http阻塞,将从client收到的http response发给User。
github地址:
grpc每个流只有一个grpc的数据帧,这个数据帧在传输的时候,会拆成多个http2的数据帧进行传输,然后在接受端,把所有http2的数据帧拼接成grpc的数据帧,再反序列化成请求的结构体。如果一次传输数据过大,在序列化和反序列化的时候,都会占用大量的cpu,不仅仅序列化,单纯的内存复制,也会占用大量cpu,而且,传输的时候,对带宽的影响也是很大的。因此,grpc不推荐一次传输大量数据,如果有大量数据要传输,则使用stream模式。【当然,grpc单次数据传输的大小限制是可以修改的,但是不建议你这么做】【默认最大消息大小为4MB
】
【不断修改增加服务端和客户端消息大小,每次请求不一定需要全部数据,会导致性能上和资源上的浪费】
【grpc协议层是基于http2设计的(但之前看一片测评文章,结构发现文件传输的时候速度有点慢,因为大量数据传输的场景瓶颈在于tcp,如果还在一个tcp上进行多路复用,那只会加剧锁竞争)】
【4 MB 的限制是为了保护没有考虑过消息大小限制的客户端/服务器。 gRPC 本身可以提高很多(100 MB),但大多数应用程序可能会受到轻微攻击或意外内存不足,从而允许该大小的消息。】
【grpc本质是为了提高单连接的利用率,如果单个stream上传输大量的数据,那么其他stream的数据就很难得到及时的传输,grpc适用于大量的请求,但是每次请求的传输数据量不大的情况】
【如果单次传输的数据量过大,建议从新开一个tcp连接,也就是用http1.1,因为在数据量很大的情况下,瓶颈在于底层的tcp】
【同理,在IM系统中,拉消息也建议使用http1.1的接口,避免占用大量的长连带宽,影响下行推送及时性】
【http1.1有维护连接池,每次请求都会独占一个tcp连接,所以,在传输大量数据的场景下,也不会影响到其他请求的数据传输,瓶颈在于机器性能】
grpc 连接池
之前写过了Grpc服务开发和接口测试初探【Java】,中间耽搁了一些时间,Go版本的gRPC测试开发实践才有时间学习使用。其中也是由于自己Go语言不够熟悉导致的。之前有段时间想暂时放弃Go语言的学习,导致了Go的生疏,原因是从Groovy到Java性能。
回归正题,Go语言版本的gRPC实践相对Java来说是比较简单的,但是总体的工具链是比较复杂的,可能是因为Go生态目前相比Java还是比较匮乏吧。下面我先简述一下大致的步骤:
以上步骤亲自操作可能会遇到一些小问题,我本人搜到的教程什么的也是乱七八糟,踩了一些坑。我没有整理出一个亲自实践之后的可行的教程,原因有二:
Go语言的gRPC的 proto 编写跟Java大致一致,只有一个报名的参数不太一样。下面是我的 Hello.proto 内容:
这里主要 go_package 网上搜到的配置方式有些不一样,我没有全都尝试,大家在搜索的资料时候,尽量先看看 syntax 这个参数的值,以及文章教程写作的时间,如果距离现在太久了,我建议直接关掉。搜索引擎有过滤功能,可以过滤掉过时的教程。
这里Go语言gRPC的一点优势,就是在一个项目中即可实现,Java需要先弄一个SDK这样。Go语言的gRPC的代码可以通过生成代码命令中的参数实现指定路径。我是放在了和 proto 文件的同级目录。
服务端代码也是比较格式化的内容,如下:
其中 pb.RegisterHelloServiceServer(s, Ser{}) 如果报错,请检查自己安装的工具 protoc-gen-go 或者 protoc-gen-gofast 版本,一般提取报错 message 搜索也能得到解决办法。
下面是客户端的代码,由于学艺不精,其中大部分参数的含义目前我也不是很清楚,特别是基于 stream 的请求响应的方式使用。后面我先把Java的学完,再回过头来看Go的,按照这个顺序学习和分享。
服务端输出:
忘记打日志了。没有输出
客户端输出:
Go语言的gRPC测试开发实践已经完事儿,大概率上我不会在工作中使用Go作为主力gRPC测试语言,后面测试实践内容还是会以Java为主。