Linux 非阻塞 epoll 编程中,如何解决大量 ESTABLISHED 连接后占着茅坑不拉屎的行为? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
huahsiung
V2EX    程序员

Linux 非阻塞 epoll 编程中,如何解决大量 ESTABLISHED 连接后占着茅坑不拉屎的行为?

  •  
  •   huahsiung 2024-03-02 17:40:54 +08:00 3278 次点击
    这是一个创建于 589 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Linux socket 中,无论是
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    listen(server_fd)

    还是接过来的
    client_fd=accept(server_fd)

    全部加进 epoll 事件监听中。 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev)

    但是接到的 client_fd ,必须要收到对方发送数据才能激活事件。 如果对方一直不 send()任何数据。那么建立了 ESTABLISHED 连接后就占着茅坑不拉屎。epoll 也不会通知


    网上找到两个方法:

    1.

    epoll_wait(epoll_fd, events, MAX_EVENTS, -1); 改为 epoll_wait(epoll_fd, events, MAX_EVENTS, 10); 

    没用,这是 epoll 事件超时,而不是连接超时。
    epoll_wait 返回的是活跃事件,如果不发送任何数据,epoll_wait 不会返回这个事件的 fd

    2.

    struct timeval timeout; timeout.tv_sec = 10; timeout.tv_usec = 0; setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout); 

    没用,这是阻塞超时,recv()用的。非阻塞会立即返回。


    这种连接浪费了茅坑资源,不知道有什么解决方法。设置 accept 后 10s不拉屎就断开。
    阻塞情况下很好解决,但是非阻塞暂时没想到好办法。

    28 条回复    2024-03-20 08:49:55 +08:00
    lcdtyph
        1
    lcdtyph  
       2024-03-02 18:02:35 +08:00 via iPhone   1
    每个 socket 绑定一个 timer ,一起 epoll 就好了
    PythonYXY
        2
    PythonYXY  
       2024-03-02 18:03:52 +08:00
    我看 chatgpt 给出的答案是利用 setsockopt 函数的 SO_RCVTIMEO 和 SO_SNDTIMEO 选项,不过我没试过
    choury
        3
    choury  
       2024-03-02 18:04:20 +08:00   1
    搜下 TCP_DEFER_ACCEPT
    PythonYXY
        4
    PythonYXY  
       2024-03-02 18:05:35 +08:00   1
    没看到原来 OP 已经试过 setsockopt 了。。。我的想法是定时发送心跳信息,将长时间不响应的 fd 给手动剔除
    henix
        5
    henix  
       2024-03-02 18:05:52 +08:00   1
    这个应该要应用自己维护的吧。记录每个 fd 上一次操作的时间戳,你的第一种方法,epoll_wait 会在中途返回,返回时检查当前时间戳跟记录的 fd 上一次操作时间戳之差,如果超时了就执行某个动作,比如关闭连接。

    这里采用的数据结构是堆( heap )或者时间轮( timing wheel )

    可参考 [Linux 多线程服务端编程]( https://book.douban.com/subject/20471211/) 的“7.10 用 timing wheel 踢掉空闲连接”一章
    BBCCBB
        6
    BBCCBB  
       2024-03-02 19:52:38 +08:00   1
    要实现应用层的心跳.
    roykingz
        7
    roykingz  
       2024-03-02 20:54:08 +08:00   1
    你说的这个特性,Linux 的 TCP_DEFER_ACCEPT 标志可以支持,Nginx 源码中大量使用,Freebsd 中也有类似的特性,叫做 SO_ACCEPTFILTER
    roykingz
        8
    roykingz  
       2024-03-02 21:06:16 +08:00
    不过,这个特性是延迟通知进程,要解决 ESTABLISHED 上一直不发数据的情况,应该还是得靠自己维护超时时间来检查,时间轮用的比较多
    Sephirothictree
        9
    Sephirothictree  
       2024-03-02 21:56:16 +08:00   1
    开个线程做 select 设置阻塞超时来监控非阻塞的 client_fd ,到时间或者可读,就超时踢人或者干活 recv (成功转回阻塞逻辑了 2333 ,不过感觉还是 1 楼方案比较省事
    Nazz
        10
    Nazz  
       2024-03-02 21:59:55 +08:00 via Android   1
    网络库都有 SetDeadline 吧
    huahsiung
        11
    huahsiung  
    OP
       2024-03-02 22:12:24 +08:00
    @lcdtyph
    @choury
    @henix
    @BBCCBB
    @roykingz

    ------

    感谢各位回答

    >TCP_DEFER_ACCEPT

    我看了看 TCP_DEFER_ACCEPT 的 man,里面说(Takes an integer value (seconds), this can bound the maximum number of attempts TCP will make to complete the connection 。

    就是说当重传次数超过限制之后,并且客户端依然还在回复 ack 时,到达最大超时,客户端再次回复的 syn-ack ,那么这个 defer 的连接依然会变成 ESTABLISHED 队列。必须要应用层关闭。

    -----

    >每个 socket 绑定一个 timer

    这个方法刚才试了试,发现接到(event[i].events & EPOLLIN)后,无法区分是 timer_fd 还是 socket_fd ,就不能直接 accept(),因为可能接到 accept(timer_fd),就会错误,在程序看来都是 fd 。

    -----

    >记录每个 fd 上一次操作的时间戳,定时检查当前时间戳跟记录的 fd 上一次操作时间戳之差

    这个我最开始就是这样的,开了一个 pthread 专门处理超时,刚开始测试一切正常。但是后来发现,TCP 连接数超过 100K 时,这个 pthread 会卡死,导致整个程序退出。然后去掉了这个超时处理的 pthread ,就一切正常。

    在 800K TCP 连接左右只占了 962M 内存。每个 fd 维护一个时间轮消耗巨大,程序为每个 tcp 分配的内存只有 1k 左右,全靠 epol 的通知和内存 pointer 撑住的。遍历上万的 fd 的话时间轮这样内存会膨胀 2~3 倍,CPU 上下文切换时间也会激增。


    ----

    **最后的解决方法是暂时不解决,毕竟几十万左右的 TCP 连接才 4k~6k 的僵尸连接,好像也不是影响很大。**


    不知道怎么把 fd 省内存的加入超时队列,我是直接把 fd CRC32 放入类似 hash 表的,但是连接过多 hash 会撞的。
    huahsiung
        12
    huahsiung  
    OP
       2024-03-02 22:16:33 +08:00
    @Sephirothictree

    select 好像不行啊,连接太多,不够用。

    -----

    @Nazz

    SetDeadline 是 go 语言的,C/C++的库好像没用

    go 语言用 go route 起几十万个连接,内存会高达 10G+的。
    lcdtyph
        13
    lcdtyph  
       2024-03-02 22:17:49 +08:00
    @huahsiung #11
    啊这,你要用 event[i].data.ptr 来给这个 fd 一个私有数据结构,这样可以帮助你区分这个 fd 是什么,或者维护一些 fd 相关的上下文
    huahsiung
        14
    huahsiung  
    OP
       2024-03-02 22:29:36 +08:00
    @lcdtyph 为了省内存,我把 event[i].data.ptr 的指针当作 long long int(x64 位)用的



    后来发现这个东西不会触发,不知道哪里问题。



    x64 系统正常运行(除了不会触发超时)

    x86 系统直接“段错误”
    huahsiung
        15
    huahsiung  
    OP
       2024-03-02 22:37:52 +08:00
    @huahsiung 得到答案了,好像是编译器问题,编译器认为指针不可能为负数,帮我把“负指针”优化了。
    Sephirothictree
        16
    Sephirothictree  
       2024-03-02 22:48:15 +08:00
    @huahsiung 试试 poll 不限制 fd 数量,跟 select 差不多,不过这么多连接,就不知道 poll 效率上能不能行了
    lcdtyph
        17
    lcdtyph  
       2024-03-02 22:51:56 +08:00 via iPhone
    @huahsiung
    都 x86 了还要省内存吗

    而且你最好用 intptr_t int_ptr = (intptr_t)data.ptr;
    x86 和 amd64 的指针长度不一样
    huahsiung
        18
    huahsiung  
    OP
       2024-03-02 23:29:04 +08:00
    @lcdtyph 准备 x86 架构直接放弃了吧,就只能在 64 位上面运行。

    准备上数据的时候把 把 pointer 的最高位(符号位) 与 0x00FFFFFF 让 pointer 变成正的。下数据使用的时候再把左移一位把“符号位”顶掉,还原负的

    -----

    感觉这种就像在玩飞刀一样刺激,稍不注意就“刀起头落”。唉~正常编程内存会翻 5-10 倍的,试试奇淫技巧了
    lesismal
        19
    lesismal  
       2024-03-02 23:59:48 +08:00   1
    定时器的实现主要有两个点:
    1. 管理定时器的数据结构
    如果你用 c++ ,priority_queue 维护每个 fd 的超时时间:
    https://en.cppreference.com/w/cpp/container/priority_queue
    如果用 c ,找个或者自己实现个小堆也可以
    除非你对精确度要求非常低、时间轮间隔很小这种,否则真没必要:一是不精确,越想要精确则间隔越小越可能空跑,二是小堆做优先级队列基本是行业认可的最佳时间

    2. 定时器的触发器,简单点可以用 epoll_wait ,虽然秒级精确度但对于 read deadline 足够了,如果想更精确或者框架提供通用的精确定时器,可以用 timer_fd

    1 、2 结合起来,如果更新、设置超时时间都是在 epoll event loop 里,就是把 priority_queue 堆顶最小超时时间作为 epoll_wait 下一轮的 timeout 参数或者 timer_fd 的超时时间,如果跨线程设置还要考虑唤醒 epoll_wait 或者更新 timer_fd 相关

    这只是简单实现方案,涉及到完整框架的你还要考虑并发调用、锁、一致性等各种细节


    > 这个方法刚才试了试,发现接到(event[i].events & EPOLLIN)后,无法区分是 timer_fd 还是 socket_fd ,就不能直接 accept(),因为可能接到 accept(timer_fd),就会错误,在程序看来都是 fd 。

    #11 这就是说胡话了,你自己创建的 listener fd 、自己创建的 timer_fd ,你 switch case listener case timer default socket 一下就知道是哪个了,再不济,你存储 fd 对应的结构的地方,结构体加个字段标记 type 也就知道了
    lesismal
        20
    lesismal  
       2024-03-03 00:15:38 +08:00
    @lesismal #19

    =》除非你对精确度要求非常低、时间轮间隔很小这种,否则真没必要用时间轮:一是不精确,越想要精确则间隔越小越可能空跑,二是小堆做优先级队列定时器这种性能已经足够强、基本是行业认可的最佳实践

    虽然秒级 =》虽然毫秒级
    nuk
        21
    nuk  
       2024-03-03 01:57:18 +08:00
    倒是没必要用 timer ,可以用 3 个 epoll ,两个 epoll 间隔轮换来加入新的 fd ,然后轮换的时候清空另外一个 epoll 里所有的 fd ,然后 poll 有数据的放到第三个 epoll 里干活。
    huahsiung
        22
    huahsiung  
    OP
       2024-03-03 19:35:15 +08:00
    @lesismal

    我要接的不是 bind 和 listening 的一个 listen fd 连接,而是 accept(listen_fd)出来的几十万个 client_fd 连接。我也无法区分。


    另外:试了在每个 socket_fd 同时绑定一个 timer_fd ,文件描述符会膨胀 2 两倍。普通使用没有感觉,但是高并发测试下性能急剧下降。


    ------------------结帖-----------------

    ## 之前的奇淫技巧在 TCP 并发数超过 30 万+的时候指针会莫名其妙的跑飞,导致程序卡死无法退出。只能去掉这个。

    之中发现百度的服务器也没有进行超时处理,运行:

    `nc www.baidu.com 443`

    发现一直不发送数据,连接会一直保持。



    ## 百度也没处理,我也不处理了,就这样吧。

    另外,nginx 也可以加入
    ```ini
    client_body_timeout 5s;
    client_header_timeout 5s;
    ```
    来进行连接超时。

    使用 ab 测试,发现性能会略微下降

    # nginx 未加入超时

    ```txt
    Document Path: /
    Document Length: 146 bytes

    Concurrency Level: 2000
    Time taken for tests: 0.950 seconds
    Complete requests: 20000
    Failed requests: 14144
    (Connect: 0, Receive: 0, Length: 7072, Exceptions: 7072)
    Non-2xx responses: 12928
    Total transferred: 3736192 bytes
    HTML transferred: 1887488 bytes
    Requests per second: 21052.99 [#/sec] (mean)
    Time per request: 94.998 [ms] (mean)
    Time per request: 0.047 [ms] (mean, across all concurrent requests)
    Transfer rate: 3840.72 [Kbytes/sec] received

    Connection Times (ms)
    min mean[+/-sd] median max
    Connect: 0 40 7.8 40 60
    Processing: 16 50 12.4 51 76
    Waiting: 0 28 21.4 37 57
    Total: 58 90 8.7 90 104

    Percentage of the requests served within a certain time (ms)
    50% 90
    66% 93
    75% 97
    80% 98
    90% 101
    95% 103
    8% 103
    99% 103
    100% 104 (longest request)

    ```

    # nginx 加入 timeout 超时
    ```ini
    client_body_timeout 5s;
    client_header_timeout 5s;
    ```


    ```txt
    Document Path: /
    Document Length: 146 bytes

    Concurrency Level: 2000
    Time taken for tests: 0.971 seconds
    Complete requests: 20000
    Failed requests: 14464
    (Connect: 0, Receive: 0, Length: 7232, Exceptions: 7232)
    Non-2xx responses: 12768
    Total transferred: 3689952 bytes
    HTML transferred: 1864128 bytes
    Requests per second: 20604.20 [#/sec] (mean)
    Time per request: 97.068 [ms] (mean)
    Time per request: 0.049 [ms] (mean, across all concurrent requests)
    Transfer rate: 3712.33 [Kbytes/sec] received

    Connection Times (ms)
    min mean[+/-sd] median max
    Connect: 0 41 8.7 42 71
    Processing: 20 51 14.7 51 94
    Waiting: 0 29 22.9 38 74
    Total: 50 93 11.9 92 120

    Percentage of the requests served within a certain time (ms)
    50% 92
    66% 99
    75% 101
    80% 102
    90% 105
    95% 109
    98% 119
    99% 120
    100% 120 (longest request)

    ```

    ## 进行多次高并发测试,发现性能都低于。暂时没有探究原因

    # 就这样了,不处理了,结帖。谢谢大家的回答
    lesismal
        23
    lesismal  
       2024-03-03 22:02:05 +08:00   1
    合着我说的你根本就没好好看,或者看不懂:

    > 我要接的不是 bind 和 listening 的一个 listen fd 连接,而是 accept(listen_fd)出来的几十万个 client_fd 连接。我也无法区分。

    switch(fd) {
    case listenerfd:
    handle_accept()
    case timerfd:
    handler_timer()
    default: // 除去 listener 和 timerfd 就是已经 accept 了的 socket
    handle_socket()
    }

    再不济,你在 event 里那个 void*存储这个 fd 对应的结构体指针、或者只存一个 fd type 也是可以的

    > 另外:试了在每个 socket_fd 同时绑定一个 timer_fd ,文件描述符会膨胀 2 两倍。普通使用没有感觉,但是高并发测试下性能急剧下降。

    不需要每个 fd 一个 timer_fd ,一个 eventloop 只需要一个 timer_fd ,具体的你看我上一楼的回复吧


    我认认真真给 OP 写了一大段,OP 连看都不好好看就来随便回复,如果这次还不看,那请 OP 不要回复我了
    tuiL2
        24
    tuiL2  
       2024-03-03 22:50:03 +08:00
    这是应用层的问题,不是 epoll 和 socket 的问题
    realJamespond
        25
    realJamespond  
       2024-03-04 10:00:09 +08:00
    另外维护一个队列,每隔几秒更新一下超时的 fd
    huahsiung
        26
    huahsiung  
    OP
       2024-03-04 10:48:52 +08:00
    @lesismal 还是感谢你提供思路

    原来你的思路和我的不一样,设置一个堆 Heap ,每 5s 超时,取出堆顶最后的 fd ,进行 closed 吧。这种设计只有一个 timer_fd 。
    而我的是每一个 accept 后,就创建一个 timer_fd 。然后被挤爆了。

    C++倒简单,使用
    #include <queue>就行。

    但是我是 C 语言需要自己实现 堆 Heap ,确实比较麻烦,特别是维护几十万的数据。后来去抄 apache 的堆 Heap 实现。
    https://github.com/vy/libpqueue/blob/master/src/pqueue.c

    堆的删除只能在对顶进行,fd 接收数据后必须删除这个堆中的数据,但是没法删除堆中。
    想到的解决办法是设置 fd 标注位,fd 发现接收到数据后设置禁止 closed 的标志。


    期间把把多线程架构改为了多进程,去抄了 nginx 的 master/worker 方法,发现性能确实会提升。就是通信变复杂了

    其间发现,使用状态防火墙是最简单的,还不用改代码。状态防火墙会自动掐断空连接。
    lesismal
        27
    lesismal  
       2024-03-04 13:23:26 +08:00
    > 原来你的思路和我的不一样,设置一个堆 Heap ,每 5 超时,取出堆顶最后的 fd ,进行 closed 吧。这种设计只有一个 timer_fd 。

    用堆就不是固定 5s 超时了,而是根据堆顶的超时时间设置超时时间。
    也不是只取出堆顶,因为代码可能导致延迟误差,所以是需要循环查看堆顶是否超时、超时就 close ,没超时则更新当前堆顶的超时时间为触发器的超时时间


    > 而我的是每一个 accept 后,就创建一个 timer_fd 。然后被挤爆了。

    这种是最浪费的方式之一,没必要拿来对比,应该用来改进


    > 堆的删除只能在对顶进行,fd 接收数据后必须删除这个堆中的数据,但是没法删除堆中。

    堆可以删除任意元素,up 、down fix 位置就可以

    红黑树也可以用来做这个,但这个场景堆比红黑树要好
    ben666
        28
    ben666  
       2024-03-20 08:49:55 +08:00
    一般连接里面要放多个定时器,读超时、写超时、idle 超时等,可以自己实现一个定时器,每个连接有一个定时器,可以是时间轮定时器,也可以是 rbtree 定时器。
    大体如下:
    struct connection {
    int fd;
    struct timer read_timer;
    struct timer write_timer;
    struct timer idle_timer;
    };

    可以参考:
    - nginx 的连接 强烈推荐 https://github.com/nginx/nginx/blob/master/src/core/ngx_connection.h
    - dpvs 的连接 https://github.com/iqiyi/dpvs/blob/master/include/ipvs/conn.h
    - dperf 的连接是用单链表队列做超时的 https://github.com/baidu/dperf/blob/main/src/socket.h
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     867 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 19:22 PVG 03:22 LAX 12:22 JFK 15:22
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86