Atom

思考、故事和创意

网络

对 Google TCP BBR 的浅薄认识

关于 Google 发布的新的 TCP 拥塞控制算法 BBR (Bottleneck Bandwidth and RTT) 的一些记录,仍然在学习中,难免有误解。之后会持续修正和完善。

网络链路上的包比较少时,道路很通畅,这个阶段,对于一个 TCP 连接来说,它的速度由这个连接两端之间的距离决定,也可以说是由 RTT 决定。当发包速率变大,把道路基本上填满了之后,这个阶段,带宽的大小决定了这个连接的速度,这时两端之间可能就会有包要排队,延迟时间除了 RTT 还有排队时间。

BBR 目的是要尽量跑满带宽,并且尽量不要有排队的情况。

cwnd 是普通的拥塞控制算法里最终要求得的一个值,用来控制发包速率。BBR 也要求到这个值,但是它不是最主要的控制发包速率的变量,主要的变量是 pacing_rate

这两个变量都由探测到的带宽值和 RTT 值得到,整个过程都围绕着这两个值。在 BBR 算法中,有四种状态,几种状态可能会有如下的转换:

            |
            V
   +---> STARTUP  ----+
   |        |         |
   |        V         |
   |      DRAIN   ----+
   |        |         |
   |        V         |
   +---> PROBE_BW ----+
   |      ^    |      |
   |      |    |      |
   |      +----+      |
   |                  |
   +---- PROBE_RTT <--+

另外还有一个重要变量,是在不同状态,不同阶段取的不同增益系数,比如有时需要多发包探测到最大瓶颈带宽,有时需要把发包量降下来探测较为准确的 RTT。

STARTUP 类似与普通拥塞控制里的慢启动,增益系数是 2ln2,每一个来回都以这个系数增大发包速率,估测到带宽满了就进入 DRAIN 状态 —— 连续三个来回,测得的最大瓶颈带宽没有比上一轮增大 25% 以上,就算做带宽满了。

进入 DRAIN 状态,增益系数小于 1,也就降速了。一个包来回,把 STARTUP 状态中产生的队列“抽干”,怎么样测算到队列空了?发出去还没有 ACK 的包量 inflight,与 BDP (带宽延迟积)进行比较,inflight < BDP 说明空了,道路不那么满了,如果 inflght > BDP 说明还不能到下一个状态,继续 DRAIN。

PROBEBW 是稳定状态,这时已经测出来一个最大瓶颈带宽,而且尽量不会产生排队现象。之后的每个来回,在 PROBEBW 状态循环(除非要进入下面提到的 PROBERTT 状态),轮询下面这些增益系数,5/4, 3/4, 1, 1, 1, 1, 1, 1,如此,最大瓶颈带宽就会在其停止增长的地方上下徘徊。大部分时间都应该处于 PROBEBW 状态。

前面三种状态,都可能进入 PROBERTT 状态。超过十秒没有估测到更小的 RTT 值,这时进入 PROBERTT 状态,把发包量降低,空出道路来比较准确得测一个 RTT 值,至少 200ms 或一个包的来回之后退出这个状态。检查带宽是否是满的,进入不同的状态:如果不满,进入 STARTUP 状态,如果满,进入 PROBEBW 状态。

用 epoll 的 echo 程序

完整代码放在最前面,只是一个简单的 echo 程序,没有业务逻辑,收到数据之后立即发送回去。然而,还是有些细节值得记录一下,关于 TCP Server 通常需要设置的 TCP 套接字属性,屏蔽 SIGPIPE 信号,以及最重要的:如何使用 epoll,ET 模式和 LT 模式是怎样的。

#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/socket.h>
#include <signal.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>
#include <assert.h>

#define MAX_EVENT 1024
#define BACKLOG 128
#define MAX_DATALEN 1024

typedef struct sockaddr SA;

int set_tcp_reuse(int sock)
{
    int opt = 1;
    return setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
}

int set_tcp_nodelay(int sock)
{
    int opt = 1;
    return setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt, sizeof(opt));
}

int set_non_block(int fd)
{
    int flags;
    flags = fcntl(fd, F_GETFL, NULL);
    if (flags == -1) {
        perror("fcntl F_GETFL error");
        return -1;
    }
    flags |= O_NONBLOCK;
    if (fcntl(fd, F_SETFL, flags) == -1) {
        perror("fcntl F_SETFL error");
        return -1;
    }
    return 0;
}

