Multi-worker
(PHP 8.6+, true_async_server 0.6+)
TrueAsync Server 默认运行在单线程模式:一个 event-loop、一个线程,整个流水线 (accept → parse → dispatch → respond)都在同一颗 CPU 上。对典型的 IO 密集型负载这是最快的模型, 但它无法按核数横向扩展。
setWorkers(N) 通过 Async\ThreadPool 启动一个 N 线程的 内置池。每个 worker 在相同的 listener 上重新 bind,内核(Linux/BSD)通过 SO_REUSEPORT 分发 accept。每个 worker 拥有独立的 event-loop、独立的 opcache、独立的连接池。
基础示例
use TrueAsync\HttpServer;
use TrueAsync\HttpServerConfig;
$server = new HttpServer(
(new HttpServerConfig())
->addListener('0.0.0.0', 8080)
->setWorkers(4)
);
$server->addHttpHandler(function ($req, $res) {
$res->json(['pid' => getmypid(), 'tid' => /* TID */]);
});
$server->start(); // 阻塞直到所有 worker 都结束父进程里 HttpServer::start() 做的事:
- 起一个对应大小的
Async\ThreadPool。 - 通过
transfer_obj把 config + 处理程序集合复制到每个 worker。 - 在 worker 里启动 event-loop,重新 bind listener。
- 父进程
await所有 worker 结束。
跨线程的 stop() 还在路线图里;当前可以靠 SIGINT/SIGTERM 或正常的工作耗尽来停止。
Bootloader
worker 的重型初始化(autoload、连接池预热、JIT 预热)应该在启动时做一次,而不是每请求做。 为此提供了 setBootloader(?\Closure $cb):
$config
->setWorkers(4)
->setBootloader(function () {
// 每个 worker 在任务循环之前执行一次
require __DIR__ . '/vendor/autoload.php';
// 预热连接池
Database::initPool(min: 4, max: 16);
// 预编译关键路由
Router::compile();
});闭包会被 deep-copy 一次,并在每个 worker 真正开始接受任务之前运行。 bootloader 中抛出的异常会使整个池失败:该 worker 不会启动。
只在 setWorkers() > 1 时生效。null 取消 bootloader。
需要 TrueAsync ABI v0.15+。测试:
server/core/021-bootloader.phpt。
Per-request scope
从 0.6.5 起,每个 handler 协程都跑在自己的 scope 中,作为服务器 scope 的子 scope。 这带来两条重要语义:
Async\request_context()是整个请求协程树 (handler 与子级spawn)共享的上下文。Async\current_context()仍然是 per-coroutine 的。
use function Async\spawn;
use function Async\await;
use function Async\request_context;
$server->addHttpHandler(function ($req, $res) {
// 整棵请求协程树都能看到的上下文
request_context()->set('request_id', $req->getHeader('X-Request-Id') ?? bin2hex(random_bytes(8)));
request_context()->set('user_id', authUser($req));
// Fan-out
[$user, $posts] = await(\Async\all([
spawn(fn() => fetchUser()), // 这里看得到 request_id
spawn(fn() => fetchPosts()), // 这里也看得到
]));
$res->json(['user' => $user, 'posts' => $posts]);
});对比一下:current_context() 写入的值只在当前协程可见; request_context() 给出一个绑定到请求 scope 的共享 sub-tree。
SO_REUSEPORT 与负载均衡
在 Linux/BSD 上,内核会把入站连接均匀(但不确定)地分发给所有在同一 (host, port) 上 带 SO_REUSEPORT 打开的 socket。每个 worker 开自己的 socket;不需要 userspace 的负载均衡器, 也不需要锁。
Windows 上 SO_REUSEPORT 的等价能力可预测性更差;可以把负载均衡上移到 LB, 或者用 single-worker + 多进程不同端口的方式。
跨线程 transfer 处理程序
如果配置在一个线程里搭建、服务器在另一个线程里启动,HttpServer 支持 transfer。从 0.2.0 起, transfer 路径会正确携带协议位掩码(修复了 "silently dropped every request" 的 bug; 参考 CHANGELOG core/007-server-transfer-handler-dispatch.phpt)。
多线程模式的调试
0.6.3 加了 worker 意外退出的高声日志。$server->start() 的未捕获异常以及在 await-loop 还在等 worker 时的 clean return,现在都会输出到 stderr(以前每出一次就静悄悄丢掉 1/N 的 accept 容量,运维毫无信号)。
打开 INFO 日志:
$config
->setLogSeverity(\TrueAsync\LogSeverity::INFO)
->setLogStream(STDERR);该用多少 worker?
经验法则:
- IO 密集型(带数据库/HTTP 的常规 web):从
available_parallelism()起步,盯着 CPU 使用率调。 - CPU 密集型(渲染、压缩重活、大 JSON):
available_parallelism()或更少,盯着 p99 调。 - 混合:超配 1–2 个 worker(
N+1或N+2)常能在 IO-stall 时榨出更多核心利用率。
$config->setWorkers(\Async\available_parallelism());
Async\available_parallelism()返回进程可用的 CPU 数(考虑 cgroup 配额和 affinity)。 底层走uv_available_parallelism,回退到uv_cpu_info。