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 也不会通知
网上找到两个方法:
epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
改为
epoll_wait(epoll_fd, events, MAX_EVENTS, 10);
没用,这是 epoll 事件超时,而不是连接超时。
epoll_wait 返回的是活跃事件,如果不发送任何数据,epoll_wait 不会返回这个事件的 fd
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不拉屎就断开。
阻塞情况下很好解决,但是非阻塞暂时没想到好办法。
1
lcdtyph 289 天前 via iPhone 1
每个 socket 绑定一个 timer ,一起 epoll 就好了
|
2
PythonYXY 289 天前
我看 chatgpt 给出的答案是利用 setsockopt 函数的 SO_RCVTIMEO 和 SO_SNDTIMEO 选项,不过我没试过
|
3
choury 289 天前 1
搜下 TCP_DEFER_ACCEPT
|
4
PythonYXY 289 天前 1
没看到原来 OP 已经试过 setsockopt 了。。。我的想法是定时发送心跳信息,将长时间不响应的 fd 给手动剔除
|
5
henix 289 天前 1
这个应该要应用自己维护的吧。记录每个 fd 上一次操作的时间戳,你的第一种方法,epoll_wait 会在中途返回,返回时检查当前时间戳跟记录的 fd 上一次操作时间戳之差,如果超时了就执行某个动作,比如关闭连接。
这里采用的数据结构是堆( heap )或者时间轮( timing wheel ) 可参考 [Linux 多线程服务端编程]( https://book.douban.com/subject/20471211/) 的“7.10 用 timing wheel 踢掉空闲连接”一章 |
6
BBCCBB 288 天前 1
要实现应用层的心跳.
|
7
roykingz 288 天前 1
你说的这个特性,Linux 的 TCP_DEFER_ACCEPT 标志可以支持,Nginx 源码中大量使用,Freebsd 中也有类似的特性,叫做 SO_ACCEPTFILTER
|
8
roykingz 288 天前
不过,这个特性是延迟通知进程,要解决 ESTABLISHED 上一直不发数据的情况,应该还是得靠自己维护超时时间来检查,时间轮用的比较多
|
9
Sephirothictree 288 天前 1
开个线程做 select 设置阻塞超时来监控非阻塞的 client_fd ,到时间或者可读,就超时踢人或者干活 recv (成功转回阻塞逻辑了 2333 ,不过感觉还是 1 楼方案比较省事
|
10
Nazz 288 天前 via Android 1
网络库都有 SetDeadline 吧
|
11
huahsiung OP @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 左右,全靠 epoll 的通知和内存 pointer 撑住的。遍历上万的 fd 的话时间轮这样内存会膨胀 2~3 倍,CPU 上下文切换时间也会激增。 ---- **最后的解决方法是暂时不解决,毕竟几十万左右的 TCP 连接才 4k~6k 的僵尸连接,好像也不是影响很大。** 不知道怎么把 fd 省内存的加入超时队列,我是直接把 fd CRC32 放入类似 hash 表的,但是连接过多 hash 会撞的。 |
12
huahsiung OP @Sephirothictree
select 好像不行啊,连接太多,不够用。 ----- @Nazz SetDeadline 是 go 语言的,C/C++的库好像没用 go 语言用 go route 起几十万个连接,内存会高达 10G+的。 |
13
lcdtyph 288 天前
@huahsiung #11
啊这,你要用 event[i].data.ptr 来给这个 fd 一个私有数据结构,这样可以帮助你区分这个 fd 是什么,或者维护一些 fd 相关的上下文 |
14
huahsiung OP @lcdtyph 为了省内存,我把 event[i].data.ptr 的指针当作 long long int(x64 位)用的
后来发现这个东西不会触发,不知道哪里问题。 x64 系统正常运行(除了不会触发超时) x86 系统直接“段错误” |
16
Sephirothictree 288 天前
@huahsiung 试试 poll 不限制 fd 数量,跟 select 差不多,不过这么多连接,就不知道 poll 效率上能不能行了
|
17
lcdtyph 288 天前 via iPhone
|
18
huahsiung OP @lcdtyph 准备 x86 架构直接放弃了吧,就只能在 64 位上面运行。
准备上数据的时候把 把 pointer 的最高位(符号位) 与 0x00FFFFFF 让 pointer 变成正的。下数据使用的时候再把左移一位把“符号位”顶掉,还原负的 ----- 感觉这种就像在玩飞刀一样刺激,稍不注意就“刀起头落”。唉~正常编程内存会翻 5-10 倍的,试试奇淫技巧了 |
19
lesismal 288 天前 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 也就知道了 |
20
lesismal 288 天前
@lesismal #19
=》除非你对精确度要求非常低、时间轮间隔很小这种,否则真没必要用时间轮:一是不精确,越想要精确则间隔越小越可能空跑,二是小堆做优先级队列定时器这种性能已经足够强、基本是行业认可的最佳实践 虽然秒级 =》虽然毫秒级 |
21
nuk 288 天前
倒是没必要用 timer ,可以用 3 个 epoll ,两个 epoll 间隔轮换来加入新的 fd ,然后轮换的时候清空另外一个 epoll 里所有的 fd ,然后 poll 有数据的放到第三个 epoll 里干活。
|
22
huahsiung OP @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 98% 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) ``` ## 进行多次高并发测试,发现性能都低于。暂时没有探究原因 # 就这样了,不处理了,结帖。谢谢大家的回答 |
23
lesismal 287 天前 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 不要回复我了 |
24
tuiL2 287 天前
这是应用层的问题,不是 epoll 和 socket 的问题
|
25
realJamespond 287 天前
另外维护一个队列,每隔几秒更新一下超时的 fd
|
26
huahsiung OP @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 方法,发现性能确实会提升。就是通信变复杂了 其间发现,使用状态防火墙是最简单的,还不用改代码。状态防火墙会自动掐断空连接。 |
27
lesismal 287 天前
> 原来你的思路和我的不一样,设置一个堆 Heap ,每 5s 超时,取出堆顶最后的 fd ,进行 closed 吧。这种设计只有一个 timer_fd 。
用堆就不是固定 5s 超时了,而是根据堆顶的超时时间设置超时时间。 也不是只取出堆顶,因为代码可能导致延迟误差,所以是需要循环查看堆顶是否超时、超时就 close ,没超时则更新当前堆顶的超时时间为触发器的超时时间 > 而我的是每一个 accept 后,就创建一个 timer_fd 。然后被挤爆了。 这种是最浪费的方式之一,没必要拿来对比,应该用来改进 > 堆的删除只能在对顶进行,fd 接收数据后必须删除这个堆中的数据,但是没法删除堆中。 堆可以删除任意元素,up 、down fix 位置就可以 红黑树也可以用来做这个,但这个场景堆比红黑树要好 |
28
ben666 271 天前
一般连接里面要放多个定时器,读超时、写超时、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 |