深入 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)最初由微软开源,目标是让数据中心白盒交换机拥有和云服务器一样的可编程、可维护、可规模化能力。它把传统网络操作系统拆成了三个核心抽象:
- 控制面容器化:BGP、LAG、LLDP、DHCP Relay 等协议各自运行在独立 Docker 容器中。
- 数据库为中心:所有配置、状态、事件、计数器都通过 Redis 数据库流转,而不是传统 NOS 的本地文件或私有 IPC。
- SAI 硬件抽象:芯片差异被隔离在 SAI 适配层以下,上层应用完全无需关心是 Broadcom、NVIDIA/Mellanox、Marvell 还是国产芯片。
1.1 核心设计理念
| 理念 | 说明 |
|---|---|
| 软硬件解耦 | 交换机硬件只提供数据面转发能力,控制面由 SONiC 提供,同一套镜像可在多家白盒设备上运行。 |
| 容器化微服务 | 不同网络功能运行在不同 Docker 容器中,升级单个容器不会影响整机控制面。 |
| 数据库为中心 | 配置和状态通过 Redis 数据库存储和流转,天然支持发布/订阅、事务化批量操作和远程可观测性。 |
| SAI 抽象层 | 通过 SAI(Switch Abstraction Interface)屏蔽底层芯片差异,实现"一次开发,多平台运行"。 |
1.2 SONiC 主要组件速查
| 容器/组件 | 说明 |
|---|---|
swss | Switch 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)守护进程。 |
lldp | LLDP 协议实现与邻居发现。 |
dhcp_relay | DHCP Relay 服务。 |
pmon | 平台监控,包含 sensord、fancontrol、xcvrd 等。 |
snmp | SNMP 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 -->|同步结果| G2.1 主链路的六步模型
- 用户层:通过
config命令行、sonic-cfggen、RESTCONF/gNMI 等方式下发配置。 - 配置层(CONFIG_DB):配置被写入 Redis 的
CONFIG_DB(DB ID 4),持久化且可审计。 - 配置守护进程:
hostcfgd、vlan_cfgd、port_cfgd等监听CONFIG_DB,把配置翻译成应用层对象写入APPL_DB(DB ID 0)。 - 编排层(swss):
orchagent里的各类 *orch(如routeorch、vlanorch、portorch、aclorch)监听APPL_DB,调用 SAI 适配器生成 SAI 对象,并把操作序列写入ASIC_DB(DB ID 1)。 - 同步层(syncd):
syncd监听ASIC_DB,调用真实的libsai.so中的 SAI API,把请求交给厂商 SDK。 - 芯片层:厂商 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_DB | 0 | 应用层状态 | ROUTE_TABLE:*、INTERFACE_TABLE:*、VLAN_TABLE:* 等 | |||
| ASIC_DB | 1 | ASIC 相关状态 | ASIC_STATE:SAI_OBJECT_TYPE_*、对象 ID 映射 | |||
| COUNTERS_DB | 2 | 计数器 | 接口统计、队列统计、水线 | |||
| ERROR_DB | 3 | 错误信息 | 故障和事件记录 | |||
| CONFIG_DB | 4 | 持久化配置 | VLAN、INTERFACE、BGP_NEIGHBOR 等 | |||
| EVENT_DB | 5 | 事件日志 | 系统事件 | |||
| STATE_DB | 6 | 运行时状态 | `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 内部处理路由的子模块。它会:
- 检查下一跳
192.168.1.1是否已存在;若不存在,先创建SAI_OBJECT_TYPE_NEXT_HOP。 - 检查出接口
Ethernet0对应的 RIF(Router Interface)是否存在。 - 构造
sai_route_entry_t并设置属性,如SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID。 - 通过
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_GROUP | ECMP 下一跳组。 |
SAI_OBJECT_TYPE_NEIGHBOR_ENTRY | 邻居表项(ARP/ND)。 |
SAI_OBJECT_TYPE_VLAN | VLAN 对象。 |
SAI_OBJECT_TYPE_VLAN_MEMBER | VLAN 成员(端口与 VLAN 的绑定)。 |
SAI_OBJECT_TYPE_FDB_ENTRY | MAC 地址表项。 |
SAI_OBJECT_TYPE_ACL_TABLE / ACL_ENTRY | ACL 表与规则。 |
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。"
正确思路:
- 先确认 BGP 是否发布了该路由:
show ip bgp <prefix>。 - 检查 APPL_DB 中是否有该路由条目:
redis-cli -n 0 hgetall "ROUTE_TABLE:<prefix>"。 - 检查 ASIC_DB 中是否有对应 SAI route_entry:
redis-cli -n 1 keys "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:*<prefix>*"。 - 检查 syncd 日志是否有 SAI 调用失败(如
SAI_STATUS_INSUFFICIENT_RESOURCES)。 - 最后才检查芯片层是否处理异常。
中间环节都可能是丢包原因: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.json10. 动手实验
实验一: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 推荐资料
- SONiC Official Wiki
- SAI GitHub Repository
- sonic-buildimage
- sonic-swss:orchagent 与各类 syncd 的实现仓库
- sonic-sairedis:syncd 与 SAI-Redis 中间层
- SONiC 架构解析博客
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)