Skip to content

观测与数据源

两个粒度各有一条独立的观测链路,都不触碰收敛路径


会话级:纯控制面从 runtime snapshot 推导

零 agent 变更。 数据源是 agent 本就在上报的 runtime snapshot 里的 bgp_protocols(BIRD show protocols 的结果,约每 5 min 一份)。

每份快照入库时,FlapStore.record_snapshotapp/services/flaps.py)把每条会话的 statesince 和上一份观测(存在 bgp_flap_state,每 (节点, BIRD 协议名) 一行)逐一比对。入库钩子在 app/api/v1/agent_http.py 的快照接收里。

观测保真的两道闸

  • 只有 bgp_observation == OBSERVED 的快照参与比对。 UNAVAILABLE(采集失败)与 NOT_OBSERVED(无观察器)整份跳过——采集失败绝不能被当成「所有会话都消失了」。
  • 乱序或重复的快照整份跳过captured_at 不晚于该节点现存基准的最新时刻)。表级操作——「已删除」判定、首见建行——不能被迟到的重放快照驱动:否则它没见过的新会话会被连同累积分数一起误删(主危害是分数清零导致漏检),已删的行会被过期观测复活。

OBSERVED 的快照被当作权威:比对基准里有、但这份快照里没有的会话,视为已从配置删除,bgp_flap_state 行同步删除(转移历史仍保留)。


前缀级:节点侧 flapfeed 旁路采集

前缀级抖动的分辨率必须做在节点上、贴着 BIRD 实时看每一条 UPDATE。整条旁路管线独立于 reconcile:

BIRD(全表全路径,add paths tx,import none)
  │  passive eBGP,假 AS 65001

flapfeed_bgp.FlapfeedSession(agent 内线程,主动拨入 router-netns 的 underlay IPv6 :179,
  │                           AddPath receive;UPDATE 用 exabgp 库解码)
  │  解出的 NLRI 与 AS_PATH 进程内直喂计分器

flapfeed_scoring.PrefixFlapScorer(纯计分,带锁,agent 上报循环直接读快照)
  │  POST 控制面

控制面 PrefixFlapStore(热态整份 SET 进 Redis blob;SQL 只落 meta 与速率存档)

代码:节点侧 agent/flapfeed.py(管线监管)、agent/flapfeed_bgp.py(进程内 BGP 采集会话)、agent/flapfeed_scoring.py(计分器);控制面侧 app/services/prefix_flaps.pyapp/db/models/prefix_flap.py

各段的关键约束

BIRD 侧是一条 passive eBGP 协议(假 AS 65001,对端为宿主 underlay IPv6 网关),add paths tx; import none; export all——把全表全部路径喂出来,import none 保证对 BIRD 自己的选路零影响。

写法是往 rendered bird 目录丢一个 local_flapfeed.conf,借 bird.conf 末尾的 include "/etc/bird/local*.conf" 钩子加载,不改任何现有渲染文件。

必须是 eBGP:iBGP 不会把 iBGP 学来的路由反射出去,那样看不到全表。

file plan 的 prune 对 bird/local*.conf 有豁免(prune_exempt_globs)。这些文件与渲染集共目录但归旁路进程自行读写;没有这条豁免,local_flapfeed.conf 每轮 reconcile 都会被当孤儿删掉,喂送会话反复断建。

采集会话是 agent 进程内的一个线程。它主动连 router-netns 容器的 underlay IPv6 :179(宿主到 netns 的 v4 :179 被挡,v6 OPEN 可通),源地址绑 underlay 网关——BIRD 侧的 neighbor 只认它。

会话层自写的只有 TCP 分帧与 OPEN / KEEPALIVE 定时(约 200 行);能力协商与 UPDATE 解码全部调 exabgp 的报文解析层Capabilities().new / Negotiated / Update.unpack_message)。exabgp 是 agent wheel 的钉版依赖(纯 Python 零依赖,随 manifest 离线分发),不需要宿主独立 venv。

优雅降级:exabgp 库不可导入、或 underlay 没启 IPv6——任一前置条件不满足,FlapfeedManager 整体旁路,并声明式清掉 BIRD 侧的 flapfeed 协议,保持「未启用」的干净状态。

会话重建必须清空路径表

flapfeed 会话一旦离开 established,_paths.clear() 立即执行。

原因:AddPath 的 path_id会话作用域的,会话重建后全表重灌时 path-id 可能整体洗牌。若残留旧映射,重灌会被逐条误判成「路径实变」,把本网全部内部前缀瞬间刷上抖动榜。

清空后,重建的全表重灌按首灌处理(不计分),而真实抖动的分数仍在 _scores 里按半衰期正常消退,不受影响。


控制面收报

PrefixFlapStore.record_report 把打分条目与路径证据当作当下热态整份写入 Redis 单键 blob pflap:<node>(每报一次 SET,TTL 7 天);SQL 只 upsert node_prefix_flap_meta(喂送会话状态、累计变化数、生成时刻——用来区分「没有抖动」和「喂送断了看不见」)并落 60 s 速率存档。

迟到或重复的报告整份跳过captured_at 不晚于基准——热路径看 blob,回退路径看 meta)。整份替换语义下,乱序回退会复活旧打分、回拨新鲜度。

会话状态 feed_state 由会话线程实时翻转,established / down 直接透出。


存储分层

前缀级检测产物按变化节奏分两层存储:

内容介质节奏
热态打分条目 + 路径证据(读时续衰的当下视图)Redis 单键 blob pflap:<node>,TTL 7 天每报一次整份 SET(约 60 s)
持久node_prefix_flap_meta(喂送健康度)与 prefix_flap_rate_rollup(60 s 速率存档)SQL写率低,需持久时序

拆层依据:打分条目每约 60 s 整份替换,落 SQL 每月产生上亿行写——对 PostgreSQL 是无谓 churn;而热态本质是「丢了等下一份报告即重建」的当下快照,天然适合缓存介质。

回退语义:Redis 未配置或热路径出错时,写侧落回旧 SQL 整表替换路径(node_prefix_flaps,delete + bulk insert),报告不丢;读侧逐节点装载 blob,缺失的节点回退该节点的旧 SQL 行。测试与降级共用旧路径,行为不变。

表结构见 Flap 检测数据层