swoole面试常见问题

介绍swoole

首先 swoole 是 php 的一个扩展程序,swoole 是一个为 php 用 c 和 c++ 编写的基于事件的高性能异步 & 协程并行网络通信引擎。
swoole 是一个多进程模型的框架,当启动一个进程 swoole 应用时,一共会创建 2+n+m 个进程,n 为 worker 进程数,m 为 TaskWorker 进程数,1 个 master 进程和一个 manager 进程,关系如下图所示

Reactor 线程

  • 负责维护客户端 TCP 连接、处理网络 IO、处理协议、收发数据
  • 完全是异步非阻塞的模式
  • 全部为 C 代码,除 Start/Shudown 事件回调外,不执行任何 PHP 代码
  • TCP 客户端发来的数据缓冲、拼接、拆分成完整的一个请求数据包
  • Reactor 以多线程的方式运行

Worker 进程

  • 接受由 Reactor 线程投递的请求数据包,并执行 PHP 回调函数处理数据
  • 生成响应数据并发给 Reactor 线程,由 Reactor 线程发送给 TCP 客户端
  • 可以是异步非阻塞模式,也可以是同步阻塞模式
  • Worker 以多进程的方式运行

TaskWorker 进程

  • 接受由 Worker 进程通过 swoole_server->task/taskwait方法投递的任务
  • 处理任务,并将结果数据返回(使用 swoole_server->finish)给 Worker 进程
  • 完全是同步阻塞模式
  • TaskWorker 以多进程的方式运行

网络通信引擎

网络通信引擎,是为 php 提供网络通信能力的,传统的 php 程序都是启动 php-fpm,前边再有一层 nginx 或者 apache,打开浏览器访问就 OK,但是从浏览器访问到服务器这一阶段涉及到了网络强求,但是这一阶段跟 php 脚本没有任何的关系,php 只需要处理好数据生成需要展示的内容就完成使命了,声明周期当中,请求到来前和请求完成后都没有 php 脚本什么事,而 swoole 提供的一大能力就是扩展了 php 的生命周期,无需 php-fpm 或者 nginx 或者 apache 之类的工具帮助就可以启动一个 web 服务,并且从服务启动前,启动后,链接进入,请求到来,请求结束,链接切断,服务终止都在 php 脚本的掌控之中,这样的话 php 脚本就会涉及到大量的网络通讯处理,而这个网络通讯处理的能力正是来源于 swoole!

基于事件的高性能异步

1
2
$data = file_contents('./data.json');
echo $data;

就拿读取文件内容来说吧,ile_get_contents() 执行完才能执行下边的代码 这样就很容易造成程序的阻塞。

否则下边的代码就无法输出文件的内容
传统 php 都是这样阻塞式的顺序执行的

这是常见的同步编程
异步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$.ajax({
url: "foo",
data: 1,
success: function(a) {
$.ajax({
url: "bar",
data: a,
success: function(b) {
console.log(b)
}
})
}
})
console.log(lll)

代码在执行到 ajax 的时候,函数会直接返回,你马上就可以看到屏幕上打印出的lll

这就是异步,这样你永远不会被 IO 阻塞,但是它带来了新的问题,在你运行到 lol 之后你就不知道现在代码运行到哪里去了,你只能等待回调被触发,然后屏幕上打印响应的 log, 它的执行不是单层顺序的,而是嵌套的
如果在业务代码当中 这样层层嵌套可读性可想而知
当然这是前端异步请求后端接口
swoole 当中处理异步回调嵌套使用的是协程
你知道什么叫协程吗?你知道线程是干啥的吗?你又知道进程吗?
如果想深入了解 swoole 的强大之处 你还得要了解传统 php 的 lnmp 环境的整套运行机制,这些你都了解吗?

协程

通俗的说,协程就是一段段协作方式执行的程序,协作来完成一件事,协作就是协同作业。我们知道团队协同来做事,比个人单独来做事,效率肯定要高,因为团队协同可以发挥各成员的能动性、优势互补。这是拿人来比喻。我们拿做事来比喻举个例子:比如我们做饭,比如有以下环节洗菜、切菜、烧水、炒菜、煮米饭,人作为主体来操作,那么如果按部就班的做,先烧水,再洗菜,在切菜,再炒菜,再煮饭,那这顿饭要做很长时间比如总共 30 分钟吧,如果我们通过协同方式,先烧水,放灶火上就可以做其他洗菜、切菜的准备,再煮米饭,然后再来洗菜、切菜,再查看煮米饭,再炒菜,…,如此循环往复切换,最后水烧好,米饭也煮好了,菜也炒好了,饭也 OK 了,这样我们耗时可能只有 10-15 分钟,看到了吗,这就是生活中的 “协程”,由人来合理调度安排不同的环节,充分利用各种不同的资源和时间,来达到提高效率。协程是计算机程序,调用的则是不同的程序,处理者主要由 CPU 完成,处理对象是各种 IO 资源,处理的方式是不同的语言编写的程序。我们知道,CPU 可以调度不同的程序,让程序调用不同的 IO 资源,最初的进程是通过 CPU 频繁的切换来完成调用程序的,是操作系统按一定算法分配的时间片抢占被动方式来切换的,未考虑程序实际执行状况,这样切换程序会带来一定问题,而协程作为一种新的工作模式,可以让程序协作方式来执行,在需要使用 CPU 时,交给程序处理,遇到耗时的 IO 资源操作时会让出 CPU,交给处理其他程序,这样互相协作来执行,而不是抢占式的,就像交通规则,大家都遵守按一定规则礼让先行,不随便抢道,协同方式,程序都会执行的良好。

