首页 > 学技术 > 技术网文 > C/C++ > 正文

[保留] 惊群(thundering herd)问题在linux上可能是莫须有的问题


来源 chinaunix.net kuqin整理

"惊群"问题在linux上存在吗? 

有文章,还有权威书支持,还有解决方法。

在UNIX网络编程第1卷里有提到。当某一时刻只有一个连接过来时,N个睡眠进程会被同时叫醒,但只有一个进程可获得连接。如果每次唤醒的进程数目太多,会影响一部分系统性能 . 


大家可以编一个程序实验看存在与否?程序就是fork, 让多个进程(或THREAD)都阻塞在accept 同一个sock描述符。当有连接进来,你看是唤醒几个进程?一个还是多个?

[ 本帖最后由 思一克 于 2007-6-8 10:11 编辑 ]



 converse 回复于:2007-06-07 16:11:02

恩,今天提到这个问题的时候我去找lighttpd1.4.15的代码来看,发现里面的做法也是fork进程进行accept的.


 converse 回复于:2007-06-07 16:11:35

>>当有连接进来,你看是唤醒几个进程?一个还是多个? 

"唤醒进程"如何知道呢?


 思一克 回复于:2007-06-07 16:15:15

我跟踪了tcp_accept,

阻塞在wait_for_connect, 而这个函数根本就是用prepare_to_wait_for_exclusive,

函数的说明也是仅仅唤醒一个。 “only one process gets woken up".

跟踪的结果也是一个。


 福瑞哈哥 回复于:2007-06-07 16:21:39

引用:原帖由 思一克 于 2007-6-7 16:15 发表
我跟踪了tcp_accept,

阻塞在wait_for_connect, 而这个函数根本就是用prepare_to_wait_for_exclusive,

函数的说明也是仅仅唤醒一个。 “only one process gets woken up".

跟踪的结果也是一个。 



思一克兄好強啊!這麼深入。


 思一克 回复于:2007-06-07 16:24:31

你看net/ipv4/tcp.c
中的
wait_for_connect函数,里面有说明,仅仅唤醒一个进程

accept()调用sys_accept, 然后是tcp_accept.


 converse 回复于:2007-06-07 16:25:00

引用:原帖由 思一克 于 2007-6-7 16:15 发表
我跟踪了tcp_accept,

阻塞在wait_for_connect, 而这个函数根本就是用prepare_to_wait_for_exclusive,

函数的说明也是仅仅唤醒一个。 “only one process gets woken up".

跟踪的结果也是一个。 



有什么办法可以在应用层面不看协议栈内核源码的情况下证明这个结果的?


 思一克 回复于:2007-06-07 16:26:46

在accept()后面加printf()看有没有返回就可以。

如果唤醒多个,必然有返回。

引用:原帖由 converse 于 2007-6-7 16:25 发表


有什么办法可以在应用层面不看协议栈内核源码的情况下证明这个结果的? 




 Sorehead 回复于:2007-06-07 16:29:09

个人觉得惊群的问题应该是存在的,只是内核把这个问题给解决了,不在应用层表现出来。
记得在richard stevens的书里有例子,进程或者线程越多,效率就越低,我觉得这个就是证据。
谁看过这方面内核代码,知道具体情况的告知一下真实情况,我也学习一下,谢谢。

[ 本帖最后由 Sorehead 于 2007-6-7 16:36 编辑 ]


 converse 回复于:2007-06-07 16:35:07

引用:原帖由 思一克 于 2007-6-7 16:26 发表
在accept()后面加printf()看有没有返回就可以。

如果唤醒多个,必然有返回。

 



我觉得这个不足以作为证据的.
网络编程第一卷里面说的是那些accept失败的进程会继续阻塞住,也就是说,这些进程应该不会执行后面的这句printf.


 思一克 回复于:2007-06-07 16:38:11

在KERNEL中仅仅唤醒一个进程。