int ignore_sigpipe()
{
    struct sigaction sa;
    memset(&sa, 0, sizeof(struct sigaction));
    sa.sa_handler = SIG_IGN;
    if (sigaction(SIGPIPE, &sa, NULL)) {
        perror("sigaction error");
        return -1;
    }
    return 0;
}

int create_tcp_server(const char *ip, uint16_t port, int backlog)
{
    int ret = -1;
    socklen_t len = 0;

    if (ignore_sigpipe()) {
        printf("setting ignore sigpipe failed\n");
        return -1;
    }

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("create socket error");
        return -1;
    }
    struct sockaddr_in addr;
    addr.sin_port = htons(port);
    addr.sin_family = AF_INET;
    if (!ip) {
        addr.sin_addr.s_addr = htonl(INADDR_ANY);
    } else {
        ret = inet_pton(AF_INET, ip, (SA *)&addr.sin_addr);
        if (ret <= 0) {
            if (ret == 0) {
                fprintf(stderr, "not invalid ip: [%s]\n", ip);
            } else {
                perror("inet_pton error");
            }
            return -1;
        }
    }

    if (set_tcp_reuse(sock) == -1) {
        perror("setsockopt SO_REUSEADDR error");
        return -1;
    }

    if (set_tcp_nodelay(sock) == -1) {
        perror("setsockopt TCP_NODELAY error");
        return -1;
    }

    len = sizeof(SA);
    if (bind(sock, (SA *)&addr, len) == -1) {
        perror("bind error");
        return -1;
    }

    if (listen(sock, backlog) == -1) {
        perror("listen error");
        return -1;
    }

    return sock;
}

int set_read_event(int epfd, int ctl, int fd)
{
    struct epoll_event ev;
    ev.data.u64 = 0;
    ev.data.fd = fd;
    ev.events = EPOLLIN | EPOLLET;

    if (epoll_ctl(epfd, ctl, fd, &ev) == -1) {
        perror("epoll_ctl error");
        return -1;
    }
    return 0;
}

int handle_recv(int fd)
{
    int read = 0;
    int n = 0;
    int ret = 0;
    char buf[MAX_DATALEN + 1] = {0};
    int space = 0;
    size_t cap = MAX_DATALEN;
    while (1) {
        n = recv(fd, buf + read, cap - read, 0);
        if (n < 0) {
            if (errno == EWOULDBLOCK || errno == EAGAIN) {
                break;
            } else {
                ret = -1;
                break;
            }
        } else if (n == 0) {
            ret = -1;
            break;
        } else {
            space = cap - read;
            read += n;
            if (n == space) {
                if (cap - read == 0) {
                    break;
                }
                continue;
            } else {
                break;
            }
        }
    }

    buf[read] = '\0';

    if (ret < 0) {
        return -1;
    }

    // not a good way, send can fail.
    send(fd, buf, read, 0);
    return 0;
}

int main(int argc, char *argv[])
{
    const char *ip = NULL;
    uint16_t port = 32667;
    int srv = create_tcp_server(ip, port, BACKLOG);
    if (srv < 0) {
        fprintf(stderr, "create tcp server failed\n");
        return -1;
    }

    struct epoll_event ev, events[MAX_EVENT];
    int epfd = -1;
    if ((epfd = epoll_create(1)) == -1) {
        perror("epoll_create error");
        return -1;
    }
    memset(&ev, 0, sizeof(ev));
    ev.events = EPOLLIN;
    ev.data.fd = srv;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, srv, &ev) == -1) {
        perror("epoll_ctl add srv error");
        close(srv);
        return -1;
    }
    int i, ret, cli, fd;
    struct sockaddr_in addr;
    socklen_t addrlen = sizeof(SA);
    while (1) {
        ret = epoll_wait(epfd, events, MAX_EVENT, -1);
        if (ret > 0) {
            for (i = 0; i < ret; i++) {
                fd = events[i].data.fd;
                if (events[i].events & EPOLLIN) {
                    if (fd == srv) {
                        cli = accept(srv, (SA *)&addr, &addrlen);
                        if (cli == -1) {
                            perror("accept error");
                        } else {
                            if (set_non_block(cli) == -1) {
                                fprintf(stderr, "set cli non block failed\n");
                                close(cli);
                                continue;
                            }
                            if (set_read_event(epfd, EPOLL_CTL_ADD, cli) != 0) {
                                close(cli);
                                continue;
                            }
                        }
                    } else {
                        if (handle_recv(fd) == -1) {
                            if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL)) {
                                perror("epoll_ctl EPOLL_CTL_DEL cli error");
                            }
                            close(fd);
                            continue;
                        }
                    }
                }
            }
        } else if (ret == 0) {
            continue;
        } else {
            perror("epoll_wait error");
            continue;
        }
    }
    return 0;
}

