前些日子接手了一个老项目,是一个网络收发的linux socket程序,接收各地设备发来的数据,进行解码 – 处理数据 – 存数据 – 发数据的工作;
项目代码业务逻辑处理没有问题,问题在于该代码运行一阵子之后就收不到数据了,但是进程仍然在跑,且用top命令查看cpu占用率极高,严重影响了云服务器的性能,分析之后,记录调优和debug的心得:
这个程序采用了跨平台的多路IO转接函数 —— select()
,由于select函数设计的比较早,遗留了针对小规模多路IO的性能低下的问题,解决办法是通过用户代码逻辑添加一个额外的数组(下文称为自定义数组),来存放select从内核返回给应用程序的文件句柄数组 —— 这样的目的是在用户逻辑层以较低的时间开销处理所有就绪的文件句柄(即connected socket);
-
运行一阵子后接收不到数据的原因:
首先,该代码采用了上述方式来改善时间效率,但是由于业务的通信模型采用TCP短连接(没有心跳包 && 1小时per数据的频率导致无法维持长连接),加之设备通信频率低达1小时一次,该自定义数组存储的文件描述符fd成为失效的文件句柄,时间久了之后,失效文件句柄会填满整个自定义数组,从而无法存储新连接的有效Socket,故无法收到信息; -
CPU占用率高的原因:
此外由于上述原因,当自定义数组被填满之后,每次新数据来时都要做自定义数组长度(1024)的无效遍历,导致cpu无效空转;另外对于进程和系统来说,文件描述符是的有限资源,打开了许多文件句柄缺不关却对系统也是一种负载。
综上,该代码的主要问题在于,对于select函数的辅助自定义数组client[FD_SETSIZE],没有考虑文件描述符失效并将之关闭的情况,导致失效文件描述符充满自定义数组,新的有效文件描述符无法被读取。
解决方案就是将失效的文教描述符及时关掉,就可以正确的维护自定义数组了;
比如对端关闭的情况,select会将fd返回,read的结果是0,此时可以关闭fd并腾出其在自定义数组中的位置;或者read返回-1的时候做一些出错处理重新尝试读取,多次读取失败就也关闭fd并腾出其在自定义数组中的位置。
问题还没完,改完这个bug之后,按理来说程序可以长久的正常运行了,但是仍存在程序运行一段时间之后就永久阻塞的情况;
思考可能是某个慢系统调用导致了进程的一直阻塞,给程序加了一些注释,再次观察错误日志后发现存在这样的情况:select()函数返回了某个监听读事件的fd,但是read却阻塞在该fd上。
反复思考程序的业务逻辑,没有发现错误,于是将问题定位到select的机制上,为了程序及时正常回到线上运行,暂时先将accept()生成的fd通过sockopt()设置为定时阻塞接收,程序后续正常运行,且cpu占用率也降低了。
后续经网络查阅资料发现,select存在这样的问题:
那么愉快的将定时阻塞改为非阻塞轮询处理就可以解决这个问题了。