高并发服务端消息丢失

 

一、基本情况介绍

最近的项目中,有一个支持高并发的TCP通信服务端,因为要求设定比较高(支持百万并发左右),所以就实现了一个模拟前摄器的Epoll服务端通信。设计了为了保持弹性,把接收器和工作者以及Epoll工作监听都专门引入了相关线程中。这样可以通过配置自动扩展接收器和工作监听的数量,动态增加工作者的处理线程。
为保证上述的实现,专门购买了128(CPU)*128(Mem)的服务器。由于对数据保存不敏感,所以只是使用了自带的硬盘,没有再做更多的增加。

二、现象

当初为了实现这个版本,曾经在单机上模拟过类似情况,取得了满意的效果。在真正实现的时候儿,为了保证效率,又增加了一个内存池,这样保证在分配内存时,可以把内存分配的异常和相关时间耗费处理降低到最低点。
程序在开发环境里,没有遇到大问题,只有一些较小的数据解析问题,很快就解决。但因为开发进度的安排问题,过了两个月左右才开始进入测试环境,然后在长时间测试后,出现了一个问题,在WIFI和4G网线切换通信过程中,即安卓和IOS通信,总有一端明明在线,却收不到数据。
测试将问题反映了上来,一开始以为是网络的问题,更换网络后,确实问题解决,但很快周一,又出现了相同的问题。因为这个现象并不是一个较短时间必现的现象,根本没办法调试,只能打日志来进行监视。考虑这个问题时想到了一个事儿,原来为了保持客户端的连接,心跳较快,可以迅速清理心跳不响应的客户端,但是在这个版本中,为了高并发,将心跳延长到了3*5分钟。是不是这个原因呢?只能等日志来说话了。

三、解决方式

1、日志分析心跳当前客户端

把相关的日志打印增加,果然,在出现这种问题后,发现当前的连接中,相同的ID的Socket有两个以上。程序在遍历查找到这个ID后,就用其实Socket把数据发走了,但这个Socket其实已经失效,特别是分片是哈希随机动态增长的,导致这个问题可能是随机复现的。而最初解决问题是依靠心跳对此数据结构的自动释放,智能指针判断计数器后会自动处理相关的对象。
当时之所以为了让心跳来自动处理这个ID的Socket,目的是为了提高效率,现在为了安全,只好在插入新的ID时,将老的ID遍历删除,由于现在已经分槽,所以判断起来有点复杂,不过好歹是增加后即解决了这个问题。

2、再次出现同样问题

测试在回复测试中,证实了问题解决,但下午有开发的小朋友把手机接入了测试环境,仍然复现了这个问题,而且在之后的测试中,又小概率事件复现了几次。测试又反馈问题指到服务端。这次情况比较特殊,基本都是安卓有问题。苹果没有什么问题,至少苹果是很少有问题。然后在查找问题过程中发现了一个现象,服务端主动断开客户端后,苹果可以迅速发现,而安卓却无法接收到关闭的消息。
然后就写了一个测试程序,直接close连接,事实证明苹果可以接收到这个事件,但安卓没反映。经过查找,发现安卓不同的版本处理这个要求有一些不同,安卓需要shutdown,这也就是原来坚持写服务器时的优雅的关闭,即先shutown数据通讯,再close套接字socket。这个问题当时在网上讨论的那可是相当激烈,没想到这里还埋着一个雷。增加优雅关闭,问题解决。

3、又一次复现问题

可过了没两天,测试又复现了这个问题,这次比较随机了,可能安卓和安卓,也可能苹果和安卓。这次感觉有点蒙圈,无处下手的感觉。看日志,几十个线程穿插打印,导致数据就是乱的,而这个问题的出现,往往要发送六七百条,甚至上千条才可能出现。日志出现了一个可疑的现象,就是客户在当前的在线列表中,发送数据也找到了对应的Socket,但是发送时直接退出循环没有发送,这个现象直接把问题指向了最后发送前的对智能指针的判断上,可这个智能指针不可能为空啊,他是长期被持有的,只可能计数器减少,不可能变空。
只好不断的增加打印信息,在插入队列的数据中埋了一个桩,但是这个桩根本不起作用,这意味着这个指针确实释放掉了。到这里,基本就没有头绪了,这是根本矛盾的啊。
只好继续不断的优化和增加日志,怀疑是不是线程没有被启动或者队列数据没有执行完成有阻塞,甚至想象是不是服务器端的软件配置有问题,甚至觉得硬件是不是有问题。这时,在继续排查日志时,发现了一个现象,所有的线程切换都成功了,意味着线程没启动的猜测是不正确的,而在增加对队列打印后,一个错误的修改引入(动态的修改了队列的大小,导致手机端通信时下一次通信把上一次的通信内容显示出来),反而证明了上述的猜测都是错误的。这反而让信心大增。
这时,代码中有一个内存没有回收,就随手增加了一行回收的代码,程序跑不了几步就崩溃了,这时才开始怀疑内存池可能有问题,但仍然没有引起重视。只是把这行代码重新注释掉,结果可能就是内存自动释放,降低效率。
而此时再想到这个现象,就发现代码里有一行回收,是放在for循环中,只要有命中,就会重复回收一个智能指针,无语啊,嵌套的swith导致犯了这种低级错误。把这行代码抽到switch之外,此时已经周末,在回家的路上不断的模拟这个行为和现象间有什么关系,其结果其实就是如果耗尽预分配的一千个内存智能指针单元后,就会使用同一个内存单元的不同智能指针,而前一个使用后,会直接回收把智能指针中一个应用智能指针置成nullptr,然后,后面的线程如果先执行,则成功,如果后执行,则必然引用一个空指针,判断后直接退出循环。而在前面使用已经释放的指针,自然会崩溃。这样,一整条的链条就清晰了。
周一的测试证明了这个问题,然后把注释的另外一个内存回收解开,继续证明了这个原因。内存池的一个低级错误,引发了两天的无头绪的排查,引以为鉴。

四、总结

遇到问题,千万不要慌,不要蒙圈,要沉下心来,好好查找日志,查看代码,细节中就可以看到缘由!
小问题掩盖大问题,这是一个非常可怕的现象,网络的问题,导致大家一发现问题就推到网络上,而更换网络后恰恰又满足了预置一千条的正常现象。就一直没有引起重视,而后续的各种问题的暴露可能会一一表现出来。所以在开发测试时,一定要尽快扫除上层无关的前置问题,尽快暴露底层相关的问题并解决,才能快速的迭代开发。
2020年有两个项目遇到同样的这类问题,要高度重视!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值