Stone优化原理
快速入口:
- Stone项目地址:https://github.com/StoneGroup/stone 2016-06-07 更新地址, 原有地址包含部分公司信息, 不被允许, 所以删除了公司信息重新发布。
如果你正在考虑框架性能优化的问题, 你对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