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

深入 SONiC 与 SAI:从命令行到芯片的完整数据之旅

PacketForge
2026-08-14 / 0 评论 / 14 阅读 /

深入 SONiC 与 SAI:从命令行到芯片的完整数据之旅

本篇是「AI 网络学习模块」的第二站。我们会把 SONiC 从里到外拆开来看:它不仅是一个基于 Linux 的 NOS,更是一套以 Redis 为血脉、以容器为器官、以 SAI 为神经的分布式网络控制系统。读完本文,你应该能亲手画出「配置命令 → 数据库 → orchagent → syncd → SAI → 厂商 SDK → 芯片」的完整链路,并能在每一层停下来做排查。

0. 学习目标

完成本模块后,你将能够:

  • 解释 SONiC 的整体架构,以及 swss、syncd、database、teamd 等关键容器的职责边界。
  • 熟练使用 redis-cli 和 redis-py 操作 CONFIG_DB、APPL_DB、ASIC_DB、STATE_DB、COUNTERS_DB。
  • 理解 SAI 的对象模型与 create/remove/set/get 四件套 API,能手写常见对象的创建伪代码。
  • 建立从命令行到芯片的端到端认知,知道在哪一层该查什么、不该盲下钻。
  • 利用 Redis Pipeline、批量 SAI API、Netlink 调优等思路优化配置下发性能。

1. SONiC 是什么?不只是"交换机上的 Linux"

SONiC(Software for Open Networking in the Cloud)最初由微软开源,目标是让数据中心白盒交换机拥有和云服务器一样的可编程、可维护、可规模化能力。它把传统网络操作系统拆成了三个核心抽象:

  1. 控制面容器化:BGP、LAG、LLDP、DHCP Relay 等协议各自运行在独立 Docker 容器中。
  2. 数据库为中心:所有配置、状态、事件、计数器都通过 Redis 数据库流转,而不是传统 NOS 的本地文件或私有 IPC。
  3. SAI 硬件抽象:芯片差异被隔离在 SAI 适配层以下,上层应用完全无需关心是 Broadcom、NVIDIA/Mellanox、Marvell 还是国产芯片。

1.1 核心设计理念

理念说明
软硬件解耦交换机硬件只提供数据面转发能力,控制面由 SONiC 提供,同一套镜像可在多家白盒设备上运行。
容器化微服务不同网络功能运行在不同 Docker 容器中,升级单个容器不会影响整机控制面。
数据库为中心配置和状态通过 Redis 数据库存储和流转,天然支持发布/订阅、事务化批量操作和远程可观测性。
SAI 抽象层通过 SAI(Switch Abstraction Interface)屏蔽底层芯片差异,实现"一次开发,多平台运行"。

1.2 SONiC 主要组件速查

容器/组件说明
swssSwitch State Service,核心容器,包含 orchagent、portsyncd、intfsyncd、neighsyncd、routeorch 等。
syncd负责把 ASIC_DB 中的 SAI 操作翻译成实际的 SAI API 调用,并同步状态回 ASIC_DB。
database运行 Redis 服务器,承载 CONFIG_DB、APPL_DB、ASIC_DB 等所有逻辑数据库。
bgp运行 FRR(或历史版本 Quagga)BGP 协议栈。
teamd链路聚合(LAG)守护进程。
lldpLLDP 协议实现与邻居发现。
dhcp_relayDHCP Relay 服务。
pmon平台监控,包含 sensord、fancontrol、xcvrd 等。
snmpSNMP Agent,对外暴露 MIB。
mgmt-framework提供 REST/gNMI 等管理接口。

2. SONiC 架构全景:数据流是理解一切的钥匙

理解 SONiC 最有效的方式,不是死记容器名,而是跟着一条配置命令看它怎么一步步变成芯片表项。下面的流程图展示了从用户输入到 ASIC 硬件的主链路,以及 STATE_DB/COUNTERS_DB 等反馈链路。

flowchart TD
    subgraph User["用户侧"]
        A[CLI / config 命令<br/>REST / gNMI]
    end

    subgraph Control["控制面容器"]
        B[CONFIG_DB<br/>DB 4]
        C[hostcfgd / 配置守护进程]
        D[APPL_DB<br/>DB 0]
        E[swss / orchagent<br/>routeorch, vlanorch, aclorch ...]
        F[STATE_DB<br/>DB 6]
    end

    subgraph Data["数据面容器"]
        G[ASIC_DB<br/>DB 1]
        H[syncd]
        I[SAI API<br/>libsai.so]
        J[厂商 SDK]
        K[ASIC 芯片]
    end

    subgraph Monitor["监控"]
        L[COUNTERS_DB<br/>DB 2]
    end

    A -->|写入配置| B
    B -->|监听 KeySpace 通知| C
    C -->|生成应用层对象| D
    D -->|监听变化| E
    E -->|生成 SAI 对象操作| G
    E -->|回写运行状态| F
    G -->|监听 ASIC_DB 变化| H
    H -->|调用| I
    I -->|调用| J
    J -->|下发| K
    K -->|统计/事件| L
    H -->|同步结果| G

