首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >单线程极致性能:TrueAsync Server 如何用 C 语言重塑 PHP 服务器生态

单线程极致性能:TrueAsync Server 如何用 C 语言重塑 PHP 服务器生态

作者头像
Tinywan
发布2026-07-01 17:30:50
发布2026-07-01 17:30:50
1100
举报
文章被收录于专栏:开源技术小栈开源技术小栈

TrueAsync Server

TrueAsync 0.7.0 即将发布,附带线程池和其他一些功能。

但最有趣的部分可能是 TrueAsync Server:一个直接嵌入 PHP 的高性能 HTTP/1.1、HTTP/2 和 HTTP/3 服务器。无需单独的进程,无需反向代理。

一切都在一个线程中

首先也是最重要的:TrueAsync Server 是一个"一切都在一个线程中"的服务器。整个请求处理过程,从解析到发送响应,都在单个线程上完成。在这方面,TrueAsync Server 是 PHP 中一个几乎独一无二的项目,它不是用 PHP 编写的(尽管 Swoole 在其基础模式下也运行单个 worker)。

AMPHP 服务器使用类似的单线程事件循环模型——不同之处在于 AMPHP 是用 PHP 实现的,而 TrueAsync Server 作为原生扩展嵌入到 PHP 进程中。

"每个线程一个事件循环"模型本身并不奇特:NGINX、Envoy、Node.js 以及 Rust 技术栈 Tokio + hyper 都是这样构建的。其核心思想是一个线程从头到尾拥有连接和请求:没有接受线程和工作线程之间的交接,没有锁,没有上下文切换。

https://www.http-arena.com/leaderboard/#v=composite&res=mem

优缺点

这种架构有一个明显的缺点。如果 PHP VM 和 TrueAsync Server 驻留在同一线程上,并且 PHP VM 崩溃——服务器 worker 也会随之崩溃。客户端可能会突然失去连接。如果反应器和 PHP VM 驻留在不同的线程,甚至不同的进程上,架构看起来会更健壮:客户端至少可能会收到一个错误。

缺点到此为止——优点依然存在:

  • 无需线程间通信。 这需要复杂的算法,而这种算法永远不会对所有情况都是最优的:某些类型的网络负载表现良好,其他则不然。
  • 简单、可预测的扩展。 启动第二个 worker——大约获得 ×2 的性能。Worker 通过 setWorkers(N) 启动,内核通过 SO_REUSEPORT 在它们之间分配连接。每个 worker 都是一个独立的事件循环,没有共享状态,没有全局锁。
  • 对服务器的完全、无约束控制。 PHP VM 和服务器是一个整体。在另一个线程上管理连接可能要复杂得多;当每个操作都在一个线程内时,许多决策看起来更简单。 顺便说一下,多 worker 模式部分中和了 worker 崩溃的缺点:一个 worker 崩溃不会使其他 worker 宕机。

为什么用 C 而不是 PHP

PHP 有很多现代服务器项目。FrankenPHP 基于 Go 解决方案 Caddy,还有一个 Rust 服务器项目。TrueAsync Server 用 C 编写是有充分理由的:

  • 这是将服务器直接嵌入 PHP 的一种方便方式——尽可能地接近。
  • 在底层,它使用已成为事实标准的 C 库:nghttp2 用于 HTTP/2,ngtcp2 + nghttp3 用于 HTTP/3,llhttp(与 Node.js 使用的相同解析器)用于 HTTP/1.1。
  • 服务器直接链接 OpenSSL,它已经是 PHP 构建的一部分。也就是说,对于 HTTP/3,您必须将其替换为 3.5+ 版本——QUIC TLS API 首次出现于此。
  • 服务器使用 Zend VM。这既有优点也有缺点。优点——更好的资源控制:内存是在单个 memory_limit 下共同计算的,包括服务器和 PHP 代码。缺点——Zend VM 有性能问题,有时会影响服务器。
  • 服务器尽可能直接将数据结构解析为 PHP 数组。

单端口多协议

