Tomcat 调优之从 Linux 内核源码层面看 Tcp backlog( 四 )


  • 当 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 后才断开该连接 , 这样就会导致半连接队列快速被耗尽 , 不能处理正常的请求 。
    那是怎么防止攻击的呢?
    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;}

    经验总结扩展阅读