SONiC入门指南
为什么要做SONiC
我们知道交换机内部都有一套可大可小的操作系统,用于配置和查看交换机的状态。但是,从1986年第一台交换机面世开始,虽然各个厂商都在进行着相关的开发,到现在为止种类也相当的多,但是依然存在一些问题,比如:
- 生态封闭,不开源,主要是为了支持自家的硬件,无法很好的兼容其他厂商的设备
- 支持的场景很有限,难以使用同一套系统去支撑大规模的数据中心中复杂多变的场景
- 升级可能会导致网络中断,难以实现无缝升级,这对于云提供商来说有时候是致命的
- 设备功能升级缓慢,难以很好的支持快速的产品迭代
所以,微软在2016年发起了开源项目SONiC,希望能够通过开源的方式,让SONiC能够成为一个通用的网络操作系统,从而解决上面的问题。而且,由于微软在Azure中大范围的使用SONiC,也保证了SONiC的实现确实能够承受大规模的生产环境的考验,这也是SONiC的一个优势。
主体架构
SONiC是微软开发的基于debian的开源的网络操作系统,它的设计核心思想有三个:
- 硬件和软件解耦:通过SAI(Switch Abstraction Interface)将硬件的操作抽象出来,从而使得SONiC能够支持多种硬件平台。这一层抽象层由SONiC定义,由各个厂商来实现。
- 使用docker容器将软件微服务化:SONiC上的主要功能都被拆分成了一个个的docker容器,和传统的网络操作系统不同,升级系统可以只对其中的某个容器进行升级,而不需要整体升级和重启,这样就可以很方便的进行升级和维护,支持快速的开发和迭代。
- 使用redis作为中心数据库对服务进行解耦:绝大部分服务的配置和状态最后都被存储到中心的redis数据库中,这样不仅使得所有的服务可以很轻松的进行协作(数据存储和pubsub),也可以让我们很方便的在上面开发工具,使用统一的方法对各个服务进行操作和查询,而不用担心状态丢失和协议兼容问题,最后还可以很方便的进行状态的备份和恢复。
这让SONiC拥有了非常开放的生态(Community,Workgroups,Devices),总体而言,SONiC的架构如下图所示:
(Source: SONiC Wiki - Architecture)
当然,这样的设计也有一些缺点,比如:对磁盘的占用会变大,不过,现在一点点存储空间并不是什么很大的问题,而且这个问题也都可以通过一些方法来解决。
发展方向
虽然交换机已经发展很多很多年了,但是随着现在云的发展,对网络的要求也越来越高,不管是直观的需求,比如更大的带宽,更大的容量,还是最新的研究,比如,带内计算,端网融合等等,都对交换机的发展提出了更高的要求和挑战,也促使着各大厂商和研究机构不断的进行创新。SONiC也一样,随着时间的发展,需求一点没有减少。
关于SONiC的发展方向,我们可以在它的Roadmap中看到。如果大家对最新的动态感兴趣,也可以关注它的Workshop,比如,最近的OCP Global Summit 2022 - SONiC Workshop。这里就不展开了。
感谢
感谢以下朋友的帮助和贡献,没有你们也就没有这本入门指南!
License
本书使用 署名-非商业性使用-相同方式共享(CC BY-NC-SA)4.0 许可协议。
参考资料
- SONiC Wiki - Architecture
- SONiC Wiki - Roadmap Planning
- SONiC Landing Page
- SONiC Workgroups
- SONiC Supported Devices and Platforms
- SONiC User Manual
- OCP Global Summit 2022 - SONiC Workshop
安装
如果你自己就拥有一台交换机,或者想购买一台交换机,在上面安装SONiC,那么请认真阅读这一小节,否则可以自行跳过。:D
交换机选择和SONiC安装
首先,请确认你的交换机是否支持SONiC,SONiC目前支持的交换机型号可以在这里找到,如果你的交换机型号不在列表中,那么就需要联系厂商,看看是否有支持SONiC的计划。有很多交换机是不支持SONiC的,比如:
- 普通针对家用的交换机,这些交换机的硬件配置都比较低(即便支持的带宽很高,比如MikroTik CRS504-4XQ-IN,虽然它支持100GbE网络,但是它只有16MB的Flash存储和64MB的RAM,所以基本只能跑它自己的RouterOS了)。
- 有些虽然是数据中心用的交换机,但是可能由于型号老旧,厂商并没有计划支持SONiC。
对于安装过程,由于每一家厂商的交换机设计不同,其底层接口各有差别,所以,其安装方法也都有所差别,这些差别主要集中在两个地方:
- 每个厂商都会有自己的SONiC Build,还有的厂商会在SONiC的基础之上进行扩展开发,为自己的交换机支持更多的功能,比如:Dell Enterprise SONiC,EdgeCore Enterprise SONiC,所以需要根据自己的交换机选择对应的版本。
- 每个厂商的交换机也会支持不同的安装方式,有一些是直接使用USB对ROM进行Flash,有一些是通过ONIE进行安装,这也需要根据自己的交换机来进行配置。
所以,虽然安装方法各有差别,但是总体而言,安装的步骤都是差不多的。请联系自己的厂商,获取对应的安装文档,然后按照文档进行安装即可。
配置交换机
安装好之后,我们需要进行一些基础设置,部分设置是通用的,我们在这里简单总结一下。
设置admin密码
默认SONiC的账号密码是admin:YourPaSsWoRd,使用默认密码显然不安全:
sudo passwd admin
设置风扇转速
数据中心用的交换机风扇声音都特别的大!比如,我用的交换机是Arista 7050QX-32S,上面有4个风扇,最高能到每分钟17000转,放在车库中,高频的啸叫即便是在二楼隔着3面墙还是能听得到,所以如果你是在家使用的话,建议对其进行一些设置,将转速调低。
可惜,由于SONiC并没有cli对风扇转速的规则进行控制,所以我们需要通过手动修改pmon容器中的配置文件的方式来进行设置。
# Enter pmon container
sudo docker exec -it pmon bash
# Use pwmconfig to detect all pwm fans and create configuration file. The configuration file will be created at /etc/fancontrol.
pwmconfig
# Start fancontrol and make sure it works. If it doesn't work, you can run fancontrol directly to see what's wrong.
VERBOSE=1 /etc/init.d/fancontrol start
VERBOSE=1 /etc/init.d/fancontrol status
# Exit pmon container
exit
# Copy the configuration file from the container to the host, so that the configuration will not be lost after reboot.
# This command needs to know what is the model of your switch, for example, the command I need to run here is as follows. If your switch model is different, please modify it yourself.
sudo docker cp pmon:/etc/fancontrol /usr/share/sonic/device/x86_64-arista_7050_qx32s/fancontrol
设置交换机Management Port IP
一般的数据中心用的交换机都提供了Serial Console连接的方式,但是其速度实在是太慢了,所以我们在安装完成之后,都会尽快的把Management Port给设置好,然后通过SSH的方式来进行管理。
一般来说,management port的设备名是eth0,所以我们可以通过SONiC的配置命令来进行设置:
# sudo config interface ip add eth0 <ip-cidr> <gateway>
# IPv4
sudo config interface ip add eth0 192.168.1.2/24 192.168.1.1
# IPv6
sudo config interface ip add eth0 2001::8/64 2001::1
创建网络配置
新安装完的SONiC交换机会有一个默认的网络配置,这个配置有很多问题,比如对于10.0.0.0的IP的使用,如下:
admin@sonic:~$ show ip interfaces
Interface Master IPv4 address/mask Admin/Oper BGP Neighbor Neighbor IP
----------- -------- ------------------- ------------ -------------- -------------
Ethernet0 10.0.0.0/31 up/up ARISTA01T2 10.0.0.1
Ethernet4 10.0.0.2/31 up/up ARISTA02T2 10.0.0.3
Ethernet8 10.0.0.4/31 up/up ARISTA03T2 10.0.0.5
所以我们需要创建一个新的网络配置,然后将我们使用的Port都放入到这个网络配置中。这里简单的方法就是创建一个VLAN,使用VLAN Routing:
# Create untagged vlan
sudo config vlan add 2
# Add IP to vlan
sudo config interface ip add Vlan2 10.2.0.0/24
# Remove all default IP settings
show ip interfaces | tail -n +3 | grep Ethernet | awk '{print "sudo config interface ip remove", $1, $2}' > oobe.sh; chmod +x oobe.sh; ./oobe.sh
# Add all ports to the new vlan
show interfaces status | tail -n +3 | grep Ethernet | awk '{print "sudo config vlan member add -u 2", $1}' > oobe.sh; chmod +x oobe.sh; ./oobe.sh
# Enable proxy arp, so switch can respond to arp requests from hosts
sudo config vlan proxy_arp 2 enabled
# Save config, so it will be persistent after reboot
sudo config save -y
这样就完成了,我们可以通过show vlan brief来查看一下:
admin@sonic:~$ show vlan brief
+-----------+--------------+-------------+----------------+-------------+-----------------------+
| VLAN ID | IP Address | Ports | Port Tagging | Proxy ARP | DHCP Helper Address |
+===========+==============+=============+================+=============+=======================+
| 2 | 10.2.0.0/24 | Ethernet0 | untagged | enabled | |
...
| | | Ethernet124 | untagged | | |
+-----------+--------------+-------------+----------------+-------------+-----------------------+
配置主机
如果你家里只有一台主机使用多网口连接交换机进行测试,那么我们还需要在主机上进行一些配置,以保证流量会通过网卡,流经交换机,否则,请跳过这一步。
这里网上的攻略很多,比如使用iptables中的DNAT和SNAT创建一个虚拟地址,但是过程非常繁琐,经过一些实验,我发现最简单的办法就是将其中一个网口移动到一个新的网络命名空间中,就可以了,即便使用的是同一个网段的IP,也不会有问题。
比如,我家使用的是Netronome Agilio CX 2x40GbE,它会创建两个interface:enp66s0np0
和enp66s0np1
,我们这里可以将enp66s0np1
移动到一个新的网络命名空间中,再配置好ip地址就可以了:
# Create a new network namespace
sudo ip netns add toy-ns-1
# Move the interface to the new namespace
sudo ip link set enp66s0np1 netns toy-ns-1
# Setting up IP and default routes
sudo ip netns exec toy-ns-1 ip addr add 10.2.0.11/24 dev enp66s0np1
sudo ip netns exec toy-ns-1 ip link set enp66s0np1 up
sudo ip netns exec toy-ns-1 ip route add default via 10.2.0.1
这样就可以了,我们可以通过iperf来测试一下,并在交换机上进行确认:
# On the host (enp66s0np0 has ip 10.2.0.10 assigned)
$ iperf -s --bind 10.2.0.10
# Test within the new network namespace
$ sudo ip netns exec toy-ns-1 iperf -c 10.2.0.10 -i 1 -P 16
------------------------------------------------------------
Client connecting to 10.2.0.10, TCP port 5001
TCP window size: 85.0 KByte (default)
------------------------------------------------------------
...
[SUM] 0.0000-10.0301 sec 30.7 GBytes 26.3 Gbits/sec
[ CT] final connect times (min/avg/max/stdev) = 0.288/0.465/0.647/0.095 ms (tot/err) = 16/0
# Confirm on switch
admin@sonic:~$ show interfaces counters
IFACE STATE RX_OK RX_BPS RX_UTIL RX_ERR RX_DRP RX_OVR TX_OK TX_BPS TX_UTIL TX_ERR TX_DRP TX_OVR
----------- ------- ---------- ------------ --------- -------- -------- -------- ---------- ------------ --------- -------- -------- --------
Ethernet4 U 2,580,140 6190.34 KB/s 0.12% 0 3,783 0 51,263,535 2086.64 MB/s 41.73% 0 0 0
Ethernet12 U 51,261,888 2086.79 MB/s 41.74% 0 1 0 2,580,317 6191.00 KB/s 0.12% 0 0 0
参考资料
- SONiC Supported Devices and Platforms
- SONiC Thermal Control Design
- Dell Enterprise SONiC Distribution
- Edgecore Enterprise SONiC Distribution
- Mikrotik CRS504-4XQ-IN
虚拟测试环境
虽然SONiC功能强大,但是大部分时候一台能够支持SONiC系统的交换机价格并不便宜,如果你只是想试一试SONiC,但是又不想花钱买一台SONiC的硬件设备,那么这一章一定不能错过,这一章会总结一下如何通过GNS3在本地搭建一个虚拟的SONiC的Lab,让你可以很快的在本地体验一把SONiC的基本功能。
在本地运行SONiC的方法很好几种,比如docker + vswitch,p4软交换机等等,对于初次使用而言,用GNS3可能是最方便快捷的了,所以本文就以GNS3为例,介绍一下如何在本地搭建一个SONiC的Lab。那么,我们就开始吧!
安装GNS3
首先,为了让我们方便而且直观的建立测试用的虚拟网络,我们需要先来安装一下GNS3。
GNS3,全称为Graphical Network Simulator 3,是一个图形化的网络仿真软件。它支持多种不同的虚拟化技术,比如:QEMU、VMware、VirtualBox等等。这样,我们在等会搭建虚拟网络的时候,就不需要手动的运行很多命令,或者写脚本了,大部分的工作都可以通过图形界面来完成了。
安装依赖
安装它之前,我们需要先安装几个其他的软件:docker, wireshark, putty, qemu, ubridge, libvirt和bridge-utils,已经装好的小伙伴可以自行跳过。
首先是Docker,它们的安装过程,大家可以自己通过下面的传送门去安装:https://docs.docker.com/engine/install/
其他的在ubuntu上安装都非常简单,只需要执行下面的命令就可以了。这里安装时要注意,ubridge和Wireshark的安装过程中会询问是不是要创建wireshark用户组来bypass sudo,这里一定要选择Yes。
sudo apt-get install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils wireshark putty ubridge
安装好了之后,我们就可以来安装GNS3了。
安装GNS3
在Ubuntu上,GNS3的安装非常简单,只需要执行下面的命令就可以了。
sudo add-apt-repository ppa:gns3/ppa
sudo apt update
sudo apt install gns3-gui gns3-server
然后把你的用户加入到如下的组中,这样GNS3就可以去访问docker,wireshark等功能而不用sudo了。
for g in ubridge libvirt kvm wireshark docker; do
sudo usermod -aG $g <user-name>
done
如果你使用的不是Ubuntu,更详细的安装文档可以参考他们的官方文档。
准备SONiC的镜像
在测试之前,我们还需要一个SONiC的镜像。由于需要支持大量不同的厂商,而每个厂商的底层实现都不一样,所以最后每个厂商都会编译一个自己的镜像。这里因为我们在创建虚拟的环境,所以我们需要使用基于VSwitch的镜像来创建虚拟交换机:sonic-vs.img.gz。
SONiC镜像的项目在这里,虽然我们可以自己去编译,但是速度实在有点慢,所以为了节省时间,我们可以直接去这里下载最新的镜像。只要找一个最新的成功的Build就行,在Artifacts中找到sonic-vs.img.gz,下载就可以了。
然后,我们来准备一下项目:
git clone --recurse-submodules https://github.com/sonic-net/sonic-buildimage.git
cd sonic-buildimage/platform/vs
# 将下载的镜像放在这个目录下,然后运行下面这个命令进行解压缩。
gzip -d sonic-vs.img.gz
# 下面这个命令会生成GNS3的镜像配置文件
./sonic-gns3a.sh
执行完成之后,我们运行ls
命令就可以看到我们需要的镜像文件了。
r12f@r12f-svr:~/code/sonic/sonic-buildimage/platform/vs
$ l
total 2.8G
...
-rw-rw-r-- 1 r12f r12f 1.1K Apr 18 16:36 SONiC-latest.gns3a # <= 这个是GNS3的镜像配置文件
-rw-rw-r-- 1 r12f r12f 2.8G Apr 18 16:32 sonic-vs.img # <= 这个是我们解压出来的镜像
...
导入镜像
现在,在命令行里面输入gns3
,就可以启动GNS3了。如果你是ssh到另外一台机器上,可以试着启用X11转发,这样就可以在远程运行GNS3,但是图形界面显示在本地了。我就是这样,将GNS3运行在了远程的服务器上,但是图形界面通过MobaXterm显示在了本地的Windows机器上。
运行起来之后,GNS3会让我们创建一个项目,很简单,填个目录地址就好。如果你是使用的X11转发,请注意,这个目录是在你远程服务器上,而不是本地。
然后,我们就可以通过File -> Import appliance
来导入我们刚刚生成的镜像了。
选择我们刚刚生成的SONiC-latest.gns3a
镜像配置文件,然后点击Next
。
这个时候就可以看到我们的镜像了,点击Next
。
这个时候会开始导入镜像,这个过程可能会比较慢,因为GNS3需要将镜像转换成qcow2格式,放入我们的项目目录中。导入完成之后,我们就可以看到我们的镜像了。
好的!完成!
创建网络
好了!现在一切就绪,我们还是创建一个虚拟的网络吧!
GNS3的图形界面非常的好用,基本上就是打开侧边栏,把交换机拖进来,把VPC拖进来,然后把线连起来就可以了。连接好之后记得点上面的Play按钮开始网络模拟。这里我们就不多说了,直接上图。
接着,在交换机上点击右键,选择Custom Console
,再选择Putty,就可以打开我们的上面看到的交换机的Console了。这里,SONiC的默认用户名和密码是admin
和YourPaSsWoRd
。登录进去之后,我们就可以运行熟悉的命令,用show interfaces status
或者show ip interface
来查看网络的状态了。我们这里也可以看到,前面两个我们连接好了的Interface的状态都是up
的了。
配置网络
SONiC软交换机下,默认的端口使用的是10.0.0.x的子网(如下),而且都是eth pair:
admin@sonic:~$ show ip interfaces
Interface Master IPv4 address/mask Admin/Oper BGP Neighbor Neighbor IP
----------- -------- ------------------- ------------ -------------- -------------
Ethernet0 10.0.0.0/31 up/up ARISTA01T2 10.0.0.1
Ethernet4 10.0.0.2/31 up/up ARISTA02T2 10.0.0.3
Ethernet8 10.0.0.4/31 up/up ARISTA03T2 10.0.0.5
这里,我们比较方便的做法是创建一个小的vlan,把我们的端口都包在里面(我们这里用的是Ethernet4和Ethernet8):
# Remove old config
sudo config interface ip remove Ethernet4 10.0.0.2/31
sudo config interface ip remove Ethernet8 10.0.0.4/31
# Create VLAN with id 2
sudo config vlan add 2
# Add ports to VLAN
sudo config vlan member add -u 2 Ethernet4
sudo config vlan member add -u 2 Ethernet8
# Add IP address to VLAN
sudo config interface ip add Vlan2 10.0.0.0/24
这样,我们的vlan就创建好了,我们可以通过show vlan brief
来查看一下:
admin@sonic:~$ show vlan brief
+-----------+--------------+-----------+----------------+-------------+-----------------------+
| VLAN ID | IP Address | Ports | Port Tagging | Proxy ARP | DHCP Helper Address |
+===========+==============+===========+================+=============+=======================+
| 2 | 10.0.0.0/24 | Ethernet4 | untagged | disabled | |
| | | Ethernet8 | untagged | | |
+-----------+--------------+-----------+----------------+-------------+-----------------------+
然后,我们就可以给所有的主机配置一个10.0.0.x的IP地址了。
# VPC1
ip 10.0.0.2 255.0.0.0 10.0.0.1
# VPC2
ip 10.0.0.3 255.0.0.0 10.0.0.1
好的,现在我们来Ping一下吧!
通了!
抓包
上面,我们安装GNS3前,我们特意安装了Wireshark,这样我们就可以在GNS3里面抓包了。我们只需要右键点击图中我们想抓包的Link上,然后选择Start capture
,就可以开始抓包了。
稍等一下,Wireshark就会自动打开,实时的显示所有的包,非常的方便:
更多的网络
除了上面这种最简单的网络搭建,我们其实可以用GNS3搭建很多非常复杂的网络来进行测试,比如多层ECMP + eBGP等等。XFlow Research发布了一篇非常详细的文档来介绍这些内容,感兴趣的小伙伴可以去传送到这篇文档去看看:SONiC Deployment and Testing Using GNS3。
参考资料
常用命令
为了帮助我们查看和配置SONiC的状态,SONiC提供了大量的CLI命令供我们调用。这些命令大多分为两类:show
和config
,他们的格式基本类似,大多都符合下面的格式:
show <object> [options]
config <object> [options]
SONiC的文档提供了非常详细的命令列表:SONiC Command Line Interface Guide,但是由于其命令众多,不便于我们初期的学习和使用,所以列出了一些平时最常用的命令和解释,供大家参考。
SONiC中的所有命令的子命令都可以只打前三个字母,来帮助我们有效的节约输入命令的时间,比如:
show interface transceiver error-status
和下面这条命令是等价的:
show int tra err
为了帮助大家记忆和查找,下面的命令列表都用的全名,但是大家在实际使用的时候,可以大胆的使用缩写来减少工作量。
如果遇到不熟悉的命令,都可以通过输入-h
或者--help
来查看帮助信息,比如:
show -h
show interface --help
show interface transceiver --help
General
show version
show uptime
show platform summary
Config
sudo config reload
sudo config load_minigraph
sudo config save -y
Docker相关
docker ps
docker top <container_id>|<container_name>
如果我们想对所有的docker container进行某个操作,我们可以通过docker ps
命令来获取所有的container id,然后pipe到tail -n +2
来去掉第一行的标题,从而实现批量调用。
比如,我们可以通过如下命令来查看所有container中正在运行的所有线程:
$ for id in `docker ps | tail -n +2 | awk '{print $1}'`; do docker top $id; done
UID PID PPID C STIME TTY TIME CMD
root 7126 7103 0 Jun09 pts/0 00:02:24 /usr/bin/python3 /usr/local/bin/supervisord
root 7390 7126 0 Jun09 pts/0 00:00:24 python3 /usr/bin/supervisor-proc-exit-listener --container-name telemetry
...
Interfaces / IPs
show interface status
show interface counters
show interface portchannel
show interface transceiver info
show interface transceiver error-status
sonic-clear counters
TODO: config
MAC / ARP / NDP
# Show MAC (FDB) entries
show mac
# Show IP ARP table
show arp
# Show IPv6 NDP table
show ndp
BGP / Routes
show ip/ipv6 bgp summary
show ip/ipv6 bgp network
show ip/ipv6 bgp neighbors [IP]
show ip/ipv6 route
TODO: add
config bgp shutdown neighbor <IP>
config bgp shutdown all
TODO: IPv6
LLDP
# Show LLDP neighbors in table format
show lldp table
# Show LLDP neighbors details
show lldp neighbors
VLAN
show vlan brief
QoS相关
# Show PFC watchdog stats
show pfcwd stats
show queue counter
ACL
show acl table
show acl rule
MUXcable / Dual ToR
Muxcable mode
config muxcable mode {active} {<portname>|all} [--json]
config muxcable mode active Ethernet4 [--json]
Muxcable config
show muxcable config [portname] [--json]
Muxcable status
show muxcable status [portname] [--json]
Muxcable firmware
# Firmware version:
show muxcable firmware version <port>
# Firmware download
# config muxcable firmware download <firmware_file> <port_name>
sudo config muxcable firmware download AEC_WYOMING_B52Yb0_MS_0.6_20201218.bin Ethernet0
# Rollback:
# config muxcable firmware rollback <port_name>
sudo config muxcable firmware rollback Ethernet0
参考资料
核心组件简介
我们也许会觉得交换机是一个很简单的网络设备,但是实际上交换机上的组件非常的多,而且由于SONiC中Redis的解耦,我们很难简单的对代码进行跟踪来理解服务之间的关系,这就需要我们先建立一个比较抽象的整体模型,然后再去深入的学习每个组件的细节。所以在深入其他部分之前,我们这里先对每个组件都做一个点到为止的介绍,帮助大家建立一个大概的整体模型。
在阅读本章之前,有两个名词会经常在本章和SONiC的官方文档中出现:ASIC(Application-Specific Integrated Circuit)和ASIC状态(State)。它们指的是交换机中用来进行包处理的Pipeline的状态,比如,ACL,转发方式等等,这个和其他交换机的硬件状态,比如,端口状态(端口速度,接口类型),IP信息等等硬件状态是非常不同的。
如果大家有兴趣了解更深入的细节,可以先移步阅读两个相关资料:SAI (Switch Abstraction Interface) API和一篇RMT(Reprogrammable Match Table)的相关论文:Forwarding Metamorphosis: Fast Programmable Match-Action Processing in Hardware for SDN。
这些都会对我们阅读SONiC的文档有很大的帮助。
另外为了方便我们的理解和阅读,我们也把SONiC架构图在这里放在这一章的开头,作为引用:
(Source: SONiC Wiki - Architecture)
参考资料
- SONiC Architecture
- SAI API
- Forwarding Metamorphosis: Fast Programmable Match-Action Processing in Hardware for SDN
Redis数据库
首先,在SONiC里面最核心的服务,自然是当之无愧的中心数据库Redis了!它的主要目的有两个:存储所有服务的配置和状态,并且为各个服务提供通信的媒介。
为了提供这些功能,SONiC会在Redis中创建一个名为sonic-db
的数据库实例,其配置和分库信息我们可以在/var/run/redis/sonic-db/database_config.json
中找到:
admin@sonic:~$ cat /var/run/redis/sonic-db/database_config.json
{
"INSTANCES": {
"redis": {
"hostname": "127.0.0.1",
"port": 6379,
"unix_socket_path": "/var/run/redis/redis.sock",
"persistence_for_warm_boot": "yes"
}
},
"DATABASES": {
"APPL_DB": { "id": 0, "separator": ":", "instance": "redis" },
"ASIC_DB": { "id": 1, "separator": ":", "instance": "redis" },
"COUNTERS_DB": { "id": 2, "separator": ":", "instance": "redis" },
"LOGLEVEL_DB": { "id": 3, "separator": ":", "instance": "redis" },
"CONFIG_DB": { "id": 4, "separator": "|", "instance": "redis" },
"PFC_WD_DB": { "id": 5, "separator": ":", "instance": "redis" },
"FLEX_COUNTER_DB": { "id": 5, "separator": ":", "instance": "redis" },
"STATE_DB": { "id": 6, "separator": "|", "instance": "redis" },
"SNMP_OVERLAY_DB": { "id": 7, "separator": "|", "instance": "redis" },
"RESTAPI_DB": { "id": 8, "separator": "|", "instance": "redis" },
"GB_ASIC_DB": { "id": 9, "separator": ":", "instance": "redis" },
"GB_COUNTERS_DB": { "id": 10, "separator": ":", "instance": "redis" },
"GB_FLEX_COUNTER_DB": { "id": 11, "separator": ":", "instance": "redis" },
"APPL_STATE_DB": { "id": 14, "separator": ":", "instance": "redis" }
},
"VERSION": "1.0"
}
虽然我们可以看到SONiC中的数据库有十来个,但是我们大部分时候只需要关注以下几个最重要的数据库就可以了:
- CONFIG_DB(ID = 4):存储所有服务的配置信息,比如端口配置,VLAN配置等等。它代表着用户想要交换机达到的状态的数据模型,这也是所有CLI和外部应用程序修改配置时的主要操作对象。
- APPL_DB(Application DB, ID = 0):存储所有服务的内部状态信息。这些信息有两种:一种是各个服务在读取了CONFIG_DB的配置信息后,自己计算出来的。我们可以理解为各个服务想要交换机达到的状态(Goal State),还有一种是当最终硬件状态发生变化被写回时,有些服务会直接写回到APPL_DB,而不是我们下面马上要介绍的STATE_DB。这些信息我们可以理解为各个服务认为交换机当前的状态(Current State)。
- STATE_DB(ID = 6):存储着交换机各个部件当前的状态(Current State)。当SONiC中的服务收到了STATE_DB的状态变化,但是发现和Goal State不一致的时候,SONiC就会重新下发配置,直到两者一致。(当然,对于那些回写到APPL_DB状态,服务就会监听APPL_DB的变化,而不是STATE_DB了。)
- ASIC_DB(ID = 1):存储着SONiC想要交换机ASIC达到状态信息,比如,ACL,路由等等。和APPL_DB不同,这个数据库里面的数据模型是面向ASIC设计的,而不是面向服务抽象的。这样做的目的是为了方便各个厂商进行SAI和ASIC驱动的开发。
这里,我们会发现一个很直观的问题:交换机里面这么多服务,难道所有的配置和状态都放在一个数据库里面没有隔离的么?如果两个服务用了同一个Redis Key怎么办呢?这个问题非常的好,SONiC的解决也很直接,那就是在每个数据库里面继续分表!
我们知道Redis在每个数据库里面并没有表的概念,而是使用key-value的方式来存储数据。所以,为了进一步分表,SONiC的解决方法是将表的名字放入key中,并且使用分隔符将表和key隔开。上面的配置文件中separator
字段就是做这个了。比如:APPL_DB
中的PORT_TABLE
表中的Ethernet4
端口的状态,我们可以通过PORT_TABLE:Ethernet4
来获取,如下:
127.0.0.1:6379> select 0
OK
127.0.0.1:6379> hgetall PORT_TABLE:Ethernet4
1) "admin_status"
2) "up"
3) "alias"
4) "Ethernet6/1"
5) "index"
6) "6"
7) "lanes"
8) "13,14,15,16"
9) "mtu"
10) "9100"
11) "speed"
12) "40000"
13) "description"
14) ""
15) "oper_status"
16) "up"
当然在SONiC中,不仅仅是数据模型,包括通信机制,都是使用类似的方法来实现“表”级别的隔离的。
参考资料
服务与工作流简介
SONiC里面的服务(常驻进程)非常的多,有二三十种,它们会在随着交换机启动而启动,并一直保持运行,直到交换机关机。如果我们想快速掌握SONiC,一个一个服务的去了解,会很容易陷入细节的泥潭,所以,我们最好把这些服务和控制流进行一个大的分类,以帮助我们建立一个宏观的概念。
我们这里不会深入到某一个具体的服务中去,而是先从整体上来看看SONiC中的服务的结构,帮助我们建立一个整体的认识。关于具体的服务,我们会在工作流一章中,对常用的工作流进行介绍,而关于详细的技术细节,大家也可以查阅每个服务相关的设计文档。
服务分类
总体而言,SONiC中的服务可以分为以下几类:*syncd
, *mgrd
,feature实现,orchagent
和syncd
。
*syncd
服务
这类服务名字中都以syncd
结尾。它们做的事情都很类似:它们负责将硬件状态同步到Redis中,一般目标都以APPL_DB或者STATE_DB为主。
比如,portsyncd
就是通过监听netlink的事件,将交换机中所有Port的状态同步到STATE_DB中,而natsyncd
则是监听netlink的事件,将交换机中所有的NAT状态同步到APPL_DB中。
*mgrd
服务
这类服务名字中都以mgrd
结尾。顾名思义,这些服务是所谓的“Manager”服务,也就是说它们负责各个硬件的配置,和*syncd
完全相反。它们的逻辑主要有两个部分:
- 配置下发:负责读取配置文件和监听Redis中的配置和状态改变(主要是CONFIG_DB,APPL_DB和STATE_DB),然后将这些修改推送到交换机硬件中去。推送的方法有多种,取决于更新的目标是什么,可以通过更新APPL_DB并发布更新消息,或者是直接调用linux下的命令行,对系统进行修改。比如:
nbrmgr
就是监听CONFIG_DB,APPL_DB和STATE_DB中neighbor的变化,并调用netlink和command line来对neighbor和route进行修改,而intfmgr
除了调用command line还会将一些状态更新到APPL_DB中去。 - 状态同步:对于需要Reconcile的服务,
*mgrd
还会监听STATE_DB中的状态变化,如果发现硬件状态和当前期望状态不一致,就会重新发起配置流程,将硬件状态设置为期望状态。这些STATE_DB中的状态变化一般都是*syncd
服务推送的。比如:intfmgr
就会监听STATE_DB中,由portsyncd
推送的,端口的Up/Down状态和MTU变化,一旦发现和其内存中保存的期望状态不一致,就会重新下发配置。
功能实现服务
有一些功能并不是依靠OS本身来完成的,而是由一些特定的进程来实现的,比如BGP,或者一些外部接口。这些服务名字中经常以d
结尾,表示deamon,比如:bgpd
,lldpd
,snmpd
,teamd
等,或者干脆就是这个功能的名字,比如:fancontrol
。
orchagent
服务
这个是SONiC中最重要的一个服务,不像其他的服务只负责一两个特定的功能,orchagent
作为交换机ASIC状态的编排者(orchestrator),会检查数据库中所有来自*syncd
服务的状态,整合起来并下发给用于保存交换机ASIC配置的数据库:ASIC_DB。这些状态最后会被syncd
接收,并调用SAI API经过各个厂商提供的SAI实现和ASIC SDK和ASIC进行交互,最终将配置下发到交换机硬件中。
syncd
服务
syncd
服务是orchagent
的下游,它虽然名字叫syncd
,但是它却同时肩负着ASIC的*mgrd
和*syncd
的工作。
- 首先,作为
*mgrd
,它会监听ASIC_DB的状态变化,一旦发现,就会获取其新的状态并调用SAI API,将配置下发到交换机硬件中。 - 然后,作为
*syncd
,如果ASIC发送了任何的通知给SONiC,它也会将这些通知通过消息的方式发送到Redis中,以便orchagent
和*mgrd
服务获取到这些变化,并进行处理。这些通知的类型我们可以在SwitchNotifications.h中找到。
服务间控制流分类
有了这些分类,我们就可以更加清晰的来理解SONiC中的服务了,而其中非常重要的就是理解服务之间的控制流。有了上面的分类,我们这里也可以把主要的控制流有分为两类:配置下发和状态同步。
配置下发
配置下发的流程一般是这样的:
- 修改配置:用户可以通过CLI或者REST API修改配置,这些配置会被写入到CONFIG_DB中并通过Redis发送更新通知。或者外部程序可以通过特定的接口,比如BGP的API,来修改配置,这种配置会通过内部的TCP Socket发送给
*mgrd
服务。 *mgrd
下发配置:服务监听到CONFIG_DB中的配置变化,然后将这些配置推送到交换机硬件中。这里由两种主要情况(并且可以同时存在):- 直接下发:
*mgrd
服务直接调用linux下的命令行,或者是通过netlink来修改系统配置*syncd
服务会通过netlink或者其他方式监听到系统配置的变化,并将这些变化推送到STATE_DB或者APPL_DB中。*mgrd
服务监听到STATE_DB或者APPL_DB中的配置变化,然后将这些配置和其内存中存储的配置进行比较,如果发现不一致,就会重新调用命令行或者netlink来修改系统配置,直到它们一致为止。
- 间接下发:
*mgrd
将状态推送到APPL_DB并通过Redis发送更新通知。orchagent
服务监听到配置变化,然后根据所有相关的状态,计算出此时ASIC应该达到的状态,并下发到ASIC_DB中。syncd
服务监听到ASIC_DB的变化,然后将这些新的配置通过统一的SAI API接口,调用ASIC Driver更新交换机ASIC中的配置。
- 直接下发:
配置初始化和配置下发类似,不过是在服务启动的时候读取配置文件,这里就不展开了。
状态同步
如果这个时候,出现了一些情况,比如网口坏了,ASIC中的状态变了等等,这个时候我们就需要进行状态更新和同步了。这个流程一般是这样的:
- 检测状态变化:这个状态变化主要来源于
*syncd
服务(netlink等等)和syncd
服务(SAI Switch Notification),这些服务在检测到变化后,会将它们发送给STATE_DB或者APPL_DB。 - 处理状态变化:
orchagent
和*mgrd
服务会监听到这些变化,然后开始处理,将新的配置重新通过命令行和netlink下发给系统,或者下发到ASIC_DB中,让syncd
服务再次对ASIC进行更新。
具体例子
SONiC的官方文档中给出了几个典型的控制流流转的例子,这里就不过多的展开了,有兴趣的朋友可以去这里看看:SONiC Subsystem Interactions。我们在后面工作流一章中,也会选择一些非常常用的工作流进行展开。
参考资料
核心容器
SONiC的设计中最具特色的地方:容器化。
从SONiC的上面的设计图中,我们可以看出来,SONiC中,所有的服务都是以容器的形式存在的。在登录进交换机之后,我们可以通过docker ps
命令来查看当前运行的容器:
admin@sonic:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ddf09928ec58 docker-snmp:latest "/usr/local/bin/supe…" 2 days ago Up 32 hours snmp
c480f3cf9dd7 docker-sonic-mgmt-framework:latest "/usr/local/bin/supe…" 2 days ago Up 32 hours mgmt-framework
3655aff31161 docker-lldp:latest "/usr/bin/docker-lld…" 2 days ago Up 32 hours lldp
78f0b12ed10e docker-platform-monitor:latest "/usr/bin/docker_ini…" 2 days ago Up 32 hours pmon
f9d9bcf6c9a6 docker-router-advertiser:latest "/usr/bin/docker-ini…" 2 days ago Up 32 hours radv
2e5dbee95844 docker-fpm-frr:latest "/usr/bin/docker_ini…" 2 days ago Up 32 hours bgp
bdfa58009226 docker-syncd-brcm:latest "/usr/local/bin/supe…" 2 days ago Up 32 hours syncd
655e550b7a1b docker-teamd:latest "/usr/local/bin/supe…" 2 days ago Up 32 hours teamd
1bd55acc181c docker-orchagent:latest "/usr/bin/docker-ini…" 2 days ago Up 32 hours swss
bd20649228c8 docker-eventd:latest "/usr/local/bin/supe…" 2 days ago Up 32 hours eventd
b2f58447febb docker-database:latest "/usr/local/bin/dock…" 2 days ago Up 32 hours database
这里我们来简单介绍一下这些容器。
数据库容器:database
这个容器中运行的就是我们多次提到的SONiC中的中心数据库Redis了,它里面存放着所有交换机的配置和状态信息,SONiC也是主要通过它来向各个服务提供底层的通信机制。
我们通过docker进入这个容器,就可以看到里面正在运行的redis进程了:
admin@sonic:~$ sudo docker exec -it database bash
root@sonic:/# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
...
root 82 13.7 1.7 130808 71692 pts/0 Sl Apr26 393:27 /usr/bin/redis-server 127.0.0.1:6379
...
root@sonic:/# cat /var/run/redis/redis.pid
82
那么别的容器是如何来访问这个Redis数据库的呢?答案是通过Unix Socket。我们可以在database容器中看到这个Unix Socket,它将交换机上的/var/run/redis
目录map进database容器,让database容器可以创建这个socket:
# In database container
root@sonic:/# ls /var/run/redis
redis.pid redis.sock sonic-db
# On host
admin@sonic:~$ ls /var/run/redis
redis.pid redis.sock sonic-db
然后再将这个socket给map到其他的容器中,这样所有容器就都可以来访问这个中心数据库啦,比如,swss容器:
admin@sonic:~$ docker inspect swss
...
"HostConfig": {
"Binds": [
...
"/var/run/redis:/var/run/redis:rw",
...
],
...
交换机状态管理容器:swss(Switch State Service)
这个容器可以说是SONiC中最关键的容器了,它是SONiC的大脑,里面运行着大量的*syncd
和*mgrd
服务,用来管理交换机方方面面的配置,比如Port,neighbor,ARP,VLAN,Tunnel等等等等。另外里面还运行着上面提到的orchagent
,用来统一处理和ASIC相关的配置和状态变化。
这些服务大概的功能和流程我们上面已经提过了,所以就不再赘述了。这里我们可以通过ps
命令来看一下这个容器中运行的服务:
admin@sonic:~$ docker exec -it swss bash
root@sonic:/# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
...
root 43 0.0 0.2 91016 9688 pts/0 Sl Apr26 0:18 /usr/bin/portsyncd
root 49 0.1 0.6 558420 27592 pts/0 Sl Apr26 4:31 /usr/bin/orchagent -d /var/log/swss -b 8192 -s -m 00:1c:73:f2:bc:b4
root 74 0.0 0.2 91240 9776 pts/0 Sl Apr26 0:19 /usr/bin/coppmgrd
root 93 0.0 0.0 4400 3432 pts/0 S Apr26 0:09 /bin/bash /usr/bin/arp_update
root 94 0.0 0.2 91008 8568 pts/0 Sl Apr26 0:09 /usr/bin/neighsyncd
root 96 0.0 0.2 91168 9800 pts/0 Sl Apr26 0:19 /usr/bin/vlanmgrd
root 99 0.0 0.2 91320 9848 pts/0 Sl Apr26 0:20 /usr/bin/intfmgrd
root 103 0.0 0.2 91136 9708 pts/0 Sl Apr26 0:19 /usr/bin/portmgrd
root 104 0.0 0.2 91380 9844 pts/0 Sl Apr26 0:20 /usr/bin/buffermgrd -l /usr/share/sonic/hwsku/pg_profile_lookup.ini
root 107 0.0 0.2 91284 9836 pts/0 Sl Apr26 0:20 /usr/bin/vrfmgrd
root 109 0.0 0.2 91040 8600 pts/0 Sl Apr26 0:19 /usr/bin/nbrmgrd
root 110 0.0 0.2 91184 9724 pts/0 Sl Apr26 0:19 /usr/bin/vxlanmgrd
root 112 0.0 0.2 90940 8804 pts/0 Sl Apr26 0:09 /usr/bin/fdbsyncd
root 113 0.0 0.2 91140 9656 pts/0 Sl Apr26 0:20 /usr/bin/tunnelmgrd
root 208 0.0 0.0 5772 1636 pts/0 S Apr26 0:07 /usr/sbin/ndppd
...
ASIC管理容器:syncd
这个容器中主要是用于管理交换机上的ASIC的,里面运行着syncd
服务。我们之前提到的各个厂商提供的SAI(Switch Abstraction Interface)和ASIC Driver都是放在这个容器中的。正是因为这个容器的存在,才使得SONiC可以支持多种不同的ASIC,而不需要修改上层的服务。换句话说,如果没有这个容器,那SONiC就是一个缸中大脑,除了一些基本的配置,其他只能靠想的,什么都干不了。
在syncd容器中运行的服务并不多,就是syncd,我们可以通过ps
命令来查看,而在/usr/lib
目录下,我们也可以找到这个为了支持ASIC而编译出来的巨大无比的SAI文件:
admin@sonic:~$ docker exec -it syncd bash
root@sonic:/# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
...
root 20 0.0 0.0 87708 1544 pts/0 Sl Apr26 0:00 /usr/bin/dsserve /usr/bin/syncd --diag -u -s -p /etc/sai.d/sai.profile -b /tmp/break_before_make_objects
root 32 10.7 14.9 2724404 599408 pts/0 Sl Apr26 386:49 /usr/bin/syncd --diag -u -s -p /etc/sai.d/sai.profile -b /tmp/break_before_make_objects
...
root@sonic:/# ls -lh /usr/lib
total 343M
...
lrwxrwxrwx 1 root root 13 Apr 25 04:38 libsai.so.1 -> libsai.so.1.0
-rw-r--r-- 1 root root 343M Feb 1 06:10 libsai.so.1.0
...
各种实现特定功能的容器
SONiC中还有很多的容器是为了实现一些特定功能而存在的。这些容器一般都有着特殊的外部接口(非SONiC CLI和REST API)和实现(非OS或ASIC),比如:
- bgp:用来实现BGP协议(Border Gateway Protocol,边界网关协议)的容器
- lldp:用来实现LLDP协议(Link Layer Discovery Protocol,链路层发现协议)的容器
- teamd:用来实现Link Aggregation(链路聚合)的容器
- snmp:用来实现SNMP协议(Simple Network Management Protocol,简单网络管理协议)的容器
和SWSS类似,为了适应SONiC的架构,它们中间也都会运行着上面我们提到的那几种服务:
- 配置管理和下发(类似
*mgrd
):lldpmgrd
,zebra
(bgp) - 状态同步(类似
*syncd
):lldpsyncd
,fpmsyncd
(bgp),teamsyncd
- 服务实现或者外部接口(
*d
):lldpd
,bgpd
,teamd
,snmpd
管理服务容器:mgmt-framework
我们在之前的章节中已经看过如何使用SONiC的CLI来进行一些交换机的配置,但是在实际生产环境中,手动登录交换机使用CLI来配置所有的交换机是不现实的,所以SONiC提供了一个REST API来解决这个问题。这个REST API的实现就是在mgmt-framework
容器中。我们可以通过ps
命令来查看:
admin@sonic:~$ docker exec -it mgmt-framework bash
root@sonic:/# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
...
root 16 0.3 1.2 1472804 52036 pts/0 Sl 16:20 0:02 /usr/sbin/rest_server -ui /rest_ui -logtostderr -cert /tmp/cert.pem -key /tmp/key.pem
...
其实除了REST API,SONiC还可以通过其他方式来进行管理,如gNMI,这些也都是运行在这个容器中的。其整体架构如下图所示 [2]:
这里我们也可以发现,其实我们使用的CLI,底层也是通过调用这个REST API来实现的~
平台监控容器:pmon(Platform Monitor)
这个容器里面的服务基本都是用来监控交换机一些基础硬件的运行状态的,比如温度,电源,风扇,SFP事件等等。同样,我们可以用ps
命令来查看这个容器中运行的服务:
admin@sonic:~$ docker exec -it pmon bash
root@sonic:/# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
...
root 28 0.0 0.8 49972 33192 pts/0 S Apr26 0:23 python3 /usr/local/bin/ledd
root 29 0.9 1.0 278492 43816 pts/0 Sl Apr26 34:41 python3 /usr/local/bin/xcvrd
root 30 0.4 1.0 57660 40412 pts/0 S Apr26 18:41 python3 /usr/local/bin/psud
root 32 0.0 1.0 57172 40088 pts/0 S Apr26 0:02 python3 /usr/local/bin/syseepromd
root 33 0.0 1.0 58648 41400 pts/0 S Apr26 0:27 python3 /usr/local/bin/thermalctld
root 34 0.0 1.3 70044 53496 pts/0 S Apr26 0:46 /usr/bin/python3 /usr/local/bin/pcied
root 42 0.0 0.0 55320 1136 ? Ss Apr26 0:15 /usr/sbin/sensord -f daemon
root 45 0.0 0.8 58648 32220 pts/0 S Apr26 2:45 python3 /usr/local/bin/thermalctld
...
其中大部分的服务从名字我们就能猜出来是做什么的了,中间只有xcvrd不是那么明显,这里xcvr是transceiver的缩写,它是用来监控交换机的光模块的,比如SFP,QSFP等等。
参考资料
SAI
SAI(Switch Abstraction Interface,交换机抽象接口)是SONiC的基石,正因为有了它,SONiC才能支持多种硬件平台。我们在这个SAI API的文档中,可以看到它定义的所有接口。
在核心容器一节中我们提到,SAI运行在syncd
容器中。不过和其他组件不同,它并不是一个服务,而是一组公共的头文件和动态链接库(.so)。其中,所有的抽象接口都以c语言头文件的方式定义在了OCP的SAI仓库中,而.so文件则由各个硬件厂商提供,用于实现SAI的接口。
SAI接口
为了有一个更加直观的理解,我们拿一小部分代码来展示一下SAI的接口定义和初始化的方法,如下:
// File: meta/saimetadata.h
typedef struct _sai_apis_t {
sai_switch_api_t* switch_api;
sai_port_api_t* port_api;
...
} sai_apis_t;
// File: inc/saiswitch.h
typedef struct _sai_switch_api_t
{
sai_create_switch_fn create_switch;
sai_remove_switch_fn remove_switch;
sai_set_switch_attribute_fn set_switch_attribute;
sai_get_switch_attribute_fn get_switch_attribute;
...
} sai_switch_api_t;
// File: inc/saiport.h
typedef struct _sai_port_api_t
{
sai_create_port_fn create_port;
sai_remove_port_fn remove_port;
sai_set_port_attribute_fn set_port_attribute;
sai_get_port_attribute_fn get_port_attribute;
...
} sai_port_api_t;
其中,sai_apis_t
结构体是SAI所有模块的接口的集合,其中每个成员都是一个特定模块的接口列表的指针。我们用sai_switch_api_t
来举例,它定义了SAI Switch模块的所有接口,我们在inc/saiswitch.h
中可以看到它的定义。同样的,我们在inc/saiport.h
中可以看到SAI Port模块的接口定义。
SAI初始化
SAI的初始化其实就是想办法获取上面这些函数指针,这样我们就可以通过SAI的接口来操作ASIC了。
参与SAI初始化的主要函数有两个,他们都定义在inc/sai.h
中:
sai_api_initialize
:初始化SAIsai_api_query
:传入SAI的API的类型,获取对应的接口列表
虽然大部分厂商的SAI实现是闭源的,但是mellanox却开源了自己的SAI实现,所以这里我们可以借助其更加深入的理解SAI是如何工作的。
比如,sai_api_initialize
函数其实就是简单的设置设置两个全局变量,然后返回SAI_STATUS_SUCCESS
:
// File: platform/mellanox/mlnx-sai/SAI-Implementation/mlnx_sai/src/mlnx_sai_interfacequery.c
sai_status_t sai_api_initialize(_In_ uint64_t flags, _In_ const sai_service_method_table_t* services)
{
if (g_initialized) {
return SAI_STATUS_FAILURE;
}
// Validate parameters here (code omitted)
memcpy(&g_mlnx_services, services, sizeof(g_mlnx_services));
g_initialized = true;
return SAI_STATUS_SUCCESS;
}
初始化完成后,我们就可以使用sai_api_query
函数,通过传入API的类型来查询对应的接口列表,而每一个接口列表其实都是一个全局变量:
// File: platform/mellanox/mlnx-sai/SAI-Implementation/mlnx_sai/src/mlnx_sai_interfacequery.c
sai_status_t sai_api_query(_In_ sai_api_t sai_api_id, _Out_ void** api_method_table)
{
if (!g_initialized) {
return SAI_STATUS_UNINITIALIZED;
}
...
return sai_api_query_eth(sai_api_id, api_method_table);
}
// File: platform/mellanox/mlnx-sai/SAI-Implementation/mlnx_sai/src/mlnx_sai_interfacequery_eth.c
sai_status_t sai_api_query_eth(_In_ sai_api_t sai_api_id, _Out_ void** api_method_table)
{
switch (sai_api_id) {
case SAI_API_BRIDGE:
*(const sai_bridge_api_t**)api_method_table = &mlnx_bridge_api;
return SAI_STATUS_SUCCESS;
case SAI_API_SWITCH:
*(const sai_switch_api_t**)api_method_table = &mlnx_switch_api;
return SAI_STATUS_SUCCESS;
...
default:
if (sai_api_id >= (sai_api_t)SAI_API_EXTENSIONS_RANGE_END) {
return SAI_STATUS_INVALID_PARAMETER;
} else {
return SAI_STATUS_NOT_IMPLEMENTED;
}
}
}
// File: platform/mellanox/mlnx-sai/SAI-Implementation/mlnx_sai/src/mlnx_sai_bridge.c
const sai_bridge_api_t mlnx_bridge_api = {
mlnx_create_bridge,
mlnx_remove_bridge,
mlnx_set_bridge_attribute,
mlnx_get_bridge_attribute,
...
};
// File: platform/mellanox/mlnx-sai/SAI-Implementation/mlnx_sai/src/mlnx_sai_switch.c
const sai_switch_api_t mlnx_switch_api = {
mlnx_create_switch,
mlnx_remove_switch,
mlnx_set_switch_attribute,
mlnx_get_switch_attribute,
...
};
SAI的使用
在syncd
容器中,SONiC会在启动时启动syncd
服务,而syncd
服务会加载当前系统中的SAI组件。这个组件由各个厂商提供,它们会根据自己的硬件平台来实现上面展现的SAI的接口,从而让SONiC使用统一的上层逻辑来控制多种不同的硬件平台。
我们可以通过ps
, ls
和nm
命令来简单的对这个进行验证:
# Enter into syncd container
admin@sonic:~$ docker exec -it syncd bash
# List all processes. We will only see syncd process here.
root@sonic:/# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
...
root 21 0.0 0.0 87708 1532 pts/0 Sl 16:20 0:00 /usr/bin/dsserve /usr/bin/syncd --diag -u -s -p /etc/sai.d/sai.profile -b /tmp/break_before_make_objects
root 33 11.1 15.0 2724396 602532 pts/0 Sl 16:20 36:30 /usr/bin/syncd --diag -u -s -p /etc/sai.d/sai.profile -b /tmp/break_before_make_objects
...
# Find all libsai*.so.* files.
root@sonic:/# find / -name libsai*.so.*
/usr/lib/x86_64-linux-gnu/libsaimeta.so.0
/usr/lib/x86_64-linux-gnu/libsaimeta.so.0.0.0
/usr/lib/x86_64-linux-gnu/libsaimetadata.so.0.0.0
/usr/lib/x86_64-linux-gnu/libsairedis.so.0.0.0
/usr/lib/x86_64-linux-gnu/libsairedis.so.0
/usr/lib/x86_64-linux-gnu/libsaimetadata.so.0
/usr/lib/libsai.so.1
/usr/lib/libsai.so.1.0
# Copy the file out of switch and check libsai.so on your own dev machine.
# We will see the most important SAI export functions here.
$ nm -C -D ./libsai.so.1.0 > ./sai-exports.txt
$ vim sai-exports.txt
...
0000000006581ae0 T sai_api_initialize
0000000006582700 T sai_api_query
0000000006581da0 T sai_api_uninitialize
...
参考资料
- SONiC Architecture
- SAI API
- Forwarding Metamorphosis: Fast Programmable Match-Action Processing in Hardware for SDN
- Github: sonic-net/sonic-sairedis
- Github: opencomputeproject/SAI
- Arista 7050QX Series 10/40G Data Center Switches Data Sheet
- Github repo: Nvidia (Mellanox) SAI implementation
开发上手指南
代码仓库
SONiC的代码都托管在GitHub的sonic-net账号上,仓库数量有30几个之多,所以刚开始看SONiC的代码时,肯定是会有点懵的,不过不用担心,我们这里就来一起看看~
核心仓库
首先是SONiC中最重要的两个核心仓库:SONiC和sonic-buildimage。
Landing仓库:SONiC
https://github.com/sonic-net/SONiC
这个仓库里面存储着SONiC的Landing Page和大量的文档,Wiki,教程,以往的Talk的Slides,等等等等。这个仓库可以说是每个新人上手最常用的仓库了,但是注意,这个仓库里面没有任何的代码,只有文档。
镜像构建仓库:sonic-buildimage
https://github.com/sonic-net/sonic-buildimage
这个构建仓库为什么对于我们十分重要?和其他项目不同,SONiC的构建仓库其实才是它的主仓库!这个仓库里面包含:
- 所有的功能实现仓库,它们都以submodule的形式被加入到了这个仓库中(
src
目录) - 所有设备厂商的支持文件(
device
目录),比如每个型号的交换机的配置文件,用来访问硬件的支持脚本,等等等等,比如:我的交换机是Arista 7050 QX-32S,那么我就可以在device/arista/x86_64-arista_7050_qx32s
目录中找到它的支持文件。 - 所有ASIC芯片厂商提供的支持文件(
platform
目录),比如每个平台的驱动程序,BSP,底层支持的脚本等等。这里我们可以看到几乎所有的主流芯片厂商的支持文件,比如:Broadcom,Mellanox,等等,也有用来做模拟软交换机的实现,比如vs和p4。 - SONiC用来构建所有容器镜像的Dockerfile(
dockers
目录) - 各种各样通用的配置文件和脚本(
files
目录) - 用来做构建的编译容器的dockerfile(
sonic-slave-*
目录) - 等等……
正因为这个仓库里面将所有相关的资源全都放在了一起,所以我们学习SONiC的代码时,基本只需要下载这一个源码仓库就可以了,不管是搜索还是跳转都非常方便!
功能实现仓库
除了核心仓库,SONiC下还有很多功能实现仓库,里面都是各个容器和子服务的实现,这些仓库都被以submodule的形式放在了sonic-buildimage的src
目录下,如果我们想对SONiC进行修改和贡献,我们也需要了解一下。
SWSS(Switch State Service)相关仓库
在上一篇中我们介绍过,SWSS容器是SONiC的大脑,在SONiC下,它由两个repo组成:sonic-swss-common和sonic-swss。
SWSS公共库:sonic-swss-common
首先是公共库:sonic-swss-common(https://github.com/sonic-net/sonic-swss-common)。
这个仓库里面包含了所有*mgrd
和*syncd
服务所需要的公共功能,比如,logger,json,netlink的封装,Redis操作和基于Redis的各种服务间通讯机制的封装等等。虽然能看出来这个仓库一开始的目标是专门给swss服务使用的,但是也正因为功能多,很多其他的仓库都有它的引用,比如swss-sairedis
和swss-restapi
。
SWSS主仓库:sonic-swss
然后就是SWSS的主仓库sonic-swss了:https://github.com/sonic-net/sonic-swss。
我们可以在这个仓库中找到:
- 绝大部分的
*mgrd
和*syncd
服务:orchagent
,portsyncd/portmgrd/intfmgrd
,neighsyncd/nbrmgrd
,natsyncd/natmgrd
,buffermgrd
,coppmgrd
,macsecmgrd
,sflowmgrd
,tunnelmgrd
,vlanmgrd
,vrfmgrd
,vxlanmgrd
,等等。 swssconfig
:在swssconfig
目录下,用于在快速重启时(fast reboot)恢复FDB和ARP表。swssplayer
:也在swssconfig
目录下,用来记录所有通过SWSS进行的配置下发操作,这样我们就可以利用它来做replay,从而对问题进行重现和调试。- 甚至一些不在SWSS容器中的服务,比如
fpmsyncd
(bgp容器)和teamsyncd/teammgrd
(teamd容器)。
SAI/平台相关仓库
接下来就是作为交换机抽象接口的SAI了,虽然SAI是微软提出来并在2015年3月份发布了0.1版本,但是在2015年9月份,SONiC都还没有发布第一个版本的时候,就已经被OCP接收并作为一个公共的标准了,这也是SONiC能够在这么短的时间内就得到了这么多厂商的支持的原因之一。而也因为如此,SAI的代码仓库也被分成了两部分:
- OCP下的OpenComputeProject/SAI:https://github.com/opencomputeproject/SAI。里面包含了有关SAI标准的所有代码,包括SAI的头文件,behavior model,测试用例,文档等等。
- SONiC下的sonic-sairedis:https://github.com/sonic-net/sonic-sairedis。里面包含了SONiC中用来和SAI交互的所有代码,比如syncd服务,和各种调试统计,比如用来做replay的
saiplayer
和用来导出asic状态的saidump
。
除了这两个仓库之外,还有一个平台相关的仓库,比如:sonic-platform-vpp,它的作用是通过SAI的接口,利用vpp来实现数据平面的功能,相当于一个高性能的软交换机,个人感觉未来可能会被合并到buildimage仓库中,作为platform目录下的一部分。
管理服务(mgmt)相关仓库
然后是SONiC中所有和管理服务相关的仓库:
名称 | 说明 |
---|---|
sonic-mgmt-common | 管理服务的基础库,里面包含着translib ,yang model相关的代码 |
sonic-mgmt-framework | 使用Go来实现的REST Server,是下方架构图中的REST Gateway(进程名:rest_server ) |
sonic-gnmi | 和sonic-mgmt-framework类似,是下方架构图中,基于gRPC的gNMI(gRPC Network Management Interface)Server |
sonic-restapi | 这是SONiC使用go来实现的另一个配置管理的REST Server,和mgmt-framework不同,这个server在收到消息后会直接对CONFIG_DB进行操作,而不是走translib(下图中没有,进程名:go-server-server ) |
sonic-mgmt | 各种自动化脚本(ansible 目录),测试(tests 目录),用来搭建test bed和测试上报(test_reporting 目录)之类的, |
这里还是附上SONiC管理服务的架构图,方便大家配合食用 [4]:
平台监控相关仓库:sonic-platform-common和sonic-platform-daemons
以下两个仓库都和平台监控和控制相关,比如LED,风扇,电源,温控等等:
名称 | 说明 |
---|---|
sonic-platform-common | 这是给厂商们提供的基础包,用来定义访问风扇,LED,电源管理,温控等等模块的接口定义,这些接口都是用python来实现的 |
sonic-platform-daemons | 这里包含了SONiC中pmon容器中运行的各种监控服务:chassisd ,ledd ,pcied ,psud ,syseepromd ,thermalctld ,xcvrd ,ycabled ,它们都使用python实现,通过和中心数据库Redis进行连接,和加载并调用各个厂商提供的接口实现来对各个模块进行监控和控制 |
其他功能实现仓库
除了上面这些仓库以外,SONiC还有很多实现其方方面面功能的仓库,有些是一个或多个进程,有些是一些库,它们的作用如下表所示:
仓库 | 介绍 |
---|---|
sonic-snmpagent | AgentX SNMP subagent的实现(sonic_ax_impl ),用于连接Redis数据库,给snmpd提供所需要的各种信息,可以把它理解成snmpd的控制面,而snmpd是数据面,用于响应外部SNMP的请求 |
sonic-frr | FRRouting,各种路由协议的实现,所以这个仓库中我们可以找到如bgpd ,zebra 这类的路由相关的进程实现 |
sonic-linkmgrd | Dual ToR support,检查Link的状态,并且控制ToR的连接 |
sonic-dhcp-relay | DHCP relay agent |
sonic-dhcpmon | 监控DHCP的状态,并报告给中心数据库Redis |
sonic-dbsyncd | lldp_syncd 服务,但是repo的名字没取好,叫做dbsyncd |
sonic-pins | Google开发的基于P4的网络栈支持(P4 Integrated Network Stack,PINS),更多信息可以参看PINS的官网。 |
sonic-stp | STP(Spanning Tree Protocol)的支持 |
sonic-ztp | Zero Touch Provisioning |
DASH | Disaggregated API for SONiC Hosts |
sonic-host-services | 运行在host上通过dbus用来为容器中的服务提供支持的服务,比如保存和重新加载配置,保存dump之类的非常有限的功能,类似一个host broker |
sonic-fips | FIPS(Federal Information Processing Standards)的支持,里面有很多为了支持FIPS标准而加入的各种补丁文件 |
sonic-wpa-supplicant | 各种无线网络协议的支持 |
工具仓库:sonic-utilities
https://github.com/sonic-net/sonic-utilities
这个仓库存放着SONiC所有的命令行下的工具:
config
,show
,clear
目录:这是三个SONiC CLI的主命令的实现。需要注意的是,具体的命令实现并不一定在这几个目录里面,大量的命令是通过调用其他命令来实现的,这几个命令只是提供了一个入口。scripts
,sfputil
,psuutil
,pcieutil
,fwutil
,ssdutil
,acl_loader
目录:这些目录下提供了大量的工具命令,但是它们大多并不是直接给用户使用的,而是被config
,show
和clear
目录下的命令调用的,比如:show platform fan
命令,就是通过调用scripts
目录下的fanshow
命令来实现的。utilities_common
,flow_counter_util
,syslog_util
目录:这些目录和上面类似,但是提供的是基础类,可以直接在python中import调用。- 另外还有很多其他的命令:
fdbutil
,pddf_fanutil
,pddf_ledutil
,pddf_psuutil
,pddf_thermalutil
,等等,用于查看和控制各个模块的状态。 connect
和consutil
目录:这两个目录下的命令是用来连接到其他SONiC设备并对其进行管理的。crm
目录:用来配置和查看SONiC中的CRM(Critical Resource Monitoring)。这个命令并没有被包含在config
和show
命令中,所以用户可以直接使用。pfc
目录:用来配置和查看SONiC中的[PFC(Priority-based Flow Control)][SONiCPFC]。pfcwd
目录:用来配置和查看SONiC中的[PFC Watch Dog][SONiCPFCWD],比如启动,停止,修改polling interval之类的操作。
内核补丁:sonic-linux-kernel
https://github.com/sonic-net/sonic-linux-kernel
虽然SONiC是基于debian的,但是默认的debian内核却不一定能运行SONiC,比如某个模块默认没有启动,或者某些老版本的驱动有问题,所以SONiC需要或多或少有一些修改的Linux内核。而这个仓库就是用来存放所有的内核补丁的。
参考资料
- SONiC Architecture
- SONiC Source Repositories
- SONiC Management Framework
- SAI API
- SONiC Critical Resource Monitoring
- SONiC Zero Touch Provisioning
- SONiC Critical Resource Monitoring
- SONiC P4 Integrated Network Stack
- SONiC Disaggregated API for Switch Hosts
- SAI spec for OCP
编译
编译环境
由于SONiC是基于debian开发的,为了保证我们无论在什么平台下都可以成功的编译SONiC,并且编译出来的程序能在对应的平台上运行,SONiC使用了容器化的编译环境 —— 它将所有的工具和依赖都安装在对应debian版本的docker容器中,然后将我们的代码挂载进容器,最后在容器内部进行编译工作,这样我们就可以很轻松的在任何平台上编译SONiC,而不用担心依赖不匹配的问题,比如有一些包在debian里的版本比ubuntu更高,这样就可能导致最后的程序在debian上运行的时候出现一些意外的错误。
初始化编译环境
安装Docker
为了支持容器化的编译环境,第一步,我们需要保证我们的机器上安装了docker。
Docker的安装方法可以参考官方文档,这里我们以Ubuntu为例,简单介绍一下安装方法。
首先,我们需要把docker的源和证书加入到apt的源列表中:
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
然后,我们就可以通过apt来快速安装啦:
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
安装完docker的程序之后,我们还需要把当前的账户添加到docker的用户组中,然后退出并重新登录当前用户,这样我们就可以不用sudo来运行docker命令了!这一点非常重要,因为后续SONiC的build是不允许使用sudo的。
sudo gpasswd -a ${USER} docker
安装完成之后,别忘了通过以下命令来验证一下是否安装成功(注意,此处不需要sudo!):
docker run hello-world
安装其他依赖
sudo apt install -y python3-pip
pip3 install --user j2cli
拉取代码
在3.1 代码仓库一章中,我们提到了SONiC的主仓库是sonic-buildimage。它也是我们目前为止唯一需要安装关注的repo。
因为这个仓库通过submodule的形式将其他所有和编译相关的仓库包含在内,我们通过git命令拉取代码时需要注意加上--recuse-submodules
的选项:
git clone --recurse-submodules https://github.com/sonic-net/sonic-buildimage.git
如果在拉取代码的时候忘记拉取submodule,可以通过以下命令来补上:
git submodule update --init --recursive
当代码下载完毕之后,或者对于已有的repo,我们就可以通过以下命令来初始化编译环境了。这个命令更新当前所有的submodule到需要的版本,以帮助我们成功编译:
sudo modprobe overlay
make init
了解并设置你的目标平台
SONiC虽然支持非常多种不同的交换机,但是由于不同型号的交换机使用的ASIC不同,所使用的驱动和SDK也会不同。SONiC通过SAI来封装这些变化,为上层提供统一的配置接口,但是在编译的时候,我们需要正确的设置好,这样才能保证我们编译出来的SONiC可以在我们的目标平台上运行。
现在,SONiC主要支持如下几个平台:
- barefoot
- broadcom
- marvell
- mellanox
- cavium
- centec
- nephos
- innovium
- vs
在确认好平台之后,我们就可以运行如下命令来配置我们的编译环境了:
make PLATFORM=<platform> configure
# e.g.: make PLATFORM=mellanox configure
所有的make命令(除了make init
)一开始都会检查并创建所有debian版本的docker builder:bullseye,stretch,jessie,buster。每个builder都需要几十分钟的时间才能创建完成,这对于我们平时开发而言实在完全没有必要,一般来说,我们只需要创建最新的版本即可(当前为bullseye,bookwarm暂时还没有支持),具体命令如下:
NOJESSIE=1 NOSTRETCH=1 NOBUSTER=1 make PLATFORM=<platform> configure
当然,为了以后开发更加方便,避免重复输入,我们可以将这个命令写入到~/.bashrc
中,这样每次打开终端的时候,就会设置好这些环境变量了。
export NOJESSIE=1
export NOSTRETCH=1
export NOBUSTER=1
编译代码
编译全部代码
设置好平台之后,我们就可以开始编译代码了:
# The number of jobs can be the number of cores on your machine.
# Say, if you have 16 cores, then feel free to set it to 16 to speed up the build.
make SONIC_BUILD_JOBS=4 all
当然,对于开发而言,我们可以把SONIC_BUILD_JOBS和上面其他变量一起也加入~/.bashrc
中,减少我们的输入。
export SONIC_BUILD_JOBS=<number of cores>
编译子项目代码
我们从SONiC的Build Pipeline中就会发现,编译整个项目是非常耗时的,而绝大部分时候,我们的代码改动只会影响很小部分的代码,所以有没有办法减少我们编译的工作量呢?答案是肯定的,我们可以通过指定make target来仅编译我们需要的子项目。
SONiC中每个子项目生成的文件都可以在target
目录中找到,比如:
- Docker containers: target/
.gz,比如: target/docker-orchagent.gz
- Deb packages: target/debs/
/ .deb,比如: target/debs/bullseye/libswsscommon_1.0.0_amd64.deb
- Python wheels: target/python-wheels/
/ .whl,比如: target/python-wheels/bullseye/sonic_utilities-1.2-py3-none-any.whl
当我们找到了我们需要的子项目之后,我们便可以将其生成的文件删除,然后重新调用make命令,这里我们用libswsscommon
来举例子,如下:
# Remove the deb package for bullseye
rm target/debs/bullseye/libswsscommon_1.0.0_amd64.deb
# Build the deb package for bullseye
NOJESSIE=1 NOSTRETCH=1 NOBUSTER=1 make target/debs/bullseye/libswsscommon_1.0.0_amd64.deb
检查和处理编译错误
如果不巧在编译的时候发生了错误,我们可以通过检查失败项目的日志文件来查看具体的原因。在SONiC中,每一个子编译项目都会生成其相关的日志文件,我们可以很容易的在target
目录中找到,如下:
$ ls -l
...
-rw-r--r-- 1 r12f r12f 103M Jun 8 22:35 docker-database.gz
-rw-r--r-- 1 r12f r12f 26K Jun 8 22:35 docker-database.gz.log // Log file for docker-database.gz
-rw-r--r-- 1 r12f r12f 106M Jun 8 22:44 docker-dhcp-relay.gz
-rw-r--r-- 1 r12f r12f 106K Jun 8 22:44 docker-dhcp-relay.gz.log // Log file for docker-dhcp-relay.gz
如果我们不想每次在更新代码之后都去代码的根目录下重新编译,然后查看日志文件,SONiC还提供了一个更加方便的方法,能让我们在编译完成之后停在docker builder中,这样我们就可以直接去对应的目录运行make
命令来重新编译了:
# KEEP_SLAVE_ON=yes make <target>
KEEP_SLAVE_ON=yes make target/debs/bullseye/libswsscommon_1.0.0_amd64.deb
KEEP_SLAVE_ON=yes make all
有些仓库中的部分代码在全量编译的时候是不会编译的,比如,sonic-swss-common
中的gtest,所以使用这种方法重编译的时候,请一定注意查看原仓库的编译指南,以避免出错,如:https://github.com/sonic-net/sonic-swss-common#build-from-source。
获取正确的镜像文件
编译完成之后,我们就可以在target
目录中找到我们需要的镜像文件了,但是这里有一个问题:我们到底要用哪一种镜像来把SONiC安装到我们的交换机上呢?这里主要取决于交换机使用什么样的BootLoader或者安装程序,其映射关系如下:
Bootloader | 后缀 |
---|---|
Aboot | .swi |
ONIE | .bin |
Grub | .img.gz |
部分升级
显然,在开发的时候,每次都编译安装镜像然后进行全量安装的效率是相当低下的,所以我们可以选择不安装镜像而使用直接升级deb包的方式来进行部分升级,从而提高我们的开发效率。
我们可以将deb包上传到交换机的/etc/sonic
目录下,这个目录下的文件会被map到所有容器的/etc/sonic
目录下,接着我们可以进入到容器中,然后使用dpkg
命令来安装deb包,如下:
# Enter the docker container
docker exec -it <container> bash
# Install deb package
dpkg -i <deb-package>
参考资料
- SONiC Build Guide
- Install Docker Engine
- Github repo: sonic-buildimage
- SONiC Supported Devices and Platforms
- Wrapper for starting make inside sonic-slave container
测试
调试
SAI调试
通信机制
SONiC中主要的通信机制有三种:与内核的通信,基于Redis和基于ZMQ的服务间的通信。
- 与内核通信主要有两种方法:命令行调用和Netlink消息。
- 基于Redis的服务间通信主要有四种方法:SubscriberStateTable,NotificationProducer/Consumer,Producer/ConsumerTable,Producer/ConsumerStateTable。虽然它们都是基于Redis的,但是它们解决的问题和方法却非常不同。
- 基于ZMQ的服务间通信:现在只在
orchagent
和syncd
的通信中使用了这种通信机制。
虽然大部分的通信机制都支持多消费者的PubSub的模式,但是请特别注意:在SONiC中,所有的通信都是点对点的,即一个生产者对应一个消费者,绝对不会出现一个生产者对应多个消费者的情况!
一旦多消费者出现,那么一个消息的处理逻辑将可能发生在多个进程中,这将导致很大的问题,因为对于任何一种特定的消息,SONiC中只有一个地方来处理,所以这会导致部分消息不可避免的出错或者丢失。
所有这些基础的通信机制的实现都在sonic-swss-common这个repo中的common
目录下。另外在其之上,为了方便各个服务使用,SONiC还在sonic-swss中封装了一层Orch,将常用的表放在其中。
这一章,我们就主要来看看这些通信机制的实现吧!
参考资料
命令行调用
SONiC中的与内核通信最简单的方式就是命令行调用了,其实现放在common/exec.h文件下,且十分简单,接口如下:
// File: common/exec.h
// Namespace: swss
int exec(const std::string &cmd, std::string &stdout);
其中,cmd
是要执行的命令,stdout
是命令执行的输出。这里的exec
函数是一个同步调用,调用者会一直阻塞,直到命令执行完毕。其内部通过调用popen
函数来创建子进程,并且通过fgets
函数来获取输出。不过,虽然这个函数返回了输出,但是基本上并没有人使用,而只是通过返回值来判断是否成功,甚至连错误log中都不会写入输出的结果。
这个函数虽然粗暴,但是使用广泛,特别是在各个*mgrd
服务中,比如portmgrd
中就用它来设置每一个Port的状态等等。
// File: sonic-swss - cfgmgr/portmgr.cpp
bool PortMgr::setPortAdminStatus(const string &alias, const bool up)
{
stringstream cmd;
string res, cmd_str;
// ip link set dev <port_name> [up|down]
cmd << IP_CMD << " link set dev " << shellquote(alias) << (up ? " up" : " down");
cmd_str = cmd.str();
int ret = swss::exec(cmd_str, res);
// ...
为什么说命令行调用是一种通信机制呢?
原因是当*mgrd
服务调用exec
函数对系统进行的修改,会触发下面马上会提到的netlink事件,从而通知其他服务进行相应的修改,比如*syncd
,这样就间接的构成了一种通信。所以这里我们把命令行调用看作一种通信机制能帮助我们以后更好的理解SONiC的各种工作流。
参考资料
Netlink
Netlink是Linux内核中用于内核与用户空间进程之间的一种基于消息的通信机制。它通过套接字接口和自定义的协议族来实现,可以用来传递各种类型的内核消息,包括网络设备状态、路由表更新、防火墙规则变化、系统资源使用情况等等。而SONiC的*sync
服务就大量使用了Netlink的机制来监听系统中网络设备的变化,并将最新的状态同步到Redis中,并通知其他服务进行相应的修改。
Netlink的实现主要在这几个文件中:common/netmsg.*、common/netlink.* 和 common/netdispatcher.*,具体类图如下:
其中:
- Netlink:封装了Netlink的套接字接口,提供了Netlink消息的接口和接收消息的回调。
- NetDispatcher:它是一个单例,提供了Handler注册的接口。当Netlink类接收到原始的消息后,就会调用NetDispatcher将其解析成nl_onject,并根据消息的类型调用相应的Handler。
- NetMsg:Netlink消息Handler的基类,仅提供了onMsg的接口,其中没有实现。
举一个例子,当portsyncd
启动的时候,它会创建一个Netlink对象,用来监听Link相关的状态变化,并且会实现NetMsg的接口,对Link相关的消息进行处理。具体的实现如下:
// File: sonic-swss - portsyncd/portsyncd.cpp
int main(int argc, char **argv)
{
// ...
// Create Netlink object to listen to link messages
NetLink netlink;
netlink.registerGroup(RTNLGRP_LINK);
// Here SONiC request a fulldump of current state, so that it can get the current state of all links
netlink.dumpRequest(RTM_GETLINK);
cout << "Listen to link messages..." << endl;
// ...
// Register handler for link messages
LinkSync sync(&appl_db, &state_db);
NetDispatcher::getInstance().registerMessageHandler(RTM_NEWLINK, &sync);
NetDispatcher::getInstance().registerMessageHandler(RTM_DELLINK, &sync);
// ...
}
上面的LinkSync,就是一个NetMsg的实现,它实现了onMsg接口,用来处理Link相关的消息:
// File: sonic-swss - portsyncd/linksync.h
class LinkSync : public NetMsg
{
public:
LinkSync(DBConnector *appl_db, DBConnector *state_db);
// NetMsg interface
virtual void onMsg(int nlmsg_type, struct nl_object *obj);
// ...
};
// File: sonic-swss - portsyncd/linksync.cpp
void LinkSync::onMsg(int nlmsg_type, struct nl_object *obj)
{
// ...
// Write link state to Redis DB
FieldValueTuple fv("oper_status", oper ? "up" : "down");
vector<FieldValueTuple> fvs;
fvs.push_back(fv);
m_stateMgmtPortTable.set(key, fvs);
// ...
}
参考资料
Redis封装
Redis数据库操作层
第一层,也是最底层,是Redis的数据库操作层,封装了各种基本命令,比如,DB的连接,命令的执行,事件通知的回调接口等等。具体的类图如下:
其中:
- RedisContext:封装并保持着与Redis的连接,当其销毁时会将其连接关闭。
- DBConnector:封装了所有的底层使用到的Redis的命令,比如
SET
、GET
、DEL
等等。 - RedisTransactioner:封装了Redis的事务操作,用于在一个事务中执行多个命令,比如
MULTI
、EXEC
等等。 - RedisPipeline:封装了hiredis的redisAppendFormattedCommand API,提供了一个类似队列的异步的执行Redis命令的接口(虽然大部分使用方法依然是同步的)。它也是少有的对
SCRIPT LOAD
命令进行了封装的类,用于在Redis中加载Lua脚本实现存储过程。SONiC中绝大部分需要执行Lua脚本的类,都会使用这个类来进行加载和调用。 - RedisSelect:它实现了Selectable的接口,用来支持基于epoll的事件通知机制(Event Polling)。主要是在我们收到了Redis的回复,用来触发epoll进行回调(我们最后会更详细的介绍)。
- SonicDBConfig:这个类是一个“静态类”,它主要实现了SONiC DB的配置文件的读取和解析。其他的数据库操作类,如果需要任何的配置信息,都会通过这个类来获取。
表(Table)抽象层
在Redis数据库操作层之上,便是SONiC自己利用Redis中间的Key建立的表(Table)的抽象了,因为每一个Redis的Key的格式都是<table-name><separator><key-name>
,所以SONiC在访问数据库时需要对其进行一次转换(没有印象的小伙伴可以移步我之前的博客了解更多的信息)。
相关类的主要类图如下:
其中关键的类有三个:
- TableBase:这个类是所有表的基类,它主要封装了表的基本信息,如表的名字,Redis Key的打包,每个表发生修改时用于通信的Channel的名字,等等。
- Table:这个类就是对于每个表增删改查的封装了,里面包含了表的名称和分隔符,这样就可以在调用时构造最终的key了。
- ConsumerTableBase:这个类是各种SubscriptionTable的基类,里面主要是封装了一个简单的队列和其pop操作(对,只有pop,没有push),用来给上层调用。
参考资料
通信层
在Redis的封装和表抽象之上,便是SONiC的通信层了,由于需求的不同,这一层中提供了四种不同的PubSub的封装,用于服务间的通信。
SubscribeStateTable
最直接的就是SubscriberStateTable了。
它的原理是利用Redis数据库中自带的keyspace消息通知机制 [4] —— 当数据库中的任何一个key对应的值发生了变化,就会触发Redis发送两个keyspace的事件通知,一个是__keyspace@<db-id>__:<key>
下的<op>
事件,一个是__keyspace@<db-id>__:<op>
下的<key>>
事件,比如,在数据库0中删除了一个key,那么就会触发两个事件通知:
PUBLISH __keyspace@0__:foo del
PUBLISH __keyevent@0__:del foo
而SubscriberStateTable就是监听了第一个事件通知,然后调用相应的回调函数。和其直接相关的主要的类的类图如下,这里可以看到它继承了ConsumerTableBase,因为它是Redis的消息的Consumer:
在初始化时,我们可以看到它是如何订阅Redis的事件通知的:
// File: sonic-swss-common - common/subscriberstatetable.cpp
SubscriberStateTable::SubscriberStateTable(DBConnector *db, const string &tableName, int popBatchSize, int pri)
: ConsumerTableBase(db, tableName, popBatchSize, pri), m_table(db, tableName)
{
m_keyspace = "__keyspace@";
m_keyspace += to_string(db->getDbId()) + "__:" + tableName + m_table.getTableNameSeparator() + "*";
psubscribe(m_db, m_keyspace);
// ...
其事件接收和分发主要由两个函数负责:
readData()
负责将redis中待读取的事件读取出来,并放入ConsumerTableBase中的队列中pops()
:负责将队列中的原始事件取出来,并且进行解析,然后通过函数参数传递给调用方
// File: sonic-swss-common - common/subscriberstatetable.cpp
uint64_t SubscriberStateTable::readData()
{
// ...
reply = nullptr;
int status;
do {
status = redisGetReplyFromReader(m_subscribe->getContext(), reinterpret_cast<void**>(&reply));
if(reply != nullptr && status == REDIS_OK) {
m_keyspace_event_buffer.emplace_back(make_shared<RedisReply>(reply));
}
} while(reply != nullptr && status == REDIS_OK);
// ...
return 0;
}
void SubscriberStateTable::pops(deque<KeyOpFieldsValuesTuple> &vkco, const string& /*prefix*/)
{
vkco.clear();
// ...
// Pop from m_keyspace_event_buffer, which is filled by readData()
while (auto event = popEventBuffer()) {
KeyOpFieldsValuesTuple kco;
// Parsing here ...
vkco.push_back(kco);
}
m_keyspace_event_buffer.clear();
}
NotificationProducer / NotificationConsumer
说到消息通信,我们很容易就会联想到消息队列,这就是我们的第二种通信方式 —— NotificationProducer和NotificationConsumer。
这种通信方式通过Redis的自带的PubSub来实现,主要是对PUBLISH
和SUBSCRIBE
命令的包装,很有限的应用在最简单的通知型的场景中,比如orchagent中的timeout check, restart check之类,非传递用户配置和数据的场景:
这种通信模式下,消息的发送方Producer,主要会做两件事情:一是将消息打包成JSON格式,二是调用Redis的PUBLISH
命令将消息发送出去。而且由于PUBLISH
命令只能携带一个消息,所以请求中的op
和data
字段会被放在values
的最前面,然后再调用buildJson
函数将其打包成一个JSON数组的格式:
int64_t swss::NotificationProducer::send(const std::string &op, const std::string &data, std::vector<FieldValueTuple> &values)
{
// Pack the op and data into values array, then pack everything into a JSON string as the message
FieldValueTuple opdata(op, data);
values.insert(values.begin(), opdata);
std::string msg = JSon::buildJson(values);
values.erase(values.begin());
// Publish message to Redis channel
RedisCommand command;
command.format("PUBLISH %s %s", m_channel.c_str(), msg.c_str());
// ...
RedisReply reply = m_pipe->push(command);
reply.checkReplyType(REDIS_REPLY_INTEGER);
return reply.getReply<long long int>();
}
接收方则是利用SUBSCRIBE
命令来接收所有的通知:
void swss::NotificationConsumer::subscribe()
{
// ...
m_subscribe = new DBConnector(m_db->getDbId(),
m_db->getContext()->unix_sock.path,
NOTIFICATION_SUBSCRIBE_TIMEOUT);
// ...
// Subscribe to Redis channel
std::string s = "SUBSCRIBE " + m_channel;
RedisReply r(m_subscribe, s, REDIS_REPLY_ARRAY);
}
ProducerTable / ConsumerTable
我们可以看到NotificationProducer/Consumer实现简单粗暴,但是由于API的限制 [8],它并不适合用来传递数据,所以,SONiC中提供了一种和它非常接近的另外一种基于消息队列的通信机制 —— ProducerTable和ConsumerTable。
这种通信方式通过Redis的List来实现,和Notification不同的地方在于,发布给Channel中的消息非常的简单(单字符"G"),所有的数据都存储在List中,从而解决了Notification中消息大小限制的问题。在SONiC中,它主要用在FlexCounter,syncd
服务和ASIC_DB
中:
-
消息格式:每条消息都是一个(Key, FieldValuePairs, Op)的三元组,如果用JSON来表达这个消息,那么它的格式如下:(这里的Key是Table中数据的Key,被操作的数据是Hash,所以Field就是Hash中的Field,Value就是Hash中的Value了,也就是说一个消息可以对很多个Field进行操作)
[ "Key", "[\"Field1\", \"Value1\", \"Field2", \"Value2\", ...]", "Op" ]
-
Enqueue:ProducerTable通过Lua脚本将消息三元组原子的写入消息队列中(Key =
<table-name>_KEY_VALUE_OP_QUEUE
,并且发布更新通知到特定的Channel(Key =<table-name>_CHANNEL
)中。 -
Pop:ConsumerTable也通过Lua脚本从消息队列中原子的读取消息三元组,并在读取过程中将其中请求的改动真正的写入到数据库中。
其主要类图如下,这里我们可以看到在ProducerTable中的m_shaEnqueue
和ConsumerTable中的m_shaPop
,它们就是上面我们提到的这两个Lua脚本在加载时获得的SHA了,而之后我们就可以使用Redis的EVALSHA
命令对他们进行原子的调用了:
ProducerTable的核心逻辑如下,我们可以看到对Values的JSON打包,和使用EVALSHA
来进行Lua脚本的调用:
// File: sonic-swss-common - common/producertable.cpp
ProducerTable::ProducerTable(RedisPipeline *pipeline, const string &tableName, bool buffered)
// ...
{
string luaEnque =
"redis.call('LPUSH', KEYS[1], ARGV[1], ARGV[2], ARGV[3]);"
"redis.call('PUBLISH', KEYS[2], ARGV[4]);";
m_shaEnque = m_pipe->loadRedisScript(luaEnque);
}
void ProducerTable::set(const string &key, const vector<FieldValueTuple> &values, const string &op, const string &prefix)
{
enqueueDbChange(key, JSon::buildJson(values), "S" + op, prefix);
}
void ProducerTable::del(const string &key, const string &op, const string &prefix)
{
enqueueDbChange(key, "{}", "D" + op, prefix);
}
void ProducerTable::enqueueDbChange(const string &key, const string &value, const string &op, const string& /* prefix */)
{
RedisCommand command;
command.format(
"EVALSHA %s 2 %s %s %s %s %s %s",
m_shaEnque.c_str(),
getKeyValueOpQueueTableName().c_str(),
getChannelName(m_pipe->getDbId()).c_str(),
key.c_str(),
value.c_str(),
op.c_str(),
"G");
m_pipe->push(command, REDIS_REPLY_NIL);
}
而另一侧的ConsumerTable就稍稍复杂一点,因为其支持的op类型很多,所以逻辑都写在了一个单独的文件中(common/consumer_table_pops.lua
),我们这里就不贴代码了,有兴趣的同学可以自己去看看。
// File: sonic-swss-common - common/consumertable.cpp
ConsumerTable::ConsumerTable(DBConnector *db, const string &tableName, int popBatchSize, int pri)
: ConsumerTableBase(db, tableName, popBatchSize, pri)
, TableName_KeyValueOpQueues(tableName)
, m_modifyRedis(true)
{
std::string luaScript = loadLuaScript("consumer_table_pops.lua");
m_shaPop = loadRedisScript(db, luaScript);
// ...
}
void ConsumerTable::pops(deque<KeyOpFieldsValuesTuple> &vkco, const string &prefix)
{
// Note that here we are processing the messages in bulk with POP_BATCH_SIZE!
RedisCommand command;
command.format(
"EVALSHA %s 2 %s %s %d %d",
m_shaPop.c_str(),
getKeyValueOpQueueTableName().c_str(),
(prefix+getTableName()).c_str(),
POP_BATCH_SIZE,
RedisReply r(m_db, command, REDIS_REPLY_ARRAY);
vkco.clear();
// Parse and pack the messages in bulk
// ...
}
ProducerStateTable / ConsumerStateTable
Producer/ConsumerTable虽然直观,而且保序,但是它一个消息只能处理一个Key,并且还需要JSON的序列化,然而很多时候我们并用不到保序的功能,反而更需要更大的吞吐量,所以为了优化性能,SONiC就引入了第四种通信方式,也是最常用的通信方式:ProducerStateTable和ConsumerStateTable。
与ProducerTable不同,ProducerStateTable使用Hash的方式来存储消息,而不是List。这样虽然不能保证消息的顺序,但是却可以很好的提升性能!首先,我们省下了JSON的序列化的开销,其次,对于同一个Key下的相同的Field如果被变更多次,那么只需要保留最后一次的变更,这样就将关于这个Key的所有变更消息就合并成了一条,减少了很多不必要的消息处理。
Producer/ConsumerStateTable的底层实现相比于Producer/ConsumerTable也更加复杂一些。其相关联的类的主要类图如下,这里我们依然可以看到它的实现是通过EVALSHA
调用Lua脚本来实现的,m_shaSet
和m_shaDel
就是用来存放修改和发送消息的,而另一边m_shaPop
就是用来获取消息的:
在传递消息时:
-
首先,每个消息会被存放成两个部分:一个是KEY_SET,用来保存当前有哪些Key发生了修改,它以Set的形式存放在
<table-name_KEY_SET>
的key下,另一个是所有被修改的Key的内容,它以Hash的形式存放在_<redis-key-name>
的key下。 -
然后,消息存放之后Producer如果发现是新的Key,那么就是调用
PUBLISH
命令,来通知<table-name>_CHANNEL@<db-id>
Channel,有新的Key出现了。// File: sonic-swss-common - common/producerstatetable.cpp ProducerStateTable::ProducerStateTable(RedisPipeline *pipeline, const string &tableName, bool buffered) : TableBase(tableName, SonicDBConfig::getSeparator(pipeline->getDBConnector())) , TableName_KeySet(tableName) // ... { string luaSet = "local added = redis.call('SADD', KEYS[2], ARGV[2])\n" "for i = 0, #KEYS - 3 do\n" " redis.call('HSET', KEYS[3 + i], ARGV[3 + i * 2], ARGV[4 + i * 2])\n" "end\n" " if added > 0 then \n" " redis.call('PUBLISH', KEYS[1], ARGV[1])\n" "end\n"; m_shaSet = m_pipe->loadRedisScript(luaSet);
-
最后,Consumer会通过
SUBSCRIBE
命令来订阅<table-name>_CHANNEL@<db-id>
Channel,一旦有新的消息到来,就会使用Lua脚本调用HGETALL
命令来获取所有的Key,并将其中的值读取出来并真正的写入到数据库中去。ConsumerStateTable::ConsumerStateTable(DBConnector *db, const std::string &tableName, int popBatchSize, int pri) : ConsumerTableBase(db, tableName, popBatchSize, pri) , TableName_KeySet(tableName) { std::string luaScript = loadLuaScript("consumer_state_table_pops.lua"); m_shaPop = loadRedisScript(db, luaScript); // ... subscribe(m_db, getChannelName(m_db->getDbId())); // ...
为了方便理解,我们这里举一个例子:启用Port Ethernet0:
-
首先,我们在命令行下调用
config interface startup Ethernet0
来启用Ethernet0,这会导致portmgrd
通过ProducerStateTable向APP_DB发送状态更新消息,如下:EVALSHA "<hash-of-set-lua>" "6" "PORT_TABLE_CHANNEL@0" "PORT_TABLE_KEY_SET" "_PORT_TABLE:Ethernet0" "_PORT_TABLE:Ethernet0" "_PORT_TABLE:Ethernet0" "_PORT_TABLE:Ethernet0" "G" "Ethernet0" "alias" "Ethernet5/1" "index" "5" "lanes" "9,10,11,12" "speed" "40000"
这个命令会在其中调用如下的命令来创建和发布消息:
SADD "PORT_TABLE_KEY_SET" "_PORT_TABLE:Ethernet0" HSET "_PORT_TABLE:Ethernet0" "alias" "Ethernet5/1" HSET "_PORT_TABLE:Ethernet0" "index" "5" HSET "_PORT_TABLE:Ethernet0" "lanes" "9,10,11,12" HSET "_PORT_TABLE:Ethernet0" "speed" "40000" PUBLISH "PORT_TABLE_CHANNEL@0" "_PORT_TABLE:Ethernet0"
所以最终这个消息会在APPL_DB中被存放成如下的形式:
PORT_TABLE_KEY_SET: _PORT_TABLE:Ethernet0 _PORT_TABLE:Ethernet0: alias: Ethernet5/1 index: 5 lanes: 9,10,11,12 speed: 40000
-
当ConsumerStateTable收到消息后,也会调用
EVALSHA
命令来执行Lua脚本,如下:EVALSHA "<hash-of-pop-lua>" "3" "PORT_TABLE_KEY_SET" "PORT_TABLE:" "PORT_TABLE_DEL_SET" "8192" "_"
和Producer类似,这个脚本会执行如下命令,将
PORT_TABLE_KEY_SET
中的key,也就是_PORT_TABLE:Ethernet0
读取出来,然后再将其对应的Hash读取出来,并更新到PORT_TABLE:Ethernet0
去,同时将_PORT_TABLE:Ethernet0
从数据库和PORT_TABLE_KEY_SET
中删除。SPOP "PORT_TABLE_KEY_SET" "_PORT_TABLE:Ethernet0" HGETALL "_PORT_TABLE:Ethernet0" HSET "PORT_TABLE:Ethernet0" "alias" "Ethernet5/1" HSET "PORT_TABLE:Ethernet0" "index" "5" HSET "PORT_TABLE:Ethernet0" "lanes" "9,10,11,12" HSET "PORT_TABLE:Ethernet0" "speed" "40000" DEL "_PORT_TABLE:Ethernet0"
到这里,数据的更新才算是完成了。
参考资料
- SONiC Architecture
- Github repo: sonic-swss
- Github repo: sonic-swss-common
- Redis keyspace notifications
- Redis Transactions
- Redis Atomicity with Lua
- Redis hashes
- Redis client handling
基于ZMQ的通信
服务层 - Orch
最后,为了方便各个服务使用,SONiC还在通信层上进行了更进一步的封装,为各个服务提供了一个基类:Orch。
由于有了上面这些封装,Orch中关于消息通信的封装就相对简单了,主要的类图如下:
可以看到,Orch主要是封装了SubscriberStateTable
和ConsumerStateTable
来简化和统一消息的订阅,核心代码非常简单,就是根据不同的数据库类型来创建不同的Consumer,如下:
void Orch::addConsumer(DBConnector *db, string tableName, int pri)
{
if (db->getDbId() == CONFIG_DB || db->getDbId() == STATE_DB || db->getDbId() == CHASSIS_APP_DB) {
addExecutor(
new Consumer(
new SubscriberStateTable(db, tableName, TableConsumable::DEFAULT_POP_BATCH_SIZE, pri),
this,
tableName));
} else {
addExecutor(
new Consumer(
new ConsumerStateTable(db, tableName, gBatchSize, pri),
this,
tableName));
}
}
参考资料
事件分发和错误处理
基于epoll的事件分发机制
和很多的Linux服务一样,SONiC底层使用了epoll作为事件分发机制:
- 所有需要支持事件分发的类都需要继承
Selectable
类,并实现两个最核心的函数:int getFd();
(用于返回epoll能用来监听事件的fd)和uint64_t readData()
(用于在监听到事件到来之后进行读取)。而对于一般服务而言,这个fd就是redis通信使用的fd,所以getFd()
函数的调用,都会被最终转发到Redis的库中。 - 所有需要参与事件分发的对象,都需要注册到
Select
类中,这个类会将所有的Selectable
对象的fd注册到epoll中,并在事件到来时调用Selectable
的readData()
函数。
其类图如下:
在Select类中,我们可以很容易的找到其最核心的代码,实现也非常的简单:
int Select::poll_descriptors(Selectable **c, unsigned int timeout, bool interrupt_on_signal = false)
{
int sz_selectables = static_cast<int>(m_objects.size());
std::vector<struct epoll_event> events(sz_selectables);
int ret;
while(true) {
ret = ::epoll_wait(m_epoll_fd, events.data(), sz_selectables, timeout);
// ...
}
// ...
for (int i = 0; i < ret; ++i)
{
int fd = events[i].data.fd;
Selectable* sel = m_objects[fd];
sel->readData();
// error handling here ...
m_ready.insert(sel);
}
while (!m_ready.empty())
{
auto sel = *m_ready.begin();
m_ready.erase(sel);
// After update callback ...
return Select::OBJECT;
}
return Select::TIMEOUT;
}
然而,问题来了…… 回调呢?我们上面提过,readData()
只是把消息读出来放在一个待处理队列中,并不会真正的处理消息,真正的消息处理需要调用pops()
函数,将消息拿出来处理,所以什么地方会调用每一个上层封装的消息处理呢?
这里我们还是找到我们的老朋友portmgrd
的main
函数,从下面简化的代码中,我们可以看到和一般的Event Loop实现不同,SONiC中,最后的事件处理不是通过回调来实现的,而是需要最外层的Event Loop来主动调用完成:
int main(int argc, char **argv)
{
// ...
// Create PortMgr, which implements Orch interface.
PortMgr portmgr(&cfgDb, &appDb, &stateDb, cfg_port_tables);
vector<Orch *> cfgOrchList = {&portmgr};
// Create Select object for event loop and add PortMgr to it.
swss::Select s;
for (Orch *o : cfgOrchList) {
s.addSelectables(o->getSelectables());
}
// Event loop
while (true)
{
Selectable *sel;
int ret;
// When anyone of the selectables gets signaled, select() will call
// into readData() and fetch all events, then return.
ret = s.select(&sel, SELECT_TIMEOUT);
// ...
// Then, we call into execute() explicitly to process all events.
auto *c = (Executor *)sel;
c->execute();
}
return -1;
}
错误处理
关于Event Loop我们还有一个问题,那就是错误处理,比如,如果Redis的命令执行出错了,连接断开了,故障了等等的情况下,我们的服务会发生什么呢?
从代码上来看,SONiC中的错误处理是非常简单的,就是直接抛出异常(比如,获取命令执行结果的代码,如下),然后在Event Loop中捕获异常,打印日志,接着继续执行。
RedisReply::RedisReply(RedisContext *ctx, const RedisCommand& command)
{
int rc = redisAppendFormattedCommand(ctx->getContext(), command.c_str(), command.length());
if (rc != REDIS_OK)
{
// The only reason of error is REDIS_ERR_OOM (Out of memory)
// ref: https://github.com/redis/hiredis/blob/master/hiredis.c
throw bad_alloc();
}
rc = redisGetReply(ctx->getContext(), (void**)&m_reply);
if (rc != REDIS_OK)
{
throw RedisError("Failed to redisGetReply with " + string(command.c_str()), ctx->getContext());
}
guard([&]{checkReply();}, command.c_str());
}
关于异常和错误的种类及其原因,在代码里面并没有看到用于统计和Telemetry的代码,所以监控上说是比较薄弱的。另外还需要考虑数据出错的场景,比如数据库写到一半突然断开导致的脏数据,不过简单的重启相关的*syncd
和*mgrd
服务可能可以解决此类问题,因为启动时会进行全量同步。
参考资料
核心组件解析
这一章,我们会从代码的层面上来深入的分析一下SONiC中一些比较有代表性的核心组件和它们的工作流。
为了方便阅读和理解,所有的代码都只是列出了最核心的代码来展现流程,并不是完整的代码,如果需要查看完整代码,请参考仓库中的原始代码。
另外,每个代码块的开头都给出了相关文件的路径,其使用的是仓库均为SONiC的主仓库:sonic-buildimage。
Syncd和SAI
Syncd容器是SONiC中专门负责管理ASIC的容器,其中核心进程syncd
负责与Redis数据库沟通,加载SAI并与其交互,以完成ASIC的初始化,配置和状态上报的处理等等。
由于SONiC中大量的工作流最后都需要通过Syncd和SAI来和ASIC进行交互,所以这一部分也就成为了这些工作流的公共部分,所以,在展开其他工作流之前,我们先来看一下Syncd和SAI是如何工作的。
Syncd启动流程
syncd
进程的入口在syncd_main.cpp
中的syncd_main
函数,其启动的整体流程大致分为两部分。
第一部分是创建各个对象,并进行初始化:
sequenceDiagram autonumber participant SDM as syncd_main participant SD as Syncd participant SAI as VendorSai SDM->>+SD: 调用构造函数 SD->>SD: 加载和解析命令行参数和配置文件 SD->>SD: 创建数据库相关对象,如:<br/>ASIC_DB Connector和FlexCounterManager SD->>SD: 创建MDIO IPC服务器 SD->>SD: 创建SAI上报处理逻辑 SD->>SD: 创建RedisSelectableChannel用于接收Redis通知 SD->>-SAI: 初始化SAI
第二个部分是启动主循环,并且处理初始化事件:
sequenceDiagram autonumber box purple 主线程 participant SDM as syncd_main participant SD as Syncd participant SAI as VendorSai end box darkblue 通知处理线程 participant NP as NotificationProcessor end box darkgreen MDIO IPC服务器线程 participant MIS as MdioIpcServer end SDM->>+SD: 启动主线程循环 SD->>NP: 启动SAI上报处理线程 NP->>NP: 开始通知处理循环 SD->>MIS: 启动MDIO IPC服务器线程 MIS->>MIS: 开始MDIO IPC服务器事件循环 SD->>SD: 初始化并启动事件分发机制,开始主循环 loop 处理事件 alt 如果是创建Switch的事件或者是WarmBoot SD->>SAI: 创建Switch对象,设置通知回调 else 如果是其他事件 SD->>SD: 处理事件 end end SD->>-SDM: 退出主循环返回
然后我们再从代码的角度来更加仔细的看一下这个流程。
syncd_main函数
syncd_main
函数本身非常简单,主要逻辑就是创建Syncd对象,然后调用其run
方法:
// File: src/sonic-sairedis/syncd/syncd_main.cpp
int syncd_main(int argc, char **argv)
{
auto vendorSai = std::make_shared<VendorSai>();
auto syncd = std::make_shared<Syncd>(vendorSai, commandLineOptions, isWarmStart);
syncd->run();
return EXIT_SUCCESS;
}
其中,Syncd
对象的构造函数负责初始化Syncd
中的各个功能,而run
方法则负责启动Syncd的主循环。
Syncd构造函数
Syncd
对象的构造函数负责创建或初始化Syncd
中的各个功能,比如用于连接数据库的对象,统计管理,和ASIC通知的处理逻辑等等,其主要代码如下:
// File: src/sonic-sairedis/syncd/Syncd.cpp
Syncd::Syncd(
_In_ std::shared_ptr<sairedis::SaiInterface> vendorSai,
_In_ std::shared_ptr<CommandLineOptions> cmd,
_In_ bool isWarmStart):
m_vendorSai(vendorSai),
...
{
...
// Load context config
auto ccc = sairedis::ContextConfigContainer::loadFromFile(m_commandLineOptions->m_contextConfig.c_str());
m_contextConfig = ccc->get(m_commandLineOptions->m_globalContext);
...
// Create FlexCounter manager
m_manager = std::make_shared<FlexCounterManager>(m_vendorSai, m_contextConfig->m_dbCounters);
// Create DB related objects
m_dbAsic = std::make_shared<swss::DBConnector>(m_contextConfig->m_dbAsic, 0);
m_mdioIpcServer = std::make_shared<MdioIpcServer>(m_vendorSai, m_commandLineOptions->m_globalContext);
m_selectableChannel = std::make_shared<sairedis::RedisSelectableChannel>(m_dbAsic, ASIC_STATE_TABLE, REDIS_TABLE_GETRESPONSE, TEMP_PREFIX, modifyRedis);
// Create notification processor and handler
m_notifications = std::make_shared<RedisNotificationProducer>(m_contextConfig->m_dbAsic);
m_client = std::make_shared<RedisClient>(m_dbAsic);
m_processor = std::make_shared<NotificationProcessor>(m_notifications, m_client, std::bind(&Syncd::syncProcessNotification, this, _1));
m_handler = std::make_shared<NotificationHandler>(m_processor);
m_sn.onFdbEvent = std::bind(&NotificationHandler::onFdbEvent, m_handler.get(), _1, _2);
m_sn.onNatEvent = std::bind(&NotificationHandler::onNatEvent, m_handler.get(), _1, _2);
// Init many other event handlers here
m_handler->setSwitchNotifications(m_sn.getSwitchNotifications());
...
// Initialize SAI
sai_status_t status = vendorSai->initialize(0, &m_test_services);
...
}
SAI的初始化与VendorSai
Syncd
初始化的最后也是最重要的一步,就是对SAI进行初始化。在核心组件的SAI介绍中,我们简单的展示了SAI的初始化,实现,以及它是如何为SONiC提供不同平台的支持,所以这里我们主要来看看Syncd
是如何对SAI进行封装和调用的。
Syncd
使用VendorSai
来对SAI的所有API进行封装,方便上层调用。其初始化过程也非常直接,基本就是对上面两个函数的直接调用和错误处理,如下:
// File: src/sonic-sairedis/syncd/VendorSai.cpp
sai_status_t VendorSai::initialize(
_In_ uint64_t flags,
_In_ const sai_service_method_table_t *service_method_table)
{
...
// Initialize SAI
memcpy(&m_service_method_table, service_method_table, sizeof(m_service_method_table));
auto status = sai_api_initialize(flags, service_method_table);
// If SAI is initialized successfully, query all SAI API methods.
// sai_metadata_api_query will also update all extern global sai_*_api variables, so we can also use
// sai_metadata_get_object_type_info to get methods for a specific SAI object type.
if (status == SAI_STATUS_SUCCESS) {
memset(&m_apis, 0, sizeof(m_apis));
int failed = sai_metadata_apis_query(sai_api_query, &m_apis);
...
}
...
return status;
}
当获取好所有的SAI API之后,我们就可以通过VendorSai
对象来调用SAI的API了。当前调用SAI的API方式主要有两种。
第一种是通过sai_object_type_into_t
来调用,它类似于为所有的SAI Object实现了一个虚表,如下:
// File: src/sonic-sairedis/syncd/VendorSai.cpp
sai_status_t VendorSai::set(
_In_ sai_object_type_t objectType,
_In_ sai_object_id_t objectId,
_In_ const sai_attribute_t *attr)
{
...
auto info = sai_metadata_get_object_type_info(objectType);
sai_object_meta_key_t mk = { .objecttype = objectType, .objectkey = { .key = { .object_id = objectId } } };
return info->set(&mk, attr);
}
另外一种是通过保存在VendorSai
对象中的m_apis
来调用,这种方式更加直接,但是调用前需要先根据SAI Object的类型来调用不同的API。
sai_status_t VendorSai::getStatsExt(
_In_ sai_object_type_t object_type,
_In_ sai_object_id_t object_id,
_In_ uint32_t number_of_counters,
_In_ const sai_stat_id_t *counter_ids,
_In_ sai_stats_mode_t mode,
_Out_ uint64_t *counters)
{
sai_status_t (*ptr)(
_In_ sai_object_id_t port_id,
_In_ uint32_t number_of_counters,
_In_ const sai_stat_id_t *counter_ids,
_In_ sai_stats_mode_t mode,
_Out_ uint64_t *counters);
switch ((int)object_type)
{
case SAI_OBJECT_TYPE_PORT:
ptr = m_apis.port_api->get_port_stats_ext;
break;
case SAI_OBJECT_TYPE_ROUTER_INTERFACE:
ptr = m_apis.router_interface_api->get_router_interface_stats_ext;
break;
case SAI_OBJECT_TYPE_POLICER:
ptr = m_apis.policer_api->get_policer_stats_ext;
break;
...
default:
SWSS_LOG_ERROR("not implemented, FIXME");
return SAI_STATUS_FAILURE;
}
return ptr(object_id, number_of_counters, counter_ids, mode, counters);
}
可以明显看出,第一种调用方式代码要精炼和直观许多。
Syncd主循环
Syncd
的主循环也是使用的SONiC中标准的事件分发机制:在启动时,Syncd
会将所有用于事件处理的Selectable
对象注册到用于获取事件的Select
对象中,然后在主循环中调用Select
的select
方法,等待事件的发生。核心代码如下:
// File: src/sonic-sairedis/syncd/Syncd.cpp
void Syncd::run()
{
volatile bool runMainLoop = true;
std::shared_ptr<swss::Select> s = std::make_shared<swss::Select>();
onSyncdStart(m_commandLineOptions->m_startType == SAI_START_TYPE_WARM_BOOT);
// Start notification processing thread
m_processor->startNotificationsProcessingThread();
// Start MDIO threads
for (auto& sw: m_switches) { m_mdioIpcServer->setSwitchId(sw.second->getRid()); }
m_mdioIpcServer->startMdioThread();
// Registering selectable for event polling
s->addSelectable(m_selectableChannel.get());
s->addSelectable(m_restartQuery.get());
s->addSelectable(m_flexCounter.get());
s->addSelectable(m_flexCounterGroup.get());
// Main event loop
while (runMainLoop)
{
swss::Selectable *sel = NULL;
int result = s->select(&sel);
...
if (sel == m_restartQuery.get()) {
// Handling switch restart event and restart switch here.
} else if (sel == m_flexCounter.get()) {
processFlexCounterEvent(*(swss::ConsumerTable*)sel);
} else if (sel == m_flexCounterGroup.get()) {
processFlexCounterGroupEvent(*(swss::ConsumerTable*)sel);
} else if (sel == m_selectableChannel.get()) {
// Handle redis updates here.
processEvent(*m_selectableChannel.get());
} else {
SWSS_LOG_ERROR("select failed: %d", result);
}
...
}
...
}
其中,m_selectableChannel
就是主要负责处理Redis数据库中的事件的对象。它使用ProducerTable / ConsumerTable的方式与Redis数据库进行交互,所以,所有orchagent
发送过来的操作都会以三元组的形式保存在Redis中的list中,等待Syncd
的处理。其核心定义如下:
// File: src/sonic-sairedis/meta/RedisSelectableChannel.h
class RedisSelectableChannel: public SelectableChannel
{
public:
RedisSelectableChannel(
_In_ std::shared_ptr<swss::DBConnector> dbAsic,
_In_ const std::string& asicStateTable,
_In_ const std::string& getResponseTable,
_In_ const std::string& tempPrefix,
_In_ bool modifyRedis);
public: // SelectableChannel overrides
virtual bool empty() override;
...
public: // Selectable overrides
virtual int getFd() override;
virtual uint64_t readData() override;
...
private:
std::shared_ptr<swss::DBConnector> m_dbAsic;
std::shared_ptr<swss::ConsumerTable> m_asicState;
std::shared_ptr<swss::ProducerTable> m_getResponse;
...
};
另外,在主循环启动时,Syncd
还会额外启动两个线程:
- 用于接收ASIC上报通知的通知处理线程:
m_processor->startNotificationsProcessingThread();
- 用于处理MDIO通信的MDIO IPC处理线程:
m_mdioIpcServer->startMdioThread();
它们的细节我们在初始化的部分不做过多展开,等后面介绍相关工作流时再来详细介绍。
创建Switch对象,初始化通知机制
在主循环启动后,Syncd
就会开始调用SAI的API来创建Switch对象,这里的入口有两个,一个是ASIC_DB收到创建Switch的通知,另外一个是Warm Boot时,Syncd
来主动调用,但是创建Switch这一步的内部流程都类似。
在这一步中间,有一个很重要的步骤,就是初始化SAI内部实现中的通知回调,将我们之前已经创建好的通知处理逻辑传递给SAI的实现,比如FDB的事件等等。这些回调函数会被当做Switch的属性(Attributes)通过参数的形式传给SAI的create_switch
方法,SAI的实现会将其保存起来,这样就可以在事件发生时调用回调函数,来通知Syncd
了。这里的核心代码如下:
// File: src/sonic-sairedis/syncd/Syncd.cpp
sai_status_t Syncd::processQuadEvent(
_In_ sai_common_api_t api,
_In_ const swss::KeyOpFieldsValuesTuple &kco)
{
// Parse event into SAI object
sai_object_meta_key_t metaKey;
...
SaiAttributeList list(metaKey.objecttype, values, false);
sai_attribute_t *attr_list = list.get_attr_list();
uint32_t attr_count = list.get_attr_count();
// Update notifications pointers in attribute list
if (metaKey.objecttype == SAI_OBJECT_TYPE_SWITCH && (api == SAI_COMMON_API_CREATE || api == SAI_COMMON_API_SET))
{
m_handler->updateNotificationsPointers(attr_count, attr_list);
}
if (isInitViewMode())
{
// ProcessQuadEventInInitViewMode will eventually call into VendorSai, which calls create_swtich function in SAI.
sai_status_t status = processQuadEventInInitViewMode(metaKey.objecttype, strObjectId, api, attr_count, attr_list);
syncUpdateRedisQuadEvent(status, api, kco);
return status;
}
...
}
// File: src/sonic-sairedis/syncd/NotificationHandler.cpp
void NotificationHandler::updateNotificationsPointers(_In_ uint32_t attr_count, _In_ sai_attribute_t *attr_list) const
{
for (uint32_t index = 0; index < attr_count; ++index) {
...
sai_attribute_t &attr = attr_list[index];
switch (attr.id) {
...
case SAI_SWITCH_ATTR_SHUTDOWN_REQUEST_NOTIFY:
attr.value.ptr = (void*)m_switchNotifications.on_switch_shutdown_request;
break;
case SAI_SWITCH_ATTR_FDB_EVENT_NOTIFY:
attr.value.ptr = (void*)m_switchNotifications.on_fdb_event;
break;
...
}
...
}
}
// File: src/sonic-sairedis/syncd/Syncd.cpp
// Call stack: processQuadEvent
// -> processQuadEventInInitViewMode
// -> processQuadInInitViewModeCreate
// -> onSwitchCreateInInitViewMode
void Syncd::onSwitchCreateInInitViewMode(_In_ sai_object_id_t switchVid, _In_ uint32_t attr_count, _In_ const sai_attribute_t *attr_list)
{
if (m_switches.find(switchVid) == m_switches.end()) {
sai_object_id_t switchRid;
sai_status_t status;
status = m_vendorSai->create(SAI_OBJECT_TYPE_SWITCH, &switchRid, 0, attr_count, attr_list);
...
m_switches[switchVid] = std::make_shared<SaiSwitch>(switchVid, switchRid, m_client, m_translator, m_vendorSai);
m_mdioIpcServer->setSwitchId(switchRid);
...
}
...
}
从Mellanox的SAI实现,我们可以看到其具体的保存的方法:
static sai_status_t mlnx_create_switch(_Out_ sai_object_id_t * switch_id,
_In_ uint32_t attr_count,
_In_ const sai_attribute_t *attr_list)
{
...
status = find_attrib_in_list(attr_count, attr_list, SAI_SWITCH_ATTR_SWITCH_STATE_CHANGE_NOTIFY, &attr_val, &attr_idx);
if (!SAI_ERR(status)) {
g_notification_callbacks.on_switch_state_change = (sai_switch_state_change_notification_fn)attr_val->ptr;
}
status = find_attrib_in_list(attr_count, attr_list, SAI_SWITCH_ATTR_SHUTDOWN_REQUEST_NOTIFY, &attr_val, &attr_idx);
if (!SAI_ERR(status)) {
g_notification_callbacks.on_switch_shutdown_request =
(sai_switch_shutdown_request_notification_fn)attr_val->ptr;
}
status = find_attrib_in_list(attr_count, attr_list, SAI_SWITCH_ATTR_FDB_EVENT_NOTIFY, &attr_val, &attr_idx);
if (!SAI_ERR(status)) {
g_notification_callbacks.on_fdb_event = (sai_fdb_event_notification_fn)attr_val->ptr;
}
status = find_attrib_in_list(attr_count, attr_list, SAI_SWITCH_ATTR_PORT_STATE_CHANGE_NOTIFY, &attr_val, &attr_idx);
if (!SAI_ERR(status)) {
g_notification_callbacks.on_port_state_change = (sai_port_state_change_notification_fn)attr_val->ptr;
}
status = find_attrib_in_list(attr_count, attr_list, SAI_SWITCH_ATTR_PACKET_EVENT_NOTIFY, &attr_val, &attr_idx);
if (!SAI_ERR(status)) {
g_notification_callbacks.on_packet_event = (sai_packet_event_notification_fn)attr_val->ptr;
}
...
}
ASIC状态更新
ASIC状态更新是Syncd
中最重要的工作流之一,当orchagent
发现任何变化并开始修改ASIC_DB时,就会触发该工作流,通过SAI来对ASIC进行更新。在了解了Syncd
的主循环之后,理解ASIC状态更新的工作流就很简单了。
所有的步骤都发生在主线程一个线程中,顺序执行,总结成时序图如下:
sequenceDiagram autonumber participant SD as Syncd participant RSC as RedisSelectableChannel participant SAI as VendorSai participant R as Redis loop 主线程循环 SD->>RSC: 收到epoll通知,通知获取所有到来的消息 RSC->>R: 通过ConsumerTable获取所有到来的消息 critical 给Syncd加锁 loop 所有收到的消息 SD->>RSC: 获取一个消息 SD->>SD: 解析消息,获取操作类型和操作对象 SD->>SAI: 调用对应的SAI API,更新ASIC SD->>RSC: 发送调用结果给Redis RSC->>R: 将调用结果写入Redis end end end
首先,orchagent
通过Redis发送过来的操作会被RedisSelectableChannel
对象接收,然后在主循环中被处理。当Syncd
处理到m_selectableChannel
时,就会调用processEvent
方法来处理该操作。这几步的核心代码我们上面介绍主循环时已经介绍过了,这里就不再赘述。
然后,processEvent
会根据其中的操作类型,调用对应的SAI的API来对ASIC进行更新。其逻辑是一个巨大的switch-case语句,如下:
// File: src/sonic-sairedis/syncd/Syncd.cpp
void Syncd::processEvent(_In_ sairedis::SelectableChannel& consumer)
{
// Loop all operations in the queue
std::lock_guard<std::mutex> lock(m_mutex);
do {
swss::KeyOpFieldsValuesTuple kco;
consumer.pop(kco, isInitViewMode());
processSingleEvent(kco);
} while (!consumer.empty());
}
sai_status_t Syncd::processSingleEvent(_In_ const swss::KeyOpFieldsValuesTuple &kco)
{
auto& op = kfvOp(kco);
...
if (op == REDIS_ASIC_STATE_COMMAND_CREATE)
return processQuadEvent(SAI_COMMON_API_CREATE, kco);
if (op == REDIS_ASIC_STATE_COMMAND_REMOVE)
return processQuadEvent(SAI_COMMON_API_REMOVE, kco);
...
}
sai_status_t Syncd::processQuadEvent(
_In_ sai_common_api_t api,
_In_ const swss::KeyOpFieldsValuesTuple &kco)
{
// Parse operation
const std::string& key = kfvKey(kco);
const std::string& strObjectId = key.substr(key.find(":") + 1);
sai_object_meta_key_t metaKey;
sai_deserialize_object_meta_key(key, metaKey);
auto& values = kfvFieldsValues(kco);
SaiAttributeList list(metaKey.objecttype, values, false);
sai_attribute_t *attr_list = list.get_attr_list();
uint32_t attr_count = list.get_attr_count();
...
auto info = sai_metadata_get_object_type_info(metaKey.objecttype);
// Process the operation
sai_status_t status;
if (info->isnonobjectid) {
status = processEntry(metaKey, api, attr_count, attr_list);
} else {
status = processOid(metaKey.objecttype, strObjectId, api, attr_count, attr_list);
}
// Send response
if (api == SAI_COMMON_API_GET) {
sai_object_id_t switchVid = VidManager::switchIdQuery(metaKey.objectkey.key.object_id);
sendGetResponse(metaKey.objecttype, strObjectId, switchVid, status, attr_count, attr_list);
...
} else {
sendApiResponse(api, status);
}
syncUpdateRedisQuadEvent(status, api, kco);
return status;
}
sai_status_t Syncd::processEntry(_In_ sai_object_meta_key_t metaKey, _In_ sai_common_api_t api,
_In_ uint32_t attr_count, _In_ sai_attribute_t *attr_list)
{
...
switch (api)
{
case SAI_COMMON_API_CREATE:
return m_vendorSai->create(metaKey, SAI_NULL_OBJECT_ID, attr_count, attr_list);
case SAI_COMMON_API_REMOVE:
return m_vendorSai->remove(metaKey);
...
default:
SWSS_LOG_THROW("api %s not supported", sai_serialize_common_api(api).c_str());
}
}
ASIC状态变更上报
反过来,当ASIC状态发生任何变化,或者需要上报数据,它也会通过SAI来通知我们,此时Syncd会监听这些通知,然后通过ASIC_DB上报给orchagent。其主要工作流如下:
sequenceDiagram box purple SAI实现事件处理线程 participant SAI as SAI Impl end box darkblue 通知处理线程 participant NP as NotificationProcessor participant SD as Syncd participant RNP as RedisNotificationProducer participant R as Redis end loop SAI实现事件处理消息循环 SAI->>SAI: 通过ASIC SDK获取事件 SAI->>SAI: 解析事件,并转换成SAI通知对象 SAI->>NP: 将通知对象序列化,<br/>并发送给通知处理线程的队列中 end loop 通知处理线程消息循环 NP->>NP: 从队列中获取通知 NP->>SD: 获取Syncd锁 critical 给Syncd加锁 NP->>NP: 反序列化通知对象,并做一些处理 NP->>RNP: 重新序列化通知对象,并请求发送 RNP->>R: 将通知以NotificationProducer<br/>的形式写入ASIC_DB end end
这里我们也来看一下具体的实现。为了更加深入的理解,我们还是借助开源的Mellanox的SAI实现来进行分析。
最开始,SAI的实现需要接受到ASIC的通知,这一步是通过ASIC的SDK来实现的,Mellanox的SAI会创建一个事件处理线程(event_thread),然后使用select
函数来获取并处理ASIC发送过来的通知,核心代码如下:
// File: platform/mellanox/mlnx-sai/SAI-Implementation/mlnx_sai/src/mlnx_sai_switch.c
static void event_thread_func(void *context)
{
#define MAX_PACKET_SIZE MAX(g_resource_limits.port_mtu_max, SX_HOST_EVENT_BUFFER_SIZE_MAX)
sx_status_t status;
sx_api_handle_t api_handle;
sx_user_channel_t port_channel, callback_channel;
fd_set descr_set;
int ret_val;
sai_object_id_t switch_id = (sai_object_id_t)context;
sai_port_oper_status_notification_t port_data;
sai_fdb_event_notification_data_t *fdb_events = NULL;
sai_attribute_t *attr_list = NULL;
...
// Init SDK API
if (SX_STATUS_SUCCESS != (status = sx_api_open(sai_log_cb, &api_handle))) {
if (g_notification_callbacks.on_switch_shutdown_request) {
g_notification_callbacks.on_switch_shutdown_request(switch_id);
}
return;
}
if (SX_STATUS_SUCCESS != (status = sx_api_host_ifc_open(api_handle, &port_channel.channel.fd))) {
goto out;
}
...
// Register for port and channel notifications
port_channel.type = SX_USER_CHANNEL_TYPE_FD;
if (SX_STATUS_SUCCESS != (status = sx_api_host_ifc_trap_id_register_set(api_handle, SX_ACCESS_CMD_REGISTER, DEFAULT_ETH_SWID, SX_TRAP_ID_PUDE, &port_channel))) {
goto out;
}
...
for (uint32_t ii = 0; ii < (sizeof(mlnx_trap_ids) / sizeof(*mlnx_trap_ids)); ii++) {
status = sx_api_host_ifc_trap_id_register_set(api_handle, SX_ACCESS_CMD_REGISTER, DEFAULT_ETH_SWID, mlnx_trap_ids[ii], &callback_channel);
}
while (!event_thread_asked_to_stop) {
FD_ZERO(&descr_set);
FD_SET(port_channel.channel.fd.fd, &descr_set);
FD_SET(callback_channel.channel.fd.fd, &descr_set);
...
ret_val = select(FD_SETSIZE, &descr_set, NULL, NULL, &timeout);
if (ret_val > 0) {
// Port state change event
if (FD_ISSET(port_channel.channel.fd.fd, &descr_set)) {
// Parse port state event here ...
if (g_notification_callbacks.on_port_state_change) {
g_notification_callbacks.on_port_state_change(1, &port_data);
}
}
if (FD_ISSET(callback_channel.channel.fd.fd, &descr_set)) {
// Receive notification event.
packet_size = MAX_PACKET_SIZE;
if (SX_STATUS_SUCCESS != (status = sx_lib_host_ifc_recv(&callback_channel.channel.fd, p_packet, &packet_size, receive_info))) {
goto out;
}
// BFD packet event
if (SX_TRAP_ID_BFD_PACKET_EVENT == receive_info->trap_id) {
const struct bfd_packet_event *event = (const struct bfd_packet_event*)p_packet;
// Parse and check event valid here ...
status = mlnx_switch_bfd_packet_handle(event);
continue;
}
// Same way to handle BFD timeout event, Bulk counter ready event. Emiited.
// FDB event and packet event handling
if (receive_info->trap_id == SX_TRAP_ID_FDB_EVENT) {
trap_name = "FDB event";
} else if (SAI_STATUS_SUCCESS != (status = mlnx_translate_sdk_trap_to_sai(receive_info->trap_id, &trap_name, &trap_oid))) {
continue;
}
if (SX_TRAP_ID_FDB_EVENT == receive_info->trap_id) {
// Parse FDB events here ...
if (g_notification_callbacks.on_fdb_event) {
g_notification_callbacks.on_fdb_event(event_count, fdb_events);
}
continue;
}
// Packet event handling
status = mlnx_get_hostif_packet_data(receive_info, &attrs_num, callback_data);
if (g_notification_callbacks.on_packet_event) {
g_notification_callbacks.on_packet_event(switch_id, packet_size, p_packet, attrs_num, callback_data);
}
}
}
}
out:
...
}
接下来,我们用FDB事件来举例,当ASIC收到FDB事件,就会被上面的事件处理循环获取到,并调用g_notification_callbacks.on_fdb_event
函数来处理。这个函数接下来就会调用到Syncd
初始化时设置好的NotificationHandler::onFdbEvent
函数,这个函数会将该事件序列化后,通过消息队列转发给通知处理线程来进行处理:
// File: src/sonic-sairedis/syncd/NotificationHandler.cpp
void NotificationHandler::onFdbEvent(_In_ uint32_t count, _In_ const sai_fdb_event_notification_data_t *data)
{
std::string s = sai_serialize_fdb_event_ntf(count, data);
enqueueNotification(SAI_SWITCH_NOTIFICATION_NAME_FDB_EVENT, s);
}
而此时通知处理线程会被唤醒,从消息队列中取出该事件,然后通过Syncd
获取到Syncd
的锁,再开始处理该通知:
// File: src/sonic-sairedis/syncd/NotificationProcessor.cpp
void NotificationProcessor::ntf_process_function()
{
std::mutex ntf_mutex;
std::unique_lock<std::mutex> ulock(ntf_mutex);
while (m_runThread) {
// When notification arrives, it will signal this condition variable.
m_cv.wait(ulock);
// Process notifications in the queue.
swss::KeyOpFieldsValuesTuple item;
while (m_notificationQueue->tryDequeue(item)) {
processNotification(item);
}
}
}
// File: src/sonic-sairedis/syncd/Syncd.cpp
// Call from NotificationProcessor::processNotification
void Syncd::syncProcessNotification(_In_ const swss::KeyOpFieldsValuesTuple& item)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_processor->syncProcessNotification(item);
}
接下来就是事件的分发和处理了,syncProcessNotification
函数是一系列的if-else
语句,根据事件的类型,调用不同的处理函数来处理该事件:
// File: src/sonic-sairedis/syncd/NotificationProcessor.cpp
void NotificationProcessor::syncProcessNotification( _In_ const swss::KeyOpFieldsValuesTuple& item)
{
std::string notification = kfvKey(item);
std::string data = kfvOp(item);
if (notification == SAI_SWITCH_NOTIFICATION_NAME_SWITCH_STATE_CHANGE) {
handle_switch_state_change(data);
} else if (notification == SAI_SWITCH_NOTIFICATION_NAME_FDB_EVENT) {
handle_fdb_event(data);
} else if ...
} else {
SWSS_LOG_ERROR("unknown notification: %s", notification.c_str());
}
}
而每个事件处理函数都类似,他们会对发送过来的事件进行反序列化,然后调用真正的处理逻辑发送通知,比如,fdb事件对应的handle_fdb_event
函数和process_on_fdb_event
:
// File: src/sonic-sairedis/syncd/NotificationProcessor.cpp
void NotificationProcessor::handle_fdb_event(_In_ const std::string &data)
{
uint32_t count;
sai_fdb_event_notification_data_t *fdbevent = NULL;
sai_deserialize_fdb_event_ntf(data, count, &fdbevent);
process_on_fdb_event(count, fdbevent);
sai_deserialize_free_fdb_event_ntf(count, fdbevent);
}
void NotificationProcessor::process_on_fdb_event( _In_ uint32_t count, _In_ sai_fdb_event_notification_data_t *data)
{
for (uint32_t i = 0; i < count; i++) {
sai_fdb_event_notification_data_t *fdb = &data[i];
// Check FDB event notification data here
fdb->fdb_entry.switch_id = m_translator->translateRidToVid(fdb->fdb_entry.switch_id, SAI_NULL_OBJECT_ID);
fdb->fdb_entry.bv_id = m_translator->translateRidToVid(fdb->fdb_entry.bv_id, fdb->fdb_entry.switch_id, true);
m_translator->translateRidToVid(SAI_OBJECT_TYPE_FDB_ENTRY, fdb->fdb_entry.switch_id, fdb->attr_count, fdb->attr, true);
...
}
// Send notification
std::string s = sai_serialize_fdb_event_ntf(count, data);
sendNotification(SAI_SWITCH_NOTIFICATION_NAME_FDB_EVENT, s);
}
具体发送事件的逻辑就非常直接了,最终就是通过NotificationProducer来发送通知到ASIC_DB中:
// File: src/sonic-sairedis/syncd/NotificationProcessor.cpp
void NotificationProcessor::sendNotification(_In_ const std::string& op, _In_ const std::string& data)
{
std::vector<swss::FieldValueTuple> entry;
sendNotification(op, data, entry);
}
void NotificationProcessor::sendNotification(_In_ const std::string& op, _In_ const std::string& data, _In_ std::vector<swss::FieldValueTuple> entry)
{
m_notifications->send(op, data, entry);
}
// File: src/sonic-sairedis/syncd/RedisNotificationProducer.cpp
void RedisNotificationProducer::send(_In_ const std::string& op, _In_ const std::string& data, _In_ const std::vector<swss::FieldValueTuple>& values)
{
std::vector<swss::FieldValueTuple> vals = values;
// The m_notificationProducer is created in the ctor of RedisNotificationProducer as below:
// m_notificationProducer = std::make_shared<swss::NotificationProducer>(m_db.get(), REDIS_TABLE_NOTIFICATIONS_PER_DB(dbName));
m_notificationProducer->send(op, data, vals);
}
到此,Syncd
中的通知上报的流程就结束了。
参考资料
BGP
BGP可能是交换机里面最常用,最重要,或者线上使用的最多的功能了。这一节,我们就来深入的看一下BGP相关的工作流。
BGP相关进程
SONiC使用FRRouting作为BGP的实现,用于负责BGP的协议处理。FRRouting是一个开源的路由软件,支持多种路由协议,包括BGP,OSPF,IS-IS,RIP,PIM,LDP等等。当FRR发布新版本后,SONiC会将其同步到SONiC的FRR实现仓库:sonic-frr中,每一个版本都对应这一个分支,比如frr/8.2
。
FRR主要由两个大部分组成,第一个部分是各个协议的实现,这些进程的名字都叫做*d
,而当它们收到路由更新的通知的时候,就会告诉第二个部分,也就是zebra
进程,然后zebra
进程会进行选路,并将最优的路由信息同步到kernel中,其主体结构如下图所示:
+----+ +----+ +-----+ +----+ +----+ +----+ +-----+
|bgpd| |ripd| |ospfd| |ldpd| |pbrd| |pimd| |.....|
+----+ +----+ +-----+ +----+ +----+ +----+ +-----+
| | | | | | |
+----v-------v--------v-------v-------v-------v--------v
| |
| Zebra |
| |
+------------------------------------------------------+
| | |
| | |
+------v------+ +---------v--------+ +------v------+
| | | | | |
| *NIX Kernel | | Remote dataplane | | ........... |
| | | | | |
+-------------+ +------------------+ +-------------+
在SONiC中,这些FRR的进程都跑在bgp
的容器中。另外,为了将FRR和Redis连接起来,SONiC在bgp
容器中还会运行一个叫做fpgsyncd
的进程(Forwarding Plane Manager syncd),它的主要功能是监听kernel的路由更新,然后将其同步到APP_DB中。但是因为这个进程不是FRR的一部分,所以它的实现被放在了sonic-swss仓库中。
参考资料
- SONiC Architecture
- Github repo: sonic-swss
- Github repo: sonic-frr
- RFC 4271: A Border Gateway Protocol 4 (BGP-4)
- FRRouting
BGP命令实现
由于BGP是使用FRR来实现的,所以自然而然的,show
命令会将直接请求转发给FRR的vtysh
,核心代码如下:
# file: src/sonic-utilities/show/bgp_frr_v4.py
# 'summary' subcommand ("show ip bgp summary")
@bgp.command()
@multi_asic_util.multi_asic_click_options
def summary(namespace, display):
bgp_summary = bgp_util.get_bgp_summary_from_all_bgp_instances(
constants.IPV4, namespace, display)
bgp_util.display_bgp_summary(bgp_summary=bgp_summary, af=constants.IPV4)
# file: src/sonic-utilities/utilities_common/bgp_util.py
def get_bgp_summary_from_all_bgp_instances(af, namespace, display):
# IPv6 case is omitted here for simplicity
vtysh_cmd = "show ip bgp summary json"
for ns in device.get_ns_list_based_on_options():
cmd_output = run_bgp_show_command(vtysh_cmd, ns)
def run_bgp_command(vtysh_cmd, bgp_namespace=multi_asic.DEFAULT_NAMESPACE, vtysh_shell_cmd=constants.VTYSH_COMMAND):
cmd = ['sudo', vtysh_shell_cmd] + bgp_instance_id + ['-c', vtysh_cmd]
output, ret = clicommon.run_command(cmd, return_cmd=True)
这里,我们也可以通过直接运行vtysh
来进行验证:
root@7260cx3:/etc/sonic/frr# which vtysh
/usr/bin/vtysh
root@7260cx3:/etc/sonic/frr# vtysh
Hello, this is FRRouting (version 7.5.1-sonic).
Copyright 1996-2005 Kunihiro Ishiguro, et al.
7260cx3# show ip bgp summary
IPv4 Unicast Summary:
BGP router identifier 10.1.0.32, local AS number 65100 vrf-id 0
BGP table version 6410
RIB entries 12809, using 2402 KiB of memory
Peers 4, using 85 KiB of memory
Peer groups 4, using 256 bytes of memory
Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd PfxSnt
10.0.0.57 4 64600 3702 3704 0 0 0 08:15:03 6401 6406
10.0.0.59 4 64600 3702 3704 0 0 0 08:15:03 6401 6406
10.0.0.61 4 64600 3705 3702 0 0 0 08:15:03 6401 6406
10.0.0.63 4 64600 3702 3702 0 0 0 08:15:03 6401 6406
Total number of neighbors 4
而config
命令则是通过直接操作CONFIG_DB来实现的,核心代码如下:
# file: src/sonic-utilities/config/main.py
@bgp.group(cls=clicommon.AbbreviationGroup)
def remove():
"Remove BGP neighbor configuration from the device"
pass
@remove.command('neighbor')
@click.argument('neighbor_ip_or_hostname', metavar='<neighbor_ip_or_hostname>', required=True)
def remove_neighbor(neighbor_ip_or_hostname):
"""Deletes BGP neighbor configuration of given hostname or ip from devices
User can specify either internal or external BGP neighbor to remove
"""
namespaces = [DEFAULT_NAMESPACE]
removed_neighbor = False
...
# Connect to CONFIG_DB in linux host (in case of single ASIC) or CONFIG_DB in all the
# namespaces (in case of multi ASIC) and do the sepcified "action" on the BGP neighbor(s)
for namespace in namespaces:
config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace)
config_db.connect()
if _remove_bgp_neighbor_config(config_db, neighbor_ip_or_hostname):
removed_neighbor = True
...
参考资料
- SONiC Architecture
- Github repo: sonic-frr
- Github repo: sonic-utilities
- RFC 4271: A Border Gateway Protocol 4 (BGP-4)
- FRRouting
BGP路由变更下发
路由变更几乎是SONiC中最重要的工作流,它的整个流程从bgpd
进程开始,到最终通过SAI到达ASIC芯片,中间参与的进程较多,流程也较为复杂,但是弄清楚之后,我们就可以很好的理解SONiC的设计思想,并且举一反三的理解其他配置下发的工作流了。所以这一节,我们就一起来深入的分析一下它的整体流程。
为了方便我们理解和从代码层面来展示,我们把这个流程分成两个大块来介绍,分别是FRR是如何处理路由变化的,和SONiC的路由变更工作流以及它是如何与FRR进行整合的。
FRR处理路由变更
sequenceDiagram autonumber participant N as 邻居节点 box purple bgp容器 participant B as bgpd participant ZH as zebra<br/>(请求处理线程) participant ZF as zebra<br/>(路由处理线程) participant ZD as zebra<br/>(数据平面处理线程) participant ZFPM as zebra<br/>(FPM转发线程) participant FPM as fpmsyncd end participant K as Linux Kernel N->>B: 建立BGP会话,<br/>发送路由变更 B->>B: 选路,变更本地路由表(RIB) alt 如果路由发生变化 B->>N: 通知其他邻居节点路由变化 end B->>ZH: 通过zlient本地Socket<br/>通知Zebra更新路由表 ZH->>ZH: 接受bgpd发送的请求 ZH->>ZF: 将路由请求放入<br/>路由处理线程的队列中 ZF->>ZF: 更新本地路由表(RIB) ZF->>ZD: 将路由表更新请求放入<br/>数据平面处理线程<br/>的消息队列中 ZF->>ZFPM: 请求FPM处理线程转发路由变更 ZFPM->>FPM: 通过FPM协议通知<br/>fpmsyncd下发<br/>路由变更 ZD->>K: 发送Netlink消息更新内核路由表
关于FRR的实现,这里更多的是从代码的角度来阐述其工作流的过程,而不是其对BGP的实现细节,如果想要了解FRR的BGP实现细节,可以参考官方文档。
bgpd处理路由变更
bgpd
是FRR中专门用来处理BGP会话的进程,它会开放TCP 179端口与邻居节点建立BGP连接,并处理路由表的更新请求。当路由发生变化后,FRR也会通过它来通知其他邻居节点。
请求来到bgpd
之后,它会首先来到它的io线程:bgp_io
。顾名思义,bgpd
中的网络读写工作都是在这个线程上完成的:
// File: src/sonic-frr/frr/bgpd/bgp_io.c
static int bgp_process_reads(struct thread *thread)
{
...
while (more) {
// Read packets here
...
// If we have more than 1 complete packet, mark it and process it later.
if (ringbuf_remain(ibw) >= pktsize) {
...
added_pkt = true;
} else break;
}
...
if (added_pkt)
thread_add_event(bm->master, bgp_process_packet, peer, 0, &peer->t_process_packet);
return 0;
}
当数据包读完后,bgpd
会将其发送到主线程进行路由处理。在这里,bgpd
会根据数据包的类型进行分发,其中路由更新的请求会交给bpg_update_receive
来进行解析:
// File: src/sonic-frr/frr/bgpd/bgp_packet.c
int bgp_process_packet(struct thread *thread)
{
...
unsigned int processed = 0;
while (processed < rpkt_quanta_old) {
uint8_t type = 0;
bgp_size_t size;
...
/* read in the packet length and type */
size = stream_getw(peer->curr);
type = stream_getc(peer->curr);
size -= BGP_HEADER_SIZE;
switch (type) {
case BGP_MSG_OPEN:
...
break;
case BGP_MSG_UPDATE:
...
mprc = bgp_update_receive(peer, size);
...
break;
...
}
// Process BGP UPDATE message for peer.
static int bgp_update_receive(struct peer *peer, bgp_size_t size)
{
struct stream *s;
struct attr attr;
struct bgp_nlri nlris[NLRI_TYPE_MAX];
...
// Parse attributes and NLRI
memset(&attr, 0, sizeof(struct attr));
attr.label_index = BGP_INVALID_LABEL_INDEX;
attr.label = MPLS_INVALID_LABEL;
...
memset(&nlris, 0, sizeof(nlris));
...
if ((!update_len && !withdraw_len && nlris[NLRI_MP_UPDATE].length == 0)
|| (attr_parse_ret == BGP_ATTR_PARSE_EOR)) {
// More parsing here
...
if (afi && peer->afc[afi][safi]) {
struct vrf *vrf = vrf_lookup_by_id(peer->bgp->vrf_id);
/* End-of-RIB received */
if (!CHECK_FLAG(peer->af_sflags[afi][safi], PEER_STATUS_EOR_RECEIVED)) {
...
if (gr_info->eor_required == gr_info->eor_received) {
...
/* Best path selection */
if (bgp_best_path_select_defer( peer->bgp, afi, safi) < 0)
return BGP_Stop;
}
}
...
}
}
...
return Receive_UPDATE_message;
}
然后,bgpd
会开始检查是否出现更优的路径,并更新自己的本地路由表(RIB,Routing Information Base):
// File: src/sonic-frr/frr/bgpd/bgp_route.c
/* Process the routes with the flag BGP_NODE_SELECT_DEFER set */
int bgp_best_path_select_defer(struct bgp *bgp, afi_t afi, safi_t safi)
{
struct bgp_dest *dest;
int cnt = 0;
struct afi_safi_info *thread_info;
...
/* Process the route list */
for (dest = bgp_table_top(bgp->rib[afi][safi]);
dest && bgp->gr_info[afi][safi].gr_deferred != 0;
dest = bgp_route_next(dest))
{
...
bgp_process_main_one(bgp, dest, afi, safi);
...
}
...
return 0;
}
static void bgp_process_main_one(struct bgp *bgp, struct bgp_dest *dest, afi_t afi, safi_t safi)
{
struct bgp_path_info *new_select;
struct bgp_path_info *old_select;
struct bgp_path_info_pair old_and_new;
...
const struct prefix *p = bgp_dest_get_prefix(dest);
...
/* Best path selection. */
bgp_best_selection(bgp, dest, &bgp->maxpaths[afi][safi], &old_and_new, afi, safi);
old_select = old_and_new.old;
new_select = old_and_new.new;
...
/* FIB update. */
if (bgp_fibupd_safi(safi) && (bgp->inst_type != BGP_INSTANCE_TYPE_VIEW)
&& !bgp_option_check(BGP_OPT_NO_FIB)) {
if (new_select && new_select->type == ZEBRA_ROUTE_BGP
&& (new_select->sub_type == BGP_ROUTE_NORMAL
|| new_select->sub_type == BGP_ROUTE_AGGREGATE
|| new_select->sub_type == BGP_ROUTE_IMPORTED)) {
...
if (old_select && is_route_parent_evpn(old_select))
bgp_zebra_withdraw(p, old_select, bgp, safi);
bgp_zebra_announce(dest, p, new_select, bgp, afi, safi);
} else {
/* Withdraw the route from the kernel. */
...
}
}
/* EVPN route injection and clean up */
...
UNSET_FLAG(dest->flags, BGP_NODE_PROCESS_SCHEDULED);
return;
}
最后,bgp_zebra_announce
会通过zclient
通知zebra
更新内核路由表。
// File: src/sonic-frr/frr/bgpd/bgp_zebra.c
void bgp_zebra_announce(struct bgp_node *rn, struct prefix *p, struct bgp_path_info *info, struct bgp *bgp, afi_t afi, safi_t safi)
{
...
zclient_route_send(valid_nh_count ? ZEBRA_ROUTE_ADD : ZEBRA_ROUTE_DELETE, zclient, &api);
}
zclient
使用本地socket与zebra
通信,并且提供一系列的回调函数用于接收zebra
的通知,核心代码如下:
// File: src/sonic-frr/frr/bgpd/bgp_zebra.c
void bgp_zebra_init(struct thread_master *master, unsigned short instance)
{
zclient_num_connects = 0;
/* Set default values. */
zclient = zclient_new(master, &zclient_options_default);
zclient_init(zclient, ZEBRA_ROUTE_BGP, 0, &bgpd_privs);
zclient->zebra_connected = bgp_zebra_connected;
zclient->router_id_update = bgp_router_id_update;
zclient->interface_add = bgp_interface_add;
zclient->interface_delete = bgp_interface_delete;
zclient->interface_address_add = bgp_interface_address_add;
...
}
int zclient_socket_connect(struct zclient *zclient)
{
int sock;
int ret;
sock = socket(zclient_addr.ss_family, SOCK_STREAM, 0);
...
/* Connect to zebra. */
ret = connect(sock, (struct sockaddr *)&zclient_addr, zclient_addr_len);
...
zclient->sock = sock;
return sock;
}
在bgpd
容器中,我们可以在/run/frr
目录下找到zebra
通信使用的socket文件来进行简单的验证:
root@7260cx3:/run/frr# ls -l
total 12
...
srwx------ 1 frr frr 0 Jun 16 09:16 zserv.api
zebra更新路由表
由于FRR支持的路由协议很多,如果每个路由协议处理进程都单独的对内核进行操作则必然会产生冲突,很难协调合作,所以FRR使用一个单独的进程用于和所有的路由协议处理进程进行沟通,整合好信息之后统一的进行内核的路由表更新,这个进程就是zebra
。
在zebra
中,内核的更新发生在一个独立的数据面处理线程中:dplane_thread
。所有的请求都会通过zclient
发送给zebra
,经过处理之后,最后转发给dplane_thread
来处理,这样路由的处理就是有序的了,也就不会产生冲突了。
zebra
启动时,会将所有的请求处理函数进行注册,当请求到来时,就可以根据请求的类型调用相应的处理函数了,核心代码如下:
// File: src/sonic-frr/frr/zebra/zapi_msg.c
void (*zserv_handlers[])(ZAPI_HANDLER_ARGS) = {
[ZEBRA_ROUTER_ID_ADD] = zread_router_id_add,
[ZEBRA_ROUTER_ID_DELETE] = zread_router_id_delete,
[ZEBRA_INTERFACE_ADD] = zread_interface_add,
[ZEBRA_INTERFACE_DELETE] = zread_interface_delete,
[ZEBRA_ROUTE_ADD] = zread_route_add,
[ZEBRA_ROUTE_DELETE] = zread_route_del,
[ZEBRA_REDISTRIBUTE_ADD] = zebra_redistribute_add,
[ZEBRA_REDISTRIBUTE_DELETE] = zebra_redistribute_delete,
...
我们这里拿添加路由zread_route_add
作为例子,来继续分析后续的流程。从以下代码我们可以看到,当新的路由到来后,zebra
会开始查看并更新自己内部的路由表:
// File: src/sonic-frr/frr/zebra/zapi_msg.c
static void zread_route_add(ZAPI_HANDLER_ARGS)
{
struct stream *s;
struct route_entry *re;
struct nexthop_group *ng = NULL;
struct nhg_hash_entry nhe;
...
// Decode zclient request
s = msg;
if (zapi_route_decode(s, &api) < 0) {
return;
}
...
// Allocate new route entry.
re = XCALLOC(MTYPE_RE, sizeof(struct route_entry));
re->type = api.type;
re->instance = api.instance;
...
// Init nexthop entry, if we have an id, then add route.
if (!re->nhe_id) {
zebra_nhe_init(&nhe, afi, ng->nexthop);
nhe.nhg.nexthop = ng->nexthop;
nhe.backup_info = bnhg;
}
ret = rib_add_multipath_nhe(afi, api.safi, &api.prefix, src_p, re, &nhe);
// Update stats. IPv6 is omitted here for simplicity.
if (ret > 0) client->v4_route_add_cnt++;
else if (ret < 0) client->v4_route_upd8_cnt++;
}
// File: src/sonic-frr/frr/zebra/zebra_rib.c
int rib_add_multipath_nhe(afi_t afi, safi_t safi, struct prefix *p,
struct prefix_ipv6 *src_p, struct route_entry *re,
struct nhg_hash_entry *re_nhe)
{
struct nhg_hash_entry *nhe = NULL;
struct route_table *table;
struct route_node *rn;
int ret = 0;
...
/* Find table and nexthop entry */
table = zebra_vrf_get_table_with_table_id(afi, safi, re->vrf_id, re->table);
if (re->nhe_id > 0) nhe = zebra_nhg_lookup_id(re->nhe_id);
else nhe = zebra_nhg_rib_find_nhe(re_nhe, afi);
/* Attach the re to the nhe's nexthop group. */
route_entry_update_nhe(re, nhe);
/* Make it sure prefixlen is applied to the prefix. */
/* Set default distance by route type. */
...
/* Lookup route node.*/
rn = srcdest_rnode_get(table, p, src_p);
...
/* If this route is kernel/connected route, notify the dataplane to update kernel route table. */
if (RIB_SYSTEM_ROUTE(re)) {
dplane_sys_route_add(rn, re);
}
/* Link new re to node. */
SET_FLAG(re->status, ROUTE_ENTRY_CHANGED);
rib_addnode(rn, re, 1);
/* Clean up */
...
return ret;
}
rib_addnode
会将这个路由添加请求转发给rib的处理线程,并由它顺序的进行处理:
static void rib_addnode(struct route_node *rn, struct route_entry *re, int process)
{
...
rib_link(rn, re, process);
}
static void rib_link(struct route_node *rn, struct route_entry *re, int process)
{
rib_dest_t *dest = rib_dest_from_rnode(rn);
if (!dest) dest = zebra_rib_create_dest(rn);
re_list_add_head(&dest->routes, re);
...
if (process) rib_queue_add(rn);
}
请求会来到RIB的处理线程:rib_process
,并由它来进行进一步的选路,然后将最优的路由添加到zebra
的内部路由表(RIB)中:
/* Core function for processing routing information base. */
static void rib_process(struct route_node *rn)
{
struct route_entry *re;
struct route_entry *next;
struct route_entry *old_selected = NULL;
struct route_entry *new_selected = NULL;
struct route_entry *old_fib = NULL;
struct route_entry *new_fib = NULL;
struct route_entry *best = NULL;
rib_dest_t *dest;
...
dest = rib_dest_from_rnode(rn);
old_fib = dest->selected_fib;
...
/* Check every route entry and select the best route. */
RNODE_FOREACH_RE_SAFE (rn, re, next) {
...
if (CHECK_FLAG(re->flags, ZEBRA_FLAG_FIB_OVERRIDE)) {
best = rib_choose_best(new_fib, re);
if (new_fib && best != new_fib)
UNSET_FLAG(new_fib->status, ROUTE_ENTRY_CHANGED);
new_fib = best;
} else {
best = rib_choose_best(new_selected, re);
if (new_selected && best != new_selected)
UNSET_FLAG(new_selected->status, ROUTE_ENTRY_CHANGED);
new_selected = best;
}
if (best != re)
UNSET_FLAG(re->status, ROUTE_ENTRY_CHANGED);
} /* RNODE_FOREACH_RE */
...
/* Update fib according to selection results */
if (new_fib && old_fib)
rib_process_update_fib(zvrf, rn, old_fib, new_fib);
else if (new_fib)
rib_process_add_fib(zvrf, rn, new_fib);
else if (old_fib)
rib_process_del_fib(zvrf, rn, old_fib);
/* Remove all RE entries queued for removal */
/* Check if the dest can be deleted now. */
...
}
对于新的路由,会调用rib_process_add_fib
来将其添加到zebra
的内部路由表中,然后通知dplane进行内核路由表的更新:
static void rib_process_add_fib(struct zebra_vrf *zvrf, struct route_node *rn, struct route_entry *new)
{
hook_call(rib_update, rn, "new route selected");
...
/* If labeled-unicast route, install transit LSP. */
if (zebra_rib_labeled_unicast(new))
zebra_mpls_lsp_install(zvrf, rn, new);
rib_install_kernel(rn, new, NULL);
UNSET_FLAG(new->status, ROUTE_ENTRY_CHANGED);
}
void rib_install_kernel(struct route_node *rn, struct route_entry *re,
struct route_entry *old)
{
struct rib_table_info *info = srcdest_rnode_table_info(rn);
enum zebra_dplane_result ret;
rib_dest_t *dest = rib_dest_from_rnode(rn);
...
/* Install the resolved nexthop object first. */
zebra_nhg_install_kernel(re->nhe);
/* If this is a replace to a new RE let the originator of the RE know that they've lost */
if (old && (old != re) && (old->type != re->type))
zsend_route_notify_owner(rn, old, ZAPI_ROUTE_BETTER_ADMIN_WON, info->afi, info->safi);
/* Update fib selection */
dest->selected_fib = re;
/* Make sure we update the FPM any time we send new information to the kernel. */
hook_call(rib_update, rn, "installing in kernel");
/* Send add or update */
if (old) ret = dplane_route_update(rn, re, old);
else ret = dplane_route_add(rn, re);
...
}
这里有两个重要的操作,一个自然是调用dplane_route_*
函数来进行内核的路由表更新,另一个则是出现了两次的hook_call
,fpm的钩子函数就是挂在这个地方,用来接收并转发路由表的更新通知。这里我们一个一个来看:
dplane更新内核路由表
首先是dplane的dplane_route_*
函数,它们的做的事情都一样:把请求打包,然后放入dplane_thread
的消息队列中,并不会做任何实质的操作:
// File: src/sonic-frr/frr/zebra/zebra_dplane.c
enum zebra_dplane_result dplane_route_add(struct route_node *rn, struct route_entry *re) {
return dplane_route_update_internal(rn, re, NULL, DPLANE_OP_ROUTE_INSTALL);
}
enum zebra_dplane_result dplane_route_update(struct route_node *rn, struct route_entry *re, struct route_entry *old_re) {
return dplane_route_update_internal(rn, re, old_re, DPLANE_OP_ROUTE_UPDATE);
}
enum zebra_dplane_result dplane_sys_route_add(struct route_node *rn, struct route_entry *re) {
return dplane_route_update_internal(rn, re, NULL, DPLANE_OP_SYS_ROUTE_ADD);
}
static enum zebra_dplane_result
dplane_route_update_internal(struct route_node *rn, struct route_entry *re, struct route_entry *old_re, enum dplane_op_e op)
{
enum zebra_dplane_result result = ZEBRA_DPLANE_REQUEST_FAILURE;
int ret = EINVAL;
/* Create and init context */
struct zebra_dplane_ctx *ctx = ...;
/* Enqueue context for processing */
ret = dplane_route_enqueue(ctx);
/* Update counter */
atomic_fetch_add_explicit(&zdplane_info.dg_routes_in, 1, memory_order_relaxed);
if (ret == AOK)
result = ZEBRA_DPLANE_REQUEST_QUEUED;
return result;
}
然后,我们就来到了数据面处理线程dplane_thread
,其消息循环很简单,就是从队列中一个个取出消息,然后通过调用其处理函数:
// File: src/sonic-frr/frr/zebra/zebra_dplane.c
static int dplane_thread_loop(struct thread *event)
{
...
while (prov) {
...
/* Process work here */
(*prov->dp_fp)(prov);
/* Check for zebra shutdown */
/* Dequeue completed work from the provider */
...
/* Locate next provider */
DPLANE_LOCK();
prov = TAILQ_NEXT(prov, dp_prov_link);
DPLANE_UNLOCK();
}
}
默认情况下,dplane_thread
会使用kernel_dplane_process_func
来进行消息的处理,内部会根据请求的类型对内核的操作进行分发:
static int kernel_dplane_process_func(struct zebra_dplane_provider *prov)
{
enum zebra_dplane_result res;
struct zebra_dplane_ctx *ctx;
int counter, limit;
limit = dplane_provider_get_work_limit(prov);
for (counter = 0; counter < limit; counter++) {
ctx = dplane_provider_dequeue_in_ctx(prov);
if (ctx == NULL) break;
/* A previous provider plugin may have asked to skip the kernel update. */
if (dplane_ctx_is_skip_kernel(ctx)) {
res = ZEBRA_DPLANE_REQUEST_SUCCESS;
goto skip_one;
}
/* Dispatch to appropriate kernel-facing apis */
switch (dplane_ctx_get_op(ctx)) {
case DPLANE_OP_ROUTE_INSTALL:
case DPLANE_OP_ROUTE_UPDATE:
case DPLANE_OP_ROUTE_DELETE:
res = kernel_dplane_route_update(ctx);
break;
...
}
...
}
...
}
static enum zebra_dplane_result
kernel_dplane_route_update(struct zebra_dplane_ctx *ctx)
{
enum zebra_dplane_result res;
/* Call into the synchronous kernel-facing code here */
res = kernel_route_update(ctx);
return res;
}
而kernel_route_update
则是真正的内核操作了,它会通过netlink来通知内核路由更新:
// File: src/sonic-frr/frr/zebra/rt_netlink.c
// Update or delete a prefix from the kernel, using info from a dataplane context.
enum zebra_dplane_result kernel_route_update(struct zebra_dplane_ctx *ctx)
{
int cmd, ret;
const struct prefix *p = dplane_ctx_get_dest(ctx);
struct nexthop *nexthop;
if (dplane_ctx_get_op(ctx) == DPLANE_OP_ROUTE_DELETE) {
cmd = RTM_DELROUTE;
} else if (dplane_ctx_get_op(ctx) == DPLANE_OP_ROUTE_INSTALL) {
cmd = RTM_NEWROUTE;
} else if (dplane_ctx_get_op(ctx) == DPLANE_OP_ROUTE_UPDATE) {
cmd = RTM_NEWROUTE;
}
if (!RSYSTEM_ROUTE(dplane_ctx_get_type(ctx)))
ret = netlink_route_multipath(cmd, ctx);
...
return (ret == 0 ? ZEBRA_DPLANE_REQUEST_SUCCESS : ZEBRA_DPLANE_REQUEST_FAILURE);
}
// Routing table change via netlink interface, using a dataplane context object
static int netlink_route_multipath(int cmd, struct zebra_dplane_ctx *ctx)
{
// Build netlink request.
struct {
struct nlmsghdr n;
struct rtmsg r;
char buf[NL_PKT_BUF_SIZE];
} req;
req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
req.n.nlmsg_flags = NLM_F_CREATE | NLM_F_REQUEST;
...
/* Talk to netlink socket. */
return netlink_talk_info(netlink_talk_filter, &req.n, dplane_ctx_get_ns(ctx), 0);
}
FPM路由更新转发
FPM(Forwarding Plane Manager)是FRR中用于通知其他进程路由变更的协议,其主要逻辑代码在src/sonic-frr/frr/zebra/zebra_fpm.c
中。它默认有两套协议实现:protobuf和netlink,SONiC就是使用的是netlink协议。
上面我们已经提到,它通过钩子函数实现,监听RIB中的路由变化,并通过本地Socket转发给其他的进程。这个钩子会在启动的时候就注册好,其中和我们现在看的最相关的就是rib_update
钩子了,如下所示:
static int zebra_fpm_module_init(void)
{
hook_register(rib_update, zfpm_trigger_update);
hook_register(zebra_rmac_update, zfpm_trigger_rmac_update);
hook_register(frr_late_init, zfpm_init);
hook_register(frr_early_fini, zfpm_fini);
return 0;
}
FRR_MODULE_SETUP(.name = "zebra_fpm", .version = FRR_VERSION,
.description = "zebra FPM (Forwarding Plane Manager) module",
.init = zebra_fpm_module_init,
);
当rib_update
钩子被调用时,zfpm_trigger_update
函数会被调用,它会将路由变更信息再次放入fpm的转发队列中,并触发写操作:
static int zfpm_trigger_update(struct route_node *rn, const char *reason)
{
rib_dest_t *dest;
...
// Queue the update request
dest = rib_dest_from_rnode(rn);
SET_FLAG(dest->flags, RIB_DEST_UPDATE_FPM);
TAILQ_INSERT_TAIL(&zfpm_g->dest_q, dest, fpm_q_entries);
...
zfpm_write_on();
return 0;
}
static inline void zfpm_write_on(void) {
thread_add_write(zfpm_g->master, zfpm_write_cb, 0, zfpm_g->sock, &zfpm_g->t_write);
}
这个写操作的回调就会将其从队列中取出,并转换成FPM的消息格式,然后通过本地Socket转发给其他进程:
static int zfpm_write_cb(struct thread *thread)
{
struct stream *s;
do {
int bytes_to_write, bytes_written;
s = zfpm_g->obuf;
// Convert route info to buffer here.
if (stream_empty(s)) zfpm_build_updates();
// Write to socket until we don' have anything to write or cannot write anymore (partial write).
bytes_to_write = stream_get_endp(s) - stream_get_getp(s);
bytes_written = write(zfpm_g->sock, stream_pnt(s), bytes_to_write);
...
} while (1);
if (zfpm_writes_pending()) zfpm_write_on();
return 0;
}
static void zfpm_build_updates(void)
{
struct stream *s = zfpm_g->obuf;
do {
/* Stop processing the queues if zfpm_g->obuf is full or we do not have more updates to process */
if (zfpm_build_mac_updates() == FPM_WRITE_STOP) break;
if (zfpm_build_route_updates() == FPM_WRITE_STOP) break;
} while (zfpm_updates_pending());
}
到此,FRR的工作就完成了。
SONiC路由变更工作流
当FRR变更内核路由配置后,SONiC便会收到来自Netlink和FPM的通知,然后进行一系列操作将其下发给ASIC,其主要流程如下:
sequenceDiagram autonumber participant K as Linux Kernel box purple bgp容器 participant Z as zebra participant FPM as fpmsyncd end box darkred database容器 participant R as Redis end box darkblue swss容器 participant OA as orchagent end box darkgreen syncd容器 participant SD as syncd end participant A as ASIC K->>FPM: 内核路由变更时通过Netlink发送通知 Z->>FPM: 通过FPM接口和Netlink<br/>消息格式发送路由变更通知 FPM->>R: 通过ProducerStateTable<br/>将路由变更信息写入<br/>APPL_DB R->>OA: 通过ConsumerStateTable<br/>接收路由变更信息 OA->>OA: 处理路由变更信息<br/>生成SAI路由对象 OA->>SD: 通过ProducerTable<br/>或者ZMQ将SAI路由对象<br/>发给syncd SD->>R: 接收SAI路由对象,写入ASIC_DB SD->>A: 通过SAI接口<br/>配置ASIC
fpmsyncd更新Redis中的路由配置
首先,我们从源头看起。fpmsyncd
在启动的时候便会开始监听FPM和Netlink的事件,用于接收路由变更消息:
// File: src/sonic-swss/fpmsyncd/fpmsyncd.cpp
int main(int argc, char **argv)
{
...
DBConnector db("APPL_DB", 0);
RedisPipeline pipeline(&db);
RouteSync sync(&pipeline);
// Register netlink message handler
NetLink netlink;
netlink.registerGroup(RTNLGRP_LINK);
NetDispatcher::getInstance().registerMessageHandler(RTM_NEWROUTE, &sync);
NetDispatcher::getInstance().registerMessageHandler(RTM_DELROUTE, &sync);
NetDispatcher::getInstance().registerMessageHandler(RTM_NEWLINK, &sync);
NetDispatcher::getInstance().registerMessageHandler(RTM_DELLINK, &sync);
rtnl_route_read_protocol_names(DefaultRtProtoPath);
...
while (true) {
try {
// Launching FPM server and wait for zebra to connect.
FpmLink fpm(&sync);
...
fpm.accept();
...
} catch (FpmLink::FpmConnectionClosedException &e) {
// If connection is closed, keep retrying until it succeeds, before handling any other events.
cout << "Connection lost, reconnecting..." << endl;
}
...
}
}
这样,所有的路由变更消息都会以Netlink的形式发送给RouteSync
,其中EVPN Type 5必须以原始消息的形式进行处理,所以会发送给onMsgRaw
,其他的消息都会统一的发给处理Netlink的onMsg
回调:(关于Netlink如何接收和处理消息,请移步4.1.2 Netlink)
// File: src/sonic-swss/fpmsyncd/fpmlink.cpp
// Called from: FpmLink::readData()
void FpmLink::processFpmMessage(fpm_msg_hdr_t* hdr)
{
size_t msg_len = fpm_msg_len(hdr);
nlmsghdr *nl_hdr = (nlmsghdr *)fpm_msg_data(hdr);
...
/* Read all netlink messages inside FPM message */
for (; NLMSG_OK (nl_hdr, msg_len); nl_hdr = NLMSG_NEXT(nl_hdr, msg_len))
{
/*
* EVPN Type5 Add Routes need to be process in Raw mode as they contain
* RMAC, VLAN and L3VNI information.
* Where as all other route will be using rtnl api to extract information
* from the netlink msg.
*/
bool isRaw = isRawProcessing(nl_hdr);
nl_msg *msg = nlmsg_convert(nl_hdr);
...
nlmsg_set_proto(msg, NETLINK_ROUTE);
if (isRaw) {
/* EVPN Type5 Add route processing */
/* This will call into onRawMsg() */
processRawMsg(nl_hdr);
} else {
/* This will call into onMsg() */
NetDispatcher::getInstance().onNetlinkMessage(msg);
}
nlmsg_free(msg);
}
}
void FpmLink::processRawMsg(struct nlmsghdr *h)
{
m_routesync->onMsgRaw(h);
};
接着,RouteSync
收到路由变更的消息之后,会在onMsg
和onMsgRaw
中进行判断和分发:
// File: src/sonic-swss/fpmsyncd/routesync.cpp
void RouteSync::onMsgRaw(struct nlmsghdr *h)
{
if ((h->nlmsg_type != RTM_NEWROUTE) && (h->nlmsg_type != RTM_DELROUTE))
return;
...
onEvpnRouteMsg(h, len);
}
void RouteSync::onMsg(int nlmsg_type, struct nl_object *obj)
{
// Refill Netlink cache here
...
struct rtnl_route *route_obj = (struct rtnl_route *)obj;
auto family = rtnl_route_get_family(route_obj);
if (family == AF_MPLS) {
onLabelRouteMsg(nlmsg_type, obj);
return;
}
...
unsigned int master_index = rtnl_route_get_table(route_obj);
char master_name[IFNAMSIZ] = {0};
if (master_index) {
/* If the master device name starts with VNET_PREFIX, it is a VNET route.
The VNET name is exactly the name of the associated master device. */
getIfName(master_index, master_name, IFNAMSIZ);
if (string(master_name).find(VNET_PREFIX) == 0) {
onVnetRouteMsg(nlmsg_type, obj, string(master_name));
}
/* Otherwise, it is a regular route (include VRF route). */
else {
onRouteMsg(nlmsg_type, obj, master_name);
}
} else {
onRouteMsg(nlmsg_type, obj, NULL);
}
}
从上面的代码中,我们可以看到这里会有四种不同的路由处理入口,这些不同的路由会被最终通过各自的ProducerStateTable写入到APPL_DB
中的不同的Table中:
路由类型 | 处理函数 | Table |
---|---|---|
MPLS | onLabelRouteMsg | LABLE_ROUTE_TABLE |
Vnet VxLan Tunnel Route | onVnetRouteMsg | VNET_ROUTE_TUNNEL_TABLE |
其他Vnet路由 | onVnetRouteMsg | VNET_ROUTE_TABLE |
EVPN Type 5 | onEvpnRouteMsg | ROUTE_TABLE |
普通路由 | onRouteMsg | ROUTE_TABLE |
这里以普通路由来举例子,其他的函数的实现虽然有所不同,但是主体的思路是一样的:
// File: src/sonic-swss/fpmsyncd/routesync.cpp
void RouteSync::onRouteMsg(int nlmsg_type, struct nl_object *obj, char *vrf)
{
// Parse route info from nl_object here.
...
// Get nexthop lists
string gw_list;
string intf_list;
string mpls_list;
getNextHopList(route_obj, gw_list, mpls_list, intf_list);
...
// Build route info here, including protocol, interface, next hops, MPLS, weights etc.
vector<FieldValueTuple> fvVector;
FieldValueTuple proto("protocol", proto_str);
FieldValueTuple gw("nexthop", gw_list);
...
fvVector.push_back(proto);
fvVector.push_back(gw);
...
// Push to ROUTE_TABLE via ProducerStateTable.
m_routeTable.set(destipprefix, fvVector);
SWSS_LOG_DEBUG("RouteTable set msg: %s %s %s %s", destipprefix, gw_list.c_str(), intf_list.c_str(), mpls_list.c_str());
...
}
orchagent处理路由配置变化
接下来,这些路由信息会来到orchagent。在orchagent启动的时候,它会创建好VNetRouteOrch
和RouteOrch
对象,这两个对象分别用来监听和处理Vnet相关路由和EVPN/普通路由:
// File: src/sonic-swss/orchagent/orchdaemon.cpp
bool OrchDaemon::init()
{
...
vector<string> vnet_tables = { APP_VNET_RT_TABLE_NAME, APP_VNET_RT_TUNNEL_TABLE_NAME };
VNetRouteOrch *vnet_rt_orch = new VNetRouteOrch(m_applDb, vnet_tables, vnet_orch);
...
const int routeorch_pri = 5;
vector<table_name_with_pri_t> route_tables = {
{ APP_ROUTE_TABLE_NAME, routeorch_pri },
{ APP_LABEL_ROUTE_TABLE_NAME, routeorch_pri }
};
gRouteOrch = new RouteOrch(m_applDb, route_tables, gSwitchOrch, gNeighOrch, gIntfsOrch, vrf_orch, gFgNhgOrch, gSrv6Orch);
...
}
所有Orch对象的消息处理入口都是doTask
,这里RouteOrch
和VNetRouteOrch
也不例外,这里我们以RouteOrch
为例子,看看它是如何处理路由变化的。
从RouteOrch
上,我们可以真切的感受到为什么这些类被命名为Orch
。RouteOrch
有2500多行,其中会有和很多其他Orch的交互,以及各种各样的细节…… 代码是相对难读,请大家读的时候一定保持耐心。
RouteOrch
在处理路由消息的时候有几点需要注意:
- 从上面
init
函数,我们可以看到RouteOrch
不仅会管理普通路由,还会管理MPLS路由,这两种路由的处理逻辑是不一样的,所以在下面的代码中,为了简化,我们只展示普通路由的处理逻辑。 - 因为
ProducerStateTable
在传递和接受消息的时候都是批量传输的,所以,RouteOrch
在处理消息的时候,也是批量处理的。为了支持批量处理,RouteOrch
会借用EntityBulker<sai_route_api_t> gRouteBulker
将需要改动的SAI路由对象缓存起来,然后在doTask()
函数的最后,一次性将这些路由对象的改动应用到SAI中。 - 路由的操作会需要很多其他的信息,比如每个Port的状态,每个Neighbor的状态,每个VRF的状态等等。为了获取这些信息,
RouteOrch
会与其他的Orch对象进行交互,比如PortOrch
,NeighOrch
,VRFOrch
等等。
// File: src/sonic-swss/orchagent/routeorch.cpp
void RouteOrch::doTask(Consumer& consumer)
{
// Calling PortOrch to make sure all ports are ready before processing route messages.
if (!gPortsOrch->allPortsReady()) { return; }
// Call doLabelTask() instead, if the incoming messages are from MPLS messages. Otherwise, move on as regular routes.
...
/* Default handling is for ROUTE_TABLE (regular routes) */
auto it = consumer.m_toSync.begin();
while (it != consumer.m_toSync.end()) {
// Add or remove routes with a route bulker
while (it != consumer.m_toSync.end())
{
KeyOpFieldsValuesTuple t = it->second;
// Parse route operation from the incoming message here.
string key = kfvKey(t);
string op = kfvOp(t);
...
// resync application:
// - When routeorch receives 'resync' message (key = "resync", op = "SET"), it marks all current routes as dirty
// and waits for 'resync complete' message. For all newly received routes, if they match current dirty routes,
// it unmarks them dirty.
// - After receiving 'resync complete' (key = "resync", op != "SET") message, it creates all newly added routes
// and removes all dirty routes.
...
// Parsing VRF and IP prefix from the incoming message here.
...
// Process regular route operations.
if (op == SET_COMMAND)
{
// Parse and validate route attributes from the incoming message here.
string ips;
string aliases;
...
// If the nexthop_group is empty, create the next hop group key based on the IPs and aliases.
// Otherwise, get the key from the NhgOrch. The result will be stored in the "nhg" variable below.
NextHopGroupKey& nhg = ctx.nhg;
...
if (nhg_index.empty())
{
// Here the nexthop_group is empty, so we create the next hop group key based on the IPs and aliases.
...
string nhg_str = "";
if (blackhole) {
nhg = NextHopGroupKey();
} else if (srv6_nh == true) {
...
nhg = NextHopGroupKey(nhg_str, overlay_nh, srv6_nh);
} else if (overlay_nh == false) {
...
nhg = NextHopGroupKey(nhg_str, weights);
} else {
...
nhg = NextHopGroupKey(nhg_str, overlay_nh, srv6_nh);
}
}
else
{
// Here we have a nexthop_group, so we get the key from the NhgOrch.
const NhgBase& nh_group = getNhg(nhg_index);
nhg = nh_group.getNhgKey();
...
}
...
// Now we start to create the SAI route entry.
if (nhg.getSize() == 1 && nhg.hasIntfNextHop())
{
// Skip certain routes, such as not valid, directly routes to tun0, linklocal or multicast routes, etc.
...
// Create SAI route entry in addRoute function.
if (addRoute(ctx, nhg)) it = consumer.m_toSync.erase(it);
else it++;
}
/*
* Check if the route does not exist or needs to be updated or
* if the route is using a temporary next hop group owned by
* NhgOrch.
*/
else if (m_syncdRoutes.find(vrf_id) == m_syncdRoutes.end() ||
m_syncdRoutes.at(vrf_id).find(ip_prefix) == m_syncdRoutes.at(vrf_id).end() ||
m_syncdRoutes.at(vrf_id).at(ip_prefix) != RouteNhg(nhg, ctx.nhg_index) ||
gRouteBulker.bulk_entry_pending_removal(route_entry) ||
ctx.using_temp_nhg)
{
if (addRoute(ctx, nhg)) it = consumer.m_toSync.erase(it);
else it++;
}
...
}
// Handle other ops, like DEL_COMMAND for route deletion, etc.
...
}
// Flush the route bulker, so routes will be written to syncd and ASIC
gRouteBulker.flush();
// Go through the bulker results.
// Handle SAI failures, update neighbors, counters, send notifications in add/removeRoutePost functions.
...
/* Remove next hop group if the reference count decreases to zero */
...
}
}
解析完路由操作后,RouteOrch
会调用addRoute
或者removeRoute
函数来创建或者删除路由。这里以添加路由addRoute
为例子来继续分析。它的逻辑主要分为几个大部分:
- 从NeighOrch中获取下一跳信息,并检查下一跳是否真的可用。
- 如果是新路由,或者是重新添加正在等待删除的路由,那么就会创建一个新的SAI路由对象
- 如果是已有的路由,那么就更新已有的SAI路由对象
// File: src/sonic-swss/orchagent/routeorch.cpp
bool RouteOrch::addRoute(RouteBulkContext& ctx, const NextHopGroupKey &nextHops)
{
// Get nexthop information from NeighOrch.
// We also need to check PortOrch for inband port, IntfsOrch to ensure the related interface is created and etc.
...
// Start to sync the SAI route entry.
sai_route_entry_t route_entry;
route_entry.vr_id = vrf_id;
route_entry.switch_id = gSwitchId;
copy(route_entry.destination, ipPrefix);
sai_attribute_t route_attr;
auto& object_statuses = ctx.object_statuses;
// Create a new route entry in this case.
//
// In case the entry is already pending removal in the bulk, it would be removed from m_syncdRoutes during the bulk call.
// Therefore, such entries need to be re-created rather than set attribute.
if (it_route == m_syncdRoutes.at(vrf_id).end() || gRouteBulker.bulk_entry_pending_removal(route_entry)) {
if (blackhole) {
route_attr.id = SAI_ROUTE_ENTRY_ATTR_PACKET_ACTION;
route_attr.value.s32 = SAI_PACKET_ACTION_DROP;
} else {
route_attr.id = SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID;
route_attr.value.oid = next_hop_id;
}
/* Default SAI_ROUTE_ATTR_PACKET_ACTION is SAI_PACKET_ACTION_FORWARD */
object_statuses.emplace_back();
sai_status_t status = gRouteBulker.create_entry(&object_statuses.back(), &route_entry, 1, &route_attr);
if (status == SAI_STATUS_ITEM_ALREADY_EXISTS) {
return false;
}
}
// Update existing route entry in this case.
else {
// Set the packet action to forward when there was no next hop (dropped) and not pointing to blackhole.
if (it_route->second.nhg_key.getSize() == 0 && !blackhole) {
route_attr.id = SAI_ROUTE_ENTRY_ATTR_PACKET_ACTION;
route_attr.value.s32 = SAI_PACKET_ACTION_FORWARD;
object_statuses.emplace_back();
gRouteBulker.set_entry_attribute(&object_statuses.back(), &route_entry, &route_attr);
}
// Only 1 case is listed here as an example. Other cases are handled with similar logic by calling set_entry_attributes as well.
...
}
...
}
在创建和设置好所有的路由后,RouteOrch
会调用gRouteBulker.flush()
来将所有的路由写入到ASIC_DB中。flush()
函数很简单,就是将所有的请求分批次进行处理,默认情况下每一批是1000个,这个定义在OrchDaemon
中,并通过构造函数传入:
// File: src/sonic-swss/orchagent/orchdaemon.cpp
#define DEFAULT_MAX_BULK_SIZE 1000
size_t gMaxBulkSize = DEFAULT_MAX_BULK_SIZE;
// File: src/sonic-swss/orchagent/bulker.h
template <typename T>
class EntityBulker
{
public:
using Ts = SaiBulkerTraits<T>;
using Te = typename Ts::entry_t;
...
void flush()
{
// Bulk remove entries
if (!removing_entries.empty()) {
// Split into batches of max_bulk_size, then call flush. Similar to creating_entries, so details are omitted.
std::vector<Te> rs;
...
flush_removing_entries(rs);
removing_entries.clear();
}
// Bulk create entries
if (!creating_entries.empty()) {
// Split into batches of max_bulk_size, then call flush_creating_entries to call SAI batch create API to create
// the objects in batch.
std::vector<Te> rs;
std::vector<sai_attribute_t const*> tss;
std::vector<uint32_t> cs;
for (auto const& i: creating_entries) {
sai_object_id_t *pid = std::get<0>(i);
auto const& attrs = std::get<1>(i);
if (*pid == SAI_NULL_OBJECT_ID) {
rs.push_back(pid);
tss.push_back(attrs.data());
cs.push_back((uint32_t)attrs.size());
// Batch create here.
if (rs.size() >= max_bulk_size) {
flush_creating_entries(rs, tss, cs);
}
}
}
flush_creating_entries(rs, tss, cs);
creating_entries.clear();
}
// Bulk update existing entries
if (!setting_entries.empty()) {
// Split into batches of max_bulk_size, then call flush. Similar to creating_entries, so details are omitted.
std::vector<Te> rs;
std::vector<sai_attribute_t> ts;
std::vector<sai_status_t*> status_vector;
...
flush_setting_entries(rs, ts, status_vector);
setting_entries.clear();
}
}
sai_status_t flush_creating_entries(
_Inout_ std::vector<Te> &rs,
_Inout_ std::vector<sai_attribute_t const*> &tss,
_Inout_ std::vector<uint32_t> &cs)
{
...
// Call SAI bulk create API
size_t count = rs.size();
std::vector<sai_status_t> statuses(count);
sai_status_t status = (*create_entries)((uint32_t)count, rs.data(), cs.data(), tss.data()
, SAI_BULK_OP_ERROR_MODE_IGNORE_ERROR, statuses.data());
// Set results back to input entries and clean up the batch below.
for (size_t ir = 0; ir < count; ir++) {
auto& entry = rs[ir];
sai_status_t *object_status = creating_entries[entry].second;
if (object_status) {
*object_status = statuses[ir];
}
}
rs.clear(); tss.clear(); cs.clear();
return status;
}
// flush_removing_entries and flush_setting_entries are similar to flush_creating_entries, so we omit them here.
...
};
orchagent中的SAI对象转发
细心的小伙伴肯定已经发现了奇怪的地方,这里EntityBulker
怎么看着像在直接调用SAI API呢?难道它们不应该是在syncd中调用的吗?如果我们对传入EntityBulker
的SAI API对象进行跟踪,我们甚至会找到sai_route_api_t就是SAI的接口,而orchagent
中还有SAI的初始化代码,如下:
// File: src/sonic-sairedis/debian/libsaivs-dev/usr/include/sai/sairoute.h
/**
* @brief Router entry methods table retrieved with sai_api_query()
*/
typedef struct _sai_route_api_t
{
sai_create_route_entry_fn create_route_entry;
sai_remove_route_entry_fn remove_route_entry;
sai_set_route_entry_attribute_fn set_route_entry_attribute;
sai_get_route_entry_attribute_fn get_route_entry_attribute;
sai_bulk_create_route_entry_fn create_route_entries;
sai_bulk_remove_route_entry_fn remove_route_entries;
sai_bulk_set_route_entry_attribute_fn set_route_entries_attribute;
sai_bulk_get_route_entry_attribute_fn get_route_entries_attribute;
} sai_route_api_t;
// File: src/sonic-swss/orchagent/saihelper.cpp
void initSaiApi()
{
SWSS_LOG_ENTER();
if (ifstream(CONTEXT_CFG_FILE))
{
SWSS_LOG_NOTICE("Context config file %s exists", CONTEXT_CFG_FILE);
gProfileMap[SAI_REDIS_KEY_CONTEXT_CONFIG] = CONTEXT_CFG_FILE;
}
sai_api_initialize(0, (const sai_service_method_table_t *)&test_services);
sai_api_query(SAI_API_SWITCH, (void **)&sai_switch_api);
...
sai_api_query(SAI_API_NEIGHBOR, (void **)&sai_neighbor_api);
sai_api_query(SAI_API_NEXT_HOP, (void **)&sai_next_hop_api);
sai_api_query(SAI_API_NEXT_HOP_GROUP, (void **)&sai_next_hop_group_api);
sai_api_query(SAI_API_ROUTE, (void **)&sai_route_api);
...
sai_log_set(SAI_API_SWITCH, SAI_LOG_LEVEL_NOTICE);
...
sai_log_set(SAI_API_NEIGHBOR, SAI_LOG_LEVEL_NOTICE);
sai_log_set(SAI_API_NEXT_HOP, SAI_LOG_LEVEL_NOTICE);
sai_log_set(SAI_API_NEXT_HOP_GROUP, SAI_LOG_LEVEL_NOTICE);
sai_log_set(SAI_API_ROUTE, SAI_LOG_LEVEL_NOTICE);
...
}
相信大家第一次看到这个代码会感觉到非常的困惑。不过别着急,这其实就是orchagent
中SAI对象的转发机制。
熟悉RPC的小伙伴一定不会对proxy-stub
模式感到陌生 —— 利用统一的接口来定义通信双方调用接口,在调用方实现序列化和发送,然后再接收方实现接收,反序列化与分发。这里SONiC的做法也是类似的:利用SAI API本身作为统一的接口,并实现好序列化和发送功能给orchagent
来调用,然后再syncd
中实现接收,反序列化与分发功能。
这里,发送端叫做ClientSai
,实现在src/sonic-sairedis/lib/ClientSai.*
中。而序列化与反序列化实现在SAI metadata中:src/sonic-sairedis/meta/sai_serialize.h
:
// File: src/sonic-sairedis/lib/ClientSai.h
namespace sairedis
{
class ClientSai:
public sairedis::SaiInterface
{
...
};
}
// File: src/sonic-sairedis/meta/sai_serialize.h
// Serialize
std::string sai_serialize_route_entry(_In_ const sai_route_entry_t &route_entry);
...
// Deserialize
void sai_deserialize_route_entry(_In_ const std::string& s, _In_ sai_route_entry_t &route_entry);
...
orchagent
在编译的时候,会去链接libsairedis
,从而实现调用SAI API时,对SAI对象进行序列化和发送:
# File: src/sonic-swss/orchagent/Makefile.am
orchagent_LDADD = $(LDFLAGS_ASAN) -lnl-3 -lnl-route-3 -lpthread -lsairedis -lsaimeta -lsaimetadata -lswsscommon -lzmq
我们这里用Bulk Create作为例子,来看看ClientSai
是如何实现序列化和发送的:
// File: src/sonic-sairedis/lib/ClientSai.cpp
sai_status_t ClientSai::bulkCreate(
_In_ sai_object_type_t object_type,
_In_ sai_object_id_t switch_id,
_In_ uint32_t object_count,
_In_ const uint32_t *attr_count,
_In_ const sai_attribute_t **attr_list,
_In_ sai_bulk_op_error_mode_t mode,
_Out_ sai_object_id_t *object_id,
_Out_ sai_status_t *object_statuses)
{
MUTEX();
REDIS_CHECK_API_INITIALIZED();
std::vector<std::string> serialized_object_ids;
// Server is responsible for generate new OID but for that we need switch ID
// to be sent to server as well, so instead of sending empty oids we will
// send switch IDs
for (uint32_t idx = 0; idx < object_count; idx++) {
serialized_object_ids.emplace_back(sai_serialize_object_id(switch_id));
}
auto status = bulkCreate(object_type, serialized_object_ids, attr_count, attr_list, mode, object_statuses);
// Since user requested create, OID value was created remotely and it was returned in m_lastCreateOids
for (uint32_t idx = 0; idx < object_count; idx++) {
if (object_statuses[idx] == SAI_STATUS_SUCCESS) {
object_id[idx] = m_lastCreateOids.at(idx);
} else {
object_id[idx] = SAI_NULL_OBJECT_ID;
}
}
return status;
}
sai_status_t ClientSai::bulkCreate(
_In_ sai_object_type_t object_type,
_In_ const std::vector<std::string> &serialized_object_ids,
_In_ const uint32_t *attr_count,
_In_ const sai_attribute_t **attr_list,
_In_ sai_bulk_op_error_mode_t mode,
_Inout_ sai_status_t *object_statuses)
{
...
// Calling SAI serialize APIs to serialize all objects
std::string str_object_type = sai_serialize_object_type(object_type);
std::vector<swss::FieldValueTuple> entries;
for (size_t idx = 0; idx < serialized_object_ids.size(); ++idx) {
auto entry = SaiAttributeList::serialize_attr_list(object_type, attr_count[idx], attr_list[idx], false);
if (entry.empty()) {
swss::FieldValueTuple null("NULL", "NULL");
entry.push_back(null);
}
std::string str_attr = Globals::joinFieldValues(entry);
swss::FieldValueTuple fvtNoStatus(serialized_object_ids[idx] , str_attr);
entries.push_back(fvtNoStatus);
}
std::string key = str_object_type + ":" + std::to_string(entries.size());
// Send to syncd via the communication channel.
m_communicationChannel->set(key, entries, REDIS_ASIC_STATE_COMMAND_BULK_CREATE);
// Wait for response from syncd.
return waitForBulkResponse(SAI_COMMON_API_BULK_CREATE, (uint32_t)serialized_object_ids.size(), object_statuses);
}
最终,ClientSai
会调用m_communicationChannel->set()
,将序列化后的SAI对象发送给syncd
。而这个Channel,在202106版本之前,就是基于Redis的ProducerTable了。可能是基于效率的考虑,从202111版本开始,这个Channel已经更改为ZMQ了。
// File: https://github.com/sonic-net/sonic-sairedis/blob/202106/lib/inc/RedisChannel.h
class RedisChannel: public Channel
{
...
/**
* @brief Asic state channel.
*
* Used to sent commands like create/remove/set/get to syncd.
*/
std::shared_ptr<swss::ProducerTable> m_asicState;
...
};
// File: src/sonic-sairedis/lib/ClientSai.cpp
sai_status_t ClientSai::initialize(
_In_ uint64_t flags,
_In_ const sai_service_method_table_t *service_method_table)
{
...
m_communicationChannel = std::make_shared<ZeroMQChannel>(
cc->m_zmqEndpoint,
cc->m_zmqNtfEndpoint,
std::bind(&ClientSai::handleNotification, this, _1, _2, _3));
m_apiInitialized = true;
return SAI_STATUS_SUCCESS;
}
关于进程通信的方法,这里就不再赘述了,大家可以参考第四章描述的进程间的通信机制。
syncd更新ASIC
最后,当SAI对象生成好并发送给syncd
后,syncd
会接收,处理,更新ASIC_DB,最后更新ASIC。这一段的工作流,我们已经在Syncd-SAI工作流中详细介绍过了,这里就不再赘述了,大家可以移步去查看。
参考资料
- SONiC Architecture
- Github repo: sonic-swss
- Github repo: sonic-swss-common
- Github repo: sonic-frr
- Github repo: sonic-utilities
- Github repo: sonic-sairedis
- RFC 4271: A Border Gateway Protocol 4 (BGP-4)
- FRRouting
- FRRouting - BGP
- FRRouting - FPM
- Understanding EVPN Pure Type 5 Routes