2.1 主链路的六步模型

  1. 用户层:通过 config 命令行、sonic-cfggen、RESTCONF/gNMI 等方式下发配置。
  2. 配置层(CONFIG_DB):配置被写入 Redis 的 CONFIG_DB(DB ID 4),持久化且可审计。
  3. 配置守护进程:hostcfgd、vlan_cfgd、port_cfgd 等监听 CONFIG_DB,把配置翻译成应用层对象写入 APPL_DB(DB ID 0)。
  4. 编排层(swss):orchagent 里的各类 *orch(如 routeorch、vlanorch、portorch、aclorch)监听 APPL_DB,调用 SAI 适配器生成 SAI 对象,并把操作序列写入 ASIC_DB(DB ID 1)。
  5. 同步层(syncd):syncd 监听 ASIC_DB,调用真实的 libsai.so 中的 SAI API,把请求交给厂商 SDK。
  6. 芯片层:厂商 SDK 最终把表项写入交换芯片的 TCAM/Hash/LPM 等硬件表。

2.2 关键容器详解

swss 容器

swss(Switch State Service)是 SONiC 的核心容器,它的主要职责是:

  • 监听 APPL_DB 的变化。
  • 通过 orchagent 将应用层配置转换为 SAI 对象操作。
  • 将 SAI 操作结果写入 ASIC_DB。
  • 把运行状态(端口 up/down、邻居可达性等)回写到 STATE_DB。

主要进程:

进程职责
orchagent编排代理,是 swss 的核心,内部包含 routeorch、vlanorch、portorch、aclorch、neighorch 等子模块。
portsyncd监听端口状态变化,同步到 APPL_DB / STATE_DB。
intfsyncd监听接口配置变化。
neighsyncd监听邻居表(ARP/ND)变化。
routeorch处理路由相关事件,调用 SAI 创建/删除/修改路由。
vlanorch处理 VLAN、VLAN member、FDB 等二层对象。
aclorch处理 ACL 表和规则。

syncd 容器

syncd 是 SONiC 数据面的"守门员":

  • 监听 ASIC_DB 的变化。
  • 调用 SAI API(由厂商 SDK 实现)。
  • 将 SDK 返回的状态同步回 ASIC_DB。
  • 维护 SAI 对象 ID(OID)与 Redis Key 之间的映射。

syncd 内部通常分为两个主要部分:syncd 进程本身和 sairedis 库。sairedis 提供了一组 Redis 序列化/反序列化能力,使得 orchagent 不需要直接调用 SAI C API,而是通过 ASIC_DB 与 syncd 通信。


3. SONiC 数据库体系:Redis 是 SONiC 的血液

SONiC 使用 Redis 作为配置和状态的统一存储中心。Redis 被划分为多个逻辑数据库,每个数据库有独立的 DB ID。掌握这些 DB 的用途和典型键值格式,是排查问题的基本功。

3.1 逻辑数据库一览

数据库DB ID用途关键内容
APPL_DB0应用层状态ROUTE_TABLE:*、INTERFACE_TABLE:*、VLAN_TABLE:* 等
ASIC_DB1ASIC 相关状态ASIC_STATE:SAI_OBJECT_TYPE_*、对象 ID 映射
COUNTERS_DB2计数器接口统计、队列统计、水线
ERROR_DB3错误信息故障和事件记录
CONFIG_DB4持久化配置VLAN、INTERFACE、BGP_NEIGHBOR 等
EVENT_DB5事件日志系统事件
STATE_DB6运行时状态`PORT_TABLE*、LAG_TABLE*、BGP_STATE_TABLE*`

3.2 常用 Redis 操作:命令行版

# 进入指定 Redis 数据库
redis-cli -n <db_id>

# ============================================================
# 1. 查看 APPL_DB 中的路由表
# ============================================================
redis-cli -n 0 keys "ROUTE_TABLE:*"
redis-cli -n 0 hgetall "ROUTE_TABLE:10.0.0.0/24"

# 典型输出:
# 1) "nexthop"
# 2) "192.168.1.1"
# 3) "ifname"
# 4) "Ethernet0"

# ============================================================
# 2. 查看 ASIC_DB 中的 SAI 路由对象
# ============================================================
redis-cli -n 1 keys "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:*"
redis-cli -n 1 hgetall "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{\"dest\":\"10.0.0.0/24\",\"vr\":\"oid:0x3000000000040\"}"

# 典型输出:
# 1) "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID"
# 2) "oid:0x60000000012a"
# 3) "SAI_ROUTE_ENTRY_ATTR_PACKET_ACTION"
# 4) "SAI_PACKET_ACTION_FORWARD"

# ============================================================
# 3. 查看 STATE_DB 中的端口状态
# ============================================================
redis-cli -n 6 hgetall "PORT_TABLE|Ethernet0"

