Skip to content

BGP 抖动检测接口

控制台「抖动」板块与门户 /network/flaps 消费的端点与字段口径。示例取自生产实抓。

鉴权与错误语义同其余 /ui 端点,见 control.md

1. 两层检测,只告警不抑制

粒度数据源节奏
会话级(节点, bird 协议)控制面从节点快照推导「哪条 BGP 会话在反复断建」随快照,约 5 分钟
前缀级(前缀, 归因对端 AS)节点旁路 ExaBGP 全表喂送约 60 秒

fleet 端点已按前缀跨节点归组,因此前缀层天然是关联视图:一条前缀被几个节点同时 看见,本身就是最有信息量的一列。

检测层绝不自动抑制。处置(拒收前缀 / 断开对端)是操作员显式触发的人工动作,见第 7 节。

2. 打分模型

分数是指数衰减计数:每次 flap 事件 +1,随时间连续衰减(会话级半衰期 30 分钟, 前缀级 10 分钟)。

读取时服务端已续衰到当前时刻——前端拿到的 score 就是「此刻」的值,轮询下自然 消退,无需任何客户端计算。分数无量纲,直觉口径 ≈「最近一两个半衰期内的抖动次数」。

flapping 是服务端按榜单线的判定。前端直接用它亮红,不要自设阈值;分数本身 适合做徽章内的灰字细节或排序键。

榜单线与告警线分层

榜单线(flapping告警线(开单)消警线
前缀级2010050
会话级331.5

DN42 的低烈度背景抖动是常态:榜单标黄是可观测信息,告警才是需要人看的事件。 因此会出现「榜上 flapping=true 但无对应在开告警」的中间带行——这是设计,不是缺数。

3. 会话行的 flap 字段

以下端点的每条 BgpSessionStatus 行都带两个字段:

  • GET /ui/nodes/{id}/bgp-sessions/statussessions[]
  • GET /ui/nodes/{id}/overviewbgp_sessions[]
  • GET /ui/fleet/peering-issuesGET /ui/dashboardpeering_issues[]
字段类型口径
flappingbool衰减分 ≥3。会话此刻可能是 Established——含义是「近期反复断建」,比持续 down 更值得关注
flap_scorefloat | null当前衰减分;从无 flap 记录 → null(区别于 0.0 = 有记录但已衰完)

peering-issues 的收录规则

flapping=true此刻 Establishedhealth="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_idtransitions_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 / sessionbird 协议名 / schema 会话名
kindstate = 状态实变;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_nameregistry 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。

因此契约里有四条同源硬要求

  1. 成员一致localization != null 的前缀必是本次返回的行。前端可直接按行计算 区分布,tile 数与筛选结果天生相等。
  2. 同快照localization 与该行的 max_score / origins / nodes 出自同一次评估。
  3. 口径自洽zone === "moas" 当且仅当moas === true,且 signals.origins 等于行的 origins;单源前缀不得判 zone="moas"culprit_asnrepresentative_path 的端点必须落在该行的 origin / peers 语义内。
  4. 不给半成品:无法定位就整个 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 秒。

参数:active1 只看在开 / 0 只看已消警 / 缺省全部)、limit(1..200 默认 50)、 before_id(游标,新 → 旧)。

字段口径
kindsession(定位 node_id + name = 会话名)/ prefix(定位 name = 前缀 + peer_asn
nodes[]波及节点(prefix 告警持续期间取并集)
peak_score / last_score在开期间峰值 / 最近一轮评估值
started_at / resolved_atresolved_at=null 即在开
active服务端算好的在开判定

已消警历史服务端保留最近 500 条(时间无界,按量截断)。

9. 活动时序与速率仪表

GET /ui/fleet/flap-stats

参数 range1h(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_schanges / bucket_s
tracked_prefixes / flapping_prefixes在跟踪(有存活分数)/ 过阈的前缀数;粗桶取桶内峰值
flapping_sessions / open_alerts过阈会话数 / 在开告警数
gauge.rate_per_s90 分位截尾均值:900s 窗口内每个 60s 桶求和(缺桶按 0),升序去掉最高 10% 后取平均、折算次/秒
gauge.current_rate_per_s最近一个完整 60s 桶的瞬时速率

四条展示口径

  • null0null 是「无上报」(喂送断 / 控制面重启),画断线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>
409nodes 里指定了已退役节点node <id> is decommissioned

交互要求(处置是危险动作):

  • 二次确认必须写清影响面:拒收 =「将在 N 个节点拒收 <prefix>(精确匹配)」; 断开 =「将 disable AS-xxx 的 M 条会话,WG 隧道保留」。
  • 「处置中」清单必须常驻GET 汇总,60s 轮询),每项带一键撤销与生效范围 ——拒收被遗忘比抖动本身更伤
  • 提交后用响应的 changed 即时更新本地状态,不必等轮询;changed 为空提示 「已是目标状态」而非报错。

11. 轮询建议

端点周期说明
fleet/peering-issues(或随 dashboard)35s与概览轮询合并
fleet/bgp-flapsfleet/prefix-flaps60s数据源节奏即 ~60s(前缀)/ ~5min(会话),更快无增益
fleet/flap-peersfleet/flap-alerts60s评估循环节奏即 60s
fleet/flap-stats60s存档桶宽即 60s;range 切换时即时重拉
nodes/{id}/bgp-flapsflap-peers/{asn}打开时拉取 + 60s下钻页
admin/fleet/mitigations60s常驻清单

分数由服务端续衰,轮询间隔不影响正确性,只影响刷新手感。