yaf
PHP framework written in c and built as a PHP extension.
用C语言开发的PHP框架, 相比原生的PHP, 几乎不会带来额外的性能开销.
所有的框架类, 不需要编译, 在PHP启动的时候加载, 并常驻内存.
当路由监听到浏览器访问,就会根据访问的uri构造出一个携带Module,Controller,Action信息的request,然后去访问具体模块下控制器的方法。
默认情况下,我们的路由器是Yaf_Router, 而默认使用的路由协议是Yaf_Route_Static。 Yaf_Route_Static分析请求中的request_uri, 在去除掉base_uri以后, 获取到真正的负载路由信息的request_uri片段, 具体的策略是, 根据”/”对request_uri分段。 那么可能有两种情况: 当uri有3段时(/module/controller/action),yaf会认为第一段是模块名,第二段是控制器名,第三段是动作名。 当uri有2段时(/controller/action),yaf会认为是去找默认模块index,第一段被认为是控制器名,第二段是动作名。
php 字节码缓存 opcache
php 用户级缓存 apcu
APCu用户缓存默认使用mmap实现,因此APCu内的数据可以作为共享内存被多个进程读写。
在FPM模式下,一个进程往APCu内存段中存储的数据可以被另一个fpm进程访问到,因为这些fpm worker进程都是由fpm master进程fork出来的。
master进程在创建时就已经向系统申请内存通过mmap映射的方式创建了一个APCu用户缓存。
master进程fork子进程时,该APCu用户缓存区域会映射到所有的worker子进程的虚拟内存空间中,这些worker进程通过该ACPu内存段的指针对用户缓存进行共享。
php 请求处理流程
PHP-FPM 位于 SAPI 层,在接收到来自 Nginx 转发过来的 PHP 请求时,会将其交给某个空闲的 PHP-FPM worker进程来处理,PHP-FPM 进程会在启动阶段设置 HTTP 环境变量, 启动 PHP 核心代码 和 PHP Modules (即扩展, yaf就是这个时候加载的),并对此次请求上下文进行初始化,完成,这些操作后再调用 Zend 引擎来编译并执行业务逻辑代码(进入 Laravel 项目,从入口文件开始执行)
(即 PHP-FPM 进程 启动的时候会加载 PHP 模块, 之后每个请求执行rinit 流程)
Zend 引擎会检查 OpCode 缓存,如果代码片段已经缓存,则从缓存中读取并执行,否则还要编译成 OpCode 并缓存后才能执行。
代码执行完成后,会将处理结果打印或者发送 HTTP 响应给客户端,然后 PHP 底层代码会执行请求关闭及模块关闭函数进行后续清理工作,最后再回到 SAPI 层,调用 PHP-FPM 对应的关闭函数,从而完成此次请求的所有流程。
php-fpm
运行原理[#]
PHP-FPM 使用 master/worker 架构设计。
Master 进程[#]
CGI 初始化阶段[#]
分别调用 fcgi_init() 函数 和 sapi_start_up() 函数,注册进程信号以及初始化 sapi_globals 全局变量。
PHP 环境初始化阶段[#]
由 cgi_sapi_module.start_up 触发,实际上调用 php_cgi_start_up 函数,而 php_cgi_start_up 内部又调用 php_module_start_up 执行。
php_module_start_up 主要功能包括:
1、加载和解析 PHP 配置文件。
2、加载 PHP 模块并记入函数符号表 (function_table)。
3、加载 Zend 扩展。
4、设置禁用函数和类库配置。
5、注册回收内存方法。
PHP-FPM 初始化阶段[#]
执行 fpm_init() 函数,负责解析 php-fpm.conf 文件配置,获取进程相关参数,例如:允许进程打开的最大文件数。
初始化进程池及事件模型等操作。
PHP-FPM 运行阶段[#]
执行 fpm_run() 函数,运行后主进程发送阻塞。
此阶段包括 fork 子进程和事件循环两个部分:
1、fork 子进程交由 fpm_children_create_initial 函数处理。
2、事件循环通过 fpm_event_loop 函数处理,其内部是一个死循环,负责事件的收集工作。
Worker 进程[#]
woker 进程分为 接收客户端请求、处理请求、请求结束三个阶段。
接收客户端请求[#]
执行 fcgi_accept_request 函数,其内部通过调用 accept 函数获取客户端请求。
处理请求阶段[#]
首先,分别调用 fpm_request_info、php_request_start_up 获取请求内容及注册全局变量
($_GET、$_POST、$_SERVER、$_ENV、$FILES)
然后,根据请求信息调用 php_fopen_primary_script 访问脚本文件。
最后,交给 php_execute_script 执行。执行 php_execute_script 内部调用 zend_execute_scripts 方法将脚本交给 zend 引擎处理。
请求结束阶段[#]
执行 php_request_shutdown 函数,此时回调 register_shutdown_function 注册的函数及 __destruct() 方法,发送响应内容、释放内存等操作。
管理方式
- dynamic 动态模式
启动时分配固定的进程,随着请求数的变化,在设定的浮动范围调整 worker 进程。
开启一定数量的 PHP-FPM 进程,当请求量变大的时候,动态增加 PHP-FPM 进程数量达到上限,当空闲的时候自动释放空闲进程数到下一个下限。
会根据 max、min、idle children 配置,动态的调整进程数量。
在用户请求波动较大,或者瞬间请求量变大的时候,会进行大量进程的创建和销毁操作,而引起 Linux 系统的负载增高。
-
static 静态模式
直接开启指定数据量的 PHP-FPM 进程,不再增加或者减少。
启动固定数量的进程,占用内存高。
但是在用户请求波动大的时候,对 Linux 系统的处理耗费的系统资源低。 -
ondemand 按需模式
- PHP-FPM 的 Master 不会 Fork 任何的子进程,只有收到用户请求时才会 fork worker 进程,这种模式很少使用。
因为它基本无法适应由一定量级的线上业务,由于 PHP-FPM 是短连接,所以每次请求都会先建立连接。
在大流量的系统上 Master 进程会变得非常繁忙,占用系统 CPU 资源,不适合大流量环境的部署。
- PHP-FPM 的 Master 不会 Fork 任何的子进程,只有收到用户请求时才会 fork worker 进程,这种模式很少使用。
nginx + php-fpm
总结:http请求 由 nginx的 worker进程处理,通过fast_cgi协议与php-fpm通信,再由php-fpm worker进程处理
swoole
Coroutine-based concurrency library for PHP
我们可以看到,Swoole 主要包含以下组件:
- Master:当我们运行启动 Swoole 的 PHP 脚本时,首先会创建该进程(它是整个应用的 root 进程),然后由该进程 fork 出 Reactor 线程和 Manager 进程。
- Reactor:Reactor 是包含在 Master 进程中的多线程程序,用来处理 TCP 连接和数据收发(异步非阻塞方式)。Reactor 主线程在 Accept 新的连接后,会将这个连接分配给一个固定的 Reactor 线程,并由这个线程负责监听此 socket。在 socket 可读时读取数据,并进行协议解析,将请求投递到 Worker 进程;在 socket 可写时将数据发送给 TCP 客户端。
- Manager:Manager 进程负责 fork 并维护多个 Worker 子进程。当有 Worker 子进程中止时,Manager 负责回收并创建新的 Worker 子进程,以便保持 Worker 进程总数不变;当服务器关闭时,Manager 将发送信号给所有 Worker 子进程,通知其关闭服务。
- Worker:以多进程方式运行,每个子进程负责接受由 Reactor 线程投递的请求数据包,并执行 PHP 回调函数处理数据,然后生成响应数据并发给 Reactor 线程,由 Reactor 线程发送给 TCP 客户端。所有请求的处理逻辑都是在 Worker 子进程中完成,这是我们编写业务代码时真正要关心的部分。
- Task Worker:功能和 Worker 进程类似,同样以多进程方式运行,但仅用于任务分发,当 Worker 进程将任务异步分发到任务队列时,Task Worker 负责从队列中消费这些任务(同步阻塞方式处理),处理完成后将结果返回给 Worker 进程。
Swoole 官方对 Reactor、Worker、Task Worker有一个形象的比喻,如果把基于 Swoole 的 Web 服务器比作一个工厂,那么 Reactor 就是这个工厂的销售员,Worker 是负责生产的工人,销售员负责接订单,然后交给工人生产,而 Task Worker 可以理解为行政人员,负责提工人处理生产以外的杂事,比如订盒饭、收快递,让工人可以安心生产。
swoole 为什么性能更好
php-fpm 模式为什么慢?
php-fpm 模式是以多进程方式来运行的,一个 master 进程创建并管理多个 work 进程。master 进程只负责接收请求和返回响应,剩下的运行工作交给 work 进程来执行。
也就是说每一个请求都对应一个 work 进程,同一时刻,服务器上有多少 work 进程,这台服务器就可以处理多少的并发。 这么一看是不是觉得 php-fpm 的并发能力特别差?假设不考虑服务器配置问题,默认的 400 个进程数同时就只能支持 400 的并发。 实际情况肯定没有这么差,假设很多的脚本只需要 0.001 秒就处理完成了,如果所有的请求都可以快速处理的话,那么我们可以说 1 秒钟的并发数就等于 400*1000=40 万的并发。
这么一看是不是觉得 php-fpm 的性能也没这么差了?
但是,如果你的业务数据量很大,mysql 的查询效率不高,每次请求都需要花费 1 秒钟的时间才能返回响应的话呢? 那么每秒钟的并发数就从 40W 又下降回 400 了。
而 swoole,就是为了解决这个问题所开发出来的一个 php 扩展,它使得每个 worker 进程不会因为 1 秒钟的 io 阻塞而白白让 cpu 浪费 1 秒钟的性能。
swoole 异步编程
按照刚刚的那个例子来解释的话,swoole 的处理方式就是在一个 worker 进程开始进行 mysql io 查询的时候就将这个请求任务暂时挂起,立马开始执行下一个请求,然后等到第一个请求中的 mysql io 数据返回之后,再切换回第一个请求中继续执行代码返回响应。这样一来,对于 cpu 来说,它一直在执行代码,没有因为请求中 mysql 的 1 秒 io 耗时处于空闲状态。io 操作已经无法影响它的并发数了,因为它始终在工作,并没有浪费等待时间。
解析一下,如果使用异步的方式,那么会有两个比较关键的点:
- 发起 io 操作,添加回调函数
- 等任务完成后执行回调函数
异步编程完全没有浪费 cpu 一点性能,那如果所有的 io 耗时操作都用异步操作会怎么样呢?
了解 node.js 的朋友可能经常听见一个词回调地狱。
而 swoole 的出现,就是为了解决同步代码浪费性能的问题,让同步执行的代码变为异步执行,同时使用协程降低异步回调编程时的心智负担。
协程vs多线程vs多进程
那么协程与多线程多进程又有什么不同呢?通过实例来对比:
php-cgi 便是多进程的一种体现,每个请求对应一个 cgi 进程,带来的缺点是进程频繁创建销毁的开销以及每次都需要加载 php.ini 配置的性能浪费。 php-fpm 多进程模式的改良,通过 master/worker 的模式,让多进程少了重新加载配置与频繁创建销毁进程的开销。 假设 php 有多线程,省略多次 php.ini 的加载,省略多次开发框架初始化,相应的带来线程调度开销,多线程抢占式模型需要注意数据访问的线程安全,需要给数据加锁,并带来锁争抢与死锁问题。 协程,省略多次 php.ini 加载,省略多次开发框架初始化,由于协程是用户态的线程,所以由代码来控制什么时候进行切换,没有线程调度开销。并且 swoole 以同步的方式编写异步代码,协程的切换由底层调度器自行切换,开发者无需关注线程锁与死锁问题。
swoole 的协程切换是基于 io 来调度的,也就是说它只会在遇到 io 操作的时候才会进行切换,通过节省 io 等待时间来提高服务器性能,因此 swoole 的协程是无法进行并发计算的。不过遇到需要并行计算的场景,swoole 也提供了多进程的运行方式,如果需要多进程协同操作同一个数据,就需要加进程锁了。
事件循环–异步是如何实现的
处理异步回调的部分叫做事件循环,可以理解为每个进程有一个死循环,不断的查看当前有没有待执行的任务、已经执行完需要通知的回调。当我们进行异步任务调用的时候,就是向这个循环中投递了一个任务与对应的回调。当任务完成的时候,循环便把任务从监听数组中去除,并执行回调。
可以看到,EventLoop 类中维护了一个 event 数组,用来存储需所有需要监听的事件。在调用 addEventHandler 方法时,则需要将事件的类型、参数,以及回调函数一同传入。
swoole 为了降低上下文切换带来的消耗,没有依赖系统级事件循环而是自己实现了一套,swoole 的协程上下文切换都是内存读取,避免了 cpu 寄存器、堆栈以及系统内核态与用户态之间的切换,因此切换开销极小。
做一个不严谨的类比,你也可以认为 swoole 是一个语言层面实现的 php-fpm,毕竟 swoole 也支持完全的多进程模式,这种模式下与 php-fpm 的运行方式大同小异。不过由于在语言层面便常驻内存了,所以带来的福利便是在启动 php 脚本的开发框架时,只需要一次载入便保存在内存中了,避免了 php-fpm 每个请求都重新初始化框架的性能浪费。那么同样的由于服务常驻内存了,所以哪怕是在开发过程中,代码相关的改动都需要重启一下 swoole 服务。
swoole 对性能的提升带来的代价是编程思维的转变,因为常驻内存了,所以编写业务代码时,对内存变量的使用就需要更加小心,避免造成内存泄露。
swoft
首个基于 Swoole 原生协程的新时代 PHP 高性能协程全栈框架,内置协程网络服务器及常用的协程客户端,常驻内存,不依赖传统的 PHP-FPM,全异步非阻塞 IO 实现,以类似于同步客户端的写法实现异步客户端的使用,没有复杂的异步回调,没有繁琐的 yield, 有类似 Go 语言的协程、灵活的注解、强大的全局依赖注入容器、完善的服务治理、灵活强大的 AOP、标准的 PSR 规范实现等等,可以用于构建高性能的Web系统、API、中间件、基础服务等等。