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

68 天前
 huahsiung

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不拉屎就断开。
阻塞情况下很好解决,但是非阻塞暂时没想到好办法。

2374 次点击
所在节点    程序员
28 条回复
lcdtyph
68 天前
每个 socket 绑定一个 timer ,一起 epoll 就好了
PythonYXY
68 天前
我看 chatgpt 给出的答案是利用 setsockopt 函数的 SO_RCVTIMEO 和 SO_SNDTIMEO 选项,不过我没试过
choury
68 天前
搜下 TCP_DEFER_ACCEPT
PythonYXY
68 天前
没看到原来 OP 已经试过 setsockopt 了。。。我的想法是定时发送心跳信息,将长时间不响应的 fd 给手动剔除
henix
68 天前
这个应该要应用自己维护的吧。记录每个 fd 上一次操作的时间戳,你的第一种方法,epoll_wait 会在中途返回,返回时检查当前时间戳跟记录的 fd 上一次操作时间戳之差,如果超时了就执行某个动作,比如关闭连接。

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

可参考 [Linux 多线程服务端编程]( https://book.douban.com/subject/20471211/) 的“7.10 用 timing wheel 踢掉空闲连接”一章
BBCCBB
67 天前
要实现应用层的心跳.
roykingz
67 天前
你说的这个特性,Linux 的 TCP_DEFER_ACCEPT 标志可以支持,Nginx 源码中大量使用,Freebsd 中也有类似的特性,叫做 SO_ACCEPTFILTER
roykingz
67 天前
不过,这个特性是延迟通知进程,要解决 ESTABLISHED 上一直不发数据的情况,应该还是得靠自己维护超时时间来检查,时间轮用的比较多
Sephirothictree
67 天前
开个线程做 select 设置阻塞超时来监控非阻塞的 client_fd ,到时间或者可读,就超时踢人或者干活 recv (成功转回阻塞逻辑了 2333 ,不过感觉还是 1 楼方案比较省事
Nazz
67 天前
网络库都有 SetDeadline 吧
huahsiung
67 天前
@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 会撞的。
huahsiung
67 天前
@Sephirothictree

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

-----

@Nazz

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

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



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



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

x86 系统直接“段错误”
huahsiung
67 天前
@huahsiung 得到答案了,好像是编译器问题,编译器认为指针不可能为负数,帮我把“负指针”优化了。
Sephirothictree
67 天前
@huahsiung 试试 poll 不限制 fd 数量,跟 select 差不多,不过这么多连接,就不知道 poll 效率上能不能行了
lcdtyph
67 天前
@huahsiung
都 x86 了还要省内存吗

而且你最好用 intptr_t int_ptr = (intptr_t)data.ptr;
x86 和 amd64 的指针长度不一样
huahsiung
67 天前
@lcdtyph 准备 x86 架构直接放弃了吧,就只能在 64 位上面运行。

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

-----

感觉这种就像在玩飞刀一样刺激,稍不注意就“刀起头落”。唉~正常编程内存会翻 5-10 倍的,试试奇淫技巧了
lesismal
67 天前
定时器的实现主要有两个点:
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
67 天前
@lesismal #19

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

虽然秒级 =》虽然毫秒级

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1020072

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX