Tomcat 調優之從 Linux 內核源碼層面看 Tcp backlog( 三 )


netstat -natp | grep SYN_RECV | wc -l半連接隊列最大長度可以使用我們上述分析得到的公式計算得到
半全連接隊列溢出全連接隊列溢出
當請求量很大,全連接隊列比較小時 , 就有可能發生全連接隊列溢出的情況 。
此代碼是 linux 內核用來判斷全連接隊列是否已滿的函數,可以看到判斷用的是大于號,這也就是我們用 ss 命令可能會看到 Recv-Q > Send-Q 的原因

  1. sk_ack_backlog 是當前全連接隊列的大小
  2. sk_max_ack_backlog 是全連接隊列的最大長度,也就是 min(listen_backlog, somaxconn)

Tomcat 調優之從 Linux 內核源碼層面看 Tcp backlog

文章插圖
當全連接隊列滿了發生溢出時,會根據 /proc/sys/net/ipv4/tcp_abort_on_overflow 內核參數來決定怎么處理后續的 ack 請求,tcp_abort_on_overflow 默認值為 0 。
  1. 當 tcp_abort_on_overflow = 0 時,如果全連接隊列已滿 , 服務端會直接扔掉客戶端發送的 ACK,此時服務端處于 SYN_RECV 狀態,客戶端處于 ESTABLISHED 狀態,服務端的超時重傳定時器會重傳 SYN + ACK 包給客戶端(重傳次數由/proc/sys/net/ipv4/tcp_synack_retries 指定,默認值為 5 , 重試間隔為 1s、2s、4s、8s、16s,共 31s,第 5 次發出后還要等 32s 才知道第 5 次也超時了,所以總共需要 63s) 。超過 tcp_synack_retries 后 , 服務端不會在重傳,這時如果客戶端發送數據過來,服務端會返回 RST 包,客戶端會報 connection reset by peer 異常
  2. 當 tcp_abort_on_overflow = 1 時,如果全連接隊列已滿,服務端收到客戶端的 ACK 后,會發送一個 RST 包給客戶端,表示結束掉這個握手過程和這個連接,客戶端會報 connection reset by peer 異常
一般情況下 tcp_abort_on_overflow 保持默認值 0 就行,能提高建立連接的成功率
半連接隊列溢出
我們知道 , 服務端收到客戶端發送的 SYN 包后會將該連接放入半連接隊列中,然后回復 SYN+ACK,如果客戶端一直不回復 ACK 做第三次握手,這樣就會使得服務端有大量處于 SYN_RECV 狀態的 TCP 連接存在半連接隊列里,超過設置的隊列長度后就會發生溢出 。
下述代碼是 linux 內核判斷是否發生半連接隊列溢出的函數
// 代碼在 include/net/inet_connection_sock.h 中static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk){return reqsk_queue_is_full(&inet_csk(sk)->icsk_accept_queue);}// 代碼在 include/net/request_sock.h 中static inline int reqsk_queue_is_full(const struct request_sock_queue *queue){/** qlen 是當前半連接隊列大小* max_qlen_log 上述解釋過,如果半連接隊列大小 = 16 = 2^4,那么該值就是4* 非常巧妙的用了移位運行來判斷半連接隊列是否溢出,底層滿滿的都是細節*/return queue->listen_opt->qlen >> queue->listen_opt->max_qlen_log;}我們常說的 SYN Flood 洪水攻擊 是一種典型的 DDOS 攻擊,就是利用了這個點,給服務端發送一個 SYN 包后客戶端就下線了 , 服務端會超時重傳 SYN+ACK 包,上述也說了總共需要 63s 才停止重傳,也就是說服務端需要經過 63s 后才斷開該連接,這樣就會導致半連接隊列快速被耗?。荒艽碚5那肭?。
那是怎么防止攻擊的呢?
linux 提供個一個內核參數 /proc/sys/net/ipv4/tcp_syncookies 來應對該攻擊,當半連接隊列滿了且開啟 tcp_syncookies = 1 配置時,服務端在收到 SYN 并返回 SYN+ACK 后 , 不將該連接放入半連接隊列,而是根據這個 SYN 包 TCP 頭信息計算出一個 cookie 值 。將這個 cookie 作為第二次握手 SYN+ACK 包的初始序列號 seq 發過去,如果是攻擊者 , 就不會有響應,如果是正常連接,客戶端回復 ACK 包后 , 服務端根據頭信息計算 cookie,與返回的確認序列號進行比對,如果相同 , 則是一個正常建立連接 。
下述代碼是計算 cookie 的函數,可以看到跟這些字段有關(源 ip、源端口、目標 ip、目標端口、客戶端 syn 包序列號、時間戳、mssind)
Tomcat 調優之從 Linux 內核源碼層面看 Tcp backlog

文章插圖
下面看下第一次握手,收到 SYN 包后服務端的處理代碼,代碼太多,簡化提出跟半連接隊列溢出相關代碼
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb){/** 如果半連接隊列已滿 , 且 tcp_syncookies 未開啟,則直接丟棄該連接*/if (inet_csk_reqsk_queue_is_full(sk) && !isn) {want_cookie = tcp_syn_flood_action(sk, skb, "TCP");if (!want_cookie)goto drop;}/** 如果全連接隊列已滿,并且沒有重傳 SYN+ACk 包的連接數量大于1,則直接丟棄該連接* inet_csk_reqsk_queue_young 獲取沒有重傳 SYN+ACk 包的連接數量*/if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);goto drop;}// 分配 request sock 內核對象req = inet_reqsk_alloc(&tcp_request_sock_ops);if (!req)goto drop;if (want_cookie) {// 如果開啟了 tcp_syncookies 且半連接隊列已滿 , 則計算 cookieisn = cookie_v4_init_sequence(sk, skb, &req->mss);req->cookie_ts = tmp_opt.tstamp_ok;} else if (!isn) {/* 如果沒有開啟 tcp_syncookies 并且 max_syn_backlog - 半連接隊列當前大小 < max_syn_backlog >> 2,則丟棄該連接 */else if (!sysctl_tcp_syncookies &&(sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <(sysctl_max_syn_backlog >> 2)) &&!tcp_peer_is_proven(req, dst, false)) {LIMIT_NETDEBUG(KERN_DEBUG pr_fmt("drop open request from %pI4/%u\n"),&saddr, ntohs(tcp_hdr(skb)->source));goto drop_and_release;}isn = tcp_v4_init_sequence(skb);}tcp_rsk(req)->snt_isn = isn;// 構造 syn+ack 響應包skb_synack = tcp_make_synack(sk, dst, req,fastopen_cookie_present(&valid_foc) ? &valid_foc : NULL);if (likely(!do_fastopen)) {int err;// 發送 syn+ack 響應包err = ip_build_and_send_pkt(skb_synack, sk, ireq->loc_addr,ireq->rmt_addr, ireq->opt);err = net_xmit_eval(err);if (err || want_cookie)goto drop_and_free;tcp_rsk(req)->snt_synack = tcp_time_stamp;tcp_rsk(req)->listener = NULL;// 添加到半連接隊列,并且開啟超時重傳定時器inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);} else if (tcp_v4_conn_req_fastopen(sk, skb, skb_synack, req))goto drop_and_free;}

推薦閱讀