1、注意事项

  1. 不要在代码中执行 sleep 以及其他睡眠函数,这样会导致整个进程阻塞;协程中可以使用 Co::sleep() 或在一键协程化后使用 sleep
  2. exit/die 是危险的,会导致 Worker 进程退出;
  3. 可通过 register_shutdown_function 来捕获致命错误,在进程异常退出时做一些清理工作;
  4. PHP 代码中如果有异常抛出,必须在回调函数中进行 try/catch 捕获异常,否则会导致工作进程退出;
  5. 不支持 set_exception_handler,必须使用 try/catch 方式处理异常;
  6. Worker 进程不得共用同一个 Redis MySQL 等网络服务客户端,Redis/MySQL 创建连接的相关代码可以放到 onWorkerStart 回调函数中

2、并发编程

请务必注意与同步阻塞模式不同,协程模式下程序是并发执行的,在同一时间内 Server 会存在多个请求,因此应用程序必须为每个客户端或请求,创建不同的资源和上下文。否则不同的客户端和请求之间可能会产生数据和逻辑错乱。

3、类 / 函数重复定义

新手非常容易犯这个错误,由于 Swoole 是常驻内存的,所以加载类 / 函数定义的文件后不会释放。

因此引入类 / 函数的 php 文件时必须要使用 include_oncerequire_once,否则会发生 cannot redeclare function/class 的致命错误。

4、内存管理

编写 Server 或其他常驻进程时需要特别注意。

PHP 守护进程与普通 Web 程序的变量生命周期、内存管理方式完全不同。

Server 启动后内存管理的底层原理与普通 php-cli 程序一致。具体请参考 Zend VM 内存管理方面的文章。

5、局部变量

在事件回调函数返回后,所有局部对象和变量会全部回收,不需要unset。如果变量是一个资源类型,那么对应的资源也会被 PHP 底层释放。

  1. function test()
  2. {
  3. $a = new Object;
  4. $b = fopen('/data/t.log', 'r+');
  5. $c = new swoole_client(SWOOLE_SYNC);
  6. $d = new swoole_client(SWOOLE_SYNC);
  7. global $e;
  8. $e['client'] = $d;
  9. }

$a, $b, $c 都是局部变量,当此函数 return 时,这 3 个变量会立即释放,对应的内存会立即释放,打开的 IO 资源文件句柄会立即关闭。

$d 也是局部变量,但是 return 前将它保存到了全局变量 $e,所以不会释放。

当执行 unset($e['client']) 时,并且没有任何其他 PHP变量仍然在引用 $d 变量,那么 $d 就会被释放。

6、全局变量

在 PHP 中,有 3 类全局变量。

使用 global 关键词声明的变量

使用 static 关键词声明的类静态变量、函数静态变量

PHP 的超全局变量,包括 $_GET$_POST$GLOBALS

全局变量和对象,类静态变量,保存在 Server 对象上的变量不会被释放。

需要程序员自行处理这些变量和对象的销毁工作。

  1. class Test
  2. {
  3. static $array = array();
  4. static $string = '';
  5. }
  6. function onReceive($serv, $fd, $reactorId, $data)
  7. {
  8. Test::$array[] = $fd;
  9. Test::$string .= $data;
  10. }

在事件回调函数中需要特别注意非局部变量的 array 类型值,某些操作如 TestClass::$array[] = "string" 可能会造成内存泄漏,严重时可能发生爆内存,必要时应当注意清理大数组。

在事件回调函数中,非局部变量的字符串进行拼接操作是必须小心内存泄漏,如 TestClass::$string .= $data,可能会有内存泄漏,严重时可能发生爆内存。

解决方法

同步阻塞并且请求响应式无状态的 Server 程序可以设置 max_requesttask_max_request,当 Worker 进程 / Task 进程 结束运行时或达到任务上限后进程自动退出,该进程的所有变量 / 对象 / 资源均会被释放回收。

程序内在 onClose 或设置定时器及时使用 unset 清理变量,回收资源。

7、进程隔离

进程隔离也是很多新手经常遇到的问题。修改了全局变量的值,为什么不生效?

原因就是全局变量在不同的进程,内存空间是隔离的,所以无效。

所以使用 Swoole 开发 Server 程序需要了解进程隔离问题,Swoole\Server 程序的不同 Worker 进程之间是隔离的,在编程时操作全局变量、定时器、事件监听,仅在当前进程内有效。

不同的进程中 PHP 变量不是共享,即使是全局变量,在 A 进程内修改了它的值,在 B 进程内是无效的

如果需要在不同的 Worker 进程内共享数据,可以用 RedisMySQL文件Swoole\TableAPCushmget 等工具实现

不同进程的文件句柄是隔离的,所以在 A 进程创建的 Socket 连接或打开的文件,在 B 进程内是无效,即使是将它的 fd 发送到 B 进程也是不可用的

示例:

  1. $server = new Swoole\Http\Server('127.0.0.1', 9500);
  2. $i = 1;
  3. $server->on('Request', function ($request, $response) {
  4. global $i;
  5. $response->end($i++);
  6. });
  7. $server->start();

在多进程的服务器中,$i 变量虽然是全局变量 (global),但由于进程隔离的原因。