# 典型输出:
# 1) "oper_status"
# 2) "up"
# 3) "admin_status"
# 4) "up"
# 5) "speed"
# 6) "100000"

# ============================================================
# 4. 查看 COUNTERS_DB 中的接口统计
# ============================================================
redis-cli -n 2 hgetall "COUNTERS:oid:0x1000000000abc"
redis-cli -n 2 hget "COUNTERS:Ethernet0" "SAI_PORT_STAT_IF_IN_OCTETS"

# ============================================================
# 5. 查看 CONFIG_DB 中的 VLAN 配置
# ============================================================
redis-cli -n 4 hgetall "VLAN|Vlan100"
redis-cli -n 4 hgetall "VLAN_MEMBER|Vlan100|Ethernet0"

3.3 用 Python 操作 SONiC Redis

在生产环境中,自动化脚本、Telemetry Agent、测试框架经常通过 redis-py 与 SONiC 交互。下面给出连接、查询、Pipeline 和 KeySpace 通知的完整示例。

import redis
import time

# SONiC 默认使用 Unix Socket 或 127.0.0.1:6379
# 实际容器内通常通过 unix socket /var/run/redis/redis.sock 访问
r = redis.Redis(host='127.0.0.1', port=6379, db=0, decode_responses=True)

# --------------------------------------------------------------
# 1. 查询 APPL_DB 路由
# --------------------------------------------------------------
route = r.hgetall("ROUTE_TABLE:10.0.0.0/24")
print("Route entry:", route)

# --------------------------------------------------------------
# 2. 查询 ASIC_DB 中所有 SAI route_entry 对象
# --------------------------------------------------------------
asic_routes = r.keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:*")
print(f"Found {len(asic_routes)} ASIC route entries")

# --------------------------------------------------------------
# 3. Pipeline 批量写入 APPL_DB(性能优化利器)
# --------------------------------------------------------------
pipe = r.pipeline()
for i in range(1000):
    pipe.hmset(
        f"ROUTE_TABLE:10.0.{i}.0/24",
        {"nexthop": "192.168.1.1", "ifname": "Ethernet0"}
    )
pipe.execute()

# --------------------------------------------------------------
# 4. 监听 KeySpace 通知,追踪配置变化链路
# --------------------------------------------------------------
p = r.pubsub()
p.psubscribe('__keyspace@0__:*')   # 监听 APPL_DB

for message in p.listen():
    if message['type'] == 'pmessage':
        print(f"[APPL_DB] channel={message['channel']}  event={message['data']}")
小提示:在容器内访问 Redis 时,注意权限和 DB 隔离。database 容器监听 6379 端口,其他容器通过 Docker 网络或 Unix Socket 访问。某些安全加固的镜像会要求认证。

4. 从命令行到芯片:一次静态路由下发的完整旅程

下面我们以 config route add prefix 10.0.0.0/24 nexthop 192.168.1.1 为例,完整跟踪这条命令在 SONiC 中的流转。每一层都给出了对应的 Redis Key 和核心调用,方便你在实际排障时"按图索骥"。

sequenceDiagram
    autonumber
    participant U as 用户
    participant C as CONFIG_DB
    participant H as hostcfgd/route_cfgd
    participant A as APPL_DB
    participant O as routeorch
    participant S as ASIC_DB
    participant D as syncd
    participant I as SAI API
    participant K as 芯片 ASIC

    U->>C: config route add prefix 10.0.0.0/24 nexthop 192.168.1.1
    C->>H: KeySpace 通知:STATIC_ROUTE 变化
    H->>A: HMSET ROUTE_TABLE:10.0.0.0/24
    A->>O: 订阅到 ROUTE_TABLE 新增事件
    O->>O: 解析 nexthop、查找/创建 next_hop 对象
    O->>S: 写入 ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:...
    S->>D: KeySpace 通知:ASIC_DB 新增 SAI 对象
    D->>I: sai_route_api->create_route_entry()
    I->>K: SDK 写入硬件路由表
    K-->>I: 返回 SAI_STATUS_SUCCESS
    I-->>D: 同步结果到 ASIC_DB
    D-->>S: 更新对象状态
    O-->>A: (可选)更新路由状态

4.1 第一步:CONFIG_DB 写入

config 命令行工具本质上是 CONFIG_DB 的编辑器。执行后,你会在 CONFIG_DB 中看到:

redis-cli -n 4 hgetall "STATIC_ROUTE|default|10.0.0.0/24"

输出类似:

1) "nexthop"
2) "192.168.1.1"
3) "ifname"
4) "Ethernet0"
注意:不同版本的 SONiC 对静态路由的表名可能略有差异,可能是 STATIC_ROUTE 或 ROUTE。

4.2 第二步:配置守护进程生成 APPL_DB 条目

route_cfgd 或 staticroutebfd 等守护进程监听 CONFIG_DB 的 KeySpace 通知,然后把配置转换成应用层对象写入 APPL_DB:

