Skip to content

复合块:看板、抖动可视化、机群地图

ui/boards/ui/flaps/ui/map/ 三层。共同点:整块区域、由 data prop 驱动、 自己不取数、不做轮询——所以同一个块既能挂在控制台,也能挂在只读的门户上。

字段口径以 接口契约 为准;本文只讲组件侧的 API 与渲染约定。


boards —— 仪表盘区块

FleetTraffic

流量看板:一个 range 统辖一组互补的卡片——hero(相对变化 + 构成环 + 双 Top-5)、 唯一的绝对值趋势卡、以及按对端 AS 的模块。每个事实只陈述一次。

prop类型说明
points?TrafficPoint[]traffic.points,定长桶网格,空桶为 null(缺报,不是 0)
pointsPrevious?TrafficPoint[]traffic.points_previous,紧邻上一窗口,同网格按下标对齐
breakdown?FleetTrafficBreakdown | null当前每节点 / 每对端速率榜;这里只用 nodes 的 Top-5
asTraffic?TrafficByAs | null透传给内嵌的 AsTraffic
trafficMix?TrafficMix | null内部/外部 · 收/发 双半环
names?Map<number, string>ASN → registry as-name 的补充映射;命中即用,否则回落后端已解析的名字
loaded?boolean首次取数是否已返回——只在此之前画骨架,避免闪占位内容
asof?string
rangesreadonly string[]必填,由宿主给:管理面 24h/7d/30d,门户只有 6h/24h
rangestring当前档(读)
onRangeChange?(v) => void当前档(写)。读写分离,因为控制台把它绑在 URL 上
nodeHref?(nodeId) => string | undefined节点详情链接。不传则 Top-5 里的节点渲染成纯文本——门户没有节点详情页,渲染成死链比不渲染更糟

ranges 必须由宿主给:后端不把长时段下放到全网口径,写死在组件里必有一边是错的。

「流量相对变化」是前端算的(从 pointsPrevious 逐桶求百分比),没有对应的后端字段。

AsTraffic

Radar 的「按自治系统划分的网络流量」模块:左侧 Top-5 对端 AS 的流量趋势多线图 (图例带各自份额),右侧 Top-N 份额表 + 分页。

prop类型说明
data?TrafficByAs | nulldashboard.traffic_by_as
loaded?boolean门控首屏骨架
names?Map<number, string>ASN → registry as-name(权威,优先于载荷里的 name,后者可能是 peering 的本地叫法)
asof?string

loadeddata 分开的理由:在首次取数返回之前渲染的是镜像真实两栏版式的骨架, 而不是任何占位内容;返回之后若载荷缺失或为空,才给正向空状态。两种「没有」要区分开。

FleetRouting

全机群路由概览。版式的选择本身就是一个判断:

iBGP 让每个节点收敛到几乎相同的 RIB,所以画每节点路由数是冗余的(线全叠在一起)。 因此左侧画机群整体的路由表规模趋势(一条线,节点已收敛,中位数即代表机群)与 路由变化量;右侧用节点列表呈现真正逐节点不同的东西:收敛状态(数量对不上 = iBGP / 收敛问题)与 RPKI-invalid 计数(随各节点的外部对端而不同)。

ts
data?: FleetRoutingOverview | null;   // 随宿主的 /ui/dashboard 一起取,null = 尚未加载
asof?: string;

FlapStatsPanel

抖动活动 hero:900s 截尾均值速率表盘 + 路由变化量图 + 活跃前缀数图,共用固定桶网格。

prop类型说明
data?FleetFlapStats | null/fleet/flap-stats 响应
error?string取数失败的文案;空即无错
obsNodes?number | null观测节点数(feed_state='established' 的条数)。null → 隐藏换算开关,表盘显示原始和
range?string时间窗。类型是 string 而非联合类型,因为控制台直接 bind 到 URL 的 ?range=;组件内按白名单校验,非法值回落 '1h'
scale?string表盘口径:'fleet' 原始和 / 'node' 除以观测节点数

obsNodes 为什么需要:速率是跨节点求和,一次 churn 在每个看到它的节点各计一次, 直接读会比 FlapAlerted 这类单视角工具虚高。有这个数才能换算成每节点口径。

配色是有意的:数量序列走数据色;「已告警」是最严重的一层,占用状态红;「抖动中」 降级为中性数据色——状态色是稀缺资源,同一张图里两条线都用红会让人分不清哪条才是要处理的。

gauge 不随 range 变(固定 900s 窗口),所以表盘与时序图共用同一个响应。


flaps —— 抖动可视化

这一组的核心是把「该怪谁」画出来。四个根因区(FLAP_ZONES)按多大程度上该由自己 处理排序,这个顺序在分布磁贴、筛选 chips 和徽章之间共享:

含义可采取的动作
near近端对端在抖可以断开该对端
origin起源 AS 在抖通知对方 / 拒收前缀
path路径中某条邻接断裂观察,不宜处置
moas多源(疑似劫持)绝不动手,先核实

区的颜色是各组件里的 .z-{zone} 类;文案在 i18n 的 pflap.zone.* / pflap.rc.act.*

PrefixFlapBoard

前缀级抖动看板——完整问题清单,不是 Top-N 榜。以前缀为主体聚合,每条一行, 展开是它的 per-peer 抽屉。

prop类型说明
data?FleetPrefixFlaps | FleetPrefixFlapsGrouped | null?group=prefix 响应;分组形态自带内嵌的根因定位
error?string取数失败文案
rowCap?number取数时用的行数上限,取数方传了什么就填什么——用来判断结果是否被截断并给提示
pageSize?number默认 50:一屏滚一下能看完,rowCap 500 时正好 10 页
prefixActions?Snippet<[GroupedPrefixRow, PrefixFlapLocalizationCore | null | undefined]>前缀行的行内动作
peerActions?Snippet<[GroupedPrefixPeer]>抽屉内 per-peer 行的行内动作