单个服务器上的多种协议。HTTP/1.1、HTTP/2、WebSocket、SSE 和 gRPC 共享一个 TCP 端口和一个事件循环;协议选择通过 ALPN(对于 TLS)或 HTTP Upgrade 进行。HTTP/3 通过 QUIC 在同一 UDP 端口上运行,并通过 Alt-Svc 头向客户端通告,因此它们会在后续请求中透明地切换。 这意味着一个 $server->start() 调用就可以通过 HTTP/2 提供 REST API,通过 Server-Sent Events 推送事件,保持 WebSocket 连接,并暴露 gRPC 端点!

服务器如何优化

高吞吐量不是一个大技巧——它是许多小决策的总和:

  • 热路径上的池化。 Body 缓冲区、压缩编码器、HTTP/3 流、连接槽——一切都存在于池中。在重复请求时不会干扰分配器和内核;编码器被重用而不是重新创建。
  • 大缓冲区的几何增长。 PHP 标准的 smart_str 有一个隐藏的悬崖:超过某个阈值后,每次增长都会变成一个系统调用,其成本随大小而增长。在大型 body 上,这曾经消耗高达一半的请求时间。
  • 热路径上的零拷贝。 Multipart 解析器直接在传入缓冲区上工作。HTTP/2 提供静态内容无需中间 PHP 缓冲区,HTTP/1 对大文件回退到 sendfile()
  • 与内核网络栈友好。SO_REUSEPORT 将连接分散到 worker,将头与响应体合并,块大小与 TLS 记录大小匹配。
  • 并发请求之间的共享内存。 如果一个请求打开了一个文件,其缓冲区会被另一个请求重用。 这些优化使代码相对于实际工作负载保持轻量。服务器嵌入到 TrueAsync 事件循环中,以便它在协程"之间"工作:当 PHP 代码等待数据库响应时,服务器接受下一个请求。当协程去等待 I/O 时,反应器立即拾取下一个就绪事件——没有线程空闲。

API 概览

服务器的公共 API 由两个基本类组成:

  • HttpServerConfig ——配置对象。
  • HttpServer ——服务器本身,从配置创建并启动。

一个最小的应用程序:

代码语言:javascript
复制
use TrueAsync\HttpServer;  
use TrueAsync\HttpServerConfig;  

$server = new HttpServer(  
    (new HttpServerConfig())->addListener('0.0.0.0', 8080)  
);  

$server->addHttpHandler(function ($request, $response) {  
    $response->setStatusCode(200)->setBody('Hello, World!');  
});  

$server->start();   // 阻塞线程直到调用 stop()

监听器

监听器是"协议 + 传输 + 主机 + 端口"的组合。

  • addListener() —— TCP,HTTP/1.1 + HTTP/2(由第一个字节或 ALPN 选择);
  • addHttp1Listener() / addHttp2Listener() —— 限制为单个协议的端口;
  • addHttp3Listener() —— UDP/QUIC;
  • addUnixListener() —— Unix 域套接字。

处理器

处理器是在每个新请求上调用的函数。它们接收请求和响应对象,并可以像往常一样与它们一起工作。

服务器支持多种处理器,每种匹配特定的协议:

代码语言:javascript
复制
$server->addHttpHandler(fn ($req, $res) => /* ... */);   // HTTP/1.1 + HTTP/2  
$server->addHttp2Handler(fn ($req, $res) => /* ... */);  // HTTP/2 特定  
$server->addWebSocketHandler(fn ($req, $res) => /* ... */);

每个处理器在自己的协程中运行:HTTP/1——每个请求一个协程,HTTP/2 和 HTTP/3——每个流一个协程。

当处理器进入 await(例如等待数据库)时,它既不会阻塞其他连接,也不会阻塞其他流。

请求和响应

请求对象是只读的。其 API:getMethod()getUri()getHttpVersion()getHeader() / getHeaderLine() / getHeaders() / hasHeader()(头不区分大小写)、getContentType()getContentLength()getBody()。对于表单和上传——getPost()getFiles()getFile()响应对象是唯一的输出通道。

Setter 链式调用(流畅接口):

代码语言:javascript
复制
$response  
    ->setStatusCode(200)  
    ->setHeader('Content-Type', 'text/plain')  
    ->setBody('payload')  
    ->end();

流式传输

由于服务器支持 HTTP/2 和 HTTP/3,它从一开始就为流式传输而构建。您可以完全控制何时将数据发送到客户端,而无需等待处理器完成。服务器不会强迫您将整个响应保存在内存中以便一次性发送。