例子:

1
2
3
4
5
6
7
go(function () {
echo "hello go1 \n";
});
echo "hello main \n";
go(function () {
echo "hello go2 \n";
});

输出结果:

1
2
3
4
swoole# php co.php
hello go1
hello main
hello go2

执行结果和我们平时写代码的顺序,好像没啥区别。实际执行过程:

  • 运行此段代码,系统启动一个新进程
  • 遇到 go(), 当前进程中生成一个协程,协程中输出 heelo go1, 协程退出
  • 进程继续向下执行代码,输出 hello main
  • 再生成一个协程,协程中输出 heelo go2, 协程退出

再看看这个例子:

1
2
3
4
5
6
7
8
9
use Co;
go(function () {
Co::sleep(1); // 只新增了一行代码
echo "hello go1 \n";
});
echo "hello main \n";
go(function () {
echo "hello go2 \n";
});

输出结果:
\Co::sleep () 函数功能和 sleep () 差不多,但是它模拟的是 IO 等待 (IO 后面会细讲).

1
2
3
4
swoole# php co.php
hello main
hello go2
hello go1

可以看出代码是异步执行的

协程快在哪?减少 IO 阻塞导致的性能损失

大家可能听到使用协程的最多的理由,可能就是 协程快. 那看起来和平时写得差不多的代码,为什么就要快一些呢?一个常见的理由是,可以创建很多个协程来执行任务,所以快. 这种说法是对的,不过还停留在表面。

首先,一般的计算机任务分为 2 种:

  • CPU 密集型,比如加减乘除等科学计算
  • IO 密集型,比如网络请求,文件读写等
    其次,高性能相关的 2 个概念:

并行:同一个时刻,同一个 CPU 只能执行同一个任务,要同时执行多个

  • 任务,就需要有多个 CPU 才行
  • 并发:由于 CPU 切换任务非常快,快到人类可以感知的极限,就会有很多任务 同时执行 的错觉

了解了这些,我们再来看协程,协程适合的是 IO 密集型 应用,因为协程在 IO 阻塞 时会自动调度,减少 IO 阻塞导致的时间损失!
协程在遇到 IO 阻塞的时候会让出 cpu 的控制权,其他协程拿到去执行其他协程的任务,当 IO 阻塞过去之后回过头来继续往下执行!

要点:

  1. 协程在阻塞的时候只是阻塞了当前这个协程 并不会阻塞整个的进程 因为协程是在线程内部的,即使阻塞了也会让出控制权,挂起,等待当前协程的 IO 不阻塞在回过头来继续执行,也就是同步的代码完成了异步的功能!相当强悍!
  2. 从宏观的角度看,程序员搞出来的多个协程在不发生任何协程阻塞的前提是是顺序执行的 一旦发生阻塞 你可以把多个协程理解为并行的 同时在执行的!
  3. 协程是在单进程单线程当中实现的 你可以在里面实现成千上万的协程 并且效果极高! 每个协程去干不同的事!协作制的无需加锁没有抢占,串行的!什么叫串行呢?每次执行一个协程 遇到 IO 阻塞 挂起 执行接下来的程序 可能还是个协程 如果再遇到 IO 阻塞再挂起 继续往下执行 当 IO 阻塞完成回过头来继续往下执行没执行完的协程程序 每次都是一个协程在执行,串行化的!
  4. 协程之间每秒可以进行百万千万次切换! 线程之间切换需要加锁 加锁就很浪费资源!进程间切换更浪费资源,因为上线很大。
  5. 协程很小切换还快 每秒百万千万级别的切换 所以 一个进程里面 只要你的内存够用 你就可以无止境的创造协程出来干事情!
  6. 事件驱动和异步为 swoole 提供了高性能 而协程解决了异步回调代码嵌套的问题 提高了代码可读性和维护性 也是 swoole 最大的特色!

协程之间通讯 channel

