Time Wheel的具体算法原理,可以参考网络上的资料,本文不探讨具体的原理。
在多数的网络库中,要删除空闲的连接,都会使用Time Wheel 时间轮算法。
在不做任何优化的情况下,如果所有的TCP连接对象都存储在$connection_list数组中,那么要删除空闲连接,常规的伪代码大致如下
<?php
Class Connection {
private $fd;
//最近一次收到消息的时间戳
public $lastRecvTime; //每次连接onMessage时,都更新该字段
public $refCount;
}
$connection_list = [];
// 此处将所有的在线连接都加入$connection_list数组
//不做任何优化,删除空闲连接伪代码,算法复杂度 O(N), N=在线连接数
foreach($connection_list as $conn) {
if($conn->lastRecvTime < time() - 60 ) {
//此处认为允许连接的idle_time为60秒
$conn->close();
}
}
上述代码,如果单机在线连接有几万的话,假设定时器每秒跑一次检测idle connection,那么每秒就有上万次的foreach,对静态语言来说,这点cpu占用可以忽略,但是如果用php之类的开发网络库,那么当在线连接飙升时,因为检测空闲连接,而损失的cpu就不能忽略不计了。
在swoole最新版本中,使用的是直接for循环遍历所有连接,检测idle connection的方法。
定时轮的每格,本文都称为bucket,假设定时轮有60个刻度,那么就有60个bucket,每个bucket可以放N个连接对象。
那么用时间轮算法,就一定会高效吗?假设大部分的连接,每秒都会收到消息,那么触发onMessage回调里就需要把当前连接从定时轮的旧bucket删除,并且移动到最新的bucket中,会存在2内存读,2次内存写,那所有的连接加起来,就有4N次内存IO,那反而更慢了(在swoole的源码中,旧版本就是这样的,看图)
所以在连接收到数据时,将当前连接移动到最新的bucket时,要尽量减少内存IO操作。在c++中可以使用shared_ptr,当连接收到数据时,只需要ref_count++就可以了,当定时轮跑到要过期删除的那个bucket时,只要连接的refCount==0 就直接删除,否则refCount--。这样就可以充分利用定时轮带来的减少for循环的次数。而不会增加太多的内存IO。
下面贴一下引用计数大概思路,具体实现下回分解
<?php
class Connection
{
public $fd;
public $lastRecvTime;
public $refCount = 0;
public function __construct($fd, $t)
{
$this->fd = $fd;
$this->lastRecvTime = $t;
}
public function getFd()
{
return (int)$this->fd;
}
}
$connection_list = [];
for ($i = 0; $i < 5; $i++) {
$conn = new Connection($i + 1, time());
$connection_list[$conn->getFd()] = $conn;
}
$arr1 = [];
$arr1[] = &$connection_list[1];
$arr1[] = &$connection_list[3];
foreach ($arr1 as &$v) {
$v->refCount += 10;
}
print_r($connection_list);
综上所述,如果大部分连接的收发消息的频率很高的话,直接使用for循环遍历所有连接更高效,因为使用TimeWheel,反而增加了内存IO次数,用增加内存IO来减少cpu for循环的次数,得不偿失。
反之,如果大部分连接都是空闲的,那可以考虑TimeWheel,增加部分的内存IO来减少大量的cpu for循环次数.