下面是对代码的一些解释。

TCP Server 的三个“必要”的设置

set_tcp_reuse 函数设置了 TCP 的 SO_REUSEADDR,以免进程重启时一直无法重新监听端口。

set_tcp_nodelay 函数设置了 TCP_NODELAY 属性,关闭 TCP 的 Nagle 算法,是为了让信息尽快地真正发送出去。如果开启 Nagle 算法,有可能会多等一会才把数据真正发出去。这里的代码需要 #include &lt;netinet/tcp.h&gt; 头文件。

ignore_sigpipe 函数用来忽略 SIGPIPE 信号,对于 SIGPIPE 信号,默认的处理是退出当前进程。当客户端关闭了连接,服务端仍然在调用 send/write 发送数据,就会产生 SIGPIPE 信号,这显然是不能接受的。

epoll 的 ET 模式

使用 epoll 的默认 LT 模式,基本上不用担心 recv/read 读不全数据,内核缓冲区里只要有数据,就会得到 EPOLLIN 事件,通知进程去读数据,不需要循环调用 recv 保证读到所有数据。

使用 ET 模式的工作方式 (EPOLLET) 跟 LT 有一点区别,当 socket 可读时,只会通知一次,如果收到通知之后没有读完数据,不会再通知。为了保证数据读完,需要循环读一直到某些条件再跳出循环。

如何保证数据全部读完

下面一段代码是个例子:

// fd 为非阻塞套接字
while (1) {
    n = read(fd, buf, bufsiz, 0);
    if (n < 0) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            break;
        } else {
            // .....
        }
    } else if (n == 0) {
        break;    
    } else {
        if (n < bufsiz) {
            break;
        } else {
            continue;
        }
    }
}

从代码中可以看到跳出循环的条件:

  1. recv 返回错误是 EAGAINEWOULDBLOCK 时,说明已经没有数据可读。当设置套接字为非阻塞模式,或者设置了超时时间,才会有这样的错误。
  2. recv 返回了 0,说明对方已经关闭连接,服务端自然也不再继续读它,而是要关闭对应的套接字。
  3. recv 返回的值小于预定的 buffer 的大小(n &lt; bufsiz),说明内核缓冲区里还剩下 n 个字节数据,都已经读完了,不需要继续读了。

如果 n == bufsiz 说明还可能有数据没读完,就要进入下一个循环。也有可能恰好这次读了 bufsiz 字节的数据,而且再没有数据了,这样下一个循环调用 recv 时就会遇到上述的第一个条件: EAGAINEWOULDBLOCK 错误。

设想一下,如果是用的默认的阻塞套接字,也没有设置超时时间,这次 recv 是得不到 EAGAINEWOULDBLOCK 错误的,程序被一直阻塞在这里,所以 EPOLLET 要和非阻塞套接字配合使用

没读完数据会怎样

使用 ET 模式,一次 EPOLLIN 时间之后,即使没读完数据,也不再通知程序去读数据。一直到下一次网卡又接收到了数据,程序又收到通知,然后调用 recv,这里先读到的是上一次没读完的在内核缓冲区里地数据,或许会产生一些预料之外的问题。

具体的一个试验

场景(S 表示服务端,C 表示客户端)

  • 监听 EPOLLIN|EPOLLET 事件(使用 ET 模式)
  • S 端 每次 recv 接收 5 字节的数据

