Skip to content

Flap 检测

会话级(P0)与前缀级(P1)两级检测、告警事件(P2)与速率存档的数据层。模型在 app/db/models/flap.pyprefix_flap.py,检测机制全貌见 flap 检测

该子系统横跨四种存储角色,本页覆盖其全部落点:

落点存储内容丢失后果
Valkey pflap:<node> / bgpflap:<node>L4两级打分热态从下一份上报重新起算
flap_alerts · bgp_flap_transitions事实告警的开启与消退、状态转移不可重建
node_prefix_flap_metaL2喂送健康度下一份上报即重建
prefix_flap_rate_rollup · flap_activity_rollupL360 天速率与活跃度曲线历史断档
bgp_flap_state · node_prefix_flaps · node_prefix_flap_paths降级路径KV 不可用时接管热态

贯穿全册的分界:当下视图与事实

Flap 数据分两类,存储方式截然不同,这是本册最重要的结构:

当下视图事实
定义指数衰减[1]打分,每份上报整份重算、读时按半衰期续衰转移事件、告警的开启与消退
丢失后果从下一份上报重新起算(半衰期内影响自然消散)历史中断,且不可重建
存储位置L4 Valkey 单键 blobPostgreSQL

打分不落 SQL 的原因:前缀级热态每 60 s 整份替换,落 SQL 每月将产生上亿行写入;会话级量级虽小得多,迁入 KV 是为消除「同一概念两种存法」的不对称,该不对称最易导致后续改动出错。

热态置于 L4 而非 L5 的原因:该 blob 是唯一副本。它是对上一份已经过去的上报所做的差分基准,无法计算也无法重建(下一轮上报只能重新起算,中间一格永久缺失)。置于会 LRU 淘汰且不落盘的 Redis 上,容器重建即全部丢失。键定义与 TTL 见 KV 与缓存键索引

注意:写侧(PrefixFlapStore / FlapStore)与告警评估器(FlapAlertStore)必须注入同一个 KV 实例。 三者经 load_current_flap_state / load_session_flap_state 读取同一批键;若一方改变数据源而另一方未同步,评估器将读到空热态,把全部在开告警判定为已消退并批量写入 resolved_at。此项由 test_storage_discipline.py 静态约束。


flap_alerts

flap 告警事件的持久层(P2)。衰减分属于当下视图,消退后历史即不可见;本表由控制面背景评估循环(60 s)以边沿触发方式维护:分数上穿阈值时开启一条,下穿消警线(阈值的一半,以迟滞避免反复开关)时关闭该条。

kind=session 定位 (node_id, name=会话名);kind=prefix 定位 (name=前缀, peer_asn=归因对端),nodes 记录波及节点集合。

node_id 有意不设外键:告警属于历史事实,不应随节点删除而级联消失。

类型约束 / 默认说明
idIntegerPK,autoincrement主键
kindString(16)NOT NULLsession / prefix
node_idString(64)可空,index(无 FK)会话告警的所属节点;前缀告警为 NULL
nameString(128)NOT NULL会话名或前缀
peer_asnBigInteger可空前缀告警的归因对端 AS
nodesJSON可空波及节点集合(session 为单元素;prefix 为看到该抖动的节点,持续期间取并集)
peak_score / last_scoreFloatNOT NULL持续期间峰值分 / 最近评估分
started_atDateTime(tz)NOT NULL,server_default now开单时刻
resolved_atDateTime(tz)可空,index消警时刻;NULL 表示在开
updated_atDateTime(tz)server_default now,带 onupdate更新时刻

已消警的历史按条数截断保留 500 条_RESOLVED_KEEP)。

注意:这是全库 UPDATE 最频繁的表(约 110 行/分钟),其中绝大部分用于每轮评估刷新 last_score——一个可重算的当下值,与审计字段混存于同一行。改造方向与触发条件见写热点


bgp_flap_transitions

append-only 的会话状态转移历史,供界面下钻查询单条会话的抖动时刻。按节点保留最近 200 条_TRANSITIONS_KEEP)。

仅记录涉及 Established 的转移(断连、隐性重连、恢复);非 Established 状态之间的切换只计数不落行——长期 down 的会话每份快照切换一次状态,一天约 288 条,不足一天即会将保留窗口内的真实 flap 历史挤占殆尽。

