Swoole Heartbeat 心跳

什么是心跳呢?

心跳用于判断一个连接是正常还是断开的状态

例如:TCP中使用五元组标识一个网络连接,创建TCP连接时会发生三次握手,断开TCP连接时会发生四次挥手。不管是服务器还是客户端发起连接到关闭,都会经历完整的四次挥手的阶段。最后由系统回收客户端的文件描述符fd,应用层也可以使用onClose进行回调处理。

文件描述符

什么是文件描述符fd呢?

UNIX哲学中一切皆是文件,文件描述符fd是系统层暴露给业务层,用来表示一个五元组网络连接的标识,可以简单理解为索引,通过对文件描述符的操作,系统层可以找到相应的连接并进行一系列的操作,如发送数据到网络、连接关闭等。

Swoole中文件描述符fd是一个自增的整型数字,取值范围为1到1600万,fd超过1600万之后会自动从1开始进行复用。文件描述符之所以是一个整型的数字而非对象,主要原因是Swoole是多进程模型,在Worker进程或Task进程中随时可能要访问某个客户端连接,如果使用对象就需要进行序列化和反序列化,这样就会增加额外的性能开销,采用整型数字的好处就可以直接存储传输被使用。

为什么系统需要回收文件描述符fd呢?

如果需关闭某个连接,可以在业务层对文件描述符fd发起关闭连接的操作,文件描述符对于操作系统而言是有限的资源,必须重复利用,所以必须要回收。

$server->close($fd);

心跳机制

为什么会出现心跳机制呢?

正常情况下,客户端中断TCP连接时会发送一个FIN包进行四次断开握手来通知服务器,但在某些异常情况下,比如突然断网掉线或网络异常,服务端并不能够感知到连接的异常,而实际上连接可能已经失效。尤其在移动网络中,TCP连接非常不稳定,必须有一套机制来确保服务器和客户端之间连接的有效性。如果没有回收机制,这种连接会耗尽所有文件描述符fd,导致系统不再能够接收新的连接请求,因此就有了心跳机制。

什么是心跳机制呢?

心跳机制是业务层提供的一种判断连接是否仍旧存活的方式,让系统能够感知一个连接是否失效。

系统层面会提供心跳机制,但粒度粗糙时间稍长,更重要的是没有应用层灵活。

心跳机制有两种实现方式

  1. 客户端定时发送一个心跳包,告知服务器连接仍旧还活着,服务器会定时检测所有客户端列表,查看最后一个心跳包的时间是否过长,如果时间过长则认定已无心跳,进而判定为死连接,并主动关闭这个连接。
    客户端定时发送心跳包的方式,对服务器和网络的压力更小,更加灵活。

  2. 服务器定时询问所有客户端是否还存活,如果仍然存活则客户端给出反馈,否则认定为死连接并主动关闭。
    服务器定时询问的方式,对服务器和网络的压力更大,不推荐使用。

什么是心跳包呢?

从客户端到服务器这条巨大的链路中会经过无数的路由器,每个路由器都可能会检测多少秒时间内没有数据包,则会自动关闭连接的节能机制。为了让这个可能会出现的节能机制失效,客户端可以设置一个定时器,每隔固定时间发送一个随机字符一字节的数据包,这种数据包就是心跳包。

Swoole的心跳机制是如何实现的呢?

对于多数TCP网络服务器都会考虑心跳机制,TCP的keepalive选项可以用来检测死连接,只要客户端没有死掉,服务器会在超过keepidle闲置事件后发送一个TCP探测包,发送次数是tcp_keepcount次,每次间隔时间是tcp_keepinterval,如果客户端没有发送ack确认,服务器才会关闭连接。

在TCP中有一个Keep-Alive的机制可以检测死连接,应用层对于死连接周期不敏感或没有实现心跳机制,可以使用操作系统提供的keepalive机制来踢掉死连接。Keep-Alive机制不会强制切换连接,如果连接存在但一直不发生数据交互,Keep-Alive也不会切断连接。而应用层实现的心跳检测heartbeat_check即使连接存在,在不产生数据交互的情况下,依然会强制切断连接。

Swoole实现的心跳机制,只要客户端超过一定时间没有发送数据,不管这个连接是不是死连接,都会关闭掉。

Swoole提供了ping功能,通过配置ping值,Swoole内核可以判断当只有一个心跳包时不会将数据包转发给应用层onReceive

Swoole采用客户端定时发送心跳包服务端定时检测的方式,Swoole会在Master主进程中独立创建一个心跳线程,通过定时轮询所有客户端连接的方式,来判断连接是否已经失效,因此Swoole的心跳并不会堵塞任何业务逻辑。

Swoole使用心跳前需要提前配置服务器运行时参数,其中有两个配置参数:

  • heartbeat_check_interval
    设置服务器定时检测在线列表的时间间隔
  • heartbeat_idle_time
    设置连接最大的空闲时间,如果最后一个心跳包的时间与当前时间只差超过设定值则认为连接失效。

例如:

