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

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

2679 次点击
所在节点    程序员
28 条回复
nuk
245 天前
倒是没必要用 timer ,可以用 3 个 epoll ,两个 epoll 间隔轮换来加入新的 fd ,然后轮换的时候清空另外一个 epoll 里所有的 fd ,然后 poll 有数据的放到第三个 epoll 里干活。
huahsiung
244 天前
@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)

```

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

# 就这样了,不处理了,结帖。谢谢大家的回答
lesismal
244 天前
合着我说的你根本就没好好看,或者看不懂:

> 我要接的不是 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
244 天前
这是应用层的问题,不是 epoll 和 socket 的问题
realJamespond
243 天前
另外维护一个队列,每隔几秒更新一下超时的 fd
huahsiung
243 天前
@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
243 天前
> 原来你的思路和我的不一样,设置一个堆 Heap ,每 5s 超时,取出堆顶最后的 fd ,进行 closed 吧。这种设计只有一个 timer_fd 。

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


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

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


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

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

红黑树也可以用来做这个,但这个场景堆比红黑树要好
ben666
227 天前
一般连接里面要放多个定时器,读超时、写超时、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

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

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

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

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

© 2021 V2EX