概览 —— 这个页面做什么
InfraGuard 的仪表板无需刷新页面即可自行更新。该自动刷新建立在两种机制之上。
| 机制 | 方式 | 延迟 | 使用者 |
|---|---|---|---|
| Reverb WebSocket | 服务器 → 浏览器即时推送(事件驱动) | 毫秒级 | 以 tick 流式上报进度的实时作业 |
| 轻量轮询 | 浏览器按固定间隔 GET /…/live |
最多 30 秒 | 绝大多数仪表板(健康状态、服务器、流量、服务、安全、告警 …) |
一条设计准则可以概括全部:
实时更新是叠加在持久化数据之上的增强能力。 每个页面首先渲染数据库中已保存的数据,实时更新只是叠加在其上。因此即使 Reverb(WebSocket 服务器)已关闭、被防火墙拦截或正在重启,页面也绝不会出问题。 推送不可用时由轮询补位(优雅降级)。
在实际运行中,几乎所有页面都跑在轮询上。 Reverb WebSocket 已安装并接线完毕,目前它只驱动一项功能 —— 安全诊断的远程仿真(远程仿真 · 安全诊断),在其中流式推送每台主机的进度 tick。其余所有实时页面都依靠 30 秒轮询加上廉价的 rev 变更检测令牌来刷新。
何时使用
这是一篇概念参考。它不是带实时路由的页面,而是面向希望理解以下内容的运维人员和工程师:
- 仪表板如何自行保持最新
- 在 WebSocket 无法连接的环境中(内网、代理、混合内容)会发生什么
- 轮询如何节省电量和服务器负载(标签页切到后台时会暂停)
页面构成
双路径架构
[持久化数据] server_status · infr_db_metric · blacklist_info · …
│
▼ (首次页面加载:服务器把 data-* JSON 注入视图)
[浏览器页面] ← 立即渲染(没有实时更新也完整)
▲ ▲
│ (A) 轮询 │ (B) Reverb 推送
│ GET /…/live (≤30s) │ WS 事件 'tick'
│ 仅当 rev↑ 时重绘 │ ShouldBroadcastNow
[Laravel 控制器] [Reverb 服务器]
- (A) 轮询 —— 前端通过
setInterval每 30 秒调用一次/{screen}/live。响应中带有廉价的变更令牌rev;只有当它发生变化时才重新渲染图表和卡片。标签页切到后台时(document.hidden)直接跳过该调用。 - (B) Reverb 推送 —— 服务器立即广播事件(
ShouldBroadcastNow—— 不走队列),已订阅的浏览器通过 WebSocket 接收。目前它仅用于远程仿真的进度 tick。
Reverb 拓扑
| 项目 | 值 | 来源 |
|---|---|---|
| 客户端库 | laravel-echo + pusher-js |
动态导入(不计入主打包产物) |
WebSocket 连接细节在安装时配置完成。运维人员只需打开页面,无需做任何配置。
如何使用
运维人员 —— 无需操作
实时更新是自动的。让页面保持打开,轮询每 30 秒取回最新数值;如果 WebSocket 已启用,事件驱动的页面会即时更新。为了省电,标签页处于后台时轮询会暂停,回到该标签页后恢复。
启用 Reverb WebSocket(可选)
WebSocket 推送由部署联系人在安装时配置 —— 它不是控制台里给运维人员的开关。如果需要实时推送,请联系管理员或供应商。
WebSocket 推送处于关闭状态并不是错误。这是一个有意为之的关闭开关 —— 实时页面仅靠轮询也能照常工作。
轮询实际做了什么
// 代表性写法(例如 service-overview)
poll_ms: 30000,
this._poll = setInterval(() => {
if (this.live && !document.hidden) this.poll_tick(); // 标签页在后台时跳过
}, this.poll_ms);
// poll_tick → GET /service/live → 仅当响应的 rev 发生变化时才重新渲染
服务端的 rev 是尽可能廉价的变更信号。例如服务器指标页面使用遗留表 server_status 的 MAX(seq) 作为其 rev —— 只有采集器插入新快照时该值才会上升。
// App\Services\Metric\ServerMetricService::rev()
return (int) DB::connection('legacy')->table('server_status')->max('seq');
唯一使用 Reverb 推送的实时作业 —— 远程仿真
安全诊断的远程仿真会在每台主机完成时广播一个进度 tick。
- 事件:
App\Events\InfrRemoteEmulateTick(ShouldBroadcastNow) - 频道:公开频道
infr.emulate.{runId}——runId是无法猜测的随机令牌(一种能力凭证)。由于是公开频道,它不需要基于会话的/broadcasting/auth接线。 - 事件名:
.tick· 载荷很轻量(仅包含主机摘要与进度计数) - 兜底:前端在订阅的同时还会运行一个2 秒的兜底轮询(
/asset/remote/emulate-status/{runId}),因此即使 WS 不存在或漏掉了最早的 tick,进度也不会丢失。
数据来源
| 文件 | 作用 |
|---|---|
resources/js/echo.js |
应用中唯一的 Echo 构建器(SOT)—— 动态导入 · 配置门控 → null · 运行时 forceTLS |
app/Support/Realtime.php |
广播的安全网(up() 的 fsockopen 探测 · fire() 的异常吸收) |
routes/channels.php |
频道鉴权回调(目前只有默认的 App.Models.User.{id}) |
app/Events/InfrRemoteEmulateTick.php |
当前实际在用的唯一广播事件 |
各页面的 …/live 控制器 + …Service::rev() |
轮询用的实时载荷 + 变更令牌 |
Realtime —— 两层防御(WS 可以死掉,但不会拖垮原本的工作)
// (1) up() —— 150ms 的 fsockopen 探测(按请求记忆化)。事件的 broadcastWhen() 以此为门控,
// 因此 Reverb 宕机时,ShouldBroadcastNow 事件不会因连接超时而就地抛出异常。
public static function up(): bool // 从配置中探测(不硬编码)
// (2) fire() —— 即使 Reverb 在线却拒绝请求(413,或重启期间的 5xx),也保护调用方。
// 失败会通过 report() 送到 Pulse Exceptions;原本的工作(已保存的扫描结果)依然成功。
public static function fire(object $event): void
事件的 broadcastWhen() 返回 Realtime::up() —— 如果 Reverb 已关闭,就完全跳过发送。
访问权限
频道授权
| 频道类型 | 鉴权 |
|---|---|
公开频道(infr.emulate.{runId}) |
无 —— runId 这个能力令牌本身就是访问凭证(无法猜测) |
| private / presence 频道(设计路线图) | routes/channels.php 的回调检查会话的 user_level 与角色范围 |
CLAUDE.md 中的实时频道表(
private-infr.server.{id}、private-infr.threats、presence-infr.ops…)是一份设计路线图。今天已实现的广播只有单一的公开频道infr.emulate.{runId},而routes/channels.php只注册了框架默认的私有频道App.Models.User.{id}。将来新增 private / presence 频道时,务必按user_level与范围授权(例如infr.threats要求user_level>=2 && scope∋security)。
轮询端点的访问权限
/{screen}/live 轮询端点继承其所属页面控制器的读取级别门控(demo=1、staff=5、admin=10;部分区域由 infr.level:N 中间件把关)。例如远程仿真的状态兜底轮询(emulate-status)需要写入权限(级别 ≥2)。完整策略参见 RBAC 级别。
排障与 FAQ
Q. WebSocket 连不上,但页面还在持续更新?
这是预期行为。没有 WS 时,实时页面仅靠轮询也完全可用。WebSocket 只是为远程仿真这类事件驱动作业降低延迟。
Q. 我启用了 Reverb,但浏览器连不上
- 混合内容 ——
echo.js根据运行时页面协议决定forceTLS(页面为https:时使用wss)。它刻意不从构建期环境变量强制指定,这样可以避免以 https 提供的打包产物去尝试ws://而被浏览器拦截。如果前端是 https,那么 Reverb 也必须提供 TLS(wss),或者由代理来终结 TLS。 - CSP / CORS ——
echo.js设置了enabledTransports: ['ws','wss']与disableStats: true,用以屏蔽 pusher-js 的 sockjs CDN 回退和统计上报(它们会违反自托管环境的 CSP)。
Q. 仪表板会不会把服务器/数据库压垮?
- 轮询间隔为 30 秒,且标签页切到后台时会停止(
document.hidden)。 rev变更令牌(例如MAX(seq))会在没有新数据时跳过昂贵的重绘。- 实时构建器服务大多有 20–30 秒的缓存。
Q. 可以把实时相关的守护进程放进调度器吗?
绝对不要。 不要从调度器(
Schedule::command(...))启动reverb:start、queue:work或pulse:check这类无限循环的守护进程。永不退出的进程会在每个 tick 累积,并耗尽共享 MySQL 的连接池(2026-08-06 用pulse:check实测:16 小时内累积 169 个 →[1040] Too many connections,波及生产数据库)。守护进程只能从启动器的单个常驻窗格(开发环境)或 supervisor / systemd(生产环境)运行。守护进程的部署位置参见 开发 · 可观测性。
Q. 改完代码后如何重启 Reverb?
生产环境使用零停机的 php artisan reverb:restart(或在 supervisor 下重启)。开发环境则重启启动器窗格。如果重启期间 WS 短暂拒绝连接,Realtime::fire() 会吸收异常,空档由轮询补上。
相关内容
- 远程仿真 —— 唯一真正使用 Reverb 推送的实时作业
- 安全诊断 —— 远程仿真进度 tick 流经的页面
- 开发 · 可观测性(Pulse) —— 守护进程的运行纪律 · Realtime 异常的落点
- 基础设施 ▸ 服务器 —— 30 秒轮询 +
MAX(seq)rev 令牌的范例 - 服务概览 —— 实时看板的轮询模式
- RBAC 级别 —— 频道与轮询端点的访问门控
