本文的copyleft归gfree.wind@gmail.com所有,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接,严禁用于任何商业用途。
作者:gfree.wind@gmail.com
博客:linuxfocus.blog.chinaunix.net
今天有一个客户问题,问题的现象的大致情形如下:有两个不同的daemon服务进程,负责不同的服务。在某种情况下,进程A可以作为进程B的一个代理。某些client在通过进程A的认证后,通过进程A获得进程B分配的资源。客户的问题是在认证过后,无法获得进程B分配的资源。
我检查了log,在问题发生的时候,进程B有分配资源的记录,而且从时间上看,分配并没有耗费太多的时间。那么是否是进程A的超时机制处理有问题,导致进程A错误的判断超时了呢。比如没有考虑时间溢出的问题。因为进程A的代码不是我写的,所以我重新review了一些代码。发现虽然代码还是有些问题,不仅没有考虑到时间溢出的问题,而且在保存时间和比较时,使用的是有符号数。这样即使没有到时间溢出的时候,有符号的时间值就可能会被当成负数了,从而引发问题。不过按照当前时间,可以排除这个情况。还有其它的一些小问题,也不会有大的影响。
这样,排除了超时的错误后,我检查了进程A用于向进程B发送请求的代码,果然发现了问题。进程A是一个多线程的进程。估计是当时为了简单处理和并发性,在向进程B发送请求时,使用的是局部变量socket来发送UDP请求给B,而不是有一个独立的线程来做这件事情。且在使用socket时,又bind了本地地址和知名的本地端口。而为了多个socket可以同时bind本地地址和相同端口,特意在bind前,设置了SO_REUSEADDR。结果,恰恰是这种行为导致了这个bug的发生!之所以要使用固定端口,因为进程B这个协议,在发送response时,不会根据client发过来的port回复,而只是回复到client的知名端口,所以不能进程A不能使用随机端口。
在揭开谜底之前,让我们先温习一下究竟在什么情况下需要使用SO_REUSEADDR呢。已故的W.Richard Stevens大师,在他的UNP1中列举了四种情况。
- 当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启
动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。另外作为服务daemon一般都会使用SO_REUSEADDR,避免服务意外崩溃而原有socket还未被kernel释放时,重启的daemon仍然可以bind成功。 - SO_REUSEADDR允许同一port上启动同一服务器的多个实例。但
每个实例绑定的IP地址是不能相同的。在有多块网卡或用IP Alias技术的机器可
以测试这种情况。 - SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个soc
ket绑定的ip地址不同。这和2很相似,区别请看UNPv1。 - SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的
多播,不用于TCP。
而当前的情况并不属于这4种的任何一种。那么这时,假设进程A同时打开了5个socket,并都成功bind了本地地址和相同的端口,且发送了请求给进程B。那么当进程B回应时,是什么样的情形呢?
按照第一感觉,似乎kernel应该把数据包发给每一个socket,也就是说如果这5个socket没有关闭,每个socket都应该收到5个数据包。——至少以前的我,会这么理解。
可现实往往与人们的第一感觉向左的。linux kernel在收到数据包,除了raw socket,只会选择一个最匹配的socket来接收数据包。以UDP为例,执行这一个工作的是__udp4_lib_lookup_skb函数。通过比对源地址,源端口,目的地址,目的端口以及bind的网卡,来找到最匹配的socket。使用compute_score这个函数来计算每个socket的分数,具体行为请参考kernel代码。当没有完全匹配的时候,才会匹配具有通配地址或端口的socket。
正因为linux kernel的这一行为,导致了这5个socket中,只有1个socket收到了所有的5个数据包。而这个socket只关心其中的一个并成功处理而后关闭socket。而其他4个socket则根本收不到任何数据包。这时这4个socket重发请求,而4个回复仍然只会被其中的1个socket收到,而导致另外3个socket失败。这里之所以每次都是1个socket收到所有的数据包,是因为每个socket都bind相同的地址和端口,那么对于kernel来说,这些socket的匹配得分肯定都相等。那么每次数据包到来,kernel都只会选择第一个socket。
通过上面的讲解,就可以确定了正是对SO_REUSEADDR的滥用,才导致了这个bug。那么,究竟怎样解决这个bug呢。我想出了2个方案:
1. 在进程A中,创建一个新的线程,专门用于向进程B发送请求和接受回复,来代替当前的机制。不过这样的改动会比较大些。
2. 利用linux中raw socket的特性——linux会复制所有符合raw socket过滤条件(使用bind和connect)的数据包到每一个raw socket,替换现有的UDP socket。这样每个socket都可以收到所有的回复,当然只会处理对应该socket发送出的请求的回复。这样的改动最简单,不过在效率上稍微差了一点。
根据我们的情况,因为通过进程A来申请进程B资源的客户请求并不经常使用。所有,最终,我选择使用raw socket这个方案来解决这个Bug。
这次我能够很快的发现这个根本原因。主要是因为这段时间我正在学些linux的TCP/IP的源代码。所以在看到进程A使用SO_REUSEADDR的情况时,直接想起了kernel挑选socket的最佳匹配策略。因此,直接找到了根本原因。可见,即使我们并不是kernel 开发人员,也要尽量的多了解kernel的内部原理和机制,对于应用层的开发有着极大的益处——如果我不知道这个匹
配策略,估计这个bug需要我研究好几天,而非今天的迅速解决。
另外从这个bug中,我们也应该得到教训。切莫滥用SO_REUSEADDR,只能在正确的情况下使用!不然的话,等着bug吧
阅读(7288) | 评论(0) | 转发(2) |