过程:

  1. C 发送 10 字节数据 “helloworld” 到 S,S 网卡收到这 10 字节数据,EPOLLIN 触发,S recv 5 字节数据”hello”,仍然有 5 字节数据在内核缓冲区内,不再通知
  2. C 又发送 5 字节数据 “12345” 到 S,S 网卡收到这 5 字节数据,EPOLLIN 触发,S 继续 recv 5 字节,收到的是 “world”,上一次发送没从内核缓冲区取完的数据。
  3. C 又发送 3 字节数据 “xyz” 到 S,S 网卡收到这 3 字节数据,EPOLLIN 触发,S 继续 recv 5 字节,收到 “12345”
  4. C 又发生 1 字节数据 “a” 到 S,S 网卡收到这 1 字节数据,EPOLLIN 触发,S recv 之后,recv 返回值为 4,收到的是 “xyza”
  5. C 又发送 10 字节数据 “helloworld” 到 S,S 收到 “hello”
  6. C 关闭连接退出,再次触发 EPOLLIN 事件,S 调用 recv,收到 “world”。

如果是一个 echo 程序,那么客户端每次发送了消息之后,“反射”回来的内容都不是预期的了,并且在上面这个试验中,最后一步,S 错过了关闭套接字的好机会。如果程序有更复杂的业务逻辑已经把人搞得焦头烂额,再出现这个例子的情况,或许会使人痛苦不堪。

另外两点

继续以上面的试验为例,如果因为某些原因,S 一直没去读 C 发送的数据, C 仍然按照上面的方式发送了几次数据,总共 23 个字节的数据,都被放在 S 的内核缓冲区内,这样就只会被触发一次,S 只会收到最初的 5 个字节。

还有一点要提一下。当触发一次 EPOLLIN 之后,在处理过程中,而且这次处理有没有读完的数据,又调用了 epoll_ctl,(重复)设置监听 EPOLLIN 事件,那么 EPOLLIN 是会被再次触发的。

比如在最开始的代码中这样修改

int handle_recv(int epfd, int fd)
{
    ....
    ....
    set_read_event(epfd, fd);
    return send(fd, buf, read, 0);
}

处理完这一次 EPOLLIN 事件,又重新设置去监听 EPOLLIN 事件,内核缓冲区里有数据,那就会再来一次 EPOLLIN(好像 LT 模式),这是个奇怪的用法。这里只是为了描述重复设置 EPOLLIN 事件会出现的情况。

检测递归 DNS 的后端 IP

我想知道当我请求 114.114.114.114 或者 8.8.8.8,它们的后端都有什么。我在授权 DNS 设置了分线路的记录,请求当地递归却收到了其他线路的结果,我想知道这个当地递归后端是什么。实际上,当我向递归发起请求之后,最终授权 DNS 收到的来自递归 DNS 的请求,是来自什么地址。

我们要找个办法,把递归向授权的请求“引导”到一个容易看到的地方来。

首先给我一个子域名设置一条 NS 记录,这个 NS 指向我自己的 VPS 地址

back.fixatom.com    NS  ns.fixatom.com
ns.fixatom.com	    A   1.2.3.4

通过这样,当我向递归请求 back.intxt.net 以及它的更多级子域名时,递归就会到我自己设置的 NS 上来请求。“引导”完成。

然后我在我的 VPS 上跑这样一段代码

#!/usr/bin/env python

import socket
import dnslib

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', 53))

while True:
    data, addr = sock.recvfrom(1024)
    d = dnslib.DNSRecord.parse(data)
    print addr[0], d.questions[0].qname

作用很简单,就是输出请求来源 IP 和请求域名。


因为这个自己设置的 NS 根本就没有返回内容,所以发起的请求自然是没有结果的。

有的递归 DNS 后端 IP 不多,或者只会为你所在区域分配一个或一段 IP,这时候能够检测到得 IP 也就是这一个(段)。也有些递归 DNS 后端有好多 IP,为了避免递归缓存,不再继续向这个 NS 请求,最好变换子域名来请求。

for i in {1..10}
do
    dig xxxx$i.back.intxt.net @114.114.114.114
done

这是在我电脑上跑上面的 shell,在 VPS 上得到的结果

