加载中…
正文 字体大小:

【负载均衡技术】TCP复用所产生的故障解析 --- 一个非常实用的案例

(2011-08-29 16:46:49)
标签:

tcp

tcp端口复用

time_out

负载均衡

分类: 互联网运维技术

  下面的这个案例是我来本单位后,解决的第一个比较有难度的技术问题,该问题在我来之前已经困扰公司的系统部足足8个月,包括外包公司的几位工程师也是一直摸不到头绪。总算我过来之后,和这边的一位日本工程师一起把问题追查明白了。在这里简单的分享给各位同学,我想类似的问题在大型网站的维护工作里会经常碰到,希望能起到抛砖引玉的作用。

 

  系统构架: 系统采用A10的AX2500进行典型的负载均衡构架,在均衡设备下面下接多台真实WEB服务器,系统采用的TCP模式的SNAT服务构架。(非http代理模式)

  问题现象:当发生超大量的HTTP访问的时候,大量的TCP初始连接出现了3秒左右的延迟,大大影响了用户访问体验

 

  分析过程:

      1 修改配置为HTTP PROXY模式的后,发现3S延迟现象大量减少。

     抓包分析的主要TCP FLOW如下记载

     [Service-Type tcp 发生3秒延迟现象]

Client            LB               Server
|------ syn1 ----->|                  |
                 |------ syn1 ----->|
                 |<----- ack -------| *为什么这里回复的是ACK?  
|<----- ack -------|                  |
|------ rst ------>|                  |
                 |------ rst ------>|
                                 |
                                 |
|------ syn2 ----->|                  | * 此处发生3秒延迟 
                 |------ syn2 ----->|

 
[Service-Type http 未发生3秒延迟现象]

Client            LB                Server
|------ syn1 ----->|                  |
|<--- syn_ack -----|                  |
|------ ack ------>|                  |
|------ GET ------>|                  |
                 |------ syn1 ----->|
                 |<------ ack ------| *为什么这里回复的是ACK? 
                 |------ rst ------>|
                 |------ syn2 ----->| *1ms以内重传syn
                 |<--- syn_ack -----|
                 |------ ack ------>|
                 |------ GET ------>|


