PHP-FPM
1.1 传统 Nginx+FPM 架构
1.1.1 PHP-FPM 并发模型
图 1.1.1 一个 HTTP 申请的流转过程在网络应用场景下,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 过程。
1.1.2 PHP-FPM 初始化启动 及 Worker 申请解决
PHP-FPM 从初始化启动到 Worker 申请解决大略波及到以下步骤:fpm_init() Master 读取 php-fpm.conf 文件初始化内存配置变量、创立管道、套接字、启动事件治理 fpm_run() Master 过程 fork 出子过程后阻塞,worker 进城去 accept 申请,执行 php 脚本 (1) 期待申请:worker 过程阻塞在 fcgi_accept_request()期待申请;(2)解析申请:fastcgi 申请达到后被 worker 接管,而后开始接管并解析申请数据,直到 request 数据齐全达到;(3)申请初始化:执行 php_request_startup(),此阶段会调用每个扩大的:PHP_RINIT_FUNCTION();(4)编译、执行:由 php_execute_script()实现 PHP 脚本的编译、执行;(5)敞开申请:申请实现后执行 php_request_shutdown(),此阶段会调用每个扩大的:PHP_RSHUTDOWN_FUNCTION(),而后进入步骤 (1) 期待下一个申请。
图 1.1.2 php 生命周期生成的语法树和 opcode,同一个 PHP 脚本每次运行的后果都是一样的,在 PHP-FPM 模式下,每次申请都要解决一遍,是对系统资源极大的节约
1.2 Laravel 框架的性能问题
laravel 是 fpm 社区外面十分受欢迎的一款 web 框架,联合 composer 治理开发组件,能够帮忙开发者在框架提供的多种优良组件(ORM,Router,Middleware,Artisan 等)和解决方案在 PSR- 4 标准下高效的进行 Web 服务端开发。所以有人说,如果 Wordpress 让 php 焕发第一春,那么 Laravel+Composer 让 PHP 焕发第二春。尽管 laravel 框架开发起来简略高效,中文社区也十分沉闷,生态丰盛。然而用 laravel 开发的利用性能问题却始终被重复提及。造成 Laravel 性能问题的次要起因有以下几点:框架代码的深度封装,导致把 fpm 模式下框架启动须要加载大量类和文件的性能问题放大(须要 new 太多类,每次 new 一个类都要 autoloader 找到类所在文件再 require)每一次申请都会将所有的路由和配置全副加在到内存中,即使这次申请可能用不到(这其实也要归咎于 fpm 一次申请完结后就会销毁内存的个性)在框架启动时会调用 composer autoload,依照 classmap、prs-4、psr- 0 等标准生成所有类的引入门路所以针对 Laravel 进行性能优化的方向也就是针对以上几点来进行的:服务器启用 PHP OPcache 扩大缓存 opcodephp artisan route:cache php artisan config:cache 等缓存命令通过 composer install –optimize-autoloader –no-dev 初始化我的项目依赖,以便减速 Composer 定位指定类对应的加载文件,同时不装置开发环境应用的依赖
1.3 FPM 架构的局限
并发模型带来的单台服务器性能局限只能开发 HTTP 服务器,如果须要 WebSocket,TCP 等其余协定的服务器怎么办服务解决中的内存状态无奈长久化,绝大多数只能开发无状态服务,有状态服务须要借助本地文件或者 mmap 形式来实现 Swoole-PHP 的高性能通信引擎扩大
2.1 什么是 Swoole
Swoole 是一个应用 C++ 语言编写的基于异步事件驱动和协程的并行网络通信引擎,为 PHP 提供协程、高性能网络编程反对。提供了多种通信协议的网络服务器和客户端模块,能够不便疾速的实现 TCP/UDP 服务、高性能 Web、WebSocket 服务、物联网、实时通信、游戏、微服务等,使 PHP 不再局限于传统的 Web 畛域。Swoole 的呈现能够关上上述 FPM 所面临的局限 Swoole 是通过 cli 模式启动的常驻内存服务,通过本身提供的异步 / 协程服务端能够进步单机服务的并发性能(即使是同步 I / O 的服务性能也会有较大晋升)Swoole 提供了多种通信协议的网络服务器,包含 TCP、UDP、HTTP、WebSocket、MQTTSwoole 提供该性能共享内存 SwooleTable、过程间无锁计数器 Atomic、过程间 API 等形式保障有状态服务过程间的同步底层 hook 原生 PHP 函数,使其可能更好的运行在协程 server 内(底层替换了 ZendVM Stream 的函数指针,所有应用 php_stream 进行 socket 操作均变成协程调度的异步 IO)
2.2 如何应用 Swoole
2.2.1 协程格调 HTTP 服务端
<?php
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("<h1>Stop</h1>");
$server->shutdown();});
$server->start();
});
2.2.2 协程格调 TCP 服务端
<?php
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();
2.3 Swoole 并发模型
Swoole 服务端过程线程结图 2.3.1 swoole_processSWOOLE_PROCESS 模式的 Server 所有客户端的 TCP 连贯都是和主过程建设的,外部实现比较复杂,用了大量的过程间通信、过程管理机制。适宜业务逻辑非常复杂的场景。Swoole 提供了欠缺的过程治理、内存保护机制。在业务逻辑非常复杂的状况下,也能够长期稳固运行。长处:连贯与数据申请发送是拆散的,不会因为某些连贯数据量大某些连贯数据量小导致 Worker 过程不平衡 Worker 过程产生致命谬误时,连贯并不会被切断可实现单连贯并发,仅放弃大量 TCP 连贯,申请能够并发地在多个 Worker 过程中解决毛病:存在 2 次 IPC 的开销,master 过程与 worker 过程须要应用 unixSocket 进行通信 2.3.2 swoole_baseSWOOLE_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 队列塞满问题。
2.4 Swoole Coroutine 和 Go Goroutine 的区别
swoole 的协程须要再协程上下文中应用 swoole 的协程是通过 hook 底层 php 函数实现的,go 是原生反对的 swoole 是单线程协程,同一时间只会调度一个协程,而 go 是多线程实现的 <?php
$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();2.5 Swoole 和 Go HTTP 服务器性能比照 2.5.1 压测代码 Swoole HTTP 代码如下应用 Swoole/Server 设置 2 个 worker 过程 $http = new Swoole\Http\Server(“127.0.0.1”, 9502, SWOOLE_PROCESS);
$http->set(array(
'worker_num' => 2, // worker process num
));
$http->on(‘request’, function ($request, $response) {
$response->end("test");
});
$http->start();Go HTTP 代码如下 package main
import (
"fmt"
"time"
"net/http"
"log"
)
func test(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "test") // 这个写入到 w 的是输入到客户端的
}
func main(){
http.HandleFunc("/test", test) // 设置拜访的路由
err := http.ListenAndServe(":9502", nil) // 设置监听的端口
if err != nil {log.Fatal("ListenAndServe:", err)
}
}
2.5.2 压测命令 wrk -t8 -c200 -d30s –latency “http://127.0.0.1:9502/test”2.5.3 压测后果 Running 30s test @ http://127.0.0.1:9502/test
8 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.42ms 318.83us 11.60ms 75.70%
Req/Sec 17.62k 1.70k 20.29k 62.75%
Latency Distribution
50% 1.35ms
75% 1.58ms
90% 1.82ms
99% 2.29ms
4222085 requests in 30.10s, 628.13MB read
Socket errors: connect 0, read 60, write 0, timeout 0
Requests/sec: 140262.55
Transfer/sec: 20.87MBRunning 30s test @ http://127.0.0.1:9502/test
8 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 2.29ms 0.90ms 33.35ms 86.15%
Req/Sec 10.99k 1.74k 17.86k 71.76%
Latency Distribution
50% 2.36ms
75% 2.70ms
90% 2.97ms
99% 3.50ms
2632268 requests in 30.10s, 301.24MB read
Socket errors: connect 0, read 51, write 0, timeout 0
Requests/sec: 87441.90
Transfer/sec: 10.01MB 注:压测后果上 Swoole 和 Golang 的差距次要体现在 go 是应用单线程 eventloop 解决 IO 事件,多线程实现协程调度,执行用户层代码 swoole 应用多线程 eventloop 解决 IO 事件,多过程执行用户层 php 代码 Hyperf- 基于 Swoole 的 PHP 协程框架 Hyperf 的呈现是为了解决 Swoole 门槛高,上手难的问题。联合 Composer 组件,帮忙使用者以低成本打造一套高性能的灵便的应用服务。
3.1 Hyperf 个性
Hyperf 内置大量的协程组件,以保障申请不回进化到阻塞模式。Hyperf 反对多端口监听,可同时启动 HTTP 和 WebSocket 端口反对 jsonRPC,gRPC 微服务配套设施(熔断、降级、配置核心、链路追踪、Metric 监控等)因为是常驻内存的服务,所以引入连接池(Mysql、Redis、GuzzleHttp)ORM 基于 Laravel ORM 进行革新,从 Laravel/Lumen 移植过去十分顺畅 3.2 Hyperf 框架外围 3.2.1 依赖注入 Hyperf/DI 是框架中治理对象类创立和依赖关系的组件,也是实现 注解 和 AOP 性能的要害。DI 中治理的对象都是用于服务长生命周期的单例对象,短生命周期应用 make()办法创立。根据创建对象的需要不同可分为以下三种注入形式简略对象注入形象对象注入工厂对象注入 3.2.2 注解 和 AOPHyperf 基于 AOP 和注解实现了诸多框架根底性能,如依赖注入 @Inject()路由定义 @AutoController()中间件 @Middleware(FooMiddleware::class) 缓存 @Cacheable(prefix=”user”, ttl=9000, listener=”user-update”)3.2.3 事件机制 和 生命周期 Hyperf 的事件机制存在于框架启动时,可能帮忙用户更好的和框架配合实现逻辑的解耦,例如 DBqueryLisenner。在解决每个连贯时,会默认创立一个协程去解决,次要体现在 onRequest、onReceive、onConnect 事件,所以能够了解为每个申请都是一个协程,因为创立协程也是个惯例操作,所以一个申请协程外面可能会蕴含很多个协程,同一个过程内协程之间是内存共享的,但调度程序是非程序的,且协程间实质上是互相独立的没有父子关系,所以对每个协程的状态解决都须要通过 协程上下文 来治理。
最初这次分享不是不是为了比拟一个高下,不是说 FPM 模式下的开发方式和我的项目就不好,而是想表白 fpm 可能曾经不适宜当下谋求并发,尽可能榨取服务器 CPU 性能的要求了。Swoole 在 php 原生语法简略高效的根底上,为 php 搭建了基于协程(异步非阻塞)的运行模式,再加上 Hyperf 这样组件化的成熟框架,使得上手 Swoole 不在艰难。让大家理解到这种开发方式的可能性就能够了。