60.215.138.233 xxxx1.back.intxt.net.
60.215.138.233 xxxx1.back.intxt.net.
60.215.138.233 xxxx1.back.intxt.net.
60.215.138.233 xxxx1.back.intxt.net.
60.215.138.208 xxxx1.back.intxt.net.
60.215.138.208 xxxx1.back.intxt.net.
60.215.138.233 xxxx1.back.intxt.net.
60.215.138.208 xxxx1.back.intxt.net.
60.215.138.208 xxxx1.back.intxt.net.
60.215.138.208 xxxx2.back.intxt.net.
60.215.138.208 xxxx2.back.intxt.net.
60.215.138.208 xxxx1.back.intxt.net.
60.215.138.208 xxxx2.back.intxt.net.
60.215.138.208 xxxx2.back.intxt.net.
60.215.138.233 xxxx3.back.intxt.net.
60.215.138.233 xxxx3.back.intxt.net.
60.215.138.233 xxxx3.back.intxt.net.
60.215.138.208 xxxx4.back.intxt.net.
60.215.138.208 xxxx4.back.intxt.net.
60.215.138.208 xxxx5.back.intxt.net.
60.215.138.208 xxxx5.back.intxt.net.
60.215.138.208 xxxx5.back.intxt.net.
60.215.138.233 xxxx5.back.intxt.net.
60.215.138.233 xxxx6.back.intxt.net.
60.215.138.208 xxxx6.back.intxt.net.
60.215.138.208 xxxx6.back.intxt.net.
60.215.138.233 xxxx7.back.intxt.net.
60.215.138.208 xxxx6.back.intxt.net.
60.215.138.208 xxxx8.back.intxt.net.
60.215.138.233 xxxx8.back.intxt.net.
60.215.138.233 xxxx8.back.intxt.net.
60.215.138.233 xxxx9.back.intxt.net.
60.215.138.208 xxxx9.back.intxt.net.
60.215.138.208 xxxx10.back.intxt.net.
60.215.138.208 xxxx9.back.intxt.net.
60.215.138.208 xxxx10.back.intxt.net.

可以看出,我向 114.114.114.114 请求,它会把这个请求分发给 60.215.138.* 这一段的后端服务器,这些后端服务器到授权去请求。

HTTP 和 TCP 的 KEEP ALIVE

先把结论放这:TCP 的 keepalive 和 HTTP 请求和响应的包头里的 keepalive 不是一回事。

TCP 的 keepalive 是用来检查 TCP 连接的对方是否还“活着”,Linux 有三个参数跟 keepalive 有关。

  • tcp_keepalive_time 一个连接闲了一定时间,就发 keepalive 的消息,这个时间长度是由 tcp_keepalive_time 来指定。
  • tcp_keepalive_probes 指定发送多少个 keepalive 探测包。如果对方回了 keepalive 探测包,说明对方还在,就继续保持这个连接。
  • tcp_keepalive_intvl 指定发送 keepalive 探测报的间隔时间。跟 tcp_keepalive_probes 相乘(发送多个 keepalive 探测包,对方没有响应),得到的就是从开始 keepalive 检查到放弃这个连接的时间。

HTTP 的 keepalive 是指客户端和 HTTP 服务端建立起 TCP 连接后,在这一个 TCP 连接上进行多次 HTTP 请求和响应,而不是建立 TCP 连接,请求,应答 HTTP,然后就断开 TCP 连接。建立、断开 TCP 的次数大大减少了,TCP 的连接数也减少了。

对 Linux TCP 的若干疑点和误会

整理了一下 Linux 的 TCP 相关的几个疑点和对以往错误认识的纠正。主要是系统出现大量 TIME_WAIT 和 SYN 请求的一些问题,以及一些 TCP 内核参数的意义。

提到的参数(选项)大都在 /proc/sys/net/ipv4/,如果需要永久生效,希望做到重启也不变,可以修改 /etc/sysctl.confnet.ipv4.tcp_*

1. 关于 TIME_WAIT

按照网络上很多文章的说法,当 Linux 服务器上出现大量 TIME_WAIT 状态时,可以通过对 TCP 相关的几个内核参数的修改,来减少 TIME_WAIT 状态。最常看到的是这两个

  • tcp_tw_reuse
  • tcp_tw_recycle

tcp_tw_reuse 设置为 1 时,就允许系统重用处于 TIME_WAIT 状态的 socket。如果 tcp_timestamp 没有设置为 1,只把 tcp_tw_reuse 设置为 1 是无效的。

