Stone优化原理
快速入口:
如果你正在考虑框架性能优化的问题, 你对PHP应该已经有足够的了解了。 如你所知, PHP每次的每次请求结束, 都会释放掉执行中建立的所有资源。这样有一个很大的好处:PHP程序员基本不用费力去考虑资源释放的问题,诸如内存,IO句柄,数据库连接等,请求结束时PHP将全部释放。PHP程序员几乎不用关心内存释放的问题,也很难写出内存泄露的程序。这让PHP变得更加简单容易上手, 直抒心意。但是也带来了一个坏处:PHP很难在请求间复用资源, 类似PHP框架这种耗时的工作, 每次请求都需要反复做——即使每次都在做同样的事情。也正因为如此,在PHP发展过程中,关于是否使用框架的争论也从未停止过。
Stone主要优化的就是这个问题。 在框架资源初始化结束后再开启一个FastCGI服务,这样, 新的请求过来是直接从资源初始化结束后的状态开始,避免每次请求去做资源初始化的事情。所以, 本质上, Stone运行时是常驻内存的,它和PHP-FPM一样,是一个FastCGI的实现,不同的是, FPM每次执行请求都需要重新初始化框架, Stone直接使用初始化的结果。
同样,事情总是有好有坏。坏处是:PHP编程变得更难了, 你需要考虑内存的释放,需要关心PHP如何使用内存。甚至, 你需要了解使用的框架,以免『不小心』写出让人『惊喜』的效果。同时, PHP的调试变得更难, 因为每次修改程序后需要重启进程才能看到效果。事实上开发Stone时针对这方面做了不少工作。好处是:程序的性能得到极大的提高。 当然, 客观上的一些利好因素是: PHP的内存回收已经相当稳定和高效, Swoole稳定性已经在相当多的项目中得到验证,Laravel代码质量相当高。
在设计Stone时的另外一个目的是简单。 希望使用者能5分钟完成部署, 并且不需要对原有功能进行改造,可以安全地停止使用。
1. 关于 Stone-Web 和 Stone-Server
Stone支持两种运行方式, Web方式用来优化Web页面的执行, 执行流程和现在的Laravel页面完全一致。 Server方式用来解决对性能要求很高的场合, 执行流程与artisan command一致, 绕过了laravel MVC的流程, 需要自己去实现请求处理的Handler。
比如一个抢购活动, 想在用户实际下单前拦截请求避免对订单系统造成冲击, 可以使用Stone-Server实现一个抢购功能, 获得抢购资格的用户才进入下单流程。
2. 性能对比
应用类型 | 原始Laravel | Stone-Web | Stone-Server |
---|---|---|---|
laravel5 默认页面 | 150 | 3000 | – |
laravel5 简单接口 | 150 | 3000 | 8500 |
laravel4 实际项目简单页面 | 70 | 1000 | – |
laravel4 简单接口 | 120 | – | 8200 |
laravel4 实际项目首页 | 35 | 380 | – |
测试环境如下:
PHP 5.6.17-0+deb8u1 (cli) (built: Jan 13 2016 09:10:12)
Copyright (c) 1997-2015 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2015 Zend Technologies
with Zend OPcache v7.0.6-dev, Copyright (c) 1999-2015, by Zend Technologies
Linux office 3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt20-1+deb8u3 (2016-01-17) x86_64 GNU/Linux
16核 Intel(R) Xeon(R) CPU E5-2640 v2 @ 2.00GHz
16G 内存
Laravel 4.2
Laravel 5.2
Stone快速指南
注意:
- 如果你不能在5分钟内完成部署, 你应该停止使用Stone了 :)
- 目前Stone处在alpha阶段,本文档随时可能会失效
特别提示:
- 新项目中尝试Stone, 建议从Stone-Server开始, 这是一个针对API的优化方案。
- 已有项目中尝试Stone,建议从Stone-Web开始, 这是一个针对Web的优化方案。
- 请阅读一下风险提示
Stone安装指引
安装Stone
安装依赖包
sudo pcel install swoole sudo pcel install runkit
composer安装Stone
laravel 5:
composer require qufenqi/stone:dev-master
laravel 4:
composer require qufenqi/stone:dev-laravel-4.x
修改config/app.php, 加载Stone的Service Provider,
注意Laravel4的配置文件路径和写法有细微差别。
'providers' => [ // laravel定义的provider Illuminate\Auth\AuthServiceProvider::class, Illuminate\Broadcasting\BroadcastServiceProvider::class, .... .... // 中间省略的其他provider .... Qufenqi\Stone\StoneServiceProvider::class, // 应用层定义的provider App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, ],
配置Stone: 新建 config/stone.php
注意Laravel4的配置文件路径有细微差别。
return [ // server模式配置 'server' => [ 'handler' => 'App\Servers\Handler', // request handler 'user' => 'apple', // run user 'group' => 'apple', // run group 'domain' => '/var/run/stone-server-fpm.sock', 'pid' => '/run/stone-fpm.pid', 'process_name' => 'stone-server-fpm', 'worker_num' => 30, ], // web模式配置 'web' => [ 'user' => 'apple', // run user 'group' => 'apple', // run group 'domain' => '/var/run/stone-web-fpm.sock', // unix domain socket 'pid' => '/run/stone-web.pid', 'process_name' => 'stone-web-server', 'worker_num' => 30, // 需要建立快照的绑定 'snap_bindings' => [ 'view', 'cookie', 'session', 'session.store', //'config', // debugbar 需要重置config ], ], ];
在Laravel 5 项目上使用Stone-Web:
修改app/Http/Kernel.php, 让Stone的Kernel接管请求的处理。
// 根据当前的运行sapi决定使用哪个kernel来处理请求, 这样FPM和Stone可以完全使用一套程序 if (php_sapi_name() == 'cli') { class BaseKernel extends StoneKernel {} } else { class BaseKernel extends HttpKernel {} } class Kernel extends BaseKernel
运行Stone-Web, Web模式处在开发阶段, 所以默认不会以deamon模式启动, 便于调试
sudo php ./public/index.php
修改nginx配置
location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_index index.php; # fastcgi_pass unix:/var/run/php5-fpm.sock; # PHP-FPM fastcgi_pass unix:/var/run/stone-web-fpm.sock; # Stone include fastcgi_params; }
sudo nginx -s reload
完成
在Laravel 4 项目上使用Stone-Web
修改public/index.php与bootstrap/start.php, 让Stone的Kernel接管请求的处理。
// 修改public/index.php if (PHP_SAPI == 'cli') { define('STONE_WEB_MODE', true); $_SERVER['RUNENV'] = 'local'; }
// 修改bootstrap/start.php if (defined('STONE_WEB_MODE')) { $app = new Qufenqi\Stone\Foundation\Application; } else { $app = new Illuminate\Foundation\Application; }
运行Stone-Web, Web模式处在开发阶段, 所以默认不会以deamon模式启动, 便于调试
sudo php ./public/index.php
修改nginx配置
location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_index index.php; # fastcgi_pass unix:/var/run/php5-fpm.sock; # PHP-FPM fastcgi_pass unix:/var/run/stone-web-fpm.sock; # Stone include fastcgi_params; }
sudo nginx -s reload
完成
在Laravel 5 中使用Stone-Server
修改app\Console\Kernel.php
protected $commands = [ // Commands\Inspire::class, \Qufenqi\Stone\Console\Commands\StoneServer::class, // 添加这一行 ];
定义请求处理类, 我定义在app\Servers\Handler.php
注意 这个其实就是 stone.php 配置里的 server.handler
<?php namespace App\Servers; use Qufenqi\Stone\Contracts\RequestHandler; use Response; class Handler implements RequestHandler { public function process() { return Response::make('hello, stone server!'); } public function onWorkerStart() { } }
运行Stone-Server
sudo php ./artisan stone:server
修改nginx配置
location /server/ { fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_index index.php; fastcgi_pass unix:/var/run/stone-server-fpm.sock; # Stone include fastcgi_params; }
sudo nginx -s reload
完成
在Laravel 4 中使用Stone-Server
修改app\start\artisan.php
Artisan::add(new Qufenqi\Stone\Console\Commands\StoneServer);
定义请求处理类, 我定义在app\Servers\Handler.php
注意 这个其实就是 stone.php 配置里的 server.handler
<?php namespace App\Servers; use Qufenqi\Stone\Contracts\RequestHandler; use Response; class Handler implements RequestHandler { public function process() { return Response::make('hello, stone server!'); } public function onWorkerStart() { } }
运行Stone-Server
sudo php ./artisan stone:server
修改nginx配置
location /server/ { fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_index index.php; fastcgi_pass unix:/var/run/stone-server-fpm.sock; # Stone include fastcgi_params; }
sudo nginx -s reload
完成
Stone进阶指南
1. Stone的使用风险
使用Stone的一个很重要的事情是, 需要始终对内存使用抱有敬畏之心。 在未充分了解风险前, 请不要在实际项目中轻易尝试,否则可能产生非常严重的后果!!
比如:Laravle的Cookie实现了单例模式, 是为了让开发者在任何地方可以往Response里追加cookie, 使用:
Cookie::queue('key', 'value', 60);
这在PHP-FPM下不会有任何问题。 但Stone常驻内存后, 请求的资源没有在请求结束后释放, 因此上一个请求在Cookie在下一个请求中仍然存在。 想像下, 如果这是一个关于Session Id的cookie, 会产生多么严重的后果。 同样,Laravel的Auth对象, 也可能出现类似的问题。
内存泄露什么的可能只是影响稳定性, 但是这个处理不当会带来难以估计的损失, 这是使用Stone或者类似常驻内存方案的最大风险。
2. Boot Service Provider 与 Request Service Provider
使用Stone(或者使用类似常驻内存解决方案)最重要的一点, 是需要区分哪些资源可以请求间共享, 哪些资源不能共享。这在 Stone中被区别为: boot service provider 和 request service provider。
- boot service provider在进程初始化时被执行, 请求间共享。
- request service provider在请求时执行, 请求间不共享, 每次请求重新执行。
- laravel原来定义的service provider默认都是boot service Provider, 除非你在stone配置里重新定义。
比如, Laravel的路由规则的解析, 是不需要每次请求都去执行的,而在一个时间较长的项目中, 路由规则可能有几千行,解析这些规则需要耗费不少时间, 所以设置成boot service provider是比较合适的。 而有一些service provider是需要每次都执行的,比如debugbar, 这些设置成request service provider比较合适。
3. 实例快照与runkit
还有一种情况,不需要每次执行具体的代码, 但是需要做一些重置操作。 比如Cookie, Laravel将cookie放到Response的数组中, 在结束响应时发送到浏览器端,请求结束后并没有清空而带到了下一个请求。 Stone的一个解决办法是将Cookie在创建时建立一个快照, 保存当前的状态, 等请求结束时再通过快照恢复, 避免Cookie被带到下一个请求的问题。
这个在config/stone.php里可以定义。
runkit在这个过程中的意义是给予PHP运行时注入实例的能力。 在app初始化时通过runkit注入指定实例, 让他们具有快照恢复的功能, 再建立快照, 请求结束后再通过快照恢复。
快照和request service provider都能解决Cookie在请求间共享的问题, 快照的方式避免了再次执行service provider里register和boot的工作, 效率会更好一些。 如果你只是需要每次请求得到一个新的实例, 而不是需要在请求中再次执行一段程序, 使用快照的方式会更好一些。
4. 理解请求间共享资源
这是一把双刃剑, 利用好了性能能得到极大提升, 否则就是bug的深渊。
比如, 我们常需要实现权限系统, 如果能在初始化的时候将权限系统加载, 并在请求间共享, 这样每次请求就不需要再去从数据库里加载解析权限规则, 这样效率能得到提高。 利用好了这一点, 有助于你写出更高效的程序。
而如果你没理解好这一点, 比如你使用了一个单例模式, 这个实例被维持在类的静态变量里, 因此不会在请求结束后自然销毁。 而这个实例在请求间共享又会出现问题,类似cookie的问题,这样就会造成bug。
5. 沙盒模式
我希望在未来能支持沙盒模式, 在初始化后建立一个沙盒, 把请求防止到沙盒里执行, 这样就可以更安全方便的实现。
设计Stone的一些想法
1. 保持与PHP-FPM的兼容
这样做能带来几个好处:
- 调试方便, 在开发中, 程序员完全可以使用php-fpm来开发, 这样可以避免开发时反复重启进程的问题。 当然, 测试时还是应该使用Stone,免得一些问题需要在线上时才发现。
- 使用方便, 5分钟内快速使用这个目标不会改变
- 停用方便, 出现一些暂时无法解决的问题的时候, 可以通过修改nginx配置快速切换会PHP-FPM
2. 什么场合下适合使用Stone
Stone的目标定位于解决已有PHP程序的性能问题。 随着开源程序的越来越完善, 现在解决高并发问题的技术方案越来越多, 有些已经非常成熟。 Stone的优势在于在解决性能问题的同时可以100%重用现在的业务逻辑。
比如, 现有系统中需要加入一个抢购的功能, 我们可能会在抢购之前根据业务规则进行流量拦截, 可能需要使用到redis, 现有的用户系统,现有的业务规则。 使用Stone-Server, 你可以直接使用。 但是如果你使用其他语言的解决方案, 你可能需要把这些规则使用另外的语言再实现一遍。 这加大了开发和维护的成本。
3. 继续降低使用Stone的难度
使用Stone很可能会踩坑。 一方面可能是开发者对于运行机制的理解不充分; 一方面可能是现有PHP程序没有考虑请求结束后的资源销毁的问题; 也可能是Stone本身程序存在一些bug。 Stone会持续完善, 并尽量对应用程序提供一些保护机制, 降低程序使用的难度。
在其他框架下使用Stone
由于精力有限, 暂时不考虑其他框架, 但是应该是可以较快移至到其他框架的。 如果你有兴趣, 不妨fork代码自己实现一下。
问题反馈
希望感兴趣的朋友能积极给我反馈, 甚至参与到Stone的开发中来, 我们一起完善。 我的邮箱是: rssidea(at)qq.com