引用:原帖由 Sorehead 于 2007-6-7 16:29 发表
个人觉得惊群的问题应该是存在的,只是内核把这个问题给解决了,不在应用层表现出来。
谁看过这方面内核代码,知道具体情况的告知一下真实情况,我也学习一下,谢谢。 




 思一克 回复于:2007-06-07 16:39:41

所以我才看KERNEL中的wait_for_connect()唤醒不。就是有同样的疑问

引用:原帖由 converse 于 2007-6-7 16:35 发表


我觉得这个不足以作为证据的.
网络编程第一卷里面说的是那些accept失败的进程会继续阻塞住,也就是说,这些进程应该不会执行后面的这句printf. 




 converse 回复于:2007-06-07 17:02:06

还得麻烦思版主也看看select函数是不是也会有惊群问题....前面你看的是accept函数的实现.


 思一克 回复于:2007-06-07 17:12:35

其它的再研究。accept是没有的。

我又看了KERNEL的wake_up_XXXX,它唤醒多个等待某资源(EXCLUSIVE)的进程其中的一个比唤醒全部代价还小,它为什么自己找麻烦唤醒所有的?

引用:原帖由 converse 于 2007-6-7 17:02 发表
还得麻烦思版主也看看select函数是不是也会有惊群问题....前面你看的是accept函数的实现. 




 思一克 回复于:2007-06-07 17:22:02

TO converse,

你可以写个程序,让多个进程阻塞在select 同一个sock, 加printf, 看是唤醒多个还是一个。


引用:原帖由 converse 于 2007-6-7 17:02 发表
还得麻烦思版主也看看select函数是不是也会有惊群问题....前面你看的是accept函数的实现. 




 yulc 回复于:2007-06-08 10:03:28

版主的专研精神,真是PFPF...
纠正了我很长时间来,在linux中对于多进程服务器设计上的一个细节概念.
由于在Apache2的prefork MPM的模型中,是在accpet前面加锁,所以我潜意识中认为这种设计是更优的.
但却从未考虑过accpet()的"惊群"问题.
原来,这个问题在早期的2.2的kernel中,早就讨论过.至从2.2.9的内核起,这个问题就不复存在了.

在英文中,"惊群"原来是"thundering herd"
http://www.citi.umich.edu/projects/linux-scalability/reports/accept.html


 醉卧水云间 回复于:2007-06-08 10:08:35

各操作系统都不存在这个问题, 太古老了.


 思一克 回复于:2007-06-08 10:09:25

TO yulc,

问题不存在了从2。2。9? 谢谢你的结论。

估计是早期的什么地方存在。后来的应该不存在了。

因为KERNEL在该唤醒许多PROCESS中的一个时候,它唤醒一个是很简单和自然的事情,为什么不这样?

如果唤醒许多,执行了又回到等待(甚至连KERNEL都没有走出来),那浪费时间空间,又没有意义。所以
从LINUX某版本后,该问题应该没有了。

所以,也不能说权威书是完全错的。对原来可能是对的。


引用:原帖由 yulc 于 2007-6-8 10:03 发表
版主的专研精神,真是PFPF...
纠正了我很长时间来,在linux中对于多进程服务器设计上的一个细节概念.
由于在Apache2的prefork MPM的模型中,是在accpet前面加锁,所以我潜意识中认为这种设计是更优的.
但却从未考虑 ... 




 flw2 回复于:2007-06-08 12:12:40

accept来说,资源是EXCLUSIVE
但是select就不是了,一个文件可以读,如果有两个进程要读,那么应该唤醒两个。


 flw2 回复于:2007-06-08 12:14:00

就算唤醒n个进程,如果得不到资源,那么也到不了user态了,马上又阻塞的kernel态


 flw2 回复于:2007-06-08 12:40:30

通常我么
poll+read的时候都没有注意,如果是多个进程,那么我们很可能需要在poll返回时非阻塞的读,否则,即使poll返回了,也可能导致后果很严重的阻塞


 思一克 回复于:2007-06-08 12:42:49

to converse,

我实验select了,一个进程fork 多个CHILD读阻塞在read pipe. 和用select阻塞在select 
main进程写数据。

