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: HTTP API
守护进程默认绑定 127.0.0.1(仅限本地;在网络边界进行限制)。
POST /dispatch 接受 {app, target, args?, stream?, async?}:
| 键 | 使用者 | 说明 |
|---|---|---|
app |
运行时助手 | 要运行的绝对 .../app.php 路径。知道自己应用的调用者直接进行调度;池是通过应用路径进行键控的。 |
内置的 WebSocket 服务器不使用此端点:它通过注册表(从 config/daemon.js 中的 hosts 映射填充)将每个连接的 Host 解析为一个应用,并在进程内进行调度。POST /message(广播桥)和 GET /health 完善了 API。
响应取决于模式:
| 请求 | 响应 |
|---|---|
| 默认 | {status:"ok", result} |
async: true |
202 {status:"ok", queued:true}(火并忘记;一旦接受就返回,而不是运行后返回) |
stream: true |
一个 application/x-ndjson 流,格式为 {t:line,data}* 然后是 {t:done,result} 或 {t:error} |
GET /health 返回活跃工作者总数与上限的对比、按应用路径键控的每个池的统计信息、每个主机的连接套接字以及配置的主机:
{
"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 } },
"registered": ["app.example.com", "dev.example.com"]
}
busy 是当前正在处理呼叫的 workers,queued 是等待空闲 worker 的呼叫;workers/cap 是实际总数与上限的比较(比核心数量少一个)。在负载下,池会自动增长到上限,并将闲置的 workers 收回,因此这些是观察结果,而不是调节参数。
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 常量选择加入 (X.5)。 |
| 调度器 | 内置 (X.6),每分钟为每个服务的应用运行 tasks::run,替代 cron。 |
Phlo WhatsApp |
保持其独立服务:一个 WhatsApp 网关持有一个持久的电话会话,这不是一个工作池的任务。它是被监控的,而不是被吸收的。 |
16.8: 何时运行它
当应用需要 WebSockets、在没有 cron 的情况下调度任务,或者当热路径在每个请求中分发到多个应用调用时,运行守护进程。一个只提供普通 HTTP 并且从不广播的小网站不需要它:一次性路径就足够了,保持守护进程不在可以减少移动部件。守护进程是可选的性能和实时层,绝不是核心请求服务的依赖。