今天把最近写的代码放到测试环境去运行,发现在没有连接创建的时候进程CPU的使用率大约在5%,但是一旦创建了网络连接,进程的CPU使用会立即飙升到99%。发现了情况之后,就开始对代码进行排查。

首先在本地先执行一下代码,执行完毕之后sleep10秒钟观察进程的CPU使用状况,结果发现在本地的CPU占用同样达到了99%,那么就可以确定是代码最新的修改所引入的问题了,接下来就需要对新加入的代码进行仔细的排查。

我们先使用Pycharm的Profile选项来运行程序,之后结果显示大量的资源消耗在了sleep方法上,但是这并不能解决我们的问题,因为我们需要除了sleep方法之外的代码检测。之后想到了我们最新的代码中在select模型中加入了fd可写事件的监听,那么有没有可能是因为操作系统的写缓冲区一直处于空余的状态导致fd的可写事件一直能够被触发,因为我还会在fd可写的时候试图执行写操作,这样一来导致程序一直在执行空的写操作,从而导致CPU100%了呢?

为了验证我的猜想,我停止在select模型中去监控fd的可写事件,取而代之的是在socket需要写数据的时候,立即把数据通过socket.send()方法写到操作系统的缓冲区中。在经过这个修改之后,再次启动进程发现进程的CPU的使用率降到了1.3%,可见我们之前的猜想都是正确的,确实是因为select模型中的可读事件一直能够被触发导致了CPU的高使用率。

至于为什么select模型会一直触发可写事件呢?这是因为select模型默认使用的是水平触发(Level Trigger)模型,所谓的水平触发就是只要现在的fd处于某个符合要求的状态,那么某个事件就会被一直触发。与之相对的是边沿触发(Edge Trigger),边沿触发只会在某个fd发生了某个符合要求的状态的变化的时候才会触发。水平触发和边沿触发是来自于通信领域的专用名词,例如下图:

  • t1时刻电平从低变高,这就是一次边沿触发;
  • t2和t3时刻电平一直处于1,这是水平触发,并且会触发高电平的事件两次;
  • t4同样是一次边沿触发,事件为电平由高变低;

知道了区别后我们再深入了解下,例如对于可写事件,边沿触发只会在fd从不可写状态变为可写状态的时候才会触发一次事件,而水平触发则是只要现在写缓冲区有空余空间就会被一直的触发。边沿触发是不会导致某个事件被一直触发的,不过因为事件只会被触发一次所以有可能会导致事件被丢失,开发时具体使用哪种模型要依据业务情况来考虑。

参考:https://stackoverflow.com/a/34009083/4614538