16: 守护进程
Phlo Daemon 是一个 可选 的 Node sidecar (phlo-daemon.js):一个通用引擎,将任何 Phlo 目标分发到一组持久的 worker。核心 Phlo 在没有它的情况下也能工作;该 daemon 加快了重路径的速度,并支持 WebSockets 和定时任务。
16.1: 它是什么,以及为什么它是可选的
您应用中的每个例程都可以作为一次性 CLI 进程调用(php www/app.php <target> [args...],请参见工具章节)。这就是 async helpers、tasks 和 WebSocket 事件默认运行的方式:每次调用一个新的 PHP 进程,启动应用,完成工作,然后退出。简单且完全隔离,但每次调用都需要启动。
守护进程消除了这种启动。它是一个小型 Node 进程,为每个应用保持一个常驻 PHP worker 池:每个 worker 只启动一次应用,然后通过管道响应调用,这与 FrankenPHP 的 HTTP worker 模式相同,但用于非 HTTP 工作。它增加了两样东西:
- 一个worker 池,可以在不进行每次调用启动的情况下运行相同的调用,以及
- WebSockets 和计划任务所需的长生命周期主机进程。
守护进程是一个通用的中央引擎:其调度核心对任何特定功能一无所知。WebSocket 服务器(Phlo Realtime)和调度器内置于守护进程中;PHP 运行时助手通过 HTTP 访问它。采用它是可选的且可逆的:如果不使用它,所有内容将回退到一次性路径。
16.2: worker 协议
每个 worker 运行 php <app.php> phlo_serve,启动应用程序一次,然后在标准输入上以换行分隔的 JSON 格式响应,每个 worker 处理一个请求(并发等于池的大小):
in {"id", "target", "args"?, "stream"?}
out {"t":"ready"} // once, after boot
{"id", "t":"line", "data"} // 0..N, only when stream
{"id", "t":"done", "result"} | {"id", "t":"error", "message"} // exactly one, terminal
target 使用与 CLI 相同的解析器进行调度:Class::method、object.method 或裸 function(请参见工具章节)。根据请求,worker 会重置状态(phlo('tech/reset')、会话关闭、GC),与 FrankenPHP worker 循环完全相同,因此作业之间不会相互泄漏。编写安全的 worker 目标:在 static 中不要有请求或用户状态,提交或回滚数据库工作。
16.3: The HTTP API
The daemon binds 127.0.0.1 by default (local only; gate it at the network boundary).
POST /dispatch takes {app, target, args?, stream?, async?}:
| Key | Used by | Meaning |
|---|---|---|
app |
the runtime helpers | The absolute .../app.php path to run. A caller that knows its own app dispatches directly; the pool is keyed by the app path. |
The built-in WebSocket server does not use this endpoint: it resolves each connection's Host to an app through the registry (populated from the hosts map in config/daemon.js) and dispatches in-process. POST /message (the broadcast bridge) and GET /health round out the API.
The response depends on the mode:
| Request | Response |
|---|---|
| default | {status:"ok", result} |
async: true |
202 {status:"ok", queued:true} (fire and forget; returns once accepted, not once run) |
stream: true |
an application/x-ndjson stream of {t:line,data}* then {t:done,result} or {t:error} |
GET /health returns the live worker total against the cap, the per-pool stats keyed by app path, the connected sockets per host, and the configured hosts:
{
"status": "ok",
"workers": 5,
"cap": 7,
"pools": { "/var/www/example/www/app.php": { "workers": 4, "busy": 1, "queued": 0 } },
"sockets": { "app.example.com": { "tokens": 12, "sockets": 18, "perToken": { "9c11922f8c7fff2d...": 2 } } },
"registered": ["app.example.com", "dev.example.com"]
}
perToken gives the socket count per connected token, keyed by sha256 of the token so the endpoint never hands out a credential. It is how an app answers "is this client connected" without keeping a tally of its own, which drifts the moment the daemon restarts.
busy is the workers currently handling a call and queued the calls waiting for a free worker; workers/cap is the live total against the ceiling (one less than the core count). The pool grows itself toward the cap under load and reaps idle workers back down, so these are observations, not knobs.
16.4: 配置
守护进程接受三个参数:一个端口、PHP 二进制文件和主机映射。没有池大小限制 - 池根据需求进行扩展。
require('./phlo-daemon.js')(3001, '/usr/bin/php-zts', {
'dashboard.example.nl': { app: '/var/www/dashboard/www/app.php', build: true },
'demo.example.nl': { app: '/var/www/demo/www/app.php', build: true },
})
| 参数 | 默认值 | 说明 |
|---|---|---|
port |
(必需) | 绑定在 127.0.0.1 的端口;在代理处进行限制,绝不要暴露 |
php |
(必需) | 工作进程运行的 PHP 二进制文件(线程安全构建使用 /usr/bin/php-zts) |
hosts |
{} |
主机→应用映射,{ host: { app, build } }:守护进程服务的每个应用(见下文) |
主机映射是配置声明的。 第三个参数是主机到应用的映射,在 config/daemon.js 中声明,并在启动时加载到注册表中:每个条目将 Host 固定到其 app.php 路径和 build 标志。没有 /register 端点,也没有 registry.json;应用不会自我注册。内置的 WebSocket 服务器使用此映射来解析连接的 Host;运行时助手根据自己的 app 路径进行调度,无需条目。
池的大小自动调整。 每个应用都有自己的池,根据需求生成工作进程,最多达到核心数减一的全局上限,然后在工作进程空闲时回收它们。工作进程在调用一定次数后被回收,卡住的工作进程会被杀死并重新生成。所有这些都不需要配置。
一次性或池化遵循构建标志。 build: true 的应用(开发模式)将每次调用作为一个新的一次性进程运行,以实现完全隔离和热重载。发布应用在常驻池中运行。对于 WebSocket,该标志是在 config/daemon.js 中的每个主机设置;对于 CLI 调度,运行时助手在每次调用时发送 build。
在进程管理器下运行:
node config.js
# or
pm2 start config.js --name phlo-daemon16.5: 池上的运行时助手
默认情况下,所有的 async helpers 都保持一次性子进程的行为,当应用程序设置可选的 daemon const 时,它们会切换到守护进程池:
phlo_app(
id: 'Api',
host: 'api.example.com',
daemon: 3001,
);
设置常量后,这些路由通过它们自己的 app 路径访问 /dispatch;如果没有,它们将像之前一样生成一次性进程:
| 助手 | 功能 |
|---|---|
phlo_sync('Class::method', ...$args) |
运行目标并等待其返回值 |
phlo_async('Class::method', ...$args) |
排队一个目标,火并忘记;一旦接受就返回 |
await($job, $job, ...) |
并发运行多个目标并收集它们的结果 |
phlo_stream('Class::method', ...$args) |
逐行输出目标的结果 |
当一个请求扩展为多个调用时,收益最大:await() 对于 100 个缺失的翻译意味着在一次性路径上启动 100 个应用,但在守护进程的常驻池上进行 100 次调度。在池上,工作也是有界并行的(根据工作者数量排队),而不是无界的子进程爆发。
$results = await(
['translate::run', 'nl', $text],
['translate::run', 'de', $text],
['translate::run', 'fr', $text],
)
因为采用守护进程只是 daemon 常量,应用程序在有或没有它的情况下表现相同;只有吞吐量发生变化。
16.6: 调度:cron 的替代方案
每个主机映射中的应用程序都会接收到替代 cron 的分钟滴答:守护进程每分钟在每个服务的应用程序上运行一次 tasks::run,第一次运行是在启动后的一分钟。每个任务没有需要配置的内容:哪些任务触发以及触发频率在应用程序内部声明(%app->tasks,请参见任务章节)。未加载 tasks 资源的应用程序在第一次滴答后会自动跳过。
这里没有针对目标的调度。运行的内容和时间都在每个应用程序的 %app->tasks 中(every/daily/weekly,请参见任务章节),因此调度有一个单一的归属;守护进程仅提供分钟滴答。这取代了任务章节中的每个应用程序的 cron 条目:由于守护进程每分钟运行 tasks::run,您根本不需要 crontab 行。Cron 仍然是无守护进程的后备;无论哪种方式,任务模型、到期检查和磁盘状态都是相同的。
16.7: 消费者
| 消费者 | 关系 |
|---|---|
Phlo Realtime |
守护进程内置的 WebSocket 服务器;拥有套接字并在进程池中运行 websocket::{auth,connect,receive,close} 钩子(receive 流)。请参阅 WebSocket 章节。 |
| 运行时助手 | phlo_sync / phlo_async / await / phlo_stream,通过 daemon 常量选择加入(16.5)。 |
| 调度器 | 内置(16.6),每分钟为每个服务的应用运行 tasks::run,替代 cron。 |
Phlo WhatsApp |
保持其独立服务:一个 WhatsApp 网关持有一个持久的电话会话,这不是一个工作池的任务。它是被监控的,而不是被吸收的。 |
16.8: 何时运行它
当应用需要 WebSockets、在没有 cron 的情况下调度任务,或者当热路径在每个请求中分发到多个应用调用时,运行守护进程。一个只提供普通 HTTP 并且从不广播的小网站不需要它:一次性路径就足够了,保持守护进程不在可以减少移动部件。守护进程是可选的性能和实时层,绝不是核心请求服务的依赖。
最近更新于 2026年8月15日