外观
BGP 抖动检测接口
控制台「抖动」板块与门户 /network/flaps 消费的端点与字段口径。示例取自生产实抓。
鉴权与错误语义同其余 /ui 端点,见 control.md。
1. 两层检测,只告警不抑制
| 层 | 粒度 | 数据源 | 节奏 |
|---|---|---|---|
| 会话级 | (节点, bird 协议) | 控制面从节点快照推导「哪条 BGP 会话在反复断建」 | 随快照,约 5 分钟 |
| 前缀级 | (前缀, 归因对端 AS) | 节点旁路 ExaBGP 全表喂送 | 约 60 秒 |
fleet 端点已按前缀跨节点归组,因此前缀层天然是关联视图:一条前缀被几个节点同时 看见,本身就是最有信息量的一列。
检测层绝不自动抑制。处置(拒收前缀 / 断开对端)是操作员显式触发的人工动作,见第 7 节。
2. 打分模型
分数是指数衰减计数:每次 flap 事件 +1,随时间连续衰减(会话级半衰期 30 分钟, 前缀级 10 分钟)。
读取时服务端已续衰到当前时刻——前端拿到的 score 就是「此刻」的值,轮询下自然 消退,无需任何客户端计算。分数无量纲,直觉口径 ≈「最近一两个半衰期内的抖动次数」。
flapping 是服务端按榜单线的判定。前端直接用它亮红,不要自设阈值;分数本身 适合做徽章内的灰字细节或排序键。
榜单线与告警线分层:
| 层 | 榜单线(flapping) | 告警线(开单) | 消警线 |
|---|---|---|---|
| 前缀级 | 20 | 100 | 50 |
| 会话级 | 3 | 3 | 1.5 |
DN42 的低烈度背景抖动是常态:榜单标黄是可观测信息,告警才是需要人看的事件。 因此会出现「榜上 flapping=true 但无对应在开告警」的中间带行——这是设计,不是缺数。
3. 会话行的 flap 字段
以下端点的每条 BgpSessionStatus 行都带两个字段:
GET /ui/nodes/{id}/bgp-sessions/status的sessions[]GET /ui/nodes/{id}/overview的bgp_sessions[]GET /ui/fleet/peering-issues与GET /ui/dashboard的peering_issues[]
| 字段 | 类型 | 口径 |
|---|---|---|
flapping | bool | 衰减分 ≥3。会话此刻可能是 Established——含义是「近期反复断建」,比持续 down 更值得关注 |
flap_score | float | null | 当前衰减分;从无 flap 记录 → null(区别于 0.0 = 有记录但已衰完) |
peering-issues 的收录规则
flapping=true 但此刻 Established(health="up")的会话也会出现在 issues 里。 渲染必须按 flapping 分支:
health="up" && flapping → 「抖动中」
其余 → 「中断」把 issues 列表整体当成故障列表会把抖动中的会话渲染成中断。
排序:抖动中最前(按 flap_score 降序)→ down → connecting;同级外部会话优先。 前端按响应序渲染即可。
4. 会话层榜单
GET /ui/fleet/bgp-flaps
全机群会话 flap 榜,分数降序。参数 min_score(默认 0.05,含未过阈的「冷却中」 会话)、limit(1..500,默认 100)。
行 = BgpSessionStatus 全部字段 + node_id、transitions_total(累计状态转移数, 不衰减)、flaps_total(其中计分的事件数)、last_transition_at。
空列表 = 机群健康,是常态,前端给正向空状态(「近期无会话抖动」)而不是空表格。
GET /ui/nodes/{id}/bgp-flaps
单节点下钻:sessions[](同上)+ transitions[](最近状态转移,新 → 旧; transitions_limit 1..200 默认 50)。节点无上报 → 404 (detail no runtime status reported for node {id})。
transitions[] 字段 | 口径 |
|---|---|
name / session | bird 协议名 / schema 会话名 |
kind | state = 状态实变;restart = 隐性重连 |
from_state / to_state | 转移前后状态(restart 时两者相同) |
captured_at | 观测到转移的快照时刻(非发生时刻,分辨率约 5 分钟) |
restart 值得单独一句文案(如「重连(快照间隔内)」):两份快照都是 Established 但 since 变了,说明间隔内断建过——状态灯全程绿、实际抖了。这是 5 分钟快照节奏下 最常见也最容易被忽视的形态。
5. 前缀层榜单
GET /ui/fleet/prefix-flaps
参数:min_score(默认 1.0)、limit(1..500,默认 100)、group。
group=prefix 返回前缀优先的层级形态,控制台与门户的板块都消费这一种; 不带 group 是扁平的 (prefix, peer_asn) 行。
jsonc
{
"prefixes": [ {
"prefix": "fd41:2631:6416::/48",
"origin_asn": 4242423182, "origin_name": "CRXN-GATEWAY",
"moas": false, "origins": [ { "asn": 4242423182, "name": "CRXN-GATEWAY" } ],
"peer_count": 11,
"max_score": 6821.079, "total_changes": 22656, "flapping": true,
"rate_per_s": 9.75, "flapping_since": "…", "flap_duration_s": 61189,
"nodes": [ { "node_id": "sha2", "score": 6821.079, "changes": 13795,
"last_change_at": "…", "rate_per_s": 4.1 } ],
"peers": [ { "peer_asn": 4242422466, "peer_name": "AS-SESS-DN42", … } ],
"localization": { … } // 见第 6 节;未能定位为 null
} ],
"feeds": [ { "node_id": "can2", "feed_state": "established",
"captured_at": "…", "total_changes": 1300057 } ],
"localization_summary": { "near": 84, "origin": 216, "path": 74, "moas": 3 }
}| 字段 | 口径 |
|---|---|
peer_asn | 归因对端 AS = AS_PATH 剥本 AS 后的第一跳,即抖动进入 fleet 的门;null = 本网自起源 / 无从归因 |
origin_asn | 起源 AS = 同一条 AS_PATH(归一后)的最后一跳;null 口径同上 |
origin_name / peer_name | registry as-name,未收录 null |
moas | 同一前缀在不同对端方向解析出不同 origin。MOAS 本身即异常信号,前端标红 |
max_score | 各节点分数的最大值(排序键) |
total_changes | 各节点累计变化数之和(不衰减) |
rate_per_s | 当前变化速率(次/秒,上一报告间隔差分)。行级 = 跨节点/跨方向求和;nodes[] 展开项 = 单节点视角。null = 无差分基准(新上榜 / agent 首报),不是 0,显示 — |
flapping_since | 抖动起点(同键在开告警的上穿时刻) |
flap_duration_s | 持续秒数。只对告警级强度(≥100)有值——中间带行、或刚过告警线开单前(≤60s)均为 null |
nodes[] | 哪些节点看到、各自分数(降序)。len>1 = 全网性抖动;=1 = 仅接入节点可见的本地对端抖动 |
MOAS 的行为约定:若同一前缀在不同对端方向解析出不同 origin,各行按行给真实 origin,不强行统一。前端据此:各行 origin 一致 → 显示该 origin;不一致 → 显示 「多源(MOAS)」并标红。
展示口径:分数可以很大(生产见过 6800+,等效「每秒 8 次变化并持续」的极端抖源), 用 flapping 徽章 + 分数缩写(6.8k),不要按分数线性画条。
榜单 limit 与单节点条目上限都是 500,无分页——超限截断是设计内的,榜单只关心头部。 localization_summary 给的是全量计数(含被截断的部分),前端把它做成只读的 「fleet 根因构成」数字条,与行内那排可点筛选的 tile 分开——既给全局真相,又不让筛选 数字对不上。
feeds[]:喂送管线健康度
渲染在榜单旁,它区分的是「没有抖动」和「喂送断了看不见」:
feed_state | 含义 | 建议展示 |
|---|---|---|
established | 喂送正常 | 绿点 / 不显示 |
down | 节点侧喂送 BGP 会话断开 | 黄点 + 「喂送中断」 |
stale | 该节点超 5 分钟没上报(agent 离线 / 管线停摆) | 灰点 + 「数据陈旧」+ captured_at |
unknown | 过渡态(刚启动) | 同 stale 弱化处理 |
空榜 + feeds 全 established = 「全网路由平稳」正向空状态。
6. 根因定位(localization)
内嵌在 group=prefix 的每一行,与该行同一次评估、同一快照算出。
jsonc
"localization": {
"zone": "near", // near | origin | path | moas
"confidence": 0.94, // 0..1
"culprit_asn": 4242423999, // near/origin 填;path/moas 为 null
"culprit_name": "COWGL-AS",
"culprit_edge": null, // path 填 [A,B] 邻接;其余 null
"representative_path": [4242423999, 4242423182], // near→origin,已折叠 prepend
"signals": {
"separation": 0.9, "withdrawal_ratio": 0.7,
"flap_nodes": ["tpe1", "hkg1"],
"origins": null, // moas 时列多源;其余 null
"session_flapping": true, // near 的门控;其余 null
"session_score": 42.3
}
}signals.session_flapping 是 near 区的门控:本方与该 peer 的会话是否在抖 (true = 真近端、可断;false = 中转,别断)。
内嵌而非独立端点,是为了消除一整类 bug。 榜与定位曾是两个独立查询,前端按 prefix join,产生三重不一致:成员集不同(tile 显示 3 个多源、表里只有 1 行)、 快照时序错开(渲染出半更新的错位态)、口径打架(榜说单源、定位说多源)。这些是 join 架构的必然,不是渲染 bug。
因此契约里有四条同源硬要求:
- 成员一致:
localization != null的前缀必是本次返回的行。前端可直接按行计算 区分布,tile 数与筛选结果天生相等。 - 同快照:
localization与该行的max_score/origins/nodes出自同一次评估。 - 口径自洽:
zone === "moas"当且仅当行moas === true,且signals.origins等于行的origins;单源前缀不得判zone="moas";culprit_asn与representative_path的端点必须落在该行的 origin / peers 语义内。 - 不给半成品:无法定位就整个
null,不给缺字段的残缺对象。
独立端点 GET /ui/fleet/prefix-flaps/localize 仍保留他用,但板块不依赖前端合成。
7. 对端视角
GET /ui/fleet/flap-peers
「哪家对端最抖」:会话级 + 前缀级按对端 AS 收拢成一行,severity 降序。 参数 limit(1..200,默认 50)。
| 字段 | 口径 |
|---|---|
asn / name | 对端 AS / registry as-name |
severity | 阈值归一的综合烈度:两层各自「分数/阈值」取大者;≥1 = 至少一层过阈 |
prefix_count / prefixes_flapping | 该方向仍有分数的前缀数 / 其中过阈的 |
prefix_score_max / session_score_max | 两层各自最高分 |
nodes[] / last_change_at | 前缀层波及节点 / 最近变化时刻 |
sessions[] | 该对端在各节点有 flap 分数的 eBGP 会话:{node_id, session, state, flap_score, flapping} |
两层分数量纲不同,排序与画条一律用 severity,别用原始分(在 1.0 处画阈值线)。
两种形态该用不同文案:sessions 非空且 flapping = 会话本身在断建(本方与它的链路 问题);sessions 空但前缀层高分 = 它方向背后的路径在抖(transit 质量问题)。
GET /ui/fleet/flap-peers/{asn}
单对端下钻。参数 prefixes_limit(1..200 默认 50)、alerts_limit(1..100 默认 20)。
响应 = flap-peers 行全部字段 +
prefixes[]—— 与扁平行同形状,分数降序;min_score=0.5比榜单默认更宽,正在消退 的也可见;alerts[]—— 该对端的前缀级告警,新 → 旧,含已消警历史(回答「这家昨晚抖过吗」)。
该 AS 完全无 flap 痕迹 → 404(detail no flap data for AS{asn})。查询框应对 404 显示「该对端近期无抖动记录」的正向文案,不当错误处理。
8. 告警事件流
GET /ui/fleet/flap-alerts
flap 的历史层:衰减分消退后榜单就看不见了,这里回答「昨晚谁抖过、峰值多少、持续多久」。
边沿触发:分数上穿告警线开一条,下穿消警线(告警线的一半,迟滞)关该条。 控制面背景循环 60 秒评估一轮,事件粒度即约 60 秒。
参数:active(1 只看在开 / 0 只看已消警 / 缺省全部)、limit(1..200 默认 50)、 before_id(游标,新 → 旧)。
| 字段 | 口径 |
|---|---|
kind | session(定位 node_id + name = 会话名)/ prefix(定位 name = 前缀 + peer_asn) |
nodes[] | 波及节点(prefix 告警持续期间取并集) |
peak_score / last_score | 在开期间峰值 / 最近一轮评估值 |
started_at / resolved_at | resolved_at=null 即在开 |
active | 服务端算好的在开判定 |
已消警历史服务端保留最近 500 条(时间无界,按量截断)。
9. 活动时序与速率仪表
GET /ui/fleet/flap-stats
参数 range ∈ 1h(60s 桶,60 点)/ 6h(5min,72)/ 24h(10min,144)/ 7d(1h,168)/ 30d(4h,180),默认 1h。非法值 422。
jsonc
{
"range": "1h", "window_s": 3600, "bucket_s": 60, "generated_at": "…",
"series": [
{ "t": 1783137600, "changes": 62, "listed_changes": 12, "rate_per_s": 1.0333,
"tracked_prefixes": 214, "flapping_prefixes": 9,
"flapping_sessions": 0, "open_alerts": 11 }
],
"gauge": { "rate_per_s": 0.92, "current_rate_per_s": 1.03,
"percentile": 90, "window_s": 900 }
}| 字段 | 口径 |
|---|---|
series[].t | 桶起点 epoch 秒(不是 ISO 串)。只出完整桶,不含正在累积的当前桶 |
changes | 桶内全节点路径变化数之和(喂送级全量,含未上榜前缀) |
listed_changes | 其中榜内条目贡献的部分,恒 ≤ changes |
rate_per_s | changes / bucket_s |
tracked_prefixes / flapping_prefixes | 在跟踪(有存活分数)/ 过阈的前缀数;粗桶取桶内峰值 |
flapping_sessions / open_alerts | 过阈会话数 / 在开告警数 |
gauge.rate_per_s | 90 分位截尾均值:900s 窗口内每个 60s 桶求和(缺桶按 0),升序去掉最高 10% 后取平均、折算次/秒 |
gauge.current_rate_per_s | 最近一个完整 60s 桶的瞬时速率 |
四条展示口径:
null≠0:null是「无上报」(喂送断 / 控制面重启),画断线;0是「确实 没变化」。gauge不随range变(固定 900s 窗口),仪表和时序图共用同一个响应。- 跨节点求和口径:同一条抖动被 N 个节点看见就计 N 次(与
total_changes一致) ——它衡量的是「fleet 观测面上的扰动量」,不是去重后的事件数。 - 仪表刻度用对数或分段,不要线性满量程:平稳期个位数/秒,事件期可到百级。
数据分辨率 60 秒(agent 上报节奏),存档保留 60 天。
10. 处置动作
从抖动事件一键发起的人工处置。声明式(经 desired-state 下发,agent 秒级应用)、 幂等(重复提交空转)、可撤销,POST 全部自动进审计日志。
挂 /control/v1/admin 前缀,同一把 admin Bearer。
| 方法 / 路径 | 用途 | 请求体 |
|---|---|---|
GET /admin/fleet/mitigations | 当前生效处置汇总 | — |
POST /admin/fleet/mitigations/prefix-block | 拒收前缀:目标节点 eBGP 导入侧精确匹配 reject | {prefix, nodes?} |
POST /admin/fleet/mitigations/prefix-unblock | 撤销前缀拒收 | {prefix, nodes?} |
POST /admin/fleet/mitigations/peer-disable | 断开对端:该 AS 的全部 eBGP 会话置 disabled | {asn} |
POST /admin/fleet/mitigations/peer-enable | 恢复对端全部会话 | {asn} |
jsonc
// POST prefix-block {"prefix":"fd00:dead:beef::/48","nodes":["tpe1"]}
{ "prefix": "fd00:dead:beef::/48", "nodes": ["tpe1"], "changed": { "tpe1": 39 } }
// POST peer-disable {"asn": 4242423999}
{ "asn": 4242423999,
"sessions": [ { "id": 56, "node_id": "hkg1", "name": "cow",
"remote_asn": 4242423999, "enabled": false } ],
"changed": { "hkg1": 41 } }| 响应字段 | 口径 |
|---|---|
prefix | 服务端规范化后的前缀(大写 / 未压缩形式会被折叠成 canonical)。以响应为准更新本地状态 |
nodes[] | 操作后仍带该拒收项的节点(block 后 = 生效范围;unblock 后应为空) |
sessions[] | 该 AS 全部会话及操作后的 enabled 状态 |
changed{} | 本次实际变更并重物化的节点 → 新世代号。空对象 = 幂等空转(已是目标状态),不是错误 |
nodes 缺省 = fleet 级(全部未退役节点);传节点列表可做单节点金丝雀。
语义要点:
- 精确匹配:只拒收与
prefix完全相同的路由,不含 more-specific 子网——拒收的是 榜单上那条在抖的前缀本身。 - 导入侧:agent 收到新世代即重渲染过滤器并 reconfigure BIRD,已在 RIB 的该前缀 路由同时被重新评估并撤出,生效是秒级的。
- 生效后该前缀的分数会停止增长并按半衰期消退,不会立即从榜上消失。这是预期。
peer-disable只摘 BIRD 协议,WireGuard 隧道保留,peer-enable秒级重建会话, 无需重新握手换钥。
错误语义:
| 状态码 | 场景 | detail |
|---|---|---|
422 | 非法前缀 | {"message": "invalid prefix: <输入>"} |
404 | 未知节点 / 该 asn 无任何会话 | unknown node <id> / no bgp sessions with remote_asn <asn> |
409 | nodes 里指定了已退役节点 | node <id> is decommissioned |
交互要求(处置是危险动作):
- 二次确认必须写清影响面:拒收 =「将在 N 个节点拒收
<prefix>(精确匹配)」; 断开 =「将 disable AS-xxx 的 M 条会话,WG 隧道保留」。 - 「处置中」清单必须常驻(
GET汇总,60s 轮询),每项带一键撤销与生效范围 ——拒收被遗忘比抖动本身更伤。 - 提交后用响应的
changed即时更新本地状态,不必等轮询;changed为空提示 「已是目标状态」而非报错。
11. 轮询建议
| 端点 | 周期 | 说明 |
|---|---|---|
fleet/peering-issues(或随 dashboard) | 35s | 与概览轮询合并 |
fleet/bgp-flaps、fleet/prefix-flaps | 60s | 数据源节奏即 ~60s(前缀)/ ~5min(会话),更快无增益 |
fleet/flap-peers、fleet/flap-alerts | 60s | 评估循环节奏即 60s |
fleet/flap-stats | 60s | 存档桶宽即 60s;range 切换时即时重拉 |
nodes/{id}/bgp-flaps、flap-peers/{asn} | 打开时拉取 + 60s | 下钻页 |
admin/fleet/mitigations | 60s | 常驻清单 |
分数由服务端续衰,轮询间隔不影响正确性,只影响刷新手感。