redis-cli -n 0 hgetall "ROUTE_TABLE:10.0.0.0/24"

输出:

1) "nexthop"
2) "192.168.1.1"
3) "ifname"
4) "Ethernet0"

4.3 第三步:routeorch 处理

routeorch 是 orchagent 内部处理路由的子模块。它会:

  1. 检查下一跳 192.168.1.1 是否已存在;若不存在,先创建 SAI_OBJECT_TYPE_NEXT_HOP。
  2. 检查出接口 Ethernet0 对应的 RIF(Router Interface)是否存在。
  3. 构造 sai_route_entry_t 并设置属性,如 SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID。
  4. 通过 sai_route_api->create_route_entry() 把请求序列化到 ASIC_DB。

routeorch 内部大致是这个逻辑:

// orchagent/routeorch.cpp 简化伪代码
bool RouteOrch::addRoute(IpPrefix prefix, NextHopGroupKey nhg)
{
    sai_route_entry_t route_entry;
    route_entry.vr_id = gVirtualRouterId;
    route_entry.switch_id = gSwitchId;
    copy(prefix, route_entry.destination);

    sai_attribute_t attr;
    attr.id = SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID;
    attr.value.oid = nhg.getNextHopId();

    sai_status_t status = sai_route_api->create_route_entry(
                              &route_entry,
                              1,          // attr_count
                              &attr);     // attr_list
    return status == SAI_STATUS_SUCCESS;
}

4.4 第四步:ASIC_DB 记录

orchagent 并不直接调用厂商 SDK,而是把 SAI 操作写入 ASIC_DB。对于路由,你会看到类似这样的键:

redis-cli -n 1 keys "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:*"

键名示例:

ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{"dest":"10.0.0.0/24","vr":"oid:0x3000000000040"}

该 Hash 的内容:

1) "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID"
2) "oid:0x60000000012a"
3) "SAI_ROUTE_ENTRY_ATTR_PACKET_ACTION"
4) "SAI_PACKET_ACTION_FORWARD"

4.5 第五步:syncd 调用 SAI

syncd 监听到 ASIC_DB 新增了一个 SAI_OBJECT_TYPE_ROUTE_ENTRY 记录,于是调用:

sai_route_api->create_route_entry(&route_entry, attr_count, attr_list);

4.6 第六步:厂商 SDK 写入芯片

SAI 适配层把 create_route_entry 映射到自家 SDK 的 SDK API,例如 Broadcom SDK 的 bcm_l3_route_add 或 SAI SDK 的 sx_api_router_uc_route_set。最终,路由表项被写入芯片的 LPM/TCAM。


5. SAI 对象模型与 API 详解

SAI(Switch Abstraction Interface)是 OCP 推动的开放标准。它为不同厂商交换芯片提供统一 API,让 SONiC 这样的 NOS 可以"一套代码跑遍所有芯片"。

5.1 SAI 的设计目标

  • 统一抽象:上层网络操作系统无需关心具体芯片实现。
  • 厂商适配:芯片厂商提供 libsai.so,把 SAI API 映射到自家 SDK。
  • 软硬件解耦:促进白盒交换机生态繁荣,降低 NOS 与芯片的绑定。

5.2 SAI 对象类型速查

SAI 采用面向对象设计,几乎所有网络实体都是一个对象。常见对象类型如下:

对象类型说明
SAI_OBJECT_TYPE_SWITCH交换机全局对象,整个 SAI 实例的入口。
SAI_OBJECT_TYPE_VIRTUAL_ROUTER虚拟路由器,对应一个 VRF。
SAI_OBJECT_TYPE_ROUTER_INTERFACE三层接口(RIF),绑定到端口或 VLAN。
SAI_OBJECT_TYPE_ROUTE_ENTRY路由表项。
SAI_OBJECT_TYPE_NEXT_HOP单下一跳。
SAI_OBJECT_TYPE_NEXT_HOP_GROUPECMP 下一跳组。
SAI_OBJECT_TYPE_NEIGHBOR_ENTRY邻居表项(ARP/ND)。
SAI_OBJECT_TYPE_VLANVLAN 对象。
SAI_OBJECT_TYPE_VLAN_MEMBERVLAN 成员(端口与 VLAN 的绑定)。
SAI_OBJECT_TYPE_FDB_ENTRYMAC 地址表项。
SAI_OBJECT_TYPE_ACL_TABLE / ACL_ENTRYACL 表与规则。
SAI_OBJECT_TYPE_PORT / LAG / _BRIDGE_PORT物理端口、链路聚合、桥端口。

5.3 SAI API 的四件套

每个 SAI 对象类型通常对应四个基本操作:

sai_status_t create_xxx(
    sai_object_id_t *xxx_id,        // 输出:新创建的对象 ID
    sai_object_id_t switch_id,      // 所属交换机 ID
    uint32_t attr_count,            // 属性个数
    const sai_attribute_t *attr_list // 属性列表
);

sai_status_t remove_xxx(sai_object_id_t xxx_id);

