Swoole 关于变量作用域的问题

Swoole 目前看来官方手册还非常不完善, 只能自己一个个慢慢测试看实际结果了

测试环境 CentOS 7.0 , Swoole 2.0.7 , PHP 7.1.2

这次测试的问题是关于 swoole server 中的回调函数使用变量的作用域的问题

先说下大致的总结, 如果存在不正确的地方请务必留言告诉我哦 谢谢哈!

使用 swoole 最大的问题之一就是 内存控制, 容易遇到内存溢出

如果是在 server 之外加载的文件, 设置的变量或实例化的对象将作为常驻内存的超全局共享数据, 若要更新则必须要 shutdown server 才能彻底释放

如果想要热重载来释放和重置, 则只能把加载文件和初始化, 实例化的操作步骤放到 onWorkerStart 事件中 还有个 onReceive 事件,不过因为我目前尚未使用过 Receive 事件不是很确定暂时忽略掉吧

先上个测试代码:
关于 HTTP Server 事件相关的新手也可以先看看这篇文章
Swoole 关于 HTTP SERVER 的事件顺序

/**
 * Author: ZHOUZ
 * Blog: http://blog.csdn.net/zhouzme
 * Time: 2017-04-05 15:22
 */
function server()
{
    $server = new Swoole\Http\Server("0.0.0.0", 9501);

    /**
     * 测试在 $server 外部注册全局自定义属性, 看看会不会被覆盖
     */

    $server->myWorkerVar = 'global';

    $server->set(array(
        'worker_num' => 2,
        'daemonize' => false,
    ));

    // 服务器启动时执行一次
    $server->on('Start', function (\Swoole\Http\Server $server) {
        echo PHP_EOL . PHP_EOL . 'Start: http://blog.csdn.net/zhouzme' . PHP_EOL . PHP_EOL;
    });

    // 服务器启动时执行一次
    $server->on('ManagerStart', function (\Swoole\Http\Server $server) {
        echo 'ManagerStart: ' . PHP_EOL . PHP_EOL;
    });

    // 每个 Worker 进程启动或重启时都会执行
    $server->on('WorkerStart', function (\Swoole\Http\Server $server, int $workerId) {
        echo 'WorkerStart: ' . PHP_EOL . PHP_EOL;
        echo '    Worker ID: ' . $workerId . PHP_EOL . PHP_EOL;

        /**
         * 测试在 $server 对象上注册一个自定义的属性在不同 worker 中是否会被覆盖
         * 测试结果: 不会被覆盖, swoole 应该是对不同的 worker 中设置的自定义属性进行了分离操作
         * 估计使用了类似 PHP 的 __set 魔术方法进行了分组操作:
         */
        $myWorkerVar = $server->myWorkerVar??'NULL';
        echo '    GET My Worker['. $workerId .'] Var: ' . $myWorkerVar . PHP_EOL . PHP_EOL;
        if ($workerId == 0) {
            $server->myWorkerVar = $myWorkerVar = $workerId;
            echo '    SET My Worker['. $workerId .'] Var: ' . $myWorkerVar . PHP_EOL . PHP_EOL;
            $myWorkerVar = $server->myWorkerVar??'NULL';
            echo '    GET My Worker['. $workerId .'] Var: ' . $myWorkerVar . PHP_EOL . PHP_EOL;
        }
    });

    // 每次连接时(相当于每个浏览器第一次打开页面时)执行一次, reload 时连接不会断开, 也就不会再次触发该事件
    $server->on('Connect', function (\Swoole\Http\Server $server, int $fd, int $reactorThreadId) {
        echo 'Connect: ' . PHP_EOL . PHP_EOL;
        echo '    Worker ID: '. $server->worker_id . PHP_EOL . PHP_EOL;
        echo '        fd: ' . $fd . ' , fromId: ' . $reactorThreadId . PHP_EOL . PHP_EOL;
    });

    // 浏览器连接服务器后, 页面上的每个请求均会执行一次,
    // 每次打开链接页面默认都是接收两个请求, 一个是正常的数据请求, 一个 favicon.ico 的请求
    $server->on('Request', function (\Swoole\Http\Request $request, \Swoole\Http\Response $response) use ($server) {
        if ($request->server['request_uri'] == '/favicon.ico') {
            $response->end();
            return;
        }

        echo 'Request: ' . PHP_EOL . PHP_EOL;
        echo '    Worker ID: '. $server->worker_id . PHP_EOL . PHP_EOL;
        echo '    URL: ' . ($request->server['request_uri'] ?? '') . PHP_EOL . PHP_EOL;
        echo '        My Worker Var: '. ($server->myWorkerVar ?? 'NULL') . PHP_EOL . PHP_EOL;
        $workerId = $server->worker_id;

        $myWorkerVar = $server->myWorkerVar??'NULL';
        echo '    GET My Worker['. $workerId .'] Var: ' . $myWorkerVar . PHP_EOL . PHP_EOL;
        if ($workerId == 0) {
            $server->myWorkerVar = $myWorkerVar = $workerId;
            echo '    SET My Worker['. $workerId .'] Var: ' . $myWorkerVar . PHP_EOL . PHP_EOL;
            $myWorkerVar = $server->myWorkerVar??'NULL';
            echo '    GET My Worker['. $workerId .'] Var: ' . $myWorkerVar . PHP_EOL . PHP_EOL;
        }

        // 通过链接参数热重载 worker 进程观察触发事件
        $act = $request->get['act'] ?? '';
        if ($act == 'reload') {
            echo '    ... Swoole Reloading ! ... ' . PHP_EOL . PHP_EOL;
            // 触发 reload 之后, 貌似后面的代码也还是会执行的
            $server->reload();
            echo '    ... Under Reload ! ... ' . PHP_EOL . PHP_EOL; // 看看 reload 时是否会执行后续的代码
        } elseif ($act == 'exit') {
            // 直接立即终止当前 worker 进程, 和 reload 的效果比较相似, 新的 worker 进程的 ID 和原来的一样
            // 所以程序内部应该尽量避免使用 exit 而应该抛出异常在外部 catch
            echo '    ... Swoole Exit ! ... ' . PHP_EOL . PHP_EOL;
            exit;
        } elseif ($act == 'shutdown') {
            // 直接立即终止当前 worker 进程, 和 reload 的效果比较相似, 新的 worker 进程的 ID 和原来的一样
            // 所以程序内部应该尽量避免使用 exit 而应该抛出异常在外部 catch
            echo '    ... Swoole Shutdown ! ... ' . PHP_EOL . PHP_EOL;
            $server->shutdown();
            echo '    ... After Swoole Shutdown ! ... ' . PHP_EOL . PHP_EOL;
        }

        $response->header("X-Server", "Swoole");
        $msg = 'hello swoole !';
        $response->end($msg);
    });

    $server->start();
}

