一次 CPU100% 的排查
今天把最近写的代码放到测试环境去运行,发现在没有连接创建的时候进程 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 从不可写状态变为可写状态的时候才会触发一次事件,而水平触发则是只要现在写缓冲区有空余空间就会被一直的触发。边沿触发是不会导致某个事件被一直触发的,不过因为事件只会被触发一次所以有可能会导致事件被丢失,开发时具体使用哪种模型要依据业务情况来考虑。