epoll边沿触发漏报消息包问题

开发一个即时通讯后台,底层的网络收发使用 epoll + main loop实现网络事件(conn、read、send)的异步触发读写,以达到能最大限度减少线程的IO等待,提高cup利用率的目的。

实际使用中,会发现有少量并没有 因为访问后端阻塞 但是依然反馈慢到超时的情况,我们定义从客户端发消息到收到反馈,3s超时,而我们发现延迟可能好几分钟,这让我觉得肯定不是后端一些复杂逻辑导致的,逻辑层再复杂,也不至于这么慢,而且也没有服务器down的情况出现,而且跟踪发现这种情况是在过了午夜到凌晨这一段时间发生。虽然这种情况很少,而且难以复现,经过不遗余力的跟踪,还是让我找到了问题的结症。

问题定位:
服务器基本的架构分为接入层+逻辑层+存储层。

Created with Raphaël 2.1.0 客户端1 客户端1 客户端2 客户端2 网关 网关 逻辑层 逻辑层 存储层 存储层 (1)msgA (2)msgA (3)msgA (3')msgA'copy (4)msgA

消息流动从客户端1–>网关–>逻辑层–>网关–>客户端2依次排查。其中逻辑层往存储层拷贝消息是在和逻辑层将消息转发回网关同时进行的,采用异步入库的方式存储消息日志。

我在上图中的每一个节点都打印日志,时间精确到毫秒。通过tcpdump抓下的16进制包和机器、客户端上的日志时间戳对比分析,不断的在测试环境尝试复现,最终确定,在(3)这个环节出现了延迟问题。

原来会有这种情况出现,在逻辑层服务器收到一个包后,其在tcp缓冲区中待着不动,没有取出来,等到我再发一个消息过来的时候,才会将其冲出来。加入我一直不发消息,等个几分钟它就几分钟不出来。

此时我猛然想到曾经看到过的资料,对于epoll的边沿触发模式,据说会发生tcp包漏报的情况,我想我这里应该就是漏报了,等下回新包来的时候,再一起给报出来。

从网上找了别人的两张图

epoll边沿触发漏报消息包问题_第1张图片

epoll边沿触发漏报消息包问题_第2张图片

水平触发(LT)下只要某个 socket 处于 readable/writable 状态,无论什么时候进行 epoll_wait 都会返回该 socket;而 边沿触发(ET) 模式下只有某个 socket 从 unreadable 变为 readable,或从unwritable 变为writable时,epoll_wait 才会返回该 socket。

从这两张图中可以得知,在水平触发,或者使用select的时候,只要缓冲区中有数据,哪怕一次没有读完,main loop下次循环依然会报有数据,继续读就ok了。但是边沿触发的时候,假如上次读取了一部分数据,导致剩余数据低于边沿触发的水位线,其不会触发读回调函数,那么这个时候,消息就会一直待在tcp的缓冲区中,直到下一次有新的消息过来,触发了水位线才会再读出。

于是将tcpdump抓到的包进行分析,我的协议包头两个字节代表协议版本、控制参数什么的,第3、4两个字节代表包长。我一次性发两个逻辑包,我发现tcpdump抓到了两个我的逻辑包,但是测试服务器终端只打印了第一个逻辑包,第二个不见了,于是印证了我的猜测,第二个果然躺在tcp缓冲区中挺尸啊!然后再发一次消息,果然把上一个给挤出来了,在结合tcp抓包,对上了。这是很典型的ET工作模式!说明有多个包黏在一起同时到达的时候,每次读回调只处理一个包。当然,这里多个包同时到达的问题,我觉得不是问题,tcp本来就允许这样啊。造成这个问题是因为我只关心了一个逻辑包的处理,没想到会黏包,而且还非要用高大上的ET边沿触发。下来看看我写的改进代码

int n = 0;
while(1)  
{  
    nread = read(fd, buf + n, BUFSIZ-1);//非阻塞的读,返回每次读的数据大小
    if(nread < 0)  
    {  
        if(errno == EAGAIN || errno == EWOULDBLOCK)  //对于EAGAIN和EWOULDBLOCK要循环回去继续读,这种异常不要管它
        {  
            continue;  
        }  
        else  
        {  
            break;//or return;  
        }  
    }  
    else if(nread == 0)  
    {  
        break;//0代表数据读完了 
    }  
    else  
    {  
        n += nread;  
    }  
}  

问题解决:
找到问题后,就很容易解决了:每次读回调被触发的时候,读完一个逻辑包后,紧接着试探是否还有数据未读,有未读继续读,直到读完再退出,修改epoll_ctl到反向的模式。


创建于 2015-04-25 北京,更新于 2016-08-05 杭州

该文章在以下平台同步
- >LIBERALMAN:http://api.liberalman.cn:40000/article/58
- >CSDN:http://blog.csdn.net/socho/article/details/52127663
- >简书:

  • [ ] 引用

你可能感兴趣的:(c)