类型约束 / 默认说明
idIntegerPK,autoincrement主键
node_idString(64)FK→nodes.node_id(CASCADE),NOT NULL,index所属节点
nameString(128)NOT NULL,indexBIRD protocol 名
sessionString(128)可空schema 会话名
kindString(16)NOT NULLstate(state 实变,如 Established→Active)/ restart(隐性重连)
from_state / to_stateString(32)NOT NULL转移前后状态
captured_atString(64)NOT NULL观测到转移的快照时刻。不是转移实际发生时刻——分辨率即快照节奏
created_atDateTime(tz)server_default now写入时刻

node_prefix_flap_meta

每节点最近一次前缀级上报的元信息(喂送健康度),用于区分「无抖动」与「喂送中断导致不可见」。写入频率低(每份上报一次,约 60 s),始终走 SQL。

类型约束 / 默认说明
node_idString(64)PK,FK→nodes.node_id(CASCADE)主键即外键
captured_atString(64)NOT NULL最近上报生成时刻
feed_stateString(32)NOT NULL,默认 unknownflapfeed BGP 会话状态(established / down / unknown
total_changesBigIntegerNOT NULL,默认 0采集会话启动以来的累计路径变化总数
updated_atDateTime(tz)server_default now,带 onupdate更新时刻

total_changes 的差分正是下面速率存档的数据来源。


时序存档

两张表互补:速率回答「变化速度」,活跃度回答「多少主体处于抖动状态」。通用桶形态与保留期见时序建模通则

prefix_flap_rate_rollup

每 (节点, 60 s 桶) 的路径变化速率存档,收报时差分累加。fleet 级「路由变化速率」的时序与仪表都从这张表读时聚合。

类型约束 / 默认说明
node_idString(64)PK(复合),FK→nodes.node_id(CASCADE)所属节点
bucket_startBigIntegerPK(复合)桶起点(epoch 秒,对齐 60 s)
changesBigIntegerNOT NULL,默认 0喂送级差分——该节点看到的全部计分事件,含未上榜前缀(来自 total_changes 的差分)
listed_changesBigIntegerNOT NULL,默认 0榜内 (prefix, peer) 条目 changes 的差分之和。只算两份报告都在榜的键,新上榜键无基准不计
samplesIntegerNOT NULL,默认 0落桶报告数(>0 即「该桶有上报」,读侧据此区分 0 与无数据)

额外索引 ix_prefix_flap_rate_rollup_bucket (bucket_start)——跨节点按时间聚合时不带 node_id,走不了主键最左前缀。实测这个索引比主键还常被用(32 万次 vs 16 万次)。

flap_activity_rollup

fleet 级 flap 活跃度时间线(每 60 s 一行,无节点维度),随告警评估循环落点。ORM 类名 FlapActivitySample

类型约束 / 默认说明
bucket_startBigIntegerPK桶起点(epoch 秒,对齐 60 s)
tracked_prefixesIntegerNOT NULL,默认 0在跟踪的前缀数(有任何存活分数)
flapping_prefixesIntegerNOT NULL,默认 0过阈前缀数
flapping_sessionsIntegerNOT NULL,默认 0过阈会话数
open_alertsIntegerNOT NULL,默认 0在开告警数

降级路径的三张表

以下三张表在 KV 可用时不再被写入,仅在 Valkey 未配置或发生故障时接管。回退期间行为与改造前完全一致,区别仅在于恢复了 SQL 整表替换的写放大(测试与降级共用同一份代码,语义逐条相同)。

注意:读侧会合并两路。 load_session_flap_state / load_current_flap_state 先扫描 KV,某节点 blob 缺失时回落该节点的 SQL 行。这一设计使热路径切换过程中评估器始终能读到数据,不会因「空热态」而误判批量消退;但同时也意味着,热路径运行一段时间后,此处保留的是上一次降级时写入的陈旧行,经衰减后接近零分,表现为「该节点无抖动」而非报错。

实测佐证bgp_flap_staten_tup_upd 在 21 分钟观测窗口内稳定于 428 185 未增长,而 seq_scan 仍在增长——写侧已完全走 KV,读侧每轮仍查询一次以完成合并。

bgp_flap_state

每 (节点, BIRD 协议名) 一行的会话级 flap 打分状态。数据源是 runtime snapshot 里的 bgp_protocols(约 5 min 节奏):快照入库时与上一份逐会话比对,发现状态转移、或 state 同为 Established 但 since 变化的「隐性重连」,即记转移并给该会话的指数衰减分加分。该检测不需要 agent 侧任何改动,完全由控制面推导。会话从快照中消失(配置删除)时,对应行同步删除。

类型约束 / 默认说明
node_idString(64)PK(复合),FK→nodes.node_id(CASCADE)所属节点
nameString(128)PK(复合)BIRD protocol 名(快照内唯一)
sessionString(128)可空反查到的 schema 会话名(eBGP 有;iBGP 合成会话为 NULL)
last_stateString(32)NOT NULL上次观测的 state(比对基准)
last_sinceString(64)可空上次观测的 since(since 变而 state 不变即隐性重连)
captured_atString(64)NOT NULL分数锚点 = 上次纳入比对的快照时刻;读取时从锚点按半衰期续衰到当前
scoreFloatNOT NULL,默认 0.0指数衰减 flap 分
transitions_totalIntegerNOT NULL,默认 0累计状态转移数(不衰减,供展示)
flaps_totalIntegerNOT NULL,默认 0其中计分的 flap 事件数
last_transition_atString(64)可空最近一次转移时刻
updated_atDateTime(tz)server_default now,带 onupdate更新时刻

KV blob(bgpflap:<node>)中每个会话的条目与上述列逐字段同构,区别仅在于 node_id 提升为键名、name 提升为 map 的键。

node_prefix_flaps

一条 (节点, 前缀, 对端 AS) 的当前前缀级 flap 打分,随上报按节点整表替换(条目 ≤500,delete + bulk insert)。数据源是 agent 旁路 flapfeed 管线的周期上报(约 60 s),agent 侧已按 (prefix, 归因对端 AS) 算好指数衰减分并截断 Top-N。

类型约束 / 默认说明
node_idString(64)PK(复合),FK→nodes.node_id(CASCADE)所属节点
prefixString(64)PK(复合)前缀
peer_asnBigIntegerPK(复合),默认 0归因对端 AS。0 表示未归因或本节点自起源(主键列不可空,读出时归一回 None
origin_asnBigInteger可空前缀起源 AS(AS_PATH 最后一跳);本网自起源或无从归因时为 NULL
scoreFloatNOT NULLagent 侧算好的指数衰减分(锚定 captured_at,读取时可续衰)
changesBigIntegerNOT NULL,默认 0该 (prefix, peer) 自 flapfeed 启动以来的累计变化数(不衰减)
recent_changesBigInteger可空上一报告间隔内的变化数。上一报告无该键即无基准 → NULL,宁可漏检不虚报
rate_per_sFloat可空折算的每秒速率(榜单 Rate 列口径)
last_change_atString(64)可空最近一次变化时刻
captured_atString(64)NOT NULL所属上报批次的生成时刻
updated_atDateTime(tz)server_default now,带 onupdate更新时刻

node_prefix_flap_paths

一条 (节点, 前缀) 的根因定位路径证据,随上报按节点整表替换。只对上榜抖动前缀存(agent 侧已过滤,量小;每前缀 ≤12 条 distinct AS_PATH,整批 ≤500 条)。控制面根因定位时跨节点读出、按 AS_PATH 求并,喂给 services/flap_localize.localize

类型约束 / 默认说明
node_idString(64)PK(复合),FK→nodes.node_id(CASCADE)所属节点
prefixString(64)PK(复合)前缀
pathsJSONNOT NULL,默认 [][{"as_path": [int…], "churn": float, "present": bool}]——外部 AS_PATH 证据(已剥本网前缀)
changesFloatNOT NULL,默认 0.0前缀级衰减计数:总变化事件(源头信号的分母)
withdrawnFloatNOT NULL,默认 0.0彻底消失次数(分子)
captured_atString(64)NOT NULL上报批次生成时刻
updated_atDateTime(tz)server_default now,带 onupdate更新时刻

迁移至 KV 的代价(留档)

会话级热态迁入 KV 时,存量分数未做搬迁,从 0 重新累积。实测影响可忽略:切换时 120 个会话的残余分数最高仅 0.11(榜单阈值 0.05,告警阈值 3.0),且当时无任何 session 类告警处于开启状态,半衰期 30 分钟意味着这些分数在一小时内本也会衰减殆尽。

该结果与前缀级迁移时出现「跳过搬迁导致 34 条假消退」的差异,根源在于读侧合并两路:切换过程中评估器始终能读到数据。后续同类迁移应将这条合并逻辑视为必备前提,而非可选优化。


  1. 指数衰减打分:每次事件为该主体加 1 分,分数随时间按半衰期连续衰减。相比固定窗口计数,它兼具记忆性(短时间内多次事件可累加)与自然消退(停止抖动后分数自行降至阈值以下),且只需一个可调参数。 ↩︎