tcp_tw_recycle 设置为 1 会开启系统对 TIME_WAIT 状态的 socket 的快速回收。开启这个功能,系统就会存下 TCP 连接的时间戳,当同一个 IP 地址过来的包的时间戳小于缓存的时间戳,系统就直接丢包,“回收”这个 socket。这个选项同样需要开启 tcp_timestamp 才生效。

开启这个功能是有很大风险的,如前面所说,会根据同一个 IP 来的包得时间戳来判断是否丢包,而时间戳是根据发包的客户端的系统时间得来的,如果服务端收到的包是同一出口 IP 而系统时间不一样的两个客户端的包,就有可能会丢包,可能出现的情况就是一个局域网内有的客户端能连接服务端,有的不能。

相对 tcp_tw_recycletcp_tw_reuse 是比较安全的一个选项,但是也可能会导致问题。按照官方文档的说法

It should not be changed without advice/request of technical experts.

还有一个与 TIME_WAIT密切相关的参数,tcp_max_tw_buckets 指定系统在同一时间最多能有多少 TIME_WAIT 状态,当超过这个值时,系统会直接干掉这个 TIME_WAIT 的 socket。不要为了减少 TIME_WAIT 就把这个值改小。

2. SYN 和 listen 函数的 backlog

先从 backlog 说起,根据 Linux 的文档,listen 函数的 backlog 等待应用程序 accept 的连接队列的长度,这个队列里是 ESTABLISHED 状态的连接,而不是还未完成三次握手的连接。如果要设置未完成连接的队列长度,可以设置 tcp_max_syn_backlog 这个参数。

未完成连接,也就是系统可能收到对端的 SYN,已经发送了 SYN/ACK,在等待对端的 ACK。DDOS 攻击中的 SYN 攻击,就是发送大量的 SYN,但是不发送 ACK,服务器大量的资源在维持这些未完成的连接,等待 ACK,这时 netstat 可以看到大量的 SYN_RECV 状态。

不少文章里提到为了应对这种现象,为了能让服务端尽量提供服务,就增大 tcp_max_syn_backlog 的值,以使服务器能够接受更多的连接请求,这样做是合情合理的。但是也有不少文章里建议开启 tcp_syncookies 来减少 SYN 攻击的影响。tcp_syncookies 可以说是一个无奈的参数,它使严重违反 TCP 协议的。开启这个选项,系统会根据缓存的未完成的连接的信息算一个值,也就是 cookie,返回给客户端,如果客户端是正常的连接请求,再往服务端发包时,会把这个 cookie 带回来,服务端就会查看这个 cookie,类似加了一个验证的过程。

如果服务器收到大量的正常请求,导致服务器负载非常高,不要为了应对这种情况开启 tcp_syncookies。文档里用了大写的 “MUST NOT” 来提醒我们,然后很良心地建议去修改 tcp_max_syn_backlog, tcp_synack_retries, tcp_abort_on_overflow

tcp_max_syn_backlog 已经提到过,设置更高的值,允许系统保持更多的“未完成连接”。按照在 listen 函数的文档所说,当开启了 tcp_syncookies 之后,tcp_max_syn_backlog 的值就失去了原本意义,该值被忽略

系统发回给客户端 SYN/ACK 后,如果没收到客户端的 ACK,会重发 SYN/ACK,tcp_synack_retries 用于指定重发 SYN/ACK 的次数,默认是 5 次,可以适当减小这个值。

tcp_abort_on_overflow 是个跟应用程序更相关的参数,当被设置为 1 时,如果应用程序处理速度比较慢,来不及接受新的连接,系统就直接丢弃这个连接,给对端发个 RST。这个选项也要谨慎开启。

有时候会看到 somaxconn (/proc/sys/net/core/somaxconn) 这个值跟 tcp_max_syn_backlog 一起出现,其实 somaxconnlistenbacklog 参数的意义是基本一样的,指定用于保存已完成三次握手等待 accept 的队列长度,somaxconnbacklog 可以设置的最大值。当在应用程序里设置 backlog 的值大于 somaxconn 参数时,系统也会默默地把 backlog 减小为 somaxconn 指定的值。