响应有两种模式

  • 缓冲 —— setBody() / write() 累积 body,最后一次性发送到网络;
  • 流式 —— send() 将下一个块直接推送到线路上
代码语言:javascript
复制
$server->addHttpHandler(function ($req, $res) {  
    $res->setStatusCode(200)->setHeader('Content-Type', 'text/event-stream');  
  
    foreach (fetch_events() as $event) {   // 源可能是无限的  
        $res->send("data: {$event}\n\n");  // 块立即发出  
    }  
    $res->end();                           // 关闭流  
});

第一次 send() 提交头;从那时起,对于 HTTP/1.1,这是 Transfer-Encoding: chunked,对于 HTTP/2 和 HTTP/3,它是单独的 DATA 帧。换句话说,相同的处理器代码在任何协议上都会产生正确的流——服务器负责处理差异。

为什么这对 HTTP/2 特别有用。 HTTP/2 中的每个请求是单个连接内的单独流,每个流存在于自己的协程中。流式输出意味着您可以在数据出现时发送它——Server-Sent Events、导出大型报告、gRPC 流——而无需将整个卷展开到内存中。由于 HTTP/2 具有每流流控制窗口,慢速客户端不应该使服务器的内存膨胀。为此,有 $res->sendable()——它报告流现在是否准备好接受下一个块;如果没有,协程只需让出给其他协程,直到窗口释放。

请求 body 也可以作为流到达。使用 Request::readBody() 方法,您可以分部分读取 body,而无需等待它完全接收:

代码语言:javascript
复制
while (($chunk = $req->readBody()) !== null) {  
    $sink->write($chunk);   // 每次处理 64 KiB,而不是一次 2 GiB  
}

这样,一个千兆字节大小的上传可以代理到文件或另一个服务,而无需在内存中组装它。

文件处理器

服务器实现了一个特殊的、优化的路径来提供静态文件。 它是一个构建器类 StaticHandler,与常规处理器一起注册到服务器上:

代码语言:javascript
复制
use TrueAsync\StaticHandler;  
  
$static = (new StaticHandler('/assets/', '/var/www/public'))  
    ->setIndexFiles('index.html')  
    ->enablePrecompressed('br', 'gzip', 'zstd')  
    ->setCacheControl('public, max-age=86400');  
  
$server->addStaticHandler($static);

第一个参数是 URL 前缀(虚拟挂载路径),第二个是磁盘上的目录。处理器的主要目标是尽可能快地提供文件,而无需切换到 PHP 代码。

以下功能已经开箱即用:

  • MIME 类型 —— 内置 44 个扩展名的表(二分查找),加上通过 setMimeType() 的自定义覆盖。
  • 条件请求 —— 来自 (mtime, size, inode) 的弱 ETag,处理 If-None-Match / If-Modified-Since 并返回 304 响应,HEAD 支持。
  • 范围请求 —— 206 Partial ContentContent-Range,对损坏范围的正确 416
  • 预压缩文件 —— 如果 main.css.br / .gz / .zst 与文件相邻且客户端接受,服务器提供现成的压缩文件而不是即时压缩。
  • 安全性 —— 防止路径遍历(../%2e%2e、NUL 字节、反斜杠),点文件策略(.git/ 默认关闭),符号链接策略(Reject / Follow / OwnerMatch),通过 glob 模式隐藏文件。
  • 打开文件缓存 —— 可选的每处理器打开文件缓存,带有 LRU 和 TTL:在热数据集上,它跳过 stat、ETag 计算和 MIME 查找,在作者的基准测试中大约再增加 +20%。 尽管看起来这样的处理器对 API 应用程序没有用处,但它可能适用于提供隐藏在 URL 前缀后面的机密文件。例如,如果您即时生成报告并将它们保存到受保护的目录,它们可以通过 StaticHandler 提供,而无需打开 PHP 文件,也无需将内容读入内存。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开源技术小栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • TrueAsync Server
  • 一切都在一个线程中
  • 优缺点
  • 为什么用 C 而不是 PHP
  • 单端口多协议
  • 服务器如何优化
  • API 概览
  • 监听器
  • 处理器
  • 请求和响应
  • 流式传输
  • 文件处理器
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档