sai_status_t set_xxx_attribute(
    sai_object_id_t xxx_id,
    const sai_attribute_t *attr
);

sai_status_t get_xxx_attribute(
    sai_object_id_t xxx_id,
    uint32_t attr_count,
    sai_attribute_t *attr_list
);

其中 sai_attribute_t 是 SAI 的灵魂:

typedef struct _sai_attribute_t
{
    sai_attr_id_t id;       // 属性 ID,如 SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID
    sai_attribute_value_t value; // 属性值,可以是 oid、ip、u32、mac、list 等
} sai_attribute_t;

5.4 常用对象创建代码示例

下面给出从 Switch 到 Route 的完整链条示例。为了聚焦核心逻辑,省略了错误处理和部分初始化代码。

5.4.1 创建 Virtual Router

sai_object_id_t vr_id;
sai_attribute_t vr_attrs[1];

vr_attrs[0].id = SAI_VIRTUAL_ROUTER_ATTR_SRC_MAC_ADDRESS;
memcpy(vr_attrs[0].value.mac, src_mac, sizeof(sai_mac_t));

sai_status_t status = sai_virtual_router_api->create_virtual_router(
                          &vr_id,
                          switch_id,
                          1,
                          vr_attrs);

5.4.2 创建 Router Interface

sai_object_id_t rif_id;
sai_attribute_t rif_attrs[4];

rif_attrs[0].id = SAI_ROUTER_INTERFACE_ATTR_VIRTUAL_ROUTER_ID;
rif_attrs[0].value.oid = vr_id;

rif_attrs[1].id = SAI_ROUTER_INTERFACE_ATTR_TYPE;
rif_attrs[1].value.s32 = SAI_ROUTER_INTERFACE_TYPE_PORT;

rif_attrs[2].id = SAI_ROUTER_INTERFACE_ATTR_PORT_ID;
rif_attrs[2].value.oid = port_id;   // Ethernet0 对应的 SAI PORT OID

rif_attrs[3].id = SAI_ROUTER_INTERFACE_ATTR_SRC_MAC_ADDRESS;
memcpy(rif_attrs[3].value.mac, src_mac, sizeof(sai_mac_t));

sai_status_t status = sai_router_interface_api->create_router_interface(
                          &rif_id,
                          switch_id,
                          4,
                          rif_attrs);

5.4.3 创建 Next Hop

sai_object_id_t nh_id;
sai_attribute_t nh_attrs[3];

nh_attrs[0].id = SAI_NEXT_HOP_ATTR_TYPE;
nh_attrs[0].value.s32 = SAI_NEXT_HOP_TYPE_IP;

nh_attrs[1].id = SAI_NEXT_HOP_ATTR_IP;
nh_attrs[1].value.ipaddr.addr_family = SAI_IP_ADDR_FAMILY_IPV4;
inet_pton(AF_INET, "192.168.1.1", &nh_attrs[1].value.ipaddr.addr.ip4);

nh_attrs[2].id = SAI_NEXT_HOP_ATTR_ROUTER_INTERFACE_ID;
nh_attrs[2].value.oid = rif_id;

sai_status_t status = sai_next_hop_api->create_next_hop(
                          &nh_id,
                          switch_id,
                          3,
                          nh_attrs);

5.4.4 创建 Neighbor Entry

sai_neighbor_entry_t neigh_entry;
memset(&neigh_entry, 0, sizeof(neigh_entry));
neigh_entry.rif_id = rif_id;
neigh_entry.ip_address.addr_family = SAI_IP_ADDR_FAMILY_IPV4;
inet_pton(AF_INET, "192.168.1.1", &neigh_entry.ip_address.addr.ip4);

sai_attribute_t neigh_attr;
neigh_attr.id = SAI_NEIGHBOR_ENTRY_ATTR_DST_MAC_ADDRESS;
memcpy(neigh_attr.value.mac, neighbor_mac, sizeof(sai_mac_t));

sai_status_t status = sai_neighbor_api->create_neighbor_entry(
                          &neigh_entry,
                          1,
                          &neigh_attr);

5.4.5 创建 Route Entry

sai_route_entry_t route_entry;
memset(&route_entry, 0, sizeof(route_entry));
route_entry.vr_id = vr_id;
route_entry.destination.addr_family = SAI_IP_ADDR_FAMILY_IPV4;
inet_pton(AF_INET, "10.0.0.0", &route_entry.destination.addr.ip4);
route_entry.destination.mask.ip4 = 0xffffff00;   // /24

sai_attribute_t route_attr;
route_attr.id = SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID;
route_attr.value.oid = nh_id;

sai_status_t status = sai_route_api->create_route_entry(
                          &route_entry,
                          1,
                          &route_attr);

5.4.6 修改路由属性

sai_attribute_t attr;
attr.id = SAI_ROUTE_ENTRY_ATTR_PACKET_ACTION;
attr.value.s32 = SAI_PACKET_ACTION_DROP;