由上面的两个TCP FLOW可以看出,

  1 本次问题的根本在于,服务器对syn包直接回复ack,导致客户端发送RST后,第二次重新开始新的syn连接(无论LB采用何种工作模式);

  2 大量tcp访问的时候,LB端由于地址池不够大,产生了大量的TCP端口重用现象;

  3 另外可以看出,客户端发送RST3秒之后重发SYN包,这是符合实际的tcp syn重传实现机制的,(参考:http://www.360doc.com/content/11/0120/23/1964482_87981914.shtml),A10的LB设备实际安装则是RST发送之后立刻发送(几乎)SYN包,并不遵守tcp的普遍实现机制(事后跟厂商确认,确实如此);

  4 更改模式之所以能减少延迟现象的原因就在于:LB设备的syn重传时间并不遵守RFC的TCP SYN重传建议,而是对timeout时间进行了大大的缩短,几乎是立刻重发syn包。

 

问题进一步分析:

  现在问题的关键点在于,在什么样的情况下,TCP会针对syn包直接回复ack包?

  分析后假设以下结论

  1 服务器端处在timewait(2msl)状态下,对来自同一Ip同一端口的syn连接,直接发送ack,以表示拒绝连接(按照tcp/ip的原理分析得知);

 

有了假设之后,我们进行了如下的实验:

 实验一:在tcp的time_wait状态下,对socket对进行重利用

 1 用tcpconnect工具指定本地tcp端口,连接对端服务器

  tcpconnect -r -v -l 10.192.144.177:35380 10.192.144.174 80
  GET /index.html HTTP/1.0

  ...............

 2 确认服务器端tcp状态(处在time_wait)

   tcp 0 0 10.192.144.174:80 10.192.144.177:35380 TIME_WAIT
 3 立刻在客户端重新执行 tcpconnect -r -v -l 10.192.144.177:35380 10.192.144.174 80 命令
 4 服务器端的tcp状态从time_wait变回establish

   tcp 0 0 10.192.144.174:80 10.192.144.177:35380 ESTABLISHED

 

 实验二: 强制让服务器端产生timewait状态,然后从同一IP同一端口再度发送tcp的syn包

   用tcpconnect工具在服务器端产生time_wait状态  

 $ tcpconnect  -r -v -l 10.192.144.177:35000 10.192.144.172 80
 GET /index.html HTTP/1.0

 HTTP/1.1 404 Not Found
 Date: Thu, 14 Apr 2011 04:47:47 GMT
 Server: Apache/2.2.3 (CentOS) DAV/2
 Content-Length: 300
 Connection: close
 Content-Type: text/html; charset=iso-8859-1

 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
 <html><head>
 <title>404 Not Found</title>
 </head><body>
 <h1>Not Found</h1>
 <p>The requested URL /index.html was not found on this server.</p>
 <hr>
 <address>Apache/2.2.3 (CentOS) DAV/2 Server at localhost.localdomain Port 80</address>
 </body></html>

2 确认tcp处在time_wait状态

  tcp            0 ::ffff:10.192.144.172:80    ::ffff:10.192.144.177:35000 TIME_WAIT

  这时的tcpdump如下:

14:28:31.460914 IP 10.192.144.177.35000 > 10.192.144.172.http: S 2256667492:2256667492(0) win 65535
14:28:31.461764 IP 10.192.144.172.http > 10.192.144.177.35000: . ack 34418700 win 1448 <nop,nop,timestamp 102151303 71539512>
14:28:31.462849 IP 10.192.144.177.35000 > 10.192.144.172.http: R 34418700:34418700(0) win 0
14:29:56.534495 IP 10.192.144.177.35000 > 10.192.144.172.http: S 1557240063:1557240063(0) win 5840 <mss 1460,sackOK,timestamp 71563072 0,nop,wscale 6>
14:29:56.535583 IP 10.192.144.172.http > 10.192.144.177.35000: S 2358231624:2358231624(0) ack 1557240064 win 5792 <mss 1460,sackOK,timestamp 102236030 71563072,nop,wscale 2>
14:29:56.537262 IP 10.192.144.177.35000 > 10.192.144.172.http: . ack 2358231625 win 92 <nop,nop,timestamp 71563072 102236030>
14:30:02.904552 IP 10.192.144.177.35000 > 10.192.144.172.http: P 1557240064:1557240089(25) ack 2358231625 win 92 <nop,nop,timestamp 71564664 102236030>
14:30:02.905083 IP 10.192.144.172.http > 10.192.144.177.35000: . ack 1557240089 win 1448 <nop,nop,timestamp 102242382 71564664>
14:30:03.208222 IP 10.192.144.177.35000 > 10.192.144.172.http: P 1557240089:1557240090(1) ack 2358231625 win 92 <nop,nop,timestamp 71564740 102242382>
14:30:03.209047 IP 10.192.144.172.http > 10.192.144.177.35000: . ack 1557240090 win 1448 <nop,nop,timestamp 102242685 71564740>
14:30:03.211047 IP 10.192.144.172.http > 10.192.144.177.35000: P 2358231625:2358232110(485) ack 1557240090 win 1448 <nop,nop,timestamp 102242688 71564740>
14:30:03.211375 IP 10.192.144.172.http > 10.192.144.177.35000: F 2358232110:2358232110(0) ack 1557240090 win 1448 <nop,nop,timestamp 102242688 71564740>
14:30:03.211843 IP 10.192.144.177.35000 > 10.192.144.172.http: . ack 2358232110 win 108 <nop,nop,timestamp 71564740 102242688>
14:30:03.212353 IP 10.192.144.177.35000 > 10.192.144.172.http: F 1557240090:1557240090(0) ack 2358232111 win 108 <nop,nop,timestamp 71564740 102242688>
14:30:03.212465 IP 10.192.144.172.http > 10.192.144.177.35000: . ack 1557240091 win 1448 <nop,nop,timestamp 102242689 71564740>

  如上面显示的结果,TCP正常建立连接,并且在数据传输完成后,服务器端主动执行关闭操作,连接进入time_wait(2msl)状态

3 利用sendip工具再度连接该socket对

   $ sudo sendip -d '' -p ipv4 -p tcp -is 10.192.144.177 -ts 35000t -td 80 10.192.144.172

  此时的tcpdump结果是:

  14:30:20.923051 IP 10.192.144.177.35000 > 10.192.144.172.http: S 2380432427:2380432427(0) win 65535
14:30:20.923480 IP 10.192.144.172.http > 10.192.144.177.35000: S 2358297648:2358297648(0) ack 2380432428 win 5840
14:30:20.924389 IP 10.192.144.177.35000 > 10.192.144.172.http: R 2380432428:2380432428(0) win 0

 

 由上面的实验结果可以得知,以上的假设结果并不正确,实验证明,TCP在time_wait状态下,对于来之同一socket的连接和普通的3次握手一样,对syn包进行syn+ack的响应,从而完成会话的成立。顺便说明一下,上面tcpdump中的rst是因为测试工具sendip,syn送信完后,不再发送任何其他请求,应用程序直接发送rst结束本次会话(即本次命令),这不在本次的研究话题之内。

 

 假设失败之后,无奈之下再度翻看《TCP/ip卷一中文版》,在183页描述time_wait状态的章节里,有意外的发现,原文描述如下

  1 当tcp执行一个主动关闭,并发送会最后一个ack之后,该连接必须在time_wait状态停留时间为2msl(普遍的实现是一分钟).这样可以让tcp再次发送最后的ack以防止这个ack丢失(另一端超时并重发最后的fin),这种2msl的等待产生的另一个后果是,这个tcp连接在2msl等待期间,定义这个连接的socket(客户端ip地址和端口号,服务器端ip地址和端口号)不能再被使用,这个连接只有在2msl结束后再能被使用......

 2 但是在185页所做的一个实验中,证明了以上规定有一个例外情况:大多数的伯克利的实现,支持处在2msl状态的服务器,允许来自同一soket的tcp连接请求,前提条件是,新的tcp连接的syn的ISN序列号必须比上一个同一soket连接的最后序列号要大,否则按照规则一执行丢弃。

 

 参考此处的说明,再查看以上的实验数据,发现新的连接所采用的序列号都比原有连接大,所以才有新的tcp在同一个sokcet对上的连接成功。

 

于是有了下面的实验3

  实验三:再度使用同一socket对进行连接的时候,强制SYN的ISN序列号比之前的连接的最后序列号小

 1 对tcp服务器执行 wget http://Server/index.html

 产生tcpdump结果如下

19:25:47.477340 IP 10.192.144.177.32785 > 10.192.144.172.http: S 2054603651:2054603651(0) win 5840 <mss 1460,sackOK,timestamp 180993208 0,nop,wscale 6>
19:25:47.477378 IP 10.192.144.172.http > 10.192.144.177.32785: S 4041734616:4041734616(0) ack 2054603652 win 5792 <mss 1460,sackOK,timestamp 340710 180993208,nop,wscale 3>
19:25:47.477831 IP 10.192.144.177.32785 > 10.192.144.172.http: . ack 4041734617 win 92 <nop,nop,timestamp 180993209 340710>
19:25:47.480333 IP 10.192.144.177.32785 > 10.192.144.172.http: P 2054603652:2054603829(177) ack 4041734617 win 92 <nop,nop,timestamp 180993209 340710>
19:25:47.480360 IP 10.192.144.172.http > 10.192.144.177.32785: . ack 2054603829 win 858 <nop,nop,timestamp 340713 180993209>
19:25:47.482095 IP 10.192.144.172.http > 10.192.144.177.32785: P 4041734617:4041734861(244) ack 2054603829 win 858 <nop,nop,timestamp 340715 180993209>
19:25:47.482508 IP 10.192.144.172.http > 10.192.144.177.32785: F 4041734861:4041734861(0) ack 2054603829 win 858 <nop,nop,timestamp 340715 180993209>
19:25:47.483351 IP 10.192.144.177.32785 > 10.192.144.172.http: . ack 4041734861 win 108 <nop,nop,timestamp 180993210 340715>
19:25:47.487574 IP 10.192.144.177.32785 > 10.192.144.172.http: F 2054603829:2054603829(0) ack 4041734862 win 108 <nop,nop,timestamp 180993211 340715>
19:25:47.487621 IP 10.192.144.172.http > 10.192.144.177.32785: . ack 2054603830 win 858 <nop,nop,timestamp 340720 180993211

 

 和设想的一样 10.192.144.177(Client):32785 => 10.192.144.172(Server):80 连接成功。然后服务器端执行主动关闭。实验时记下此时客户端的最后的序列号是2054603829

2 确认服务器端处在time_wait状态

 tcp            0 ::ffff:10.192.144.172:80    ::ffff:10.192.144.177:32785 TIME_WAIT

3 在time_wait状态下,再度使用同一soeket对进行连接,次出强制使用比 2054603829更小的sequence number进行连接。 ISN为2054603820

 

$ sudo sendip -d '' -p ipv4 -p tcp -is 10.192.144.177 -ts 32785 -td 80 10.192.144.172 -tfr 0 -tfs 1 -tn 2054603820

19:26:20.606046 IP 10.192.144.177.32785 > 10.192.144.172.http: S 2054603820:2054603820(0) win 65535
19:26:20.606086 IP 10.192.144.172.http > 10.192.144.177.32785: . ack 2054603830 win 858 <nop,nop,timestamp 373795 180993211>
19:26:20.606765 IP 10.192.144.177.32785 > 10.192.144.172.http: R 2054603830:2054603830(0) win 0

此处时实验结果证明,对于新来的syn连接,服务器直接返回ack(而不是syn+ack),表示该包not accetable(RFC793)

根据这个实验结果,我们对本次的问题有了进一步的假设,

  LB的tcp模式下,对真实客户端的src ip,src port用自己地址池的ip和端口进行改写,对于其他tcp头则只是copy(例如seq)

  2 发自互联网上不同的客户端的isn是各自不同的(这个比较容易理解)

  3 由于服务器端看到的src ip以及src port虽然是LB上的地址池中的ip和端口,但是ISN却是各自真实互联网的客户端发出的ISN,因此存在同一个socket来的后一个连接的ISN号小于前一个连接的最后序列号的情况

  4 对于这种情况:服务器返回ACK从而导致客户端发送RST之后等待3秒,再次发送syn,产生了本文最初的故障现象。

 

为了证明以上假设的正确性,我们在LB上做了进一步的测试,测试LB是如何进行snat的

 tcpdump的结果如下:

 客户端数据

12:31:53.967580 IP 192.168.11.6.57516 > 110.44.179.124.443: Flags [S], seq 3348951429, win 5840, options [mss 1460,sackOK,TS val 1478305370 ecr 0,nop,wscale 6], length 0

真实WEB服务器端数据
12:31:53.970874 IP 172.24.2.21.17708 > 172.24.2.242.snpp: S 3348951429:3348951429(0) win 5840 <mss 1400,sackOK,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,wscale 6>

 上面的数据显示,数据流在经过LB之后,src Ip和port都变了,但是ISN号没有改变,和假设一致。

 

至此,问题基本明了。原因归结如下:

  1 在LB设备采用snat的情况,如果snat地址池中ip数量不够多,或者足够多而没有采用合理的地址轮询机制,则在互联网访问流量巨大的时候,LB上会产生大量的tcp端口复用现象;

  2 由于个客户端的SEQUNUM各自独立互不相同,而LB不会对序列号进行修改,则time_wait状态的soket重用发生时,如果后续连接的SYN的ISN号小于前一个连接的最后序列号,则服务器直接回复ack包,表示该连接不可接受,从而使客户端发送rst包重置该socket对,并且等待3秒之后再次发送syn包。

 

  和厂家进一步确认后,证明我们的总结是对的。后续的解决方法是

  1 增加地址池中ip数量,并且确保lb的ip轮训机制,是平均的分散的,比如IP1 (local port 1024----->65535),IP2 (local port 1024----->65535), ............IPN(local port 1024----->65535), 然后再次循环IP1 (local port 1024----->65535),IP2 (local port 1024----->65535), ............IPN(local port 1024----->65535)............

 2 A10工程师说:下一个版本中,会实现新的tcp flow,即TCP模式下,当LB发送SYN之后如果收到服务器发送的ack,则LB立刻发送rst给服务器端,强制服务器端的time_wait状态结束,促使服务器重新进入listen状态,并且LB立即重发syn包(延迟小于1ms)新的tcp flow如下:

 Client            LB               Server
|------ syn1 ----->|                  |
                 |------ syn1 ----->|
                 |<----- ack -------| 

                 |------ rst ------>|

                 |------ syn1 ----->| *1ms以内重传syn

                 |<--- syn1+ack --->|
|<--- syn1+ack --->|
|------ ack ------>|                  |
                 |------ ack ------>|
                                 |

                                                                -----  BY Andy Chou

 

 

 


 

 

0

阅读 评论 收藏 转载 喜欢 打印举报
  • 评论加载中,请稍候...
发评论

    发评论

    以上网友发言只代表其个人观点,不代表新浪网的观点或立场。

      

    新浪BLOG意见反馈留言板 电话:4006900000 提示音后按1键(按当地市话标准计费) 欢迎批评指正

    新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 会员注册 | 产品答疑

    新浪公司 版权所有