眾所周知,workerman是基于php cli的,由于php cli模式下無法使用php自帶的header、sesion、cookie等函數(shù),這導(dǎo)致將傳統(tǒng)的php項(xiàng)目無法直接在workerman容器下直接運(yùn)行。
我一度以為讓傳統(tǒng)業(yè)務(wù)在workerman中運(yùn)行,就必須更改框架甚至業(yè)務(wù)代碼以適配workerman,直到joanhey發(fā)了一個(gè)issue,打破了我的認(rèn)知。
他們發(fā)布了一個(gè)名叫AdapterMan的項(xiàng)目,它可以做到不更改傳統(tǒng)框架代碼的情況下讓你的傳統(tǒng)php項(xiàng)目放到workerman中正常運(yùn)行,并且他們公司已經(jīng)在生產(chǎn)環(huán)境用了2年。
注意,是零代碼改動(dòng)直接讓laravel、lumen、Slim等框架的項(xiàng)目在workerman上運(yùn)行。
目前他們已經(jīng)在laravel、lumen、Slim、Symfony、CakePHP、Yii2、KumbiaPHP 等做了初步壓力測(cè)試,性能有很大的提升。
以下是壓測(cè)結(jié)果
Fw | Plaintext | Json | Single query | Multiple query | Updates | Fortunes |
---|---|---|---|---|---|---|
Laravel | 14,799 | 14,770 | 9,263 | 3,247 | 1,452 | 8,354 |
Laravel Roadrunner | 482 | 478 | 474 | 375 | 359 | 472 |
Laravel Swoole | 38,824 | 37,439 | 21,687 | 3,958 | 1,588 | 16,035 |
Laravel Laravel s | 54,617 | 49,372 | 23,677 | 2,917 | 1,255 | 16,696 |
Laravel Workerman | 103,004 | 99,891 | 46,001 | 5,828 | 1,666 | 27,158 |
Laravel with Workerman % gain | 596.02% | 576.31% | 396.61% | 79.489% | 14.738% | 225.09% |
Fw | Plaintext | Json | Single query | Multiple query | Updates | Fortunes |
---|---|---|---|---|---|---|
Symfony | 38,231 | 37,557 | 12,578 | 10,741 | 3,420 | 10,741 |
Symfony Workerman | 210,796 | 197,059 | 107,050 | 13,401 | 4,062 | 71,092 |
Fw | Plaintext | Json | Single query | Multiple query | Updates | Fortunes |
---|---|---|---|---|---|---|
Lumen | 18,998 | 18,616 | 10,791 | 3,496 | 1,461 | 9,223 |
Lumen Swoole | 44,861 | 43,598 | 24,255 | 4,178 | 1,599 | 16,854 |
Lumen Laravel s | 93,335 | 82,745 | 31,567 | 3,030 | 1,282 | 21,130 |
Lumen Workerman | 185,126 | 177,667 | 58,729 | 5,857 | 1,662 | 31,430 |
Without ORM
Framework | JSON | 1-query | 20-query | Fortunes | Updates | Plaintext |
---|---|---|---|---|---|---|
Slim 4 | 38,305 | 34,272 | 12,579 | 32,634 | 2,097 | 35,251 |
Slim 4 Workerman | 129,393 | 81,889 | 15,803 | 73,212 | 2,456 | 134,531 |
Slim 4 Workerman pgsql * | 102,926 | 19,637 | 92,752 | 14,875 |
Lumen v9
接入代碼類似
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Adapterman\Adapterman;
use Workerman\Worker;
Adapterman::init();
$http_worker = new Worker('http://0.0.0.0:8080');
$http_worker->count = 8;
$http_worker->name = 'AdapterMan';
$http_worker->onWorkerStart = static function () {
//init();
require __DIR__.'/start.php';
};
$http_worker->onMessage = static function ($connection, $request) {
$connection->send(run());
};
Worker::runAll();
項(xiàng)目地址:
https://github.com/joanhey/AdapterMan 強(qiáng)烈建議大家為其點(diǎn)贊(點(diǎn)星星)
相關(guān)鏈接:
https://github.com/walkor/workerman/issues/824
先頂為敬
所有的PHP框架測(cè)試都在這里:https://github.com/joanhey/FrameworkBenchmarks/tree/master/frameworks/PHP
老大,不行哇,我拿著 laravel 項(xiàng)目測(cè)試運(yùn)行,靜態(tài)資源沒成功加載到,laravel 的 緩存使用了 phpredis,提示找不到 Redis。
php -c cli-php.ini server.php start 正常
php -c cli-php.ini server.php start -d 就報(bào)錯(cuò)
[root@localhost laravel9]# php -c cli-php.ini server.php start -d
Adapterman v0.4 OK
Workerman[server.php] start in DAEMON mode
-------------------------------------------- WORKERMAN --------------------------------------------
Workerman version:3.5.33 PHP version:8.1.13 Event-Loop:\Workerman\Events\Select
--------------------------------------------- WORKERS ---------------------------------------------
proto user worker listen processes status
tcp root AdapterMan http://0.0.0.0:7070 8 [OK]
---------------------------------------------------------------------------------------------------
Input "php server.php stop" to stop. Start success.
PHP Fatal error: Uncaught TypeError: fclose(): Argument #1 ($stream) must be of type resource, null given in /mnt/hgfs/www/laravel9/vendor/workerman/workerman/Worker.php:1245
Stack trace:
#0 /mnt/hgfs/www/laravel9/vendor/workerman/workerman/Worker.php(1245): fclose()
#1 /mnt/hgfs/www/laravel9/vendor/workerman/workerman/Worker.php(548): Workerman\Worker::resetStd()
#2 /mnt/hgfs/www/laravel9/server.php(24): Workerman\Worker::runAll()
#3 {main}
thrown in /mnt/hgfs/www/laravel9/vendor/workerman/workerman/Worker.php on line 1245
Fatal error: Uncaught TypeError: fclose(): Argument #1 ($stream) must be of type resource, null given in /mnt/hgfs/www/laravel9/vendor/workerman/workerman/Worker.php:1245
Stack trace:
#0 /mnt/hgfs/www/laravel9/vendor/workerman/workerman/Worker.php(1245): fclose()
#1 /mnt/hgfs/www/laravel9/vendor/workerman/workerman/Worker.php(548): Workerman\Worker::resetStd()
#2 /mnt/hgfs/www/laravel9/server.php(24): Workerman\Worker::runAll()
#3 {main}
thrown in /mnt/hgfs/www/laravel9/vendor/workerman/workerman/Worker.php on line 1245
PHP Fatal error: Uncaught TypeError: fclose(): Argument #1 ($stream) must be of type resource, null given in /mnt/hgfs/www/laravel9/vendor/workerman/workerman/Worker.php:1245
我的修改 不知道影響不影響
public static function resetStd()
{
if (!static::$daemonize || static::$_OS !== \OS_TYPE_LINUX) {
return;
}
global $STDOUT, $STDERR;
$handle = \fopen(static::$stdoutFile, "a");
if ($handle) {
unset($handle);
\set_error_handler(function(){});
$STDOUT && \fclose($STDOUT); //新增判斷
$STDERR && \fclose($STDERR); //新增判斷
\STDOUT && \fclose(\STDOUT); //新增判斷
\STDERR && \fclose(\STDERR); //新增判斷
$STDOUT = \fopen(static::$stdoutFile, "a");
$STDERR = \fopen(static::$stdoutFile, "a");
// change output stream
static::$_outputStream = null;
static::outputStream($STDOUT);
\restore_error_handler();
return;
}
throw new Exception('Can not open stdoutFile ' . static::$stdoutFile);
}
webman和adaptman相當(dāng)于是兩種解決方案了吧,一種是當(dāng)作fpm,一種是直接在fpm里寫服務(wù)。性能應(yīng)該還是webman好吧
laravel9 還測(cè)試到兩個(gè)問題 一個(gè)靜態(tài)文件加載失敗
第二個(gè)上傳圖片失敗
Workerman version:3.5.34 PHP version:8.1.3
------------------------ WORKERS -------------------------------
worker listen processes status
AdapterMan http://0.0.0.0:8080 8 [ok]
Error: Call to a member function getClientOriginalName() on string in E:\www\laravel9\vendor\laravel\telescope\src\Watchers\RequestWatcher.php:164
Stack trace:
#0 [internal function]: Laravel\Telescope\Watchers\RequestWatcher->Laravel\Telescope\Watchers\{closure}('files', 'name')
#1 E:\www\laravel9\vendor\laravel\telescope\src\Watchers\RequestWatcher.php(167): array_walk_recursive(Array, Object(Closure))
#2 E:\www\laravel9\vendor\laravel\telescope\src\Watchers\RequestWatcher.php(54): Laravel\Telescope\Watchers\RequestWatcher->input(Object(Illuminate\Http\Request))
#3 E:\www\laravel9\vendor\laravel\framework\src\Illuminate\Events\Dispatcher.php(421): Laravel\Telescope\Watchers\RequestWatcher->recordRequest(Object(Illuminate\Foundation\Http\Events\RequestHandled))
#4 E:\www\laravel9\vendor\laravel\framework\src\Illuminate\Events\Dispatcher.php(249): Illuminate\Events\Dispatcher->Illuminate\Events\{closure}('Illuminate\\Foun...', Array)
#5 E:\www\laravel9\vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php(142): Illuminate\Events\Dispatcher->dispatch('Illuminate\\Foun...')
#6 E:\www\laravel9\public\start.php(61): Illuminate\Foundation\Http\Kernel->handle(Object(Illuminate\Http\Request))
#7 E:\www\laravel9\server.php(21): run()
#8 E:\www\laravel9\vendor\workerman\workerman\Connection\TcpConnection.php(656): {closure}(Object(Workerman\Connection\TcpConnection), Array)
#9 E:\www\laravel9\vendor\workerman\workerman\Events\Select.php(292): Workerman\Connection\TcpConnection->baseRead(Resource id #142)
#10 E:\www\laravel9\vendor\workerman\workerman\Worker.php(2410): Workerman\Events\Select->loop()
#11 E:\www\laravel9\vendor\workerman\workerman\Worker.php(1406): Workerman\Worker->run()
#12 E:\www\laravel9\vendor\workerman\workerman\Worker.php(1349): Workerman\Worker::forkWorkersForWindows()
#13 E:\www\laravel9\vendor\workerman\workerman\Worker.php(547): Workerman\Worker::forkWorkers()
#14 E:\www\laravel9\server.php(24): Workerman\Worker::runAll()
#15 {main}
Worker process terminated
對(duì)于laravel 應(yīng)在請(qǐng)求時(shí)初使化一些laravel的單例,不初使化有可能會(huì)出現(xiàn)單例污染情況如:auth如果不重置,會(huì)導(dǎo)致登陸后,
返回的是第一個(gè)登陸的用戶信息,
在run中加入:$kernel->getApplication()->forgetInstance('auth');如下,當(dāng)然還有其它組件,用啥加啥
$kernel->getApplication()->forgetInstance('auth');
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
具體有多少單例是通過 判斷變量返結(jié)果的,得大家使用中發(fā)現(xiàn)或者參考:swoole與laravel的整合了完善!
目前用于生產(chǎn)應(yīng)核有點(diǎn)難,除非用的組件少,
加了
$kernel->getApplication()->forgetInstance('auth');
還是會(huì)污染 其他地方打開頁面出現(xiàn)的是第一個(gè)的session
laravel 使用得自己加一條動(dòng)態(tài)路由實(shí)現(xiàn),靜態(tài)路由會(huì)有一個(gè)問題:控制器中的 ,__construct 方法只會(huì)執(zhí)行首次,路由也是按單例模式只初使化一次,要嗎得按webman方法改變一下路由為每次初使化,當(dāng)然不使用__construct魔術(shù)方法就好了
這個(gè)對(duì)低版本laravel很有用只是需要手動(dòng)清理下單例那些,可以參考官方Octane擴(kuò)展去清理,高版本直接用自帶的官方擴(kuò)展,或者有人已經(jīng)出了workerman Octane的驅(qū)動(dòng)了
HttpForPHP 我這個(gè)項(xiàng)目也是一樣的 為什么就沒有人關(guān)注下
使用workerman實(shí)現(xiàn)http服務(wù),把現(xiàn)有其他框架的代碼簡單改為常駐內(nèi)存http服務(wù),未實(shí)現(xiàn)session支持,最適合用于接口服務(wù),已有在yii項(xiàng)目中運(yùn)行,參見自帶的yii示例。
原理就是對(duì)PHP的$_SERVER $_COOKIE $_FILES $_REQUEST $_POST $_GET全局變量重置數(shù)據(jù)
$_SESSION太麻煩就沒有處理 畢竟只針對(duì)接口應(yīng)用的服務(wù) 所以就沒有必要
充分說明了:不管再好的項(xiàng)目,沒有詳細(xì)清晰的文檔,也難以吸引關(guān)注。
至少要寫出這個(gè)項(xiàng)目的使用場(chǎng)景、解決的問題、如何在項(xiàng)目中引用,比如laravel中、lumen中或者其他三方項(xiàng)目中如何接入
file_get_contents('php://input')這個(gè)卡住了,,像微信支付回調(diào)都是用這個(gè)接收的,你是怎么搞的?不會(huì)都類庫都自己處理一下吧??
這是我yii項(xiàng)目里easywechat庫我的處理方式 僅供參考
$app = Factory::officialAccount($config);
$app->request = WeixinRequest::createFromGlobals(); //重置wx的request
$app->request->setContent(\Yii::$app->request->getRawBody()); //常駐內(nèi)存時(shí)推送內(nèi)容置入
我嘗試了一下適配現(xiàn)有項(xiàng)目,現(xiàn)有項(xiàng)目使用的lumen,發(fā)現(xiàn)http post body 里面的值,在lumen 的$request對(duì)象里獲取不到,是我使用姿勢(shì)不對(duì)嗎?
看了下,騷操作呀,應(yīng)該是可以適配任意項(xiàng)目,本質(zhì)就直接禁止掉部分系統(tǒng)函數(shù),手動(dòng)實(shí)現(xiàn)去適配
安裝報(bào)錯(cuò) composer require joanhey/adapterman
Problem 1
- Root composer.json requires joanhey/adapterman ^0.5.5 -> satisfiable by joanhey/adapterman[0.5.5].
- joanhey/adapterman 0.5.5 requires workerman/workerman ^3.5 -> found workerman/workerman[v3.5.0, ..., v3.5.34] but it conflicts with your root composer.json require (^4.1).
是我用tp6,已經(jīng)安裝過workerman了,版本較高
Test environment:
Mac 13.3.1
Workerman version:4.1.5
PHP version:8.1.17
Event-Loop:\Workerman\Events\Select
Adapterman: 0.6.1
Laravel : 9.33.0
當(dāng)我運(yùn)行 php server.php start
時(shí)是可正常使用的,但是當(dāng)我運(yùn)行php server.php start -d
在訪問接口的時(shí)候就報(bào)錯(cuò)了,錯(cuò)誤在這個(gè)帖子里。
https://github.com/joanhey/AdapterMan/issues/36
我現(xiàn)在的疑問是 workerman的守護(hù)模式和非守護(hù)模式有什么不一樣的地方?不知道有沒有人遇到這樣的問題
laravel 文件上傳處理,更改原碼中 http.php 500行,下面的switch部分為:
switch ($header_key) {
case "content-disposition":
// Is file data.
if (\preg_match('/name="(.*?)"; filename="(.*?)"/', $header_value, $match)) {
$error = 0;
$tmp_file = '';
$file_name = $match[2];
$size = \strlen($boundary_value);
$tmp_upload_dir = \Workerman\Protocols\Http::uploadTmpDir();
if (!$tmp_upload_dir) {
$error = UPLOAD_ERR_NO_TMP_DIR;
} else if ($boundary_value === '' && $file_name === '') {
$error = UPLOAD_ERR_NO_FILE;
} else {
$tmp_file = \tempnam($tmp_upload_dir, 'workerman.upload.');
if ($tmp_file === false || false === \file_put_contents($tmp_file, $boundary_value)) {
$error = UPLOAD_ERR_CANT_WRITE;
}
}
$upload_key = $match[1];
// Parse upload files.
$_FILES[$upload_key] = [
'name' => $file_name,
'tmp_name' => $tmp_file,
'size' => $size,
'error' => $error,
'type' => '',
];
break;
} // Is post field.
else {
// Parse $_POST.
if (\preg_match('/name="(.*?)"$/', $header_value, $match)) {
//TODO search a fast solution
$post_encode_string .= urlencode($match[1]) . '=' . urlencode($boundary_value) . '&';
}
}
break;
case "content-type":
// add file_type
$_FILES[$upload_key]['type'] = \trim($header_value);
break;
}
需要老大修改一下workerman,來解決內(nèi)存泄漏的問題
$http_worker->onMessage = static function ($connection, $request) {
$connection->send(run());
};
這里onMessage 需要改一下。增加一個(gè)控制的開關(guān):實(shí)現(xiàn)每次收到消息后都用新進(jìn)程處理,解決靜態(tài)變量、內(nèi)存泄漏的問題:
方法:
onWorkerStart執(zhí)行一些需要提前加載的代碼之后,worker進(jìn)程提前fork出很多子進(jìn)程備用,
然后每次 onMessage 接收到消息都用fork出的進(jìn)程處理,用完就銷毀,同時(shí)fork一個(gè)新的。
其中涉及到信號(hào)的處理,fork出的進(jìn)程如何接收數(shù)據(jù)和主進(jìn)程通訊等