通道(channel)是协程之间通信交换数据的唯一渠道,而协程 + 通道的开发组合即为著名的 csp 编程模型
在 swoole 当中 channel 常用于连接池的实现和协程并发的调度
如图所示 第一个协程执行完成之后我们会往 channel 通道当中 push 一个元素 第二个也是 当然第一个一定是 IO 阻塞的 第二个没有 我们在 for 循环里面 获取 channel 里面的值的时候由于第一个阻塞 chan−>pop()也是阻塞的因为整体都是在一个协程里面只有当大的协程里面的两个小协程都完成了这里的chan->pop () 也是阻塞的 因为整体都是在一个协程里面 只有当大的协程里面的两个小协程都完成了 这里的 chan−>pop()也是阻塞的因为整体都是在一个协程里面只有当大的协程里面的两个小协程都完成了这里的chan->pop 才会执行 接触阻塞 最后才会执行 echo 语句! 这是 swoole 当中协程并发的一个很好的案例应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
use Swoole\Coroutine as co;

function query(array $sql){...}

go(function() {
$chan = new co\Channel(2)
go(function() use ($chan) {
$var = query(...);
$chan->push($var);
});

go(function() use ($chan) {
$var = query(...);
$chan->push($var);
});

for($i=0; $i<2; $i++) {
$chan->pop();
}

echo "done" . PHP_EOL;
});

毫秒定时器

毫秒定时器是异步回调的方式来实现的!
还可以使用协程方式 采用同步阻塞的方式来实现定时器!
之前用 sleep 会阻塞整个进程 现在你在协程里面搞 Co:sleep (0.1) 阻塞的是当前协程 而不会阻塞整个进程!

1
2
3
4
5
6
7
8
9
10
11
12
go(function() {
$i = 0;
while(true) {
Co::sleep(0.1);
echo "Do something...\n";
if (++$i === 5) {
echo "Done\n";
break;
}
}
echo "All right!\n";
})

swoole 提升性能

  1. 进程常驻内存:
    swoole本⾝是进程常驻内存,在进程启动的时候就将PHP框架等代码读取并编译完成,不需要每次启动的时候都执⾏编译步骤,⼤⼤降低了脚本的运⾏时间;
  2. 连接池
    php-fpm的模式php因为每次请求结束时都会销毁所有资源,因此⽆法使⽤连接池;⽽基于swoole的进程常驻内存模式,可以通过连接池的⽅式来加速程序,使⽤连接池既可以降低程序的响应时间,⼜可以有效保护后端资源。
  3. 可以使⽤协程处理异步IO
    当开发中需要去请求多处的数据,⽽每⼀块的数据单独请求都要花较长时间,常规的php-fpm是阻塞式运⾏,⽆法对这类型的数据处理进⾏加速;⽽基于swoole的程序,可以将这类的业务并⾏化处理,并⾏去请求后端的数据源,能够⼤⼤优化了此类业务的运⾏时间。

swoole⾥的协程是什么,怎么⽤?为什么协程可以提⾼并发

协程是通过协作⽽不是抢占的⽅式来进⾏切换,它创建和切换对内存等资源⽐线程⼩的多(可以理解为更⼩的线程);
协程的使⽤是通过Swoole\Coroutine或者Co\命名空间短命名简化类名来创建;
协程可以异步处理任务,⽀持并发,并且资源消耗⼩

⽤了swoole以后,会不会发⽣内存泄漏?如果发⽣了怎么解决?

swoole由于是常驻内存,⼀旦资源加载进⼊后,会⼀直存在于内存中。对于局部变量,swoole会在回调函数结束后⾃动释放;对于全局变量(lobal声明的变量,static声明的对象属性或者函数内的静态变量和超全局变量),swoole不会⾃动释放;因此操作不好会发⽣内存泄漏。
解决:

  1. 在onClose的时候释放
  2. 在 Swoole 中,可以使用 max_request 和 task_max_request 来避免内存泄露
    max_request:表示 worker 进程的最大任务数量,当 worker 进程处理的任务数量超过这个参数值时,worker 进程自动退出,如此就达到了释放内存和资源的目的
    task_max_request:同 max_request 一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

<?php
// 13-swoole-server.php

$server = new Swoole\Server('0.0.0.0', 9501, SWOOLE_PROCESS);

$server->set([
'worker_num' => 1,
'task_worker_num' => 1,
'max_request' => 2,
'task_max_reqeust' => 2,
]);


$server->on('Receive', function ($server, $fd, $fromId, $data) {
$server->task($data);
});

$server->on('Task', function ($server, $taskId, $fromId, $data) {
});

$server->on('Finish', function ($server, $taskId, $data) {
});

$server->start();

缺点:
max_request 只能用于同步阻塞,无状态的请求响应式服务
纯异步的server不应该设置max_request
使用Base模式时,max_request无效