版式为什么这样:同一条前缀若经很多个对端抖进来,说明 churn 起于源头 AS 再沿 网状扩散,该看的是源头而不是其中任何一个对端——「对端数」与「源 AS」两列因此并排放。 MOAS 单独标红,它是另一类异常。

取数方注意:保留服务端默认的 min_score 下限,不要传 0。那会把每条已平息的 前缀连同它的全部 peers × nodes 一起拉回来,payload 大到能冻住标签页,而那些行本来就 没有根因可看。

分数由服务端持续衰减到当前时刻,所以平息下来的行会随轮询自己滑走,前端不需要做任何清理。

不传两个 action snippet 即整列为空——门户是只读的,处置属于管理面的写路径。

PrefixFlapTable

扁平行表:一行一个 (前缀, 归因对端)。两个使用场景共用:PrefixFlapBoard 在拿到扁平 形态时的回退渲染,以及单对端详情页(它的 prefixes[] 正是这个形状)。

ts
rows: PrefixFlapRow[];
showPeer?: boolean;   // 每行同属一个对端时隐藏「归因对端」列

Rate / Duration 两列在没有任何一行带这些字段时自动隐藏

RootCauseBadge

根因区徽章——「根因」列的头条。

ts
zone: FlapZone;
confidence: number;   // 0..1

颜色编码区,但区名永远写在旁边(颜色不单独承载信息);一条细计量条承载置信度。 低置信度的定位会把整个徽章淡化,这样一个动摇的猜测不会盖过一个扎实的

置信度刻意不做成醒目的大数字——它是修饰,不是结论。

RootCauseSummary

只读的 fleet 根因构成条,数据来自 localization_summary

ts
counts: FlapZoneCounts;

它的数字与下方的筛选 chips 不是同一组,这是有意的localization_summary 计的是 全网每一条抖动前缀(含被分页上限截断的部分),chips 计的是当前可见行。因此它做成静态 比例条 + 图例,永不可点——给全局真相,又不让筛选数字对不上。

CulpritRef

嫌疑点的紧凑形态,落在「根因」列徽章旁,按区渲染:

渲染
near / origin单个嫌疑 AS(经 AsLink 带 registry 名)
pathchurn 断裂的那条邻接:A ✕ B
moas竞争源的 chip 列表
ts
loc: PrefixFlapLocalizationCore;
origins?: PrefixFlapOriginRef[];   // 行的权威竞争源集合

纯展示——近端的「断开」动作留在看板里,这样它保持可复用。

AsPathStrip

抽屉里的招牌视图:「原来是断在这里」。从近端到起源的横向 AS chip 条,嫌疑段用区 颜色点亮。

near:   [●peer]──…──[origin]         第一跳点亮
origin: [peer]──…──[●origin]         最后一跳点亮
path:   [peer]──[A]✕[B]──[origin]    断裂邻接点亮
moas:   [peer]──…──[●origin] + 其他源
ts
loc: PrefixFlapLocalizationCore;
origins?: PrefixFlapOriginRef[];

完整条需要后端的 representative_path;缺失时降级成只画 culprit_edge(A ✕ B)或 单个嫌疑 chip——仍然是有用的那部分。确实没什么可画时渲染为空

MOAS 画得很诚实representative_path通往某一个起源的一条(churn 最高) 路径,所以画成一条止于那个起源的线,再把其他竞争源列在旁边——不是从最后一跳 分叉(它们并不都经过那一跳,而可供分叉的拓扑本来就不在数据里)。


map —— 机群地图

FleetMap

地图 + 节点列表的组合体。宽屏下左 60% 是可平移缩放的世界地图,右 40% 是与地图联动的 紧凑节点列表。

prop类型说明
nodesFleetMapNode[]
links?FleetLink[]WG 骨干链路,画成大圆弧
fitWorld?boolean初始视图适配全世界
onRequestSnapshot?(nodeId) => void | Promise<void>传了才渲染这个按钮。报错与 toast 归调用方——库不认识 api,也没有 toast 出口
health?string健康度筛选值('' = 不筛)。控制台绑到 URL 的 ?health=;只读场景不传,组件用内部状态

投影:太平洋居中等距圆柱,W=1000 H=500 LON0=165。这三个数在三处必须一致—— 本组件、首页的内联脚本、apps/home/bake.js。不一致会在首页换层时看到明显跳变。

聚簇:标记按 DN42 region → 国家 → 城市三级聚簇,缩放跨过阈值时把一簇拆成单个节点。 簇的位置取组内节点坐标的均值,颜色取组内最差健康度。

坐标来自服务端。 site → 坐标 / 城市 / 国家 / region 的注册表在控制服务器上, 每个节点行自带解析好的 geo——加一个机房不再需要发一版前端。geo.ts 里剩下的只是 展示逻辑。

geo.ts

导出说明
REGION_NAMESDN42 origin-region community(41..57)→ 英文名。协议标准,静态
RegionCodenumber(41..57)
ResolvedGeo聚簇/标签代码消费的形状
fromNodeGeo(geo, site?)把服务端的 NodeGeo 适配成 ResolvedGeo
geoLabel(geo, site?)渲染成显示用的地点标签

国家名走 Intl.DisplayNames钉死英文——与英文的城市名、region 名保持一致, 不跟随运行时语言。单城市辖区(特别行政区等)在地图/列表树里跳过冗余的城市层级。