sai_status_t status = sai_route_api->set_route_entry_attribute(
                          &route_entry,
                          &attr);

5.4.7 创建 VLAN 与 VLAN Member

// 1) 创建 VLAN
sai_object_id_t vlan_id_obj;
sai_attribute_t vlan_attr;
vlan_attr.id = SAI_VLAN_ATTR_VLAN_ID;
vlan_attr.value.u16 = 100;

sai_vlan_api->create_vlan(&vlan_id_obj, switch_id, 1, &vlan_attr);

// 2) 把端口加入 VLAN
sai_object_id_t vlan_member_id;
sai_attribute_t member_attrs[3];

member_attrs[0].id = SAI_VLAN_MEMBER_ATTR_VLAN_ID;
member_attrs[0].value.oid = vlan_id_obj;

member_attrs[1].id = SAI_VLAN_MEMBER_ATTR_BRIDGE_PORT_ID;
member_attrs[1].value.oid = bridge_port_id;

member_attrs[2].id = SAI_VLAN_MEMBER_ATTR_VLAN_TAGGING_MODE;
member_attrs[2].value.s32 = SAI_VLAN_TAGGING_MODE_TAGGED;

sai_vlan_api->create_vlan_member(&vlan_member_id, switch_id, 3, member_attrs);

5.4.8 创建 FDB Entry

sai_fdb_entry_t fdb_entry;
memset(&fdb_entry, 0, sizeof(fdb_entry));
fdb_entry.switch_id = switch_id;
fdb_entry.bv_id = vlan_id_obj;
memcpy(fdb_entry.mac_address, mac, sizeof(sai_mac_t));

sai_attribute_t fdb_attrs[2];
fdb_attrs[0].id = SAI_FDB_ENTRY_ATTR_TYPE;
fdb_attrs[0].value.s32 = SAI_FDB_ENTRY_TYPE_STATIC;

fdb_attrs[1].id = SAI_FDB_ENTRY_ATTR_BRIDGE_PORT_ID;
fdb_attrs[1].value.oid = bridge_port_id;

sai_fdb_api->create_fdb_entry(&fdb_entry, 2, fdb_attrs);

6. 多 SDK 统一 SAI 抽象:理想与现实的距离

6.1 当前团队的典型模式

很多团队目前采用的模式是:

  • 单台设备单个厂商 SDK:一台交换机只跑一种芯片。
  • SDK 之上封装抽象层:在 SAI 适配层里做特性透传和统一维护。
  • 分平台编译:不同芯片的 SAI 结构各异,需要单独出包。

这种模式实现简单,但可维护性受限:每来一款新芯片,都要复制、修改、验证一套 SAI 代码。

6.2 目标架构

更先进的团队希望实现:

  • 多 SDK 共用一个 SAI:同一套 SAI 实现可以对接不同厂商 SDK。
  • 统一抽象层:把不同 SDK 的接口差异封装在底层插件中。
  • 动态加载:启动时根据硬件平台(DMI/PDDF)动态选择加载 libsai-brcm.so、libsai-mlnx.so 或 libsai-ctx.so。

6.3 技术难点与应对

难点说明应对思路
SDK 接口差异不同厂商对同一 SAI 属性的支持程度不同。使用 sai_query_attribute_capability 在运行时查询能力,缺失时走 fallback。
增量更新 vs 整组重建例如 Broadcom 支持 ECMP 组增量,NVIDIA/Marvell/盛科可能需要整组重建。在 NextHopGroup 抽象层做 "diff" 计算,决定是调用 set_next_hop_group_attribute 还是拆组重建。
芯片特性差异某些特性在 A 芯片支持,在 B 芯片不支持,需要模拟或禁用。维护一张 per-chip 的 capability 表, orchagent 根据能力决定下发策略。
编译和依赖管理多 SDK 同时引入会大幅增加构建复杂度。使用 Docker 构建环境、分仓库管理 vendor-specific 代码、统一 SAI 头文件版本。
OID 映射一致性不同 SDK 对 Object ID 的分配策略不同。让 syncd 维护从 Redis Key 到 SDK OID 的双向映射,确保重启后仍能恢复。

7. 性能优化实战:别让 Redis 成为瓶颈

原生 SONiC 中,很多操作需要频繁读写 Redis。当配置规模达到万条路由、千条 ACL 时,Redis 往返和单条 SAI 调用会成为明显的性能瓶颈。

7.1 Redis Pipeline:把多次往返压缩成一次

Redis Pipeline 将多个命令打包一次性发送,适合批量配置下发场景。

import redis
import time

r = redis.Redis(host='127.0.0.1', port=6379, db=0)

# 单条写入:1000 次 RTT
start = time.time()
for i in range(1000):
    r.hmset(f"ROUTE_TABLE:10.0.{i}.0/24",
            {"nexthop": "192.168.1.1", "ifname": "Ethernet0"})
print(f"Single: {time.time() - start:.3f}s")

