说明:当告警源从五个涨到五十个,点对点对接会退化成蜘蛛网。本文设计一条"告警总线":所有源只管发布,路由、分级、防抖、限流、分发全部挂在总线上,附 Redis Stream 的削峰实现与回压策略。这是把本系列散落的中间件收拢成一个架构的收官之作。不放站外链接。
一、蜘蛛网问题:点对点的死亡曲线
按照本系列的协议矩阵,每个告警源直连终端。三个源时这很优雅;三十个源时它是一张蜘蛛网:
- 每个源各自实现签名、重试、防抖------同一套逻辑写了三十遍;
- 终端接口出问题时,三十个源各自报错,你要挨个安抚;
- 新增一个告警源 = 一次全链路配置变更,牵一发动全身;
- 没有任何一处能做全局的削峰------三十个源同时风暴,终端队列直接躺平。
解法是所有消息系统都走过的路:在源与终端之间插入一条总线。源只管往总线发,总线负责削峰、路由、分发;终端只听总线。网状变星型,死亡曲线变成线性。
text
[HTTP源] [MQTT] [SNMP] [邮件] [Modbus] ← 适配层:协议收编为统一消息
└──────┴──────┴──────┴──────┘
[告警总线] ← Redis Stream / MQTT Broker
削峰 · 防抖 · 分级 · 路由
┌──────┴──────┐
[终端·现场声光] [工单/IM/短信] ← 消费者各取所需
二、统一消息格式:总线上的通用语
总线的第一产品是消息 schema------所有适配层把各自的方言翻译成这一种通用语:
json
{
"id": "evt-20260919-000123",
"source": "hikvision-nvr",
"level": "critical",
"place": "仓库东侧",
"event": "视频信号丢失",
"dev": "cam-07",
"fired_at": "2026-09-19T08:54:30+08:00",
"ttl": 3600,
"dedup_key": "hikvision/仓库东侧/cam-07/videoloss"
}
字段设计的三个决策:
place与event是人话(专栏文案篇的翻译在适配层完成)------总线之后的所有消费者不用再翻译;dedup_key是防抖的锚(专栏收敛篇)------总线级去重把"三十个源各自防抖"变成"一处防抖";ttl是告警的保质期------风暴消退后总线上滞留的旧告警自动过期,专治"考古播报"(专栏收敛篇的坑 8)。
三、总线实现:Redis Stream 的削峰与回压
为什么选 Redis Stream 而不是直接上 Kafka:告警总线要的是"轻量持久 + 消费组 + 有限回压",不是亿级吞吐。Redis Stream 一台 Redis 全有:
python
# bus_core.py ------ 告警总线:生产(削峰入库)与消费(路由分发)
import json
import time
import redis
r = redis.Redis(host="192.168.10.6", port=6379, decode_responses=True)
STREAM = "alarms"
GROUP = "router"
MAXLEN = 100_000 # 流最大长度:总线自身的容量上限
RATE = {"critical": 20, "warning": 10} # 每秒发往终端的上限(终端播报能力)
_bucket: dict[str, list[float]] = {}
def publish(msg: dict) -> str:
"""适配层调用:统一入总线。超压时 critical 仍进,低级别直接丢弃"""
level = msg.get("level", "warning")
now = time.time()
_bucket.setdefault(level, [])
_bucket[level] = [t for t in _bucket[level] if now - t < 1]
if len(_bucket[level]) >= RATE[level] and level != "critical":
return "dropped:rate-limit" # 削峰:低级别超速直接丢(有日志)
_bucket[level].append(now)
msg.setdefault("fired_at", time.strftime("%FT%T%z"))
msg["ttl"] = msg.get("ttl", 3600)
r.xadd(STREAM, {"m": json.dumps(msg, ensure_ascii=False)}, maxlen=MAXLEN)
return "ok"
def consume_forever() -> None:
"""路由消费者组:读 → 按 level 分发 → Ack"""
try:
r.xgroup_create(STREAM, GROUP, id="0", mkstream=True)
except Exception:
pass # 组已存在
while True:
rows = r.xreadgroup(GROUP, "router-1", {STREAM: ">"}, count=10, block=2000)
for _, entries in rows:
for entry_id, fields in entries:
msg = json.loads(fields["m"])
if time.time() - time.mktime(time.strptime(
msg["fired_at"][:19], "%Y-%m-%dT%H:%M:%S")) < msg["ttl"]:
dispatch(msg) # 未过期才分发
r.xack(STREAM, GROUP, entry_id)
def dispatch(msg: dict) -> None:
"""按级别路由:终端声光 + 工单 + IM 各自订阅各自的世界"""
requests_post(f"http://192.168.0.66/api/api/user_msg/bus-{msg['level']}", msg)
if msg["level"] == "critical":
requests_post("http://ticket-system/api/open", msg)
def requests_post(url: str, msg: dict) -> None:
import requests
try:
requests.post(url, json=msg, timeout=3)
except requests.RequestException:
r.xadd("alarms.retry", {"m": json.dumps(msg, ensure_ascii=False)}) # 失败入重试流
架构上的四个关键点:
- 削峰在入口做 :
publish的令牌桶按级别限速------warning 超速直接丢(敢丢的前提是分级收口,critical 永不丢),终端播报能力从此不再被风暴考验(专栏队列篇的"终端兜底"升级为"总线主防"); - 消费组保证不丢 :
xreadgroup+xack,路由器崩了重启从上次的位置继续;分发失败进alarms.retry重试流------总线把"至少一次"变成了体系默认语义; - TTL 过期:分发前检查保质期,风暴中滞留五分钟的旧 warning 不再播报------队列治理(专栏队列篇)在总线层提前完成;
- 多消费者天然扩展:工单、IM、报表各自开消费组各订各的,互不影响------这就是"一次发布、多方受益"。
四、迁移路径:从蜘蛛网到总线
存量系统不必一夜重构,三步灰度:
- 新源先上总线:新接入的告警源一律走适配层进总线(先让蜘蛛网停止生长);
- 逐个收编存量源:把存量源的直连地址改为总线适配层------按源迁移,每迁一个观察一周;
- 终端只认总线 :最终态终端侧白名单(专栏安全篇)只剩总线一个源------暴露面收敛、防抖、限流、审计全部收口在一处。
迁移期保留双跑对账(专栏日志篇):同一事件直连与总线各触发一次,播报日志对账一致后再切换------架构迁移的安全感,来自对账而不是自信。
五、常见坑
- 总线做成单点 → Redis 没有持久化与主从,总线挂 = 全体失聪;持久化 + 哨兵/集群起步;
- 削峰把 critical 也丢了 → 限速豁免 critical 是铁律(专栏收敛篇),丢掉的 warning 要有日志可查;
- 消息格式没有版本字段 → 适配层升级后老消费者解析失败,schema 加
v字段提前留门; - 消费组不 Ack → pending 列表无限膨胀,Redis 内存被吃光,监控 xpending;
- TTL 没有上限 → 有人传 ttl=一年,过期检查形同虚设,服务端 clamp 到最大值;
- 重试流没有上限 →
alarms.retry无限重试毒消息,重试三次进死信流人工处理; - 总线成了新的大泥球 → 路由、文案、防抖、工单全塞进总线进程------总线只做"收、削、转",业务逻辑留在适配层与消费者;
- 跳过对账直接切换 → 直连改总线的窗口期正是漏报高发期,双跑对账是唯一安全的过桥方式。
六、验收清单
- 统一 schema 落地,全部适配层消息可互译;
- 压测:warning 100 条/秒注入,终端侧 ≤ 10 条/秒且 critical 全量到达;
- 路由器 kill -9 后重启,pending 消息无丢失续传;
- TTL 过期与死信流验证,毒消息三试后隔离;
- 存量源迁移双跑对账通过,直连通道按计划退役;
- 终端白名单收口到总线单源,暴露面清单更新;
- 总线自身纳入看门狗与监控(xlen / xpending / 消费延迟三指标)。
七、小结
告警总线解决的是规模化后的秩序问题 :统一语(schema)让三十个方言源说一种话,削峰(令牌桶)让风暴到不了终端,消费组让分发不丢不重,TTL 让考古播报成为历史。它把本系列散落在各篇的中间件------海康转发器、MQTT 桥、防抖状态机、限流闸------收拢成一处:源越来越多样,总线越来越稳,终端始终简单。
架构演进的终点很朴素:让每一个新告警源的接入,变成往适配层加一个翻译文件的事。到那一天,这张网就不再是蜘蛛网,而是电网。