一、背景

  假如应用服务器A上有若干模块连接某数据库服务机器B,当B异常假死,需要将B的请求切换到备份系统,这样已经建立的连接就遗留了下来。如果A上hang住的连接占用的服务线程较多,就可能造成业务系统受到影响,因此需要即时清理掉hang住的连接。

二、问题分析

  出现问题时需要快速释放hang住的连接,我们可能用那些方式来解决呢?

  1,重启假死的机器:受限于响应速度、需要承担重启机器的风险

  2,重启程序:能快速中断hang住的连接,但是

  多类模块(php、java、C、script等)连接数据库,重启方式各异;

  部分模块需要加载数据,启动时间长;

  如果模块同时重启,服务可能会受影响;

  部分模块重启后cache失效,对性能有影响;

 

  3,网络层清理

  根据netstat命令或/proc/net/tcp找到所有与B有关的连接,通过iptables封禁。由于iptables需要包驱动,B假死时iptables不能正常工作,同时A--->B除了TCP重传和心跳检测外,应用层一般为同步读写模式,不保证有包交互;同时长短连接并存,以短连接为主,tcp_keeplive心跳检测特性不能利用。

  4,总结

  hang住的连接主要处于连接建立和ESTABLISHED阶段,建立连接时程序可以配置连接超时,处于连接建立阶段B--->A的包如果没有收到A会重传,这样主要解决机器A上处于阻塞读的连接即可。除了重启程序还有以下方案:

  a,配置读写超时,libmysqlclient默认为1年,应用上有业务需要执行几秒到几百秒,由于系统性能波动,偶尔会有sql执行时间较长,应用不好配置这个值,时间配置太大没有意义。

  b,外部构造RST包关闭连接,由于安全原因RST包需要携带正确的seq,因此需要先记录包序。

  c,使用iptables等工具需要有A ---> B的包驱动

  d,内核API

  问题抽象为:在机器B假死时,如何产生一个A ---> B的数据包

 

三、实现方案

  1,如果是SYN_SEND状态

  tcp本身有重传,线上重传参数有优化;复用重传数据包中的seq 构造一个 [R.] seq=0 ack=seq+1的包就可以关闭连接;其它方式(配置较小的连接超时,handoff对新连接会自动转向)

 

 

  2,如果是ESTABLISHED状态,大部分连接处在这种状态

  在 RFC793(TCP/IP协议)里找到这么一段:

If an incoming segment is not acceptable,an acknowledgment should be sent in reply (unless the RST bit is set, if so drop the segment and return).

内核代码net/ipv4/tcp_input.c中,有具体实现:

static int tcp_validate_incoming(struct sock *sk, struct sk_buff *skb,

struct tcphdr *th, int syn_inerr)

{ …

省略…

/* Step 1: check sequence number */

if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {

/* RFC793, page 37: "In all states except SYN-SENT, all reset

* (RST) segments are validated by checking their SEQ-fields."

* And page 69: "If an incoming segment is not acceptable,

* an acknowledgment should be sent in reply (unless the RST

* bit is set, if so drop the segment and return)".

*/

if (!th->rst)

tcp_send_dupack(sk, skb);

goto discard;

}

…省略…

}

  因此,可以在A机器上模拟B给A发送一个不带RST标识、seq错误的数据包, A在接收到数据包后会回复一个用于ACK包,此时可以用iptables或其它方式捕获包上正确的seq,发送RST关闭连接,示例图如下:

 

  3,其它状态暂未处理(强制关闭连接的影响、非syn_send,established状态的处理)

 

 

 

四、具体实现

  环境机器A:192.168.1.4;

  环境机器B:192.168.1.13 ;

  端口3306

  方案1:使用iptables回复reset

  1,通过/proc/net/tcp(或netstat –nat|grep ESTABLISHED)获取所有A ---> B的连接

  2,添加iptables规则iptables -A OUTPUT -p tcp -d 192.168.2.13 --dport 3306 -j REJECT --reject-with tcp-reset

  3,对每一个连接,伪造B ---> A数据包hping3 --numeric --spoof 192.168.2.13 --destport <lport> --baseport 3306 --fin 192.168.2.4 #sendip -p ipv4 -is 192.168.2.13 -p tcp -ts 3306 -td <lport> -tff 1 -tfs 0 -tn 0 192.168.2.4

  4,删除iptables规则iptables -D OUTPUT -p tcp -d 192.168.2.13 --dport 3306 -j REJECT --reject-with tcp-reset

  方案2:

  1,通过/proc/net/tcp获取所有A ---> B的SYN_SENT或ESTABLISHED连接

  2,通过libpcap获取B交互的数据包副本,对匹配A ---> B包,回复RST,关闭连接;发送A--->B的fake FIN包,如果B协议栈还能工作,会回复ack;由于A的端口已被关闭,A上的协议栈会回复RST,关闭B--->A的半连接,包示例

10:08:06.338131 IP 192.168.2.10.43376 > 192.168.2.1.80: Flags [S], seq 1586426180, win 14600, options [mss 1460,sackOK,TS val 51262745 ecr 0,nop,wscale 5], length 0

10:08:06.338500 IP 192.168.2.1.80 > 192.168.2.10.43376: Flags [S.], seq 1103847096, ack 1586426181, win 8192, options [mss 1460,nop,wscale 8,sackOK,TS val 31578197 ecr 51262745], length 0

10:08:06.338614 IP 192.168.2.10.43376 > 192.168.2.1.80: Flags [.], ack 1, win 457, options [nop,nop,TS val 51262745 ecr 31578197], length 0

10:08:13.450742 IP 192.168.2.1.80 > 192.168.2.10.43376: Flags [F], seq 0, win 512, length 0

10:08:13.450766 IP 192.168.2.10.43376 > 192.168.2.1.80: Flags [.], ack 1, win 457, options [nop,nop,TS val 51264523 ecr 31578197,nop,nop,sack 1 {3191120200:3191120201}], length 0

10:08:13.452394 IP 192.168.2.1.80 > 192.168.2.10.43376: Flags [R], seq 1103847097, win 512, length 0 10:08:13.453823 IP 192.168.2.10.43376 > 192.168.2.1.80: Flags [F], seq 0, win 512, length 0

10:08:13.454021 IP 192.168.2.1.80 > 192.168.2.10.43376: Flags [.], ack 1, win 260, options [nop,nop,TS val 31578908 ecr 51264523], length 0

10:08:13.454039 IP 192.168.2.10.43376 > 192.168.2.1.80: Flags [R], seq 1586426181, win 0, length 0

  对1的每一个连接,模拟B给A发送数据包,驱动2执行。