# Pipeline 写入:通常只有 1~2 次 RTT
start = time.time()
pipe = r.pipeline()
for i in range(1000):
    pipe.hmset(f"ROUTE_TABLE:10.0.{i}.0/24",
               {"nexthop": "192.168.1.1", "ifname": "Ethernet0"})
pipe.execute()
print(f"Pipeline: {time.time() - start:.3f}s")

在真实 SONiC 环境中,Pipeline 对批量路由、ACL、VLAN member 下发通常能带来 10~100 倍 的耗时降低。

7.2 批量 SAI API:减少 SDK 与硬件交互

除了 Redis 层优化,还应在 SAI 层使用批量 API。例如:

sai_status_t create_route_entries(
    uint32_t object_count,
    const sai_route_entry_t *route_entry,
    const uint32_t *attr_count,
    const sai_attribute_t **attr_list,
    sai_bulk_op_error_mode_t mode,
    sai_status_t *object_statuses
);

类似地还有 create_next_hops、create_neighbor_entries 等。批量 API 可以把数千次 SDK 调用压缩成一次芯片表项批量下发,显著降低 CPU 占用和下发时延。

7.3 索引优化与查询策略

  • 避免 keys * 全量扫描:生产环境中应使用 scan 游标迭代。
  • 按 VRF、前缀长度分组查询:在 APPL_DB 键设计里加入 VRF 名,便于前缀分组。
  • 缓存热点对象: orchagent 内部对 vr_id、switch_id、port_oid_map 等做本地缓存,减少重复查询 ASIC_DB。

7.4 Netlink 优化

Linux 内核通过 Netlink 向用户态汇报路由、邻居、接口事件。SONiC 的 fpmsyncd、neighsyncd、intfsyncd 都依赖 Netlink。当路由规模很大时:

  • 调大 Netlink 缓冲区:

    sysctl -w net.core.rmem_max=134217728
    sysctl -w net.core.wmem_max=134217728
  • 批量处理 Netlink 消息:在 fpmsyncd 中一次性读取多条消息,而不是每次只读一条,减少上下文切换。

8. 问题排查:分层模型与面试案例

SONiC 的分层架构决定了排查要"从上到下、由软到硬"。遇到"路由丢失"或"配置未生效"时,直接下钻芯片层往往是低效且危险的。

flowchart TD
    A[问题现象<br/>路由丢失 / 配置未生效] --> B{控制面是否正常?}
    B -->|BGP/OSPF 邻居异常| C[检查协议邻居与路由发布]
    B -->|路由表正确| D{配置面是否正常?}
    D -->|CONFIG_DB 缺失| E[补配置 / 检查命令行工具]
    D -->|APPL_DB 缺失| F[检查 hostcfgd / 配置守护进程]
    D -->|APPL_DB 有但 ASIC_DB 没有| G[检查 orchagent 日志]
    G --> H{数据面是否正常?}
    H -->|ASIC_DB 缺失| I[检查 syncd 与 SAI 调用]
    H -->|SAI 返回错误| J[检查 SDK 日志与能力表]
    H -->|SAI 成功| K{硬件面是否正常?}
    K -->|表项未写入| L[检查芯片资源 TCAM/Hash/LPM]
    K -->|端口/链路异常| M[检查物理层与光模块]

8.1 四层排查模型

层级核心问题常用检查命令/位置
控制面路由协议是否学到/发布?邻居是否正常?show ip bgp summary、show ip route、FRR 日志
配置面CONFIG_DB / APPL_DB 是否有对应条目?orchagent 是否成功处理?redis-cli -n 4/0 keys *、swss 日志
数据面(SAI/SDK)ASIC_DB 是否有 SAI 对象?syncd 是否成功调用 SAI?redis-cli -n 1、syncd 日志、dmesg
硬件面芯片表项是否写入?端口/链路状态是否正常?bcm-shell / sx_api 等厂商工具、show interface status

8.2 面试案例:BGP 海量随机路由丢包

错误思路:

"肯定是芯片层丢包了,快看 TCAM。"

正确思路:

  1. 先确认 BGP 是否发布了该路由:show ip bgp <prefix>。
  2. 检查 APPL_DB 中是否有该路由条目:redis-cli -n 0 hgetall "ROUTE_TABLE:<prefix>"。
  3. 检查 ASIC_DB 中是否有对应 SAI route_entry:redis-cli -n 1 keys "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:*<prefix>*"。
  4. 检查 syncd 日志是否有 SAI 调用失败(如 SAI_STATUS_INSUFFICIENT_RESOURCES)。
  5. 最后才检查芯片层是否处理异常。

中间环节都可能是丢包原因:Redis 性能瓶颈、Netlink 队列溢出、orchagent 处理异常、NextHop 未解析、ECMP 组重建失败等。分层排查能帮你快速定位真正的根因。


9. 绕过命令行:直接操作 Redis 与调试 SAI

