侧边栏壁纸
  • 累计撰写 53 篇文章
  • 累计收到 3 条评论

SAI SDK 架构逻辑全解 —— 从 orchagent 到博通芯片的完整链路

PacketForge
2026-08-18 / 0 评论 / 1 阅读 /

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 层记账后回传状态码      │
 └───────────────────────────┘                └────────────────────────────┘

三个关键事实:

  1. orchagent 从不直接碰硬件,它手里的每个对象标识都是 VID(虚拟标识),不是芯片里的真实对象。
  2. 通信是发布/订阅触发,不是轮询;ASIC_DB 里的表是载体和存档。
  3. 每次调用的入参、属性、返回值全程可见——这是 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

评论 (0)

取消