传统Nginx+PHP-FPM架构
PHP-FPM并发模型
在网络应用场景下,PHP并没有向Golang那样去实现http网络库,而是实现了FastCGI协议,然后通过web服务器配合实现了http的处理。web服务器来处理http请求,然后将解析的结果再通过FastCGI协议转发给处理程序,处理程序处理完成后将结果返回给web服务器,web服务器再返回给客户。
PHP-FPM是经典的多进程并发模型,即Master/Worker模型。Master进程与Worker进程之间不会直接进行通信,Master进程只负责Fork和管理子进程,网络请求由子进程处理,一个Worker进程同时只能处理一个请求。Master通过共享内存获取Worker进程的信息,比如Worker进程当前状态,已处理请求数等,当Master进程要杀掉一个Worker进程时则通过发送信号的方式通知Worker进程。
PHP-FPM初始化启动和Worker处理请求
PHP-FPM从初始化启动到Worker请求处理大概涉及以下步骤
-
fpm_init()
Master读取php-fpm.conf文件初始化内存配置变量、创建管道、套接字、启动事件管理 -
fpm_run()
Master进程fork出子进程后阻塞,Worker进程去accept请求,执行php脚本
-
等待请求
Worker进程阻塞在fagi_accept_request()
等待请求 -
解析请求
fastcgi请求到达后被worker接收,然后开始接收并解析请求数据,直到request数据完全到达 -
请求初始化
执行php_request_startup()
,此阶段会调用每个扩展的PHP_RINIT_FUNCTION()
; -
编译,执行
由php_execute_script()
完成PHP脚本的编译、执行 -
关闭请求
请求完成后执行php_request_shutdown()
,此阶段会调用每个扩展的PHP_RSHUTDOWN_FUNCTION()
,然后进入步骤1等待下一个请求。
生成的语法树和opcode,同一个PHP脚本每次运行的结果都是一样的,在PHP-FPM模式下,每次请求都要处理一遍,是对系统资源极大的浪费
Laravel框架的性能问题
Laravel是fpm社区里面非常受欢迎的一款web框架,结合composer管理开发组件,可以帮助开发者在框架提供的多种优秀组件(ORM
,Router
,Middleware
,Artisan
,....)和解决方案在PSR-4规范下高效的进行Web服务端的开发。
所以有人说,如果Wordpress让PHP焕发了第一春,那么Composer让PHP焕发了第二春。
虽然laravel框架开发起来简单高效,中文社区也非常活跃,生态丰富。但是用laravel开发的应用性能问题却一直被反复提及。造成Laravel性能问题的主要原因有以下几点
- 框架代码的深度封装,导致把fpm模式下框架启动需要加载大量类和文件的性能问题放大(ps:需要new太多类,每次new一个类都要autoloader找到类所在文件再require)
- 每一次请求都会将所有的路由和配置全部加在到内存中,即便这次请求可能用不到(ps:这其实也要归咎于fpm一次请求结束后就会销毁内存的特性)
- 在框架启动时会调用composer autoload,按照classmap、prs-4、psr-0等规范生成所有类的引入路径
所以针对Laravel进行性能优化的方向也就是针对以上几点来进行的:
- 服务器启用
PHP-OPcache
扩展缓存。 - 使用
php artisan route:cache
,php artisan config:cache
等缓存命令。 - 通过
composer install --optimize-autoloader --no-dev
初始化项目依赖,以便加速Composer
定位指定类对应的加载文件,同时不安装开发环境使用的依赖。
FPM架构的局限
- 多进程并发模型带来的单台服务器性能局限。
- 只能开发HTTP服务器,如果需要WebSocket,TCP等其他协议的服务器怎么办。
- 服务处理中的内存状态无法持久化,绝大多数只能开发无状态服务,有状态服务需要借助本地文件或者mmap方式来实现。
Swoole - PHP的高性能通信引擎扩展
什么是Swoole
Swoole 是一个使用 C++ 语言编写的基于异步事件驱动和协程的并行网络通信引擎,为 PHP 提供协程、高性能网络编程支持。提供了多种通信协议的网络服务器和客户端模块,可以方便快速的实现 TCP/UDP服务、高性能Web、WebSocket服务、物联网、实时通讯、游戏、微服务等,使 PHP 不再局限于传统的 Web 领域。
Swoole的出现可以打开上述FPM所面临的局限
- Swoole是通过cli模式启动的常驻内存服务,通过自身提供的异步/协程服务端可以提高单机服务的并发性能(即便是同步I/O的服务性能也会有较大提升)
- Swoole提供了多种通信协议的网络服务器,包括
TCP
、UDP
、HTTP
、WebSocket
、MQTT
- Swoole提供该性能共享内存
SwooleTable
、进程间无锁计数器Atomic
、进程间API等方式保证有状态服务进程间的同步 - 底层hook原生PHP函数,使其能够更好的运行在协程server内(底层替换了ZendVM Stream的函数指针,所有使用php_stream进行socket操作均变成协程调度的异步IO)
协程风格的 Swoole - HTTP 服务端
use Swoole\Coroutine\Http\Server;
use function Swoole\Coroutine\run;
run(function() {
$server = new Server('127.0.0.1', 9502, false);
$server->handle('/test', function($request, $response) {
Co::sleep(1);
$response->end("test");
});
$server->handle('/stop', function($request, $response) use ($server) {
$response->end("Stop
");
$server->shutdown();
});
$server->start();
});
协程风格的 Swoole - TCP 服务端
use Swoole\Process;
use Swoole\Coroutine;
use Swoole\Coroutine\Server\Connection;
//多进程管理模块
$pool = new Process\Pool(2);
//让每个 OnWorkerStart 回调都自动创建一个协程
$pool->set(['enable_coroutine' => true]);
$pool->on('workerStart', function($pool, $id) {
//每个进程都监听 9501 端口
$server = new Swoole\Coroutine\Server('127.0.0.1', 9501, false, true);
//收到 15 信号关闭服务
Process::signal(SIGTERM, function() use ($server) {
$server->shutdown();
});
//接收到新的连接请求 并自动创建一个协程
$server->handle(function (Connection $conn) {
while(true) {
//接收数据
$data = $conn->recv(10);
if ($data === '' || $data === false) {
$errCode = swoole_last_error();
$errMsg = socket_strerror($errCode);
echo "ErrCode: {$errCode}, ErrMsg: {$errMsg}\n";
$conn->close();
break();
}
//发送数据
$conn->send('hello');
Coroutine::sleep(1);
}
});
//开始监听端口
$server->start();
});
$pool->start();
Swoole服务端进程线程结构图
swoole_process
SWOOLE_PROCESS 模式的 Server 所有客户端的 TCP 连接都是和主进程建立的,内部实现比较复杂,用了大量的进程间通信、进程管理机制。适合业务逻辑非常复杂的场景。Swoole 提供了完善的进程管理、内存保护机制。 在业务逻辑非常复杂的情况下,也可以长期稳定运行。
优点:
- 连接与数据请求发送是分离的,不会因为某些连接数据量大某些连接数据量小导致 Worker 进程不均衡
- Worker 进程发生致命错误时,连接并不会被切断
- 可实现单连接并发,仅保持少量 TCP 连接,请求可以并发地在多个 Worker 进程中处理
缺点:
- 存在 2 次 IPC 的开销,master 进程与
worker
进程需要使用unixSocket
进行通信
swoole_base
SWOOLE_BASE 这种模式就是传统的异步非阻塞 Server。与 Nginx 和 Node.js 等程序是完全一致的。
BASE 模式下没有 Master 进程的角色,只有 Manager 进程的角色。 每个 Worker 进程同时承担了 SWOOLE_PROCESS 模式下 Reactor 线程和 Worker 进程两部分职责。
优点:
- BASE 模式没有 IPC 开销,性能更好
- BASE 模式代码更简单,不容易出错
缺点:
- TCP 连接是在 Worker 进程中维持的,所以当某个 Worker 进程挂掉时,此 Worker 内的所有连接都将被关闭
- 少量 TCP 长连接无法利用到所有 Worker 进程
- TCP 连接与 Worker 是绑定的,长连接应用中某些连接的数据量大,这些连接所在的 Worker 进程负载会非常高。但某些连接数据量小,所以在 Worker 进程的负载会非常低,不同的 Worker 进程无法实现均衡。
- 如果回调函数中有阻塞操作会导致 Server 退化为同步模式,此时容易导致 TCP 的 backlog 队列塞满问题。
Swoole Coroutine 和 Go Coroutine的区别
- swoole的协程需要再协程上下文中使用
- swoole的协程是通过hook底层php函数实现的,go是原生支持的
- swoole是单线程协程,同一时间只会调度一个协程,而go是多线程实现的
$n = 4;
for ($i = 0; $i < $n; $i++) {
go(function() use($i) {
//模拟 IO 等待
Co::sleep(1);
echo microtime(true) . ": hello $i " . PHP_EOL;
});
};
echo "hello main \n";
\Swoole\Event::wait();
Hyperf - 基于 Swoole 的 PHP 协程框架
Hyperf 的出现是为了解决Swoole门槛高,上手难的问题。结合Composer组件,帮助使用者以低成本打造一套高性能的灵活的应用服务。
Hyperf特性
- Hyperf内置大量的协程组件,以保证请求不回退化到阻塞模式。
- Hyperf支持多端口监听,可同时启动HTTP和WebSocket端口
- 支持jsonRPC,gRPC微服务配套设施(熔断、降级、配置中心、链路追踪、Metric监控等)
- 因为是常驻内存的服务,所以引入连接池(
Mysql
、Redis
、GuzzleHttp
) - ORM 基于Laravel ORM进行改造,从Laravel/Lumen 移植过来非常顺畅
依赖注入
Hyperf/DI 是框架中管理对象类创建和依赖关系的组件,也是实现 注解 和 AOP 功能的关键。 DI中管理的对象都是用于服务长生命周期的单例对象,短生命周期使用make()方法创建。 依据创建对象的需求不同可分为以下三种注入方式
- 简单对象注入
- 抽象对象注入
- 工厂对象注入
注解 和 AOP
Hyperf基于AOP和注解实现了诸多框架基础功能,如
- 依赖注入
@Inject()
- 路由定义
@AutoController()
- 中间件
@Middleware(FooMiddleware::class)
- 缓存
@Cacheable(prefix="test", ttl=9000, listener="test-update")
事件机制和生命周期
Hyperf的事件机制存在于框架启动时,能够帮助用户更好的和框架配合完成逻辑的解耦,例如DBqueryLisenner
。
在处理每个连接时,会默认创建一个协程去处理,主要体现在 onRequest
、onReceive
、onConnect
事件,所以可以理解为每个请求都是一个协程,由于创建协程也是个常规操作,所以一个请求协程里面可能会包含很多个协程,同一个进程内协程之间是内存共享的,但调度顺序是非顺序的,且协程间本质上是相互独立的没有父子关系,所以对每个协程的状态处理都需要通过协程上下文
来管理。
The Last - 写在最后
这次分享不是不是为了比较一个高低,不是说FPM模式下的开发方式和项目就不好,而是想表达fpm可能已经不适合当下追求并发,尽可能榨取服务器CPU性能的要求了。Swoole在php原生语法简单高效的基础上,为php搭建了基于协程(异步非阻塞)的运行模式,再加上Hyperf这样组件化的成熟框架,使得上手Swoole不在困难。让大家了解到这种开发方式的可能性就可以了。