InfraGuard

实时更新(Reverb + 轮询)

页面自动刷新的两种机制 —— Reverb WebSocket 即时推送,以及配合廉价 rev 变更令牌的 30 秒轻量轮询 —— 以及为什么关闭 WebSocket 时控制台依然不会出问题。

概览 —— 这个页面做什么

InfraGuard 的仪表板无需刷新页面即可自行更新。该自动刷新建立在两种机制之上。

机制 方式 延迟 使用者
Reverb WebSocket 服务器 → 浏览器即时推送(事件驱动) 毫秒级 以 tick 流式上报进度的实时作业
轻量轮询 浏览器按固定间隔 GET /…/live 最多 30 秒 绝大多数仪表板(健康状态、服务器、流量、服务、安全、告警 …)

一条设计准则可以概括全部:

实时更新是叠加在持久化数据之上的增强能力。 每个页面首先渲染数据库中已保存的数据,实时更新只是叠加在其上。因此即使 Reverb(WebSocket 服务器)已关闭、被防火墙拦截或正在重启,页面也绝不会出问题。 推送不可用时由轮询补位(优雅降级)。

在实际运行中,几乎所有页面都跑在轮询上。 Reverb WebSocket 已安装并接线完毕,目前它只驱动一项功能 —— 安全诊断的远程仿真(远程仿真 · 安全诊断),在其中流式推送每台主机的进度 tick。其余所有实时页面都依靠 30 秒轮询加上廉价的 rev 变更检测令牌来刷新。


何时使用

这是一篇概念参考。它不是带实时路由的页面,而是面向希望理解以下内容的运维人员和工程师:


页面构成

双路径架构

[持久化数据]  server_status · infr_db_metric · blacklist_info · …
      │
      ▼ (首次页面加载:服务器把 data-* JSON 注入视图)
[浏览器页面]  ← 立即渲染(没有实时更新也完整)
      ▲                          ▲
      │ (A) 轮询                  │ (B) Reverb 推送
      │  GET /…/live (≤30s)     │  WS 事件 'tick'
      │  仅当 rev↑ 时重绘          │  ShouldBroadcastNow
   [Laravel 控制器]            [Reverb 服务器]

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。


数据来源

文件 作用
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,但浏览器连不上

Q. 仪表板会不会把服务器/数据库压垮?

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() 会吸收异常,空档由轮询补上。


相关内容

헤더의 LIVE 표시 — 화면이 스스로 갱신되는 중
헤더의 LIVE 표시 — 화면이 스스로 갱신되는 중