PHP True Async 最近进展以及背后的争议
PHP True Async 团队还在努力。如果 RFC 通过,将会跟着 PHP 8.6 一起发布。现在RFC 1.6 刚刚进入投票阶段,RFC 1.7 就已经准备就绪。最大的变化是:将 Fiber 作为协程生成器编织进 TrueAsync,并使用显式的 yield(Fiber::suspend)。这一优雅的改动归功于 Bob Weinand(详见 RFC:https://wiki.php.net/rfc/true_async#fiber_support)。
但真正有趣的事情发生在幕后。
原文链接 PHP True Async 最近进展以及背后的争议
WordPress 兼容性争议
在激烈的 TrueAsync RFC 1.6 辩论期间,有一个论点是异步对 PHP 不利,因为它"破坏了 WordPress"。逻辑是:WordPress 依赖全局变量,所以在协程内调用其 API 可能会破坏某些东西。
这是真的吗?作者决定用最糟糕的方式找出答案:将 WordPress 作为有状态应用运行在 TrueAsync 上。这不是第一次尝试------GitHub 上已经有使用 Swoole 实现的项目。困难的部分不在于异步,而在于 WordPress 被设计为在每个请求上"死掉"。重新定义常量、重新包含文件、到处都是全局状态。WordPress 并不是有状态生命周期的理想候选者。
这是否阻止了尝试?当然没有。为了实现这一点,作者构建了一个自定义的 TrueAsync 和 PHP,使全局变量对每个协程唯一,超全局变量对每个 Async\Scope 唯一。
翻译一下:当你在协程内运行代码时,你无法通过全局变量意外地传递数据。作用域本地的超全局变量为接触 $_GET、$_POST、$_SESSION 等的协程创建了一个沙箱,而不会泄漏到其他请求中。这让你可以在一个进程中运行多个 WordPress 副本,几乎不需要任何更改。可怕吗?绝对的。
第一项工作:一个入口点,初始化 WordPress、数据库连接,并将控制权传递给模板层:
php
include_once WP_ROOT . '/wp-config.php';
include_once WP_ROOT . '/wp-settings.php';
ob_start();
// Run WordPress
wp();
$template_loader = ABSPATH . WPINC . '/template-loader.php';
if (file_exists($template_loader)) {
include $template_loader;
}
$output = ob_get_clean();
$responseHeaders = [
'Content-Type' => 'text/html; charset=UTF-8',
'Content-Length' => strlen($output),
'X-Powered-By' => 'TrueAsync PHP',
];
$success = sendResponse($client, 200, $responseHeaders, $output, $shouldKeepAlive);
ServerStats::$requestHandled++;
// Close MySQL connection to prevent connection leaks
closeMySQLConnection();
return $success ? $shouldKeepAlive : false;
思路很简单:
- 为每个请求启动一个新的 WordPress。
- 打开一个唯一的数据库连接。
- 使用
ob_start/ob_get_clean捕获 WordPress 输出并发送回去。
要让它工作,你必须从 WordPress 中移除一些 exit/die 调用。之后,你就有了一个可以处理 WordPress 请求的入口点。
但如果我们只初始化 WordPress 一次,然后将其状态克隆到每个请求的协程中呢?如果这样可行,我们可以只克隆 WordPress 的部分内容:当数据不可变时,为多个并发请求重用相同的内存。
添加一个协程来监控进程统计信息,而不会拖慢服务器:
php
/**
* Statistics reporter coroutine
*/
spawn(function(): void {
while (true) {
delay(5000);
$activeCoroutines = count(getCoroutines());
$activeConnections = ServerStats::$connectionsStarted - ServerStats::$connectionsClosed;
echo "[Stats] Connections: Accepted=" . ServerStats::$connectionsAccepted .
" Started=" . ServerStats::$connectionsStarted .
" Active=" . $activeConnections .
" Closed=" . ServerStats::$connectionsClosed .
" | Requests: Total=" . ServerStats::$requestCount .
" Handled=" . ServerStats::$requestHandled .
" | Coroutines: " . $activeCoroutines . "\n";
}
});
令人惊讶的是:大量旧代码就这样工作了。当多个协程访问缓存代码时会出现问题。WordPress 动作处理程序在钩子分发期间有时需要本地状态。需要进行更改。你无法避免它们。
尽管如此......很难忽视有多少代码能够原样运行。并发服务器为任何 PHP 代码提供了以合理代价获得真正性能提升的机会。
并发服务器
PHP 并没有自带并发服务器(除了 Swoole 和基础的 HTTP/1.1 服务器)。如果你无法使用协程,为什么要有协程?用纯 PHP 编写自己的服务器是痛苦的,特别是对于 HTTP/2 或 HTTP/3。
RoadRunner 和 FrankenPHP 团队依靠 Go 来实现性能。让我们看看 TrueAsync 如何适配。
要将 TrueAsync 与 FrankenPHP 集成,PHP 需要一个思维转变:它永远存活,服务器变成了 PHP 驱动的插件:
php
<?php
use FrankenPHP\HttpServer;
use FrankenPHP\Request;
use FrankenPHP\Response;
set_time_limit(0);
echo "Starting FrankenPHP TrueAsync HttpServer...\n";
// Register request handler
HttpServer::onRequest(function (Request $request, Response $response) {
$method = $request->getMethod();
$uri = $request->getUri();
$headers = $request->getHeaders();
$body = $request->getBody();
$response->setStatus(200);
$response->setHeader('Content-Type', 'application/json');
$responseData = [
'message' => 'Hello from FrankenPHP TrueAsync!',
'method' => $method,
'uri' => $uri,
'total_coroutines' => count(\Async\get_coroutines()),
'memory' => round(memory_get_usage(true) / 1024 / 1024, 2),
'timestamp' => date('Y-m-d H:i:s'),
'headers_count' => count($headers),
'body_length' => strlen($body),
];
$response->write(json_encode($responseData, JSON_PRETTY_PRINT));
$response->end();
});
echo "Request handler registered. Event loop is running.\n";
// Script stays loaded, event loop handles requests
现在我们可以构建跳过每次请求都重新初始化所有内容的应用:
有状态应用
我们可以在并发协程之间共享这些预热的服务:
TrueAsync 为高性能 PHP 应用打开了大门,通过混合有状态架构和并发处理,可以与其他语言竞争。
下一步是什么?
RFC 1.7 即将进入讨论阶段,更多实验正在进行中。例如:使用 TrueAsync 和 FrankenPHP 运行 Laravel:https://github.com/true-async/laravel-test