结果是一有数据,所有进程被唤醒。

我想这是KERNEL故意为之。因为KERNEL不知道用户的心思。有数据了,数据很可能是要分给多个读者的。
KERNEL不能假设数据是给一个读者的。因此要WAKEUP所有的。

select是如此, read pipe也是如此。

但accept完全不同,kernel知道仅仅一个可以accept. 不会有2个同时accept一个进入连接。


引用:原帖由 flw2 于 2007-6-8 12:14 发表
就算唤醒n个进程,如果得不到资源,那么也到不了user态了,马上又阻塞的kernel态 



[ 本帖最后由 思一克 于 2007-6-8 12:43 编辑 ]


 flw2 回复于:2007-06-08 12:57:20

版主说的其实是一个waker函数决定的。
阻塞在某个地方的代码处,那么对应的waker也知道该怎么去唤醒等待队列。原则就是是不是排他的。它自己知道
对于poll,select,就算给一个进程读,也有可能本来有100字节,可是每个进程只要40个字节,那么肯定要唤醒,内核waker并不知道进程将来的read要读多少字节。即使只有3字节,有100个进程在等也要唤醒,它不会去检查


 思一克 回复于:2007-06-08 13:04:10

对。所以read, select ( 应该也包含poll), 都是要唤醒所有的等待进程。因为可能要分发数据。
accept没有分发数据问题,是唯一的,所以唤醒一个。

还有一个误区, 认为唤醒后立即检查如果没有资源在KERNEL中立即睡下,不返回用户空间。
我看了sys_poll, sys_select等的代码,没有这中机制(可能)。醒了没有几条就返回USER空间了。

所以在用户空间加printf测试应该没有问题,至少对于,select, poll, read等是这样。


 福瑞哈哥 回复于:2007-06-08 13:07:51

引用:原帖由 flw2 于 2007-6-8 12:40 发表
通常我么
poll+read的时候都没有注意,如果是多个进程,那么我们很可能需要在poll返回时非阻塞的读,否则,即使poll返回了,也可能导致后果很严重的阻塞 


阻塞式read在任何時候都可能被阻塞,除非真的要讀完數據,否則不能用blocking read。


 思一克 回复于:2007-06-08 13:10:27

根据研究结果,多个进程直接accept一个sock是没有问题的好程序。
但不好的程序是(如果能这样编的话)

 r = select(........);  //或 poll(....)
 if(r) {
    fd1 = accept(.........):
 }

这样有惊群问题,但不是KERNEL的问题,而是一个不好的用户程序造成的。

还有这样也是不好的:
 lock(...);
 accept(....);
 unlock(...);

程序慢了,无谓的牺牲。

[ 本帖最后由 思一克 于 2007-6-8 13:11 编辑 ]


 baohuaihuai 回复于:2007-06-08 13:40:51

不知道为啥,看到"惊群"这个名字让我有点发慌.....


 oldunix 回复于:2007-06-08 14:32:52

引用:原帖由 baohuaihuai 于 2007-6-8 13:40 发表
不知道为啥,看到"惊群"这个名字让我有点发慌..... 



万马奔腾

其实就是一个资源被释放了,等待该资源的一堆线程被唤醒,有个幸运儿获得,其他不幸的家伙一个个回去睡觉,如此反复!


 思一克 回复于:2007-06-08 15:04:39

我这个帖子说的不完全对。

对listen()后的描述符进行select, poll 好象不会“惊群”,不会象对文件描述符或accepted()后的sock那样.

listen的描述符特别处理,和accept()的情况一样。

没有时间精确测试,有兴趣的可以实验。


引用:原帖由 思一克 于 2007-6-8 13:10 发表
根据研究结果,多个进程直接accept一个sock是没有问题的好程序。
但不好的程序是(如果能这样编的话)

 r = select(........);  //或 poll(....)
 if(r) {
    fd1 = accept(.........):
 }

这样有惊群 ... 




 笨 回复于:2007-06-08 15:18:53