server();

下面说下测试具体结果:
我这里主要测试的是在 Server 内 WorkerStart 事件中初始化并保存当前进程内的全局变量, 而不用在 server 外定义超全局的共享变量

定义 worker 进程内的全局变量可以在 reload 后直接释放掉内存, 以及避免其他许多问题

一开始看文档对内存变量的说明时并没有看到对于进程内的全局变量该如何设置保存的问题, 一直没想明白, 当然要自己实现的方法有很多, 我只是觉得 swoole 本身应该早就考虑这个问题了, 但文档上却有到处找不到说明, 觉得不可能

于是想着如果是自己来实现可能用到方法, 其中之一就是动态属性, 然后通过魔术方法根据当前的 workerId 来自动区分, 这样就可以在脚本中使用相同的调用方式自动区分每个 worker 进程的数据了

这就需要在 $server 上设置动态属性

测试代码中的 $server->myWorkerVar 是自定义的动态属性, $server 本身并不存在该属性的, 这属于 PHP 的基础特性, 还可以通过 PHP 的魔术方法 __set, __get, __isset, … 等来控制他们

下面我简单写了个 PHP 的模拟效果:

class ServerFoo
{
    protected $globalVars = []; // 作用于所有 worker 进程的全局变量
    public $workerVars = []; // 仅作用当前 worker 进程内的全局变量
    public $workerId = null; // 默认 null 表示尚未完成 worker 初始化没有 id

    public function setWorkerId(?int $id)
    {
        $this->workerId = $id;
    }

    public function __set($name, $value)
    {
        if (isset($this->workerId)) {
            $this->workerVars[$this->workerId][$name] = $value;
        } else {
            $this->globalVars[$name] = $value;
        }
    }

