让 Laravel 运行在 Swoole 中
Laravel是目前比较好的PHP框架,为工匠设计。在熟悉以后,其丰富的特性可以极大的提升开发的效率。但是Laravel有一个致命的缺点:相对于其他的MVC框架来说,性能差强人意。
这里我们使用Laravel 5.1.6,PHP 5.6.9,PHP-FPM的组合进行测试,测试页面为Laravel默认的welcome。经过反复试验,PHP-FPM的worker的数量对测试结果没有明显的影响

RPS仅仅只有30.4,这性能表现对于一个正常的Hello World性质的welcome页面来说实在是太差
到底性能差在什么地方?框架加载时间
我们在Laravel的入口文件里加上时间计算

访问一次后

仅仅只是计算了框架初始化的时间,还并没真正进入到业务的逻辑就已经消耗了这么长的时间
如何解决或者说缓解这个问题,使我们可以开发效率和性能兼得呢?用Swoole
PHP-FPM的工作原理是对于每次请求,环境及上下文都是全新的。因为它并不知道相邻请求之间有没有关联关系,可能前一个请求是访问Wordpress,后一个请求却是Joomla,它只能在接收到请求之后载入PHP文件,处理完后在结束时将所有资源释放掉等待下一个请求。对于使用Laravel的应用来说,并不需要PHP-FPM这样的特性,最好是让Laravel框架的核心保持在内存里,免去每次请求的框架重复加载工作
Swoole的HttpServer可以解决这个问题。HttpServer和Node.js、Python的Tornado类似,可以使框架驻留在内存中,并且具有较强的网络性能,能直接作为Web前端服务器
谢谢Laravel干净的代码,使我们只需要进行少许的改造就可以在Swoole中运行Laravel了
首先分析Laravel的入口文件
<?php
/**
* Laravel - A PHP Framework For Web Artisans
*
* @package Laravel
* @author Taylor Otwell <taylorotwell@gmail.com>
*/
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| our application. We just need to utilize it! We'll simply require it
| into the script here so that we don't have to worry about manual
| loading any of our classes later on. It feels nice to relax.
|
*/
require __DIR__.'/../bootstrap/autoload.php';
/*
|--------------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------------
|
| We need to illuminate PHP development, so let us turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight our users.
|
*/
$app = require_once __DIR__.'/../bootstrap/app.php';
// 上述的开销可以省去
/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); // 初始化业务核心
$response = $kernel->handle( // 走完业务流程后获取到$response
$request = Illuminate\Http\Request::capture() // 将$_SERVER, $_GET, $_POST, $_FILE等转换为Laravel的$request
);
$response->send(); // 将$response发送出去
$kernel->terminate($request, $response); // 结束业务核心
一个修改的示例
<?php
use Illuminate\Http\Request as IlluminateRequest;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
class Server
{
private $swoole_http_server;
private $laravel_kernel;
//创建swoole http服务器
function __construct($host, $port)
{
$this->swoole_http_server = new swoole_http_server($host, $port);
//swoole配置项
$this->swoole_http_server->set([
'max_conn' => 1024,
'timeout' => 2.5,
'poll_thread_num' => 8, //reactor thread num
'writer_num' => 8, //writer thread num
'worker_num' => 8, //worker process num
'max_request' => 4000,
'dispatch_mode' => 1,
'log_file' => __DIR__ . '/log/log',
'daemonize' => 0
]);
}
public function start()
{
//注册workerStart回调
$this->swoole_http_server->on('WorkerStart', array($this, 'onWorkerStart'));
//注册request回调
$this->swoole_http_server->on('request', array($this, 'onRequest'));
//启动swoole
$this->swoole_http_server->start();
}
public function onWorkerStart($serv, $worker_id)
{
require __DIR__ . '/../bootstrap/autoload.php';
$app = require __DIR__.'/../bootstrap/app.php';
$this->laravel_kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
Illuminate\Http\Request::enableHttpMethodParameterOverride();
}
public function onRequest($request, $response)
{
// 转换为Swoole的请求对象
$get = isset($request->get) ? $request->get : array();
$post = isset($request->post) ? $request->post : array();
$cookie = isset($request->cookie) ? $request->cookie : array();
$server = isset($request->server) ? $request->server : array();
$files = isset($request->files) ? $request->files : array();
$server = array_change_key_case($server, CASE_UPPER);
// 创建illuminate_request
$illuminate_request = IlluminateRequest::createFromBase(
new SymfonyRequest($get, $post, array(), $cookie, $files, $server)
);
//把illuminate_request传入laravel_kernel后,取回illuminate_response
$illuminate_response = $this->laravel_kernel->handle($illuminate_request);
// 转换为Swoole的响应对象
// status
$response->status($illuminate_response->getStatusCode());
// headers
foreach ($illuminate_response->headers->allPreserveCase() as $name => $values) {
foreach ($values as $value) {
$response->header($name, $value);
}
}
// Cookies
foreach ($illuminate_response->headers->getCookies() as $cookie) {
$response->cookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly());
}
// Content
$content = $illuminate_response->getContent();
// Send content & Close
$response->end($content);
// 结束请求
$this->laravel_kernel->terminate($illuminate_request, $illuminate_response);
}
}
在一开始的测试环境下改用Swoole来运行Laravel,测试结果如下

与使用PHP-FPM相比有17.9x的加速
看上去很完美?其实有一点美中不足的地方,swoole_http_request->$files是无法兼容PHP内置的is_uploaded_file和move_uploaded_file函数的。在 Laravel 中使用Validator校验上传的文件时,由于Validator使用了is_uploaded_file,校验会失败。虽然可以使用apd之类的扩展替换掉内置函数,但是生产中并不建议这样做,还是需要其他方式的 workaround