SAI SDK 架构逻辑全解 —— 从 orchagent 到博通芯片的完整链路
面向 SONiC + Broadcom SAI SDK 开发人员。
本文不贴代码,只讲清楚一件事:一条配置从敲下到变成芯片表项,中间经过哪些进程、哪些数据库、哪些软件层,每一层的职责边界在哪里、出问题时的表现是什么。
1. 全局视图:七层链路
┌──────────┐ ┌───────────┐ ┌─────────┐ ┌────────┐ ┌─────────┐ ┌────────┐ ┌──────┐
│ FRR │ → │ orchagent │ → │ ASIC_DB │ → │ syncd │ → │brcm SAI │ → │BCM SDK │ → │ ASIC │
│ (bgp容器) │ │ (swss容器) │ │ (1号库) │ │(独立容器)│ │ 厂商实现 │ │(sdk-src)│ │ 芯片 │
└──────────┘ └───────────┘ └─────────┘ └────────┘ └─────────┘ └────────┘ └──────┘
路由计算 业务编排 序列化 反序列化 SAI 属性 SDK 调用 硬件
SAI 对象组装 命令队列 视图比对 翻译成 bcm_l3_* 表项
VID/RID转换 厂商语义 表项操作 生效排障口诀:自顶向下逐层核对,每层都有独立观测点;任何一层断了,最终表现都是"转发不对",但修法完全不同。
2. Redis 数据库地图
SONiC 用多个 Redis 逻辑库切分职责,先建立全局印象:
┌─────────────┬──────────────────────────────────────────────────┐
│ 0 APPL_DB │ 应用层请求入口,orchagent 消费的最终队列 │
├─────────────┼──────────────────────────────────────────────────┤
│ 1 ASIC_DB │ orchagent ↔ syncd 的唯一桥梁,SAI 调用的事实来源 ★ │
├─────────────┼──────────────────────────────────────────────────┤
│ 2 COUNTERS │ 计数器统计值,FlexCounter 轮询硬件后写入 │
├─────────────┼──────────────────────────────────────────────────┤
│ 4 CONFIG_DB│ 配置数据,CLI/YANG 的落点 │
├─────────────┼──────────────────────────────────────────────────┤
│ 5 FLEX_CTR │ FlexCounter 轮询配置(统计哪些计数器、多久采一次) │
├─────────────┼──────────────────────────────────────────────────┤
│ 6 STATE_DB │ 运行时状态 │
└─────────────┴──────────────────────────────────────────────────┘1 号库是本系列的主角:所有 SAI 操作都会先在 ASIC_DB 里留下痕迹,它是排障时第一个该看的地方(第三篇专门讲)。
3. sairedis:两个进程之间的"远程 SAI 调用"
SONiC 的设计里 orchagent 和 syncd 运行在不同容器,但 orchagent 写的代码看起来就像直接调用 SAI 函数——秘密在 sairedis 库:它把每一次 SAI 调用序列化成字符串写进 Redis,syncd 在另一端反序列化后才真正调用厂商 SAI。
3.1 下行命令通道(配置方向)
orchagent(swss 容器) syncd(syncd 容器)
┌───────────────────────────┐ ┌────────────────────────────┐
│ 调用 sai_create_xxx(...) │ │ 订阅 ASIC_STATE 发布订阅频道 │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ sairedis 客户端拦截该调用 │ │ 收到事件,反序列化出: │
│ │ │ │ · 对象类型 │
│ ▼ │ 写入+通知 │ · 对象 key (json 字符串) │
│ 序列化为文本记录 ──────────│───────────────→│ · 属性列表 │
│ ① 写入 ASIC_DB 对应表项 │ (Pub/Sub │ │ │
│ ② 发布到 ASIC_STATE 频道 │ 触发, │ ▼ │
│ │ │ 非轮询) │ VID → RID 翻译 │
│ │ │ │ │ │
│ │ │ │ ▼ │
│ │ │ │ meta 层预校验 │
│ │ │ │ │ │
│ │ │ │ ▼ │
│ │ │ │ 调用厂商 SAI (brcm 实现) │
│ ▼ │ 返回值回写 │ │ │
│ 阻塞等待结果 ←─────────────│───────────────│ meta 层记账后回传状态码 │
└───────────────────────────┘ └────────────────────────────┘三个关键事实:
- orchagent 从不直接碰硬件,它手里的每个对象标识都是 VID(虚拟标识),不是芯片里的真实对象。
- 通信是发布/订阅触发,不是轮询;ASIC_DB 里的表是载体和存档。
- 每次调用的入参、属性、返回值全程可见——这是 1 号库调试法成立的根基。
3.2 上行通知通道(硬件事件方向)
方向相反的一条路,负责把硬件事件送回业务层:
芯片中断/SDK 线程(链路抖动、MAC 学习/老化、BFD 超时……)
│
▼
brcm SAI 层注册的 notify 回调被触发
│
▼
syncd 通知处理第一段:把事件序列化后进入内存队列
(队列有上限,突发洪峰时 FDB 类事件会先被丢弃,连续重复事件会被去重)
│
▼
syncd 通知处理第二段(独立线程):
· 把硬件真实标识 RID 翻译回业务标识 VID
· 发布到 NOTIFICATIONS 频道
· 特例:FDB 事件额外直接回写 ASIC_DB 里的 FDB 表,保证库与硬件一致
│
▼
orchagent 的通知消费者收到事件,更新自己的业务状态理解这条通道能解释一类经典问题:"芯片明明学到了 MAC,为什么上层看不到"——问题不在学习,在通知通道。
3.3 VID / RID 虚拟化
orchagent 世界 芯片世界
┌──────────────┐ VirtualOidTranslator ┌──────────────┐
│ VID 虚拟标识 │ ════ 双向四张映射表 ════ │ RID 真实标识 │
│ 启动时预分配 │ V2R / R2V / R2O / O2R │ SAI 创建返回 │
│ 与硬件无关 │ 持久化在 ASIC_DB │ 绑定硬件资源 │
└──────────────┘ └──────────────┘- VID 由 orchagent 侧的分配器在对象创建前预分配,保证不同进程/不同上下文的标识不冲突;
- RID 是厂商 SAI 真正创建对象后返回的句柄;
- 两者映射持久化在 ASIC_DB 里,所以重启后还能对上号。
VID 是一个 8 字节整数,内部按位段编码了"属于哪个 switch、什么对象类型、第几个对象"。掌握这个结构,在 1 号库里看到一长串十六进制也能读出它是什么类型的对象。
4. syncd 核心机制
syncd 不是简单的"收命令→调 SAI"的传声筒,它内部有一套视图比对机制。这是理解 syncd 行为——尤其是启动、热重启、以及"为什么配置下了硬件没生效"——的关键。
4.1 启动流程
syncd 进程启动
│
├─ 加载通信配置(决定用 Redis 还是 ZeroMQ 通道)
├─ 加载 SAI profile ─────────────┐
│ (sai.profile 指向具体芯片 │ ★ SAI 层与 SDK 层的分界点:
│ 的 config.bcm 文件) │ 这份 bcm 文件决定芯片初始化参数,
│ │ "谁、在何时改写它"是硬件模式类问题的核心
├─ 建立 ASIC_DB / 计数器库连接 ┘
├─ 绑定全部 SAI 通知回调(上行通道的入口)
├─ 创建 VID/RID 翻译器
├─ 判断启动类型:
│ 冷启动 → 从 ASIC_DB 残留状态逐条重建 ASIC 对象
│ 热启动 → 走热重启恢复路径(视图比对,尽量不动硬件)
├─ 初始化厂商 SAI ── BCM SDK 在这里才真正初始化芯片
└─ 进入事件主循环4.2 两种工作模式:Init View 与 Apply View
Init View 模式(编排阶段) Apply View 模式(生效阶段)
┌─────────────────────────┐ ┌──────────────────────────────┐
│ orchagent 把期望的 │ │ syncd 同时读两份视图: │
│ 全部 ASIC 状态写进 │ apply 命令 │ · 期望视图(orchagent 写的) │
│ ASIC_DB 的临时视图 │ ─────────→ │ · 当前视图(芯片应有的现状) │
│ │ │ │ │
│ 此阶段完全不碰硬件 │ │ ▼ │
└─────────────────────────┘ │ 视图比对引擎逐对象 diff │
│ │ │
│ ▼ │
│ 生成 CREATE/SET/REMOVE 操作单 │
│ │ │
│ ▼ │
│ 按依赖顺序逐条执行下硬件 │
└──────────────────────────────┘这套设计的收益:变更以"差量"形式生效,没变化的对象一动不动;启动/热重启时也能精确对齐两边状态。
4.3 视图比对引擎的工作步骤
第一步:OID 对齐
两份视图里标识相同的对象,直接标记为"已匹配",后续只做属性级 diff
第二步:系统对象回填
芯片自动产生的对象(CPU 口、默认 VLAN 等)本来不在期望视图里,
把它们补进期望视图 —— 防止比对时被当成"多余对象"误删
第三步:预匹配建图
对 LAG 成员、ACL 条目这类"无标识但有业务特征"的对象,
先按特征(端口号、优先级等)建立候选对应关系
第四步:主循环
遍历期望视图中每个待处理对象:
├─ 在当前视图里找到逻辑等价对象?
│ ├─ 找到 → 逐属性比对,不同的属性生成 SET
│ └─ 没找到 → 生成 CREATE
└─ 全部处理完后,当前视图里剩余未匹配的对象 → 生成 REMOVE
执行分两阶段:
阶段一(非破坏):只生成操作单。失败 → syncd 存活,可重试
阶段二(破坏): 真正执行。中途失败 → ASIC_DB 与芯片不一致,
需要视图导出工具取证4.4 最佳匹配器:避免不必要的重建
视图比对里最关键的一个子模块负责回答:"当前视图里哪个对象和期望的这个对象是同一个东西?"它按四级优先级回答:
优先级 1:预匹配映射(LAG 成员按端口、ACL 条目按优先级……)
优先级 2:标签匹配(对象上的 label 属性)
优先级 3:依赖图匹配(对象在引用关系图里的邻接结构)
优先级 4:启发式匹配(相同属性最多的那个)为什么要在意它:如果匹配成功,改动以 SET 形式平滑生效;如果匹配失败,对象会被整体删除重建——转发闪断、ACL 瞬时空窗都这么来的。看到"syncd 把一个本该原地修改的对象删了重建",多半是匹配逻辑没找到候选。
4.5 meta 层:所有 SAI 调用的守门员
syncd 调用厂商 SAI 之前,先过一道统一校验层,模式固定为:预校验 → 转发调用 → 事后记账。它维护四本账:
┌───────────────┬────────────────────────────────────────────────┐
│ 引用计数账本 │ 谁还在引用这个对象——被路由引用的下一跳删不掉,是它拦的 │
├───────────────┼────────────────────────────────────────────────┤
│ 对象快照仓库 │ 所有已创建对象及其属性的完整快照 │
├───────────────┼────────────────────────────────────────────────┤
│ KEY 去重表 │ 带 KEY 标志的属性组合防重复创建 │
├───────────────┼────────────────────────────────────────────────┤
│ 端口关联集 │ 端口与其队列/调度组等子对象的归属关系 │
└───────────────┴────────────────────────────────────────────────┘重要推论:meta 层的报错发生在厂商代码被调用之前。看到属性非法、引用存在之类的报错,不用去翻厂商 SAI 源码,问题在调用方。
5. brcm 厂商 SAI 的内部分层
过了 meta 层,进入博通 SAI 实现的地界。它内部也是清晰的分层结构:
┌────────────────────────────────────────────────────────────┐
│ 初始化入口层 │
│ 负责 API 初始化、方法表注册(五十多个 API 模块各一张函数表) │
├────────────────────────────────────────────────────────────┤
│ SAI 对象层(约 60 个文件,一个 SAI 对象一个文件) │
│ 端口、VLAN、路由、邻居、下一跳、ACL、隧道…… │
│ 职责: SAI 属性校验、对象标识编解码、软件资源记账、 │
│ 调用下层架构适配 │
├────────────────────────────────────────────────────────────┤
│ 架构适配层(XGS 一族 / DNX 一族 两条支线) │
│ XGS: TH/TH2/TD3/TH3/TH4/TH5 │
│ DNX: Jericho/Ramon │
│ 职责: 吸收芯片差异——表容量、特性开关、SDK 接口差异 │
├────────────────────────────────────────────────────────────┤
│ L3 转发流水线(按架构再细分,TH4 另有 ltsw 变体) │
│ 职责: 把 SAI 的路由/邻居/下一跳翻译成 SDK 的路由/主机/ │
│ egress 调用,处理 ALPM、host-as-route 等芯片机制 │
├────────────────────────────────────────────────────────────┤
│ 特性子系统: ACL(含平台相关层) / 隧道 / 遥测 / MMU 缓冲 │
├────────────────────────────────────────────────────────────┤
│ 公共工具: 树/trie 结构、热重启同步库、高可用共享内存、 │
│ API 日志封装、定时器 │
├────────────────────────────────────────────────────────────┤
│ 调试 shell: 每个对象一对调试文件,在芯片交互 shell 里 │
│ 直接 dump SAI 层软件状态 │
└────────────────────────────────────────────────────────────┘5.1 一次调用的完整下沉路径(以路由为例)
orchagent "我要一条路由"
→ sairedis 序列化、进 ASIC_DB、通知 syncd
→ syncd 反序列化、VID 翻译、meta 校验
→ 厂商 SAI 对象层:参数校验、软件数据库记账
→ XGS 架构的 L3 流水线:组装 SDK 路由结构体
→ BCM SDK:ALPM/TCAM/UFT 表项写入
→ 芯片:前缀命中,转发行为改变每一层都有自己的日志前缀和错误码风格,排障时按这个顺序逐层 grep(第三篇展开讲方法)。
5.2 软件数据库:SAI 层自己的"影子状态"
厂商 SAI 内部维护一套软件数据库,记录对象归属和各类容量上限。这带来一个重要的排障陷阱:
SAI 层报的"表满"不一定是芯片满了,很可能只是这套软件数据库的上限到了。
区分方法很直接:用芯片交互命令查硬件真实用量,和软件上限对比。两者接近才是真满;软件先满就要去查上限是从哪来的(芯片能力查询、还是代码里的特判)。
5.3 双架构分叉的修改纪律
XGS 和 DNX 两条支线在多个层级分叉(同名文件分架构版本、目录也分)。改动时的铁律:
- 新功能先确认目标芯片族,双架构共用的属性要改两处;
- 芯片特判必须用芯片判断宏只圈住目标芯片,不得波及其他平台——一次改动影响面 = 一个芯片型号。
6. 总结:每层关心什么、出错时长什么样
┌──────────┬────────────────────┬──────────────────────────────────┐
│ 层 │ 关心什么 │ 出错时的典型表现 │
├──────────┼────────────────────┼──────────────────────────────────┤
│ orchagent │ 业务编排、对象组装 │ swss 容器日志里的业务级失败 │
├──────────┼────────────────────┼──────────────────────────────────┤
│ sairedis │ 序列化、进程间通道 │ 静默——答案要去 ASIC_DB 里找 │
├──────────┼────────────────────┼──────────────────────────────────┤
│ syncd 核心│ 视图比对、标识翻译 │ syncd 日志里的视图/操作单信息 │
├──────────┼────────────────────┼──────────────────────────────────┤
│ meta 层 │ 属性合法性、引用计数 │ 属性非法类错误(厂商代码未被调用) │
├──────────┼────────────────────┼──────────────────────────────────┤
│ 厂商 SAI │ SAI→SDK 语义翻译 │ 带 API 模块名+函数名的错误日志 │
├──────────┼────────────────────┼──────────────────────────────────┤
│ BCM SDK │ 芯片表项操作 │ bsl 日志——默认不进 syslog,易误判 │
├──────────┼────────────────────┼──────────────────────────────────┤
│ 芯片 │ 真实转发表 │ 芯片交互命令直接可见 │
└──────────┴────────────────────┴──────────────────────────────────┘下一篇:数据面转发逻辑分层——L2/L3/ACL 每一层在这条链上具体对应哪些对象和芯片表,以及容量、约束这些"看不见的墙"。
评论 (0)