$config = [];
// 设置每5秒服务器会侦测一次心跳
$config["heartbeat_check_interval"] = 5;
// 设置一个TCP连接如果在10秒内未向服务器发送数据则被切断
$config["heartbeat_idle_time"] = 10;
$server->set($config);

建议heartbeat_idle_timeheartbeat_check_interval的值多两倍多,两倍是为了进行容错允许丢包,多一点儿是考虑到网络延时的情况,这个可以根据实际的业务情况调整容错率。

另外,Swoole提供了swoole_server::heartbeat()方法用于手工检测心跳是否到期,heartbeat方法发会返回闲置时间超过heartbeat_idle_time的所有TCP连接,应用程序可以在这些连接中做一些操作,如发送数据或关闭连接等。

swoole_server::heartbeat()

注意,使用swoole_server::heartbeat()方法前,如果设置了heartbeat_check_interval配置选项,将会关闭超时的连接。否则会返回过时连接。

// 手工关闭超时连接
$serverr->tick(1000, funcntion($id) use($server){
  $fds = $server->heartbeat(false);
  foreach($fds as $fd){
    $server->close($fd);
  }
});

另外,如果提前设置了dispatch_mode为1或3时,底层会屏蔽onConnectonClose事件,因此也就无法回调close关闭事件了。

Swoole的心跳机制是如何判断连接是否还处于存活状态的呢?

Swoole扩展内置的心跳机制,在每次接收到客户端数据时会记录一个时间戳,当客户端在一定时间内没有向服务器发送数据时,服务器会自动切断连接。

在Swoole中connection连接的结构体中有一个time_t last_time字段,用于存放最后一次收包的时间戳,通过时间戳对比来判定连接是否存活。

心跳检测

服务器

$ vim server.php
<?php
class Server
{
    private $server;
    public function __construct($host, $port, $config)
    {
        //创建服务器
        $this->server = new swoole_server($host, $port);
        //设置运行时参数
        $this->server->set($config);
        //设置监听
        $this->server->on("Start", [$this, "onStart"]);
        $this->server->on("Connect", [$this, "onConnect"]);
        $this->server->on("Receive", [$this, "onReceive"]);
        $this->server->on("Close", [$this, "onClose"]);
        //开启服务器
        $this->server->start();
    }
    public function onStart($server)
    {
        echo "[start] master {$server->master_pid} manager {$server->manager_pid}".PHP_EOL;
    }
    public function onConnect($server, $fd, $reactor_id)
    {
        echo "[connect] reactor {$reactor_id} worker {$server->worker_pid} client {$fd}".PHP_EOL;
    }
    public function onReceive($server, $fd, $reactor_id, $data)
    {
        echo "[receive] reactor {$reactor_id} worker {$server->worker_pid} client {$fd}: {$data}".PHP_EOL;
        $server->send($fd, $data);
    }
    public function onClose($server, $fd)
    {
        echo "[close] client {$fd} close".PHP_EOL;
    }
}

$config = [];
$config["worker_num"] = 8;
$config["daemonize"] = 0;
$config["max_request"] = 1000;
$config["dispatch_mode"] = 2;
$config["debug_mode"] = 1;
$config["log_file"] = "/swoole.log";
$config["heartbeat_check_interval"] = 5;
$config["heartbeat_idle_time"] = 10;
$host = "0.0.0.0";
$port = 9000;
$server = new Server($host, $port, $config);

客户端

$ vim client.php
<?php
class Client
{
    private $client;

    public function __construct($host, $port)
    {
        $this->client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
        $this->client->on("Connect", [$this, "onConnect"]);
        $this->client->on("Receive", [$this, "onReceive"]);
        $this->client->on("Close", [$this, "onClose"]);
        $this->client->on("Error", [$this, "onError"]);
        if(!$fp = $this->client->connect($host, $port)){
            echo "error {$fp->errCode} {$fp->errMsg}";
            return;
        }
    }
    public function onConnect($client)
    {
        fwrite(STDOUT, "send: ");
        swoole_event_add(STDIN, function(){
            fwrite(STDOUT, "send: ");
            $this->client->send(trim(fgets(STDIN)));
        });
    }
    public function onReceive($client, $data)
    {
        echo $data.PHP_EOL;
    }
    public function onClose($client)
    {
        echo "close".PHP_EOL;
    }
    public function onError($client)
    {
        echo "error {$client->errCode} {$client->errMsg}".PHP_EOL;
    }
}
$client = new Client("127.0.0.1", 9000);

运行服务器

$ php server.php
[start] master 539 manager 540

运行客户端

$ php client.php
send: 

观察客户端

$ php client.php
send: HELLO
send: HELLO
close

观察服务器

[start] master 539 manager 540
[connect] reactor 0 worker 544 client 1
[receive] reactor 0 worker 544 client 1: HELLO
[close] client 1 close
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 227,401评论 6 531
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,011评论 3 413
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 175,263评论 0 373
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,543评论 1 307
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,323评论 6 404
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,874评论 1 321
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,968评论 3 439
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,095评论 0 286
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,605评论 1 331
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,551评论 3 354
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,720评论 1 369
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,242评论 5 355
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,961评论 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,358评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,612评论 1 280
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,330评论 3 390
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,690评论 2 370