    public function __isset($name)
    {
        if (isset($this->workerId)) {
            return isset($this->workerVars[$this->workerId][$name]);
        } else {
            return isset($this->globalVars[$name]);
        }
    }

    public function __get($name)
    {
        if (isset($this->workerId)) {
            return $this->workerVars[$this->workerId][$name] ?? null;
        } else {
            return $this->globalVars[$name] ?? null;
        }
    }
}

$foo = new ServerFoo();

// 注册写入

$foo->myWorkerVar = 'Global'; // 注册超全局变量, 可跨 worker 进程
$foo->setWorkerId(1);
$foo->myWorkerVar = '123';    // 注册只属于 worker id 为 1 进程的全局变量
$foo->setWorkerId(2);
$foo->myWorkerVar = '456';    // 注册只属于 worker id 为 2 进程的全局变量

// 读取

$foo->setWorkerId(1);
var_dump($foo->myWorkerVar);  // 输出只属于 worker 1 的值 123

$foo->setWorkerId(2);
var_dump($foo->myWorkerVar);  // 输出只属于 worker 2 的值 456

$foo->setWorkerId(null);
var_dump($foo->myWorkerVar);  // 输出超全局变量值 Global

现在需要验证下是否一致:

    $server = new Swoole\Http\Server("0.0.0.0", 9501);
    /**
     * 测试在 $server 外部注册全局自定义属性, 看看会不会被覆盖
     */
    $server->myWorkerVar = 'global';

上面这段代码是全局和局部变量覆盖的测试, 在 WorkerStart 事件之外定义 myWorkerVar 这个动态属性并赋值, 则会成为所有worker 进程共享的全局变量, 如果没有在 worker 进程内设置该值, 则可以读取到 $server->myWorkerVar 的值为 global

然后在 WorkerStart 事件回调中覆盖

$server->on('WorkerStart', function (\Swoole\Http\Server $server, int $workerId) {
    $myWorkerVar = $server->myWorkerVar??'NULL';
    echo '    GET My Worker['. $workerId .'] Var: ' . $myWorkerVar . PHP_EOL . PHP_EOL;
    if ($workerId < 2) {
        $server->myWorkerVar = $myWorkerVar = $workerId;
        echo '    SET My Worker['. $workerId .'] Var: ' . $myWorkerVar . PHP_EOL . PHP_EOL;
        $myWorkerVar = $server->myWorkerVar??'NULL';
        echo '    GET My Worker['. $workerId .'] Var: ' . $myWorkerVar . PHP_EOL . PHP_EOL;
    }
});

先假设总共有 三个 worker 进程,

worker0 , worker1 , worker2

然后

worker0 进程中被覆盖设置动态属性值为 var0

$server->myWorkerVar = 'var0';

worker1 进程中被设置动态属性值为var1

$server->myWorkerVar = 'var1';

worker2 进程中不设置

此时, 当 worker2 进程中读取 $server->myWorkerVar 动态属性值时, 输出结果为:

GET My Worker[2] Var: global

而不是

GET My Worker[0] Var: var1
or
GET My Worker[1] Var: var2

也就是说当 worker 进程内部对超全局的动态属性进行赋值时并没有进行覆盖或替换而是放在了当前 worker id 的独立空间下的, 所以动态属性可以放心使用而不用担心多个进程相互覆盖的问题

下面是测试代码的输出结果参考:

[root@localhost swoole]# php var.php

Start: http://blog.csdn.net/zhouzme

WorkerStart:

WorkerStart:

Worker ID: 0

Worker ID: 1

ManagerStart:

GET My Worker[1] Var: global

GET My Worker[0] Var: global

SET My Worker[0] Var: 0

GET My Worker[0] Var: 0

Connect:

Worker ID: 0

    fd: 1 , fromId: 0

Request:

Worker ID: 0

URL: /

    My Worker Var: 0

GET My Worker[0] Var: 0

SET My Worker[0] Var: 0

GET My Worker[0] Var: 0

Connect:

Worker ID: 1

    fd: 2 , fromId: 1

Request:

Worker ID: 1

URL: /

    My Worker Var: global

GET My Worker[1] Var: global

这里写图片描述

猜你喜欢

转载自blog.csdn.net/zsjangel/article/details/69367450