假设有 4 个工作进程,在进程1 中进行 $i++,实际上只有进程1 中的 $i 变成 2 了,其他另外 3 个进程内 $i 变量的值还是 1

正确的做法是使用 Swoole 提供的 Swoole\AtomicSwoole\Table 数据结构来保存数据。

如上述代码可以使用 Swoole\Atomic 实现。

  1. $server = new Swoole\Http\Server('127.0.0.1', 9500);
  2. $atomic = new Swoole\Atomic(1);
  3. $server->on('Request', function ($request, $response) use ($atomic) {
  4. $response->end($atomic->add(1));
  5. });
  6. $server->start();

Swoole\Atomic 数据是建立在共享内存之上的,使用 add 方法加 1 时,在其他工作进程内也是有效的

Swoole 提供的 TableAtomicLock 组件是可以用于多进程编程的,但必须在 Server->start 之前创建。

另外 Server 维持的 TCP 客户端连接也可以跨进程操作,如 Server->sendServer->close

8、mt_rand 随机数

Swoole 中如果在父进程内调用了 mt_rand,不同的子进程内再调用 mt_rand 返回的结果会是相同的,所以必须在每个子进程内调用 mt_srand 重新播种。

shufflearray_rand 等依赖随机数的 PHP 函数同样会受到影响。

示例:

  1. mt_rand(0, 1);
  2. //开始
  3. $worker_num = 16;
  4. //fork 进程
  5. for($i = 0; $i < $worker_num; $i++) {
  6. $process = new Swoole\Process('child_async', false, 2);
  7. $pid = $process->start();
  8. }
  9. //异步执行进程
  10. function child_async(Swoole\Process $worker) {
  11. mt_srand(); //重新播种
  12. echo mt_rand(0, 100).PHP_EOL;
  13. $worker->exit();
  14. }

9、sleep/usleep 的影响

异步 IO 的程序中,不得使用 sleep/usleep/time_sleep_until/time_nanosleep。(下文中使用 sleep 泛指所有睡眠函数)

sleep 函数会使进程陷入睡眠阻塞

直到指定的时间后操作系统才会重新唤醒当前的进程

sleep 过程中,只有信号可以打断

由于 Swoole 的信号处理是基于 signalfd 实现的,所以即使发送信号也无法中断 sleep

Swoole 提供的 swoole_event_addswoole_timer_tickswoole_timer_afterSwoole\Process::signal、在进程 sleep 后会停止工作。Swoole\Server 也无法再处理新的请求。

示例:

  1. $server = new Swoole\Server("127.0.0.1", 9501);
  2. $server->set(['worker_num' => 1]);
  3. $server->on('receive', function ($server, $fd, $from_id, $data) {
  4. sleep(100);
  5. $server->send($fd, 'Swoole: '.$data);
  6. });
  7. $server->start();

onReceive 事件中执行了 sleep 函数,Server100 秒内无法再收到任何客户端请求。

10、exit/die 函数的影响

Swoole 程序中禁止使用 exit/die,如果 PHP 代码中有 exit/die,当前工作的 Worker 进程、Task 进程、User 进程、以及 Swoole\Process 进程会立即退出。

使用 exit/dieWorker 进程会因为异常退出,被 master 进程再次拉起,最终造成进程不断退出又不断启动和产生大量警报日志.

建议使用 try/catch 的方式替换 exit/die,实现中断执行跳出 PHP 函数调用栈。

  1. go(function () {
  2. try
  3. {
  4. exit(0);
  5. } catch (Swoole\ExitException $e)
  6. {
  7. echo $e->getMessage()."\n";
  8. }
  9. });

Swoole\ExitExceptionSwoolev4.1.0 版本及以上直接支持了在协程和 Server 中使用 PHP 的 exit,此时底层会自动抛出一个可捕获的 Swoole\ExitException,开发者可以在需要的位置捕获并实现与原生 PHP 一样的退出逻辑。

异常处理的方式比 exit/die 更友好,因为异常是可控的,exit/die 不可控。

在最外层进行 try/catch 即可捕获异常,仅终止当前的任务。

Worker 进程可以继续处理新的请求,而 exit/die 会导致进程直接退出,当前进程保存的所有变量和资源都会被销毁。

如果进程内还有其他任务要处理,遇到 exit/die 也将全部丢弃。

11、while 循环的影响

异步程序如果遇到死循环,事件将无法触发。

异步 IO 程序使用 Reactor模型,运行过程中必须在 reactor->wait 处轮询。

如果遇到死循环,那么程序的控制权就在 while 中了,reactor 无法得到控制权,无法检测事件,所以 IO 事件回调函数也将无法触发。

密集运算的代码没有任何 IO 操作,所以不能称为阻塞

实例程序:

  1. $server = new Swoole\Server("127.0.0.1", 9501);
  2. $server->set(['worker_num' => 1]);
  3. $server->on('receive', function ($server, $fd, $reactorId, $data) {
  4. $i = 0;
  5. while(1)
  6. {
  7. $i++;
  8. }
  9. $server->send($fd, 'Swoole: '.$data);
  10. });
  11. $server->start();

onReceive 事件中执行了死循环,server 无法再收到任何客户端请求,必须等待循环结束才能继续处理新的事件。

注:以上内容均转载至Swoole官方文档,再加以修改。