9.1 为什么需要绕过命令行

  • 某些新功能命令行尚未支持,需要直接操作数据库验证。
  • 自动化测试或批量运维场景下,直接写 Redis 比 CLI 更高效。
  • 命令行只是上层封装,理解底层调用链路有助于深度排障。

9.2 正确方法:直接写 APPL_DB

SONiC 的命令行本质上修改的是 CONFIG_DB 或 APPL_DB。要绕过命令行,推荐直接操作 APPL_DB,让 orchagent 按正常链路处理。

# 直接修改 APPL_DB 添加路由
redis-cli -n 0 hmset "ROUTE_TABLE:10.0.0.0/24" \
    nexthop "192.168.1.1" \
    ifname "Ethernet0"

写入后,routeorch 会感知到变化,然后继续后面的 SAI 调用链路。这比直接改 ASIC_DB 安全得多。

9.3 为什么不推荐直接改 ASIC_DB

# 不推荐!除非你在做深度调试
# redis-cli -n 1 hmset "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:..." ...

直接修改 ASIC_DB 会导致:

  • syncd 与实际芯片状态不一致。
  • orchagent 的内部缓存与 Redis 不一致。
  • 重启后状态难以恢复。

一般只用于调试或 PoC,不建议生产环境使用。

9.4 通过 sairedis 做单元测试

如果你想在不启动完整 SONiC 的情况下测试 SAI 适配层,可以使用 sairedis 提供的 saiplayer 或 saidump 工具重放 ASIC_DB 操作序列。这是验证 SAI 实现的常用方法。

# 导出 ASIC_DB 当前状态
saidump > asic_dump.json

# 在测试环境重放
saiplayer asic_dump.json

10. 动手实验

实验一:SAI 调用链路追踪工具

用 Python + redis-py 监听 APPL_DB 和 ASIC_DB 变化,输出调用链路。

import redis

r_appl = redis.Redis(host='127.0.0.1', port=6379, db=0, decode_responses=True)
r_asic = redis.Redis(host='127.0.0.1', port=6379, db=1, decode_responses=True)

p = r_appl.pubsub()
p.psubscribe('__keyspace@0__:*')
p.psubscribe('__keyspace@1__:*')

print("开始监听 APPL_DB (0) 与 ASIC_DB (1) ...")
for message in p.listen():
    if message['type'] == 'pmessage':
        db, key = message['channel'].split(':', 1)
        print(f"[{db}] key={key}  event={message['data']}")

实验二:Redis Pipeline 性能对比

见 7.1 节的 Python 脚本。建议在自己的测试环境对比 100、1000、10000 条路由分别用单条写入和 Pipeline 写入的耗时。

实验三:ASIC_DB 路由探索器

编写脚本,把 ASIC_DB 中的 SAI_OBJECT_TYPE_ROUTE_ENTRY 与 APPL_DB 中的 ROUTE_TABLE 做映射,输出"前缀 -> ASIC OID -> NextHop OID"的表格。

import redis
import json

r_appl = redis.Redis(db=0, decode_responses=True)
r_asic = redis.Redis(db=1, decode_responses=True)

for key in r_appl.scan_iter(match="ROUTE_TABLE:*"):
    prefix = key.split(":", 1)[1]
    appl_entry = r_appl.hgetall(key)
    # ASIC_DB 的键是 JSON 格式,包含 dest 与 vr
    pattern = f"ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:*\"{prefix}\"*"
    asic_keys = r_asic.keys(pattern)
    for ak in asic_keys:
        asic_entry = r_asic.hgetall(ak)
        print(f"{prefix:20} -> APPL={appl_entry} -> ASIC={asic_entry}")

11. 延伸阅读与模块验收

11.1 推荐资料

11.2 模块验收标准

  • [ ] 能画出 SONiC 从命令行到芯片的完整数据流图,并说明每一层的职责。
  • [ ] 能使用 redis-cli 查询和修改 CONFIG_DB、APPL_DB、ASIC_DB、STATE_DB。
  • [ ] 能解释 orchagent、syncd、SAI、SDK 各自的职责边界。
  • [ ] 能手写 create_route_entry、set_route_attribute、create_next_hop、create_neighbor_entry 等 SAI 伪代码。
  • [ ] 能按分层模型排查一次"路由未下发"问题,并定位到具体层级。
  • [ ] 能使用 Redis Pipeline 批量下发配置,并对比单条下发的性能差异。

写在最后

SONiC 最大的魅力在于它的"透明":每一层都把自己的状态和意图暴露成可查询、可订阅、可重放的 Redis 数据。正是这种透明,让网络工程师可以像调试普通分布式系统一样调试交换机。

理解 SAI 则是理解 SONiC 数据面的最后一道门。你不需要记住每个属性 ID,但需要建立起"对象 + 属性 + API"的直觉,知道一条路由、一个邻居、一个 VLAN 在芯片里是怎么被抽象和落地的。

希望这篇深度解析能成为你后续学习 SONiC 源码、做白盒开发、或者面试网络系统岗位时的可靠参考。

0

评论 (0)

取消