如果只有 2 个进程来 accept 1 个 socket, 那么是否能够认为在大量的客户端 connect 后, 着两个进程 accepted 的 socket 的个数相当?

我比较关心这个问题, 呵呵


 思一克 回复于:2007-06-08 15:23:50

各个进程的负载均衡问题。

如果accept后有一点事情做(比如转移数据,建立新孩子进程),那么应该是均衡的。
如果accept后立即close, 就不一定。

以上是猜测。需要程序实验证实。


 笨 回复于:2007-06-08 16:47:23

验证过了, 在大数量的客户端连接后,两个进程的确 accepted 的 socket 个数相当。

两个进程很简单, 在 accept 函数返回 后,什么事情都不做,继续调用 accept, 呵呵

我的测试环境是:  FC5, 双核CPU, 2G内存, 测试时基本上没有其它进程在运行。

我想知道, kernel 怎么做到 均衡分配的,呵呵


 思一克 回复于:2007-06-08 16:51:22

你实验了。好呀

大量连接进入,KERNEL有队列,一个个分基本上就是均衡的。


引用:原帖由  于 2007-6-8 16:47 发表
验证过了, 在大数量的客户端连接后,两个进程的确 accepted 的 socket 个数相当。

两个进程很简单, 在 accept 函数返回 后,什么事情都不做,继续调用 accept, 呵呵

我的测试环境是:  FC5, 双核CPU,  ... 




 xhl 回复于:2007-06-08 17:04:28

借这里问个跟相关的问题。

假如有两个进程A, B, A首先bind udp port 10000, 同时打开reuse功能。
随后B进程bind udp port 10000. 

在这种情况下,如果本机器port 10000上有数据到达的时候, kernel会换醒那个进程?

是否能同时唤醒两个进程呢?

如果是唤醒一个进程,
是每次唤醒固定的进程(A or B), 还是随机的呢?


 思一克 回复于:2007-06-08 17:08:18

你写个测试程序发上来,如果有兴趣

引用:原帖由 xhl 于 2007-6-8 17:04 发表
借这里问个跟相关的问题。

假如有两个进程A, B, A首先bind udp port 10000, 同时打开reuse功能。
随后B进程bind udp port 10000. 

在这种情况下,如果本机器port 10000上有数据到达的时候, kernel会换醒 ... 




 yulc 回复于:2007-06-08 17:21:51

引用:原帖由 思一克 于 2007-6-8 13:10 发表
根据研究结果,多个进程直接accept一个sock是没有问题的好程序。
但不好的程序是(如果能这样编的话)

 r = select(........);  //或 poll(....)
 if(r) {
    fd1 = accept(.........):
 }

这样有惊群 ... 





对于在accept()前面调用select,  我一直觉得奇怪.
这样的模型有什么更优雅的地方? 我是从未这么设计过.

还有,版主认为在accept()前面调用lock()并不好,这一点我还是有点疑惑.
那为什么apache依然不改,而且在源码中的表现是为了安全,所以调用lock()先.
是为了兼容其它系统吗?即使这样也可以为linux加个宏呀..


 思一克 回复于:2007-06-08 17:24:22

我没有研究过APACHE代码。

accept()前lock什么?看有无和多进程/县城的lock必要,而不是为了accept自己的?

有时间我看一下apache的网络部分代码。

引用:原帖由 yulc 于 2007-6-8 17:21 发表




对于在accept()前面调用select,  我一直觉得奇怪.
这样的模型有什么更优雅的地方? 我是从未这么设计过.

还有,版主认为在accept()前面调用lock()并不好,这一点我还是有点疑惑.
那为什么apache依然不改 ... 




 yulc 回复于:2007-06-08 17:30:21

引用:原帖由 思一克 于 2007-6-8 17:24 发表
我没有研究过APACHE代码。

accept()前lock什么?看有无和多进程/县城的lock必要,而不是为了accept自己的?

有时间我看一下apache的网络部分代码。

 




好, prefork mpm的源码目录在 apache.*.tar.gz包中:
.\server\mpm\prefork\

这个源代码影响了我好几年呀...


 思一克 回复于:2007-06-08 21:03:10

我看了一下apache2.049(?)

在文件docs/manual/misc/perf-tunnng.html.en 中有详细解释lock accept的原因.
文件我记忆不是绝对清楚. 你可以grep -r "accept lock" *  或 grep -r "SINGLE_SOCKET" *
找到文件.

那里说2中方式:
1) 多个socket同时accept
2) 一个socket同时accept

使用lock关键原因还是原来的系统thurndering herd问题.

那里还说了,单个socket多进程accept也有"在KERNEL中唤醒所有的然后多数又在KERNEL中再睡下"的而浪费CPU的说法.

这些问题在LINUX 2.6.X 是不存在的.

看来, 在许多系统上, "唤醒一群" 原来的确是问题. 否则APACHE不会那样做.

但那个文件中的说法在新LINUX中却是不对的---比如再直接睡下的问题.



引用:原帖由 yulc 于 2007-6-8 17:30 发表



好, prefork mpm的源码目录在 apache.*.tar.gz包中:
.\server\mpm\prefork\

这个源代码影响了我好几年呀... 




 信赖CU 回复于:2007-06-09 11:27:29

引用:原帖由 思一克 于 2007-6-7 16:24 发表
你看net/ipv4/tcp.c
中的
wait_for_connect函数,里面有说明,仅仅唤醒一个进程

accept()调用sys_accept, 然后是tcp_accept. 


:lol:单CPU肯定是唤醒一个。一个CPU只能在同一时间作一件事根本不用跟踪。SMP的就不一样了


 思一克 回复于:2007-06-09 13:02:57

单CPU也不一定总是唤醒一个.

比如,你select pipe描述符, 一有数据就唤醒每一个. 尽管是单CPU.

一个CPU每时间做一个事情和唤醒多个没有矛盾. 说的不是一回事情.

引用:原帖由 信赖CU 于 2007-6-9 11:27 发表

:lol:单CPU肯定是唤醒一个。一个CPU只能在同一时间作一件事根本不用跟踪。SMP的就不一样了 



[ 本帖最后由 思一克 于 2007-6-9 13:04 编辑 ]


 信赖CU 回复于:2007-06-09 19:22:39

引用:原帖由 思一克 于 2007-6-9 13:02 发表
单CPU也不一定总是唤醒一个.

比如,你select pipe描述符, 一有数据就唤醒每一个. 尽管是单CPU.

一个CPU每时间做一个事情和唤醒多个没有矛盾. 说的不是一回事情.

 


:m01::m01:实际上真正醒的只有一个。


 思一克 回复于:2007-06-11 08:57:30

不要将“已经唤醒的”在运行的进程和此刻才CPU上运行的混淆了。

在一个CPU机器上
[CODE]
main()
{
int i;
   for(i = 0; i < 3; i++) fork();
   for(;;) ;
}
[/CODE]
这个程序运行后,你top看有几个进程正在运行?是8个以上。但实际每时刻仅仅一个。唤醒所有的进程也是这样。


引用:原帖由 信赖CU 于 2007-6-9 19:22 发表

:m01::m01:实际上真正醒的只有一个。 



[ 本帖最后由 思一克 于 2007-6-11 08:58 编辑 ]


 yulc 回复于:2007-06-11 09:34:02

引用:原帖由 思一克 于 2007-6-8 21:03 发表
我看了一下apache2.049(?)

在文件docs/manual/misc/perf-tunnng.html.en 中有详细解释lock accept的原因.
文件我记忆不是绝对清楚. 你可以grep -r "accept lock" *  或 grep -r "SINGLE_SOCKE ... 




apache源码包中的文档,已经很久没去看过了..
我想是文档已经过时了,
那上面用的例子都是说在p166CPU/128M内存的机器上,内核是2.0.....




原文链接:http://bbs.chinaunix.net/viewthread.php?tid=946261
转载请注明作者名及原文出处



收藏本页到: