网络编程自动化精要-全-
网络编程自动化精要(全)
原文:
zh.annas-archive.org/md5/646266457ebe16c16cfa3111e6e3d4e1译者:飞龙
前言
嗨!当我从网络工程转型到软件工程时,我理解开发者理解计算机网络概念有多难,网络工程师理解软件概念有多难。
随着应用变得基于云并且内容存储在云中,网络基础设施是当今企业最重要的资产。一个 10 GE 接口 15 分钟的故障相当于 1 TB 的数据丢失。最小化停机时间,最大化容量和速度,降低延迟和抖动,是网络提供商今天的重大性能指标。而且,没有网络自动化,从网络的概念和设计到日常网络操作,这些指标的提高变得越来越不可能。
本书将从对网络基础的一个简要描述开始,然后更深入地探讨网络编程概念和自动化生态系统。接着将描述网络编程的协议、工具和技术。所有这些都将基于网络自动化中最流行的两种计算机语言,即 Go 和 Python。
将使用实时网络仿真来支持书中涵盖的概念,通过动手实验室。到本书结束时,你将拥有足够的知识来开始编程和创建网络自动化解决方案。
本书面向的对象
本书面向希望将编程集成到网络中的网络架构师、网络工程师和软件专业人士。遵循传统技术的网络工程师可以通过阅读本书来了解现代网络自动化和编程。
本书涵盖的内容
第一章,开发网络基础,专注于解释计算机网络的基本概念和术语。目的是在整个书中建立一个良好的基础。如果你是网络工程师或在这个领域有经验,你可能想跳过这一章。如果你是网络经验较少的软件开发者,这一章就是为你准备的。它将帮助你建立一个坚实的网络术语基础,这对于编写网络自动化代码将非常有用。
第二章,可编程网络,探讨了今天用于通过软件创建网络的几种不同技术。然后我们将检查当前的标准技术,称为软件定义网络(SDNs)。
第三章,访问网络,探讨了访问网络设备用于网络自动化的最常见方法和协议。由于设备有多种方法,我们将努力提供足够的信息,以便你可以选择最适合你网络自动化代码的方法。
第四章,与网络配置和定义一起工作,探讨了如何处理网络配置以及如何定义它以有效地用于网络自动化。我们希望构建可扩展和面向未来的解决方案。本章回答了为什么我们需要网络定义来帮助自动化。
第五章,网络编程的禁忌与规范,重点关注与网络编程相关的 Python 和 Go 最重要的最佳编码实践。编写网络代码是令人兴奋的,因为当它失败时,具有挑战性,而当它成功时,则令人满意。如果你是一位经验丰富的程序员,这将是一帆风顺的,但如果你是新手,那将充满风暴。本章深入探讨了 Go 和 Python 的一些编码实践,这些实践将帮助你更容易地度过这些风暴。
第六章,使用 Go 和 Python 进行网络编程,探讨了 Python 和 Go 在网络编程中的强大和实用性,但根据需求和环境的差异,一个可能比另一个更适合。我们还将检查使用 Python 和 Go 的优缺点。
第七章,错误处理和日志记录,探讨了如何报告程序执行事件以及如何处理错误。这两个主题并不像看起来那么简单,而且大多数情况下,它们在系统中实现得并不好。本章调查了为什么以及如何处理错误,以及为什么以及如何进行事件日志记录。
第八章,代码扩展性,介绍了一些今天用来有效扩展和缩小代码的技术,这将使你的解决方案能够轻松适应网络增长,并在必要时轻松缩小规模以节省资源。
第九章,网络代码测试框架,专注于构建可用于测试你的网络自动化代码的网络测试框架的技术。它还探讨了可以用来使你的测试框架更加有用和可靠的高级技术。
第十章,动手实践与展望,探讨了使用我们学到的网络自动化技能和模拟路由器从头开始构建网络。完成的模拟网络将包含足够的组件来实验本书各章节中描述的几种技术。最后,还有一些关于未来学习和工作的评论和建议。
要充分利用这本书
你需要能够使用 Linux、Windows 或 macOS 进行特权访问,并能够安装 Go 和 Python 语言以及相应的第三方库。对计算机网络概念的基本理解也将有所帮助。*
| 本书涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| 建议 Go 语言版本 1.20 或更高版本 | Windows、macOS 或 Linux |
| 建议 Python 版本 3.10.9 或更高版本 | Windows、macOS 或 Linux |
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件github.com/PacktPublishing/Network-Programming-and-Automation-Essentials。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富图书和视频目录的代码包可供选择,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图和图表彩色图像的 PDF 文件。您可以从这里下载:packt.link/AXHbe。
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“两种模式使用相同的 Linux 设备驱动程序(通过/dev/net/tun访问),只是标志不同。使用 TAP 模式的标志是IFF_TAP,而使用 TUN 的标志是IFF_TUN。”
代码块设置如下:
from paramiko import SSHClient
client = SSHClient()
client.connect('10.1.1.1', username='user', password='pw')
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
import unittest
import mock
from paramiko import SSHClient
class TestSSHClient(unittest.TestCase):
@mock.patch('paramiko.SSHClient.connect')
def test_connect(self, mock_connect):
任何命令行输入或输出都按照以下方式编写:
claus@dev:~$ sudo ip tuntap add dev tap0 mode tap
claus@dev:~$ sudo ip link set tap0 up
claus@dev:~$ ip link show tap0
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“包含E5、S5和L5后,Clos 网络现在将有 50 个连接。”
小贴士或重要注意事项
看起来像这样。
联系我们
欢迎读者反馈。
总体反馈:如果您对本书的任何方面有任何疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,请访问www.packtpub.com/support/errata并填写表格。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packtpub.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com
分享您的想法
读完《网络编程和自动化基础》后,我们非常乐意听到您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买此书!
你喜欢在旅途中阅读,但无法携带你的印刷书籍到处走吗?你的电子书购买是否与你的选择设备不兼容?
请放心,现在,每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,你还可以获得独家折扣、时事通讯和每天收件箱中的优质免费内容
按照以下简单步骤获取福利:
- 扫描下面的二维码或访问以下链接

packt.link/free-ebook/9781803233666
-
提交您的购买证明
-
就这些!我们将直接将您的免费 PDF 和其他福利发送到您的电子邮件
.
第一部分:网络自动化的基础
第一部分致力于让您重温一些网络基础和术语,以及讨论一些网络自动化的重要方面,这些方面应作为您工作的基础。本部分还讨论了网络自动化的其他基础,例如访问网络所使用的方法和协议,以及我们应该如何使用网络配置和定义。
本部分包含以下章节:
-
第一章,开发网络基础
-
第二章,可编程网络
-
第三章,访问网络
-
第四章,与网络配置和定义一起工作
第一章:开发网络基础知识
本章的重点是解释计算机网络中使用的基礎术语。目的是建立一个良好的基础,贯穿整本书。
如果你是一个网络工程师或在这一领域有经验,你可能想跳过它,或者也许只是浏览一下。
如果你是一个对网络经验较少的软件开发者,这一章就是为你准备的。它将帮助你建立一个坚实的网络术语基础,这对于编写网络自动化代码时非常有用。
本章我们将涵盖以下主题:
-
复习协议层、网络设备类型和网络拓扑
-
描述网络架构及其组件
-
阐述网络管理组件、网络堡垒等
复习协议层、网络设备类型和网络拓扑
我们在这里有很多话要说。但由于这本书的篇幅限制,我已经整理了一个摘要,其中包括了今天网络术语最重要的方面,并对其进行了简要解释。希望你能找到一些新的信息,帮助你自动化工作。
协议层
需要注意的是,存在多种不同的协议层标准,其中最学术性的一个是被称为OSI 模型的 ISO 组织,它定义了七层。但我们将只考虑在互联网上使用的 TCP/IP 协议栈中定义的五层,以下是对每一层的简要总结:
-
物理层:在这一层中,涉及的是物理连接本身的技术,其中比特和字节被转换成物理媒介,例如光纤中的光、电缆中的电和天线中的无线电波。在这一层,可以在节点输入上实施物理检查,例如功率水平、冲突、噪声和信号失真等。
-
数据链路层:在这里,信息被称为帧,它包含一个分隔的大小,称为最大传输单元(MTU)。原因是帧是字节表示的数据,它必须从一个节点移动到另一个节点,并且以可靠的方式无中断地移动。在这一层,存在帧队列;队列用于将帧按顺序或优先级顺序放置在物理层。某些数据链路设备可以优先处理某些类型的帧,将其跳到队列的前面。在数据链路层,会进行一些检查,但这些检查是在帧本身内进行的,例如 CRC 或校验和。此外,还可以将源地址和目的地址添加到帧中,以区分共享媒体上的目的地。帧上的信息通常在同一个组织内部本地使用。这一层也被称为以太网层。
-
网络层:这也被称为IP 层,或路由器层。在这里,信息被称为数据包,它包含在层 2 域(或之前的以太网层)之间的节点之间传输的信息。在这一层,使用路由协议,网络地址转换(NAT)执行其任务,存在一些访问控制列表(ACLs),控制数据包在其它功能中。在这一层的包包含了足够的信息来知道它从哪里来以及它要去哪里。这一层还负责如果帧 MTU 小于 IP 包时将包分片成多个包。包中携带的主要信息是IP 地址,并包含源地址和目标地址。
-
80用于 HTTP 通信,并将其与主机中的通信套接字关联。端口号对于源地址和目标地址都是必需的,它将用来指定与主机通信的正确套接字。 -
应用层:这是层的顶端,通常被我的教授称为“蛋糕上的樱桃”。应用层用于将主机上的套接字与将要发送和接收的数据关联起来。应用通常处理数据的正文,例如 HTTP 中的页面请求。我们在这本书中生产的软件使用这一层来自动化网络。
局域网(LAN)、广域网(WAN)、互联网和内部网
局域网(LAN),或本地网络,用来指代本地网络。如今,它意味着使用数据链路层作为主要通信的网络,例如以太网。之所以这个名字更多地与通信层相关而不是地理相关,是因为技术已经发展,使得以太网交换机能够在数千公里内通信。因此,一个局域网通常指同一组织内部使用以太网的拓扑结构,但不一定是地理位置相同。
广域网(WAN),或宽域网络,用来指代远程连接的网络,或允许节点相隔甚远的 技术,例如已经灭绝的技术如 X.25、帧中继和异步传输模式(ATM)。现在,WAN 这个术语通常用来指代连接到不同网络或换句话说,不在同一组织、数据链路层或以太网域中的接口或网络。
信息
如需了解更多关于 ATM 的信息,请参阅 SSRN 电子期刊 1998 年 6 月的文章《技术和应用》,作者:Jeffrey Scott Ray。
互联网就是你所知道的,这个连接全球每个人的巨大网络。
当公司使用互联网协议在其网络上进行内部通信时,使用了“内部网络”这个术语。原因是当时有其他技术与互联网 TCP/IP 协议竞争,例如 SNA 和 IPX。因此,当使用“内部网络”这个术语时,它只是简单地表明公司网络使用 TCP/IP。如今,“内部网络”指的是同一组织内的网络,且不连接到外部节点。因此,该网络是安全的,不受外部干扰。
点对点连接
点对点(P2P)连接用于连接两个节点。两个节点之间的连接通常是 P2P 连接(如图 1.1 所示),除非使用卫星或广播天线等媒体。这种连接可以是背靠背的,也可以不是。术语背靠背通常用来表示节点之间直接连接,没有任何其他物理层(如中继器)介于它们之间。因此,背靠背连接由于连接中引入的噪声和失真而受到限制,随着线缆变长,距离有限,通常限制在同一房间或建筑物内。

图 1.1 – P2P 连接
星形或中心辐射拓扑
星形或中心辐射拓扑用于小型和中型公司,其中一家办公室是主要分销商,其他地点是消费者。该拓扑看起来像一颗星,远程位置的网元较小且简单,而在主要分销商处较大且复杂(见图 1.2 的示例)。
通常,这些类型的拓扑可以扩展到数百个节点,但根据流量,需求可以扩展到数千。让我们看看两个示例,以说明这些拓扑的规模。
例如,在银行中,自动柜员机分布在偏远地点,而主计算机位于总行。由于在柜员机上的字节传输量很小,因此这种拓扑可以扩展到数千台远程机器。
另一方面,如果你有一个使用星形拓扑的超市连锁店,它无法扩展到数千台远程机器,因为每个超市都需要大量数据传输来处理所有交易和员工。
因此,星形拓扑的使用受到其中央节点可以处理的流量的限制。在星形拓扑中,我们有两种设备功能,一种设备将位于远程位置或主办公室。
当处理星形拓扑时,网络容量规划很简单,因为随着其增长,主办公室节点会更新。

图 1.2 – 星形拓扑
分层或树形拓扑
分层拓扑用于优化流量,其中较大的节点用于按分层方式将流量聚合到较小的节点(参见图 1.3中的示例)。这些拓扑可以扩展到数千个节点;然而,由于路径中的节点数量,这些拓扑可能导致不希望的延迟和额外的节点成本。
互联网服务提供商通常使用分层拓扑在特定远程位置集中客户流量,然后再在其他位置进行进一步的聚合。
这种类型拓扑的节点数量没有限制,它是互联网全球基础设施的基础之一。
在分层拓扑中,我们有多个设备功能,包括客户场所设备(CPE)、聚合器、分发器、核心和对等,等等。
根据这种拓扑的大小,它可能会引入更长的路径,这将增加显著的延迟。例如,在图 1.3中,A1必须穿越五个主机才能到达A7。
网络容量规划集中在聚合点,增强网络并不困难。

图 1.3 – 分层或树形拓扑
Clos 拓扑
这种类型的拓扑也被称为Clos 网络或布线。这种拓扑用于在不影响延迟和吞吐量的情况下增加端口数量,通常用于数据中心。这种拓扑至少由三个阶段组成。请注意,与分层拓扑不同,这里没有过度预订或聚合。Clos 拓扑在输入和输出上提供相同数量的可用带宽。阶段名称通常是脊和叶。脊始终位于中心,并且仅与 Clos 节点连接。叶用于连接外部设备或网络。
图 1.4展示了 16 端口 Clos 网络的示例。请注意,通常,脊节点到叶节点的所有连接都是背靠背:

图 1.4 – Clos 拓扑
为什么使用这些拓扑?为了在不影响吞吐量的情况下增加可用的端口数量。这种拓扑也用于路由器内部,以提供接口卡之间的连接。一些公司使用小型设备来增加提供的端口数量,而不增加成本,因为小型设备通常更便宜。
重要提示
Clos 网络的另一个特点是它具有任何两个外部端口之间的相同距离(就路径中的节点而言),因此正常条件下的延迟是相同的。例如,在图 1.4中,节点 L1 上的外部端口与 L4 或 E1 上的外部端口之间的延迟是相同的。
重要提示
关于 Clos 网络的更多信息可以在一篇有趣的谷歌论文中找到,名为《Jupiter Rising:谷歌数据中心网络中 Clos 拓扑和集中式控制十年》—— ACM SIGCOMM 计算机通信评论,第 45 卷,第 4 期,2015 年 10 月。
混合拓扑
混合拓扑在大公司中使用,其中延迟和流量都很重要。通常,星型拓扑和对等网络用于缩短路径和降低延迟,而分层拓扑用于优化和聚合流量,最后,Clos 网络用于增加端口数量。
现代云服务提供商正在迁移到一个更复杂的拓扑,其中元素之间存在延迟相关的连接,以及流量相关的聚合设备功能。
网络容量规划通常更困难,因为连接不是完全分层的,聚合点也不一定是所有流量路径的一部分。这种混合拓扑的一个例子在 图 1.5 中显示:

图 1.5 – 混合拓扑
接口速度
一些工程师可能会混淆的一个非常重要的点是接口速度的表示。在内存表示中,1 KB 是 2¹⁰ 或 1,024 字节,而 1 GB 是 2³⁰,即 1,073,741,824 字节。对于接口速度,情况并不相同,1 Kbps 实际上是 1,000 比特/秒,而 1 Gbps 是 1,000,000,000 比特/秒(更多详细信息请参阅 en.wikipedia.org/wiki/Data-rate_units)。
设备类型和功能
网络设备过去具有特定的功能,因为 CPU 和内存稀缺且昂贵。如今,网络设备在需要时可以具有多种功能。在大网络中,设备的功能较少,因为当流量需求增加时,它们更容易过载。以下是一些设备可能具有的功能:
-
集线器:这是一个非常古老的术语,用来指代只重复物理信号的设备。
-
交换机:仅在数据链路层工作的设备。它通常用于局域网,通过交换帧来工作。在这些设备上用于控制路径的最常见协议是 生成树协议(STP)。
-
路由器:仅在网络层或 IP 层工作的设备。它用于连接多个局域网或创建长途远程连接。内部,路由器使用路由协议路由数据包,与其他路由器交换路由信息。一些路由器还可以交换帧或作为交换机工作。
-
NAT:NAT 是替换源和目的 IP 地址的设备,允许使用私有 IP 地址或将内部流量与外部流量隔离。
-
防火墙:通常,通过检查帧或数据包的内容来控制通过它的流量。有几种不同类型的防火墙,其中一些可能非常复杂,包括加密和解密流量。
-
负载均衡器:当服务器由于硬件限制无法处理过多的客户端时,可以使用负载均衡器通过在多个服务器之间共享客户端请求来处理客户端需求。这些设备还会检查数据包内容以确定哪个服务器将获得流量。
-
网络服务器:用于向网络提供某种服务的计算机,例如认证服务器、NTP 服务器或 Syslog 收集器。
过度订阅
在网络术语中,这个术语用来描述网络中聚合来自网络其他部分流量的节点或链接,并统计上利用它来获得优势。例如,它们有一个 1 Gbps 的接口连接到互联网,以及 1000 个拥有 10 Mbps 接口的客户使用服务,这是一个 1 到 10 的过度订阅。这种做法相当普遍,并且之所以能够这样做,是因为客户端流量的特性允许在不降低性能的情况下进行这种聚合。互联网上有许多数学模型和论文描述了这种行为以及如何利用它。
但有些流量不能在不降低性能的情况下进行聚合。在数据中心,无法过度订阅的流量是服务器之间的流量,例如远程磁盘、数据传输和数据库副本。在这种情况下,最佳解决方案是使用非阻塞 Clos 拓扑等解决方案在不进行过度订阅的情况下将它们互连。
浏览网页、观看视频和接收来自互联网上大部分流量的消息,这很容易允许在不降低性能的情况下进行聚合技术。
重要提示
关于过度订阅的更多信息可以在 A. Raju、V. Gonçalves 和 P. Ballon 发表的论文《评估过度订阅对未来互联网商业模式的影响》中找到——发表于 2012 年 5 月 25 日的网络研讨会——计算机科学。
在本节中,我们介绍了计算机网络的基本组件,包括协议、拓扑类型、接口速度和设备类型。到现在为止,你应该能够更容易地识别这些术语,并且熟悉它们的含义,因为我们将在这本书中反复使用这些术语。接下来,我们将回顾更多与网络架构相关的术语。
描述网络架构及其组件
网络架构这个术语是在 2000 年代初提出的,模仿了建筑行业中的角色,在那里建筑师设计,土木工程师建造。不同的公司使用这个术语的方式不同,但在这本书中,网络架构将用来指代网络的设计及其功能。
对于一个好的网络架构,最好有一个详细描述网络前三个层次的文档,从物理层到路由层。有了这份文档,工程师们可以很容易地理解物理连接、以太网域和使用的路由协议。
图表
网络图表大多像一张地图,其中城市是节点,道路是连接它们的链接。对于网络工程师来说,图表对于描述节点如何连接至关重要,它们还可以分组和划分重要区域。一个好的图表易于解释,并可以追踪数据流。
有多达三种类型的图表;它们可以集成在同一页面上和图表中,或者可以分离到不同的页面上。主要图表中有一个用于显示物理连接,这可能包括数据链路层中涉及的技术,以及交换和路由图表。
在 图 1.6 中,我们可以看到一个网络图表的示例:

图 1.6 – 网络图表示例
图 1.7 展示了网络图表符号的示例:

图 1.7 – 网络图表符号
网络节点名称
网络节点是一种主要用于互连和作为网络中数据传输的设备。它可以是集线器、交换机或路由器。为了帮助网络工程师识别节点功能,使用名称来描述它们的主要功能。以下是一些例子:
-
中继路由器:这些路由器与其他服务提供商有接口。这些链接通常用作访问其他网络的服务,因此它们有成本,因为它们通常连接到其他大型运营商。
-
对等路由器:这些路由器以对等配置与其他网络有接口,这意味着没有任何部分需要付费使用。在这些链接中,只有对等公司之间的流量被交换,不允许流量流向外部网络。使用中继路由器访问外部网络的情况。
-
核心路由器:这些节点位于网络中心。它们通常处理大量流量并具有高速接口。它们的吞吐量能力是网络中最高的,但它们的接口较少,因为它们集中了网络的流量。
-
分发路由器:这些节点通常连接到核心路由器和聚合路由器。它们通常连接网络的不同位置。它们没有很多接口,吞吐量能力很高,但不如核心路由器高。
-
聚合路由器:这些路由器通常聚合接入路由器的流量。它们通常位于接入路由器相同区域或位置,并且与接入路由器相比接口更少。
-
接入路由器: 一些架构师添加一个节点,连接所有最后一英里网络或 CPE 节点。这些路由器位于更靠近客户的位置,并且比其他路由器有更多的接口。
-
机架顶部 (TOR): TOR指的是可以是交换机或路由器的节点,这取决于架构。它们负责将机架中的服务器连接到整个网络。
-
Clos 机架: 如前所述,Clos 网络是一种使用小型设备向多个服务器添加连接性的技术。Clos 机架在网络中被视为一个唯一的单一块,在架构方面,它作为一个单一节点,通常用作具有大量接口的单个路由器。
-
CPE: CPE 是在客户位置安装的节点。它通常有一个接口连接到最后一英里网络,还有一个本地接口,可以是以太网或无线以太网。这些设备还可以实现 NAT、防火墙,在某些情况下,它们有多个本地接口,可以作为交换机和路由器使用。这些节点价格低廉、体积小,与其他节点相比,吞吐量能力非常低。
最后一英里网络
这个术语用来描述连接客户到网络的架构。通常,这个术语仅用于 ISP,但一些公司也用它来互联他们的分支机构。
最后一英里网络的覆盖范围有限,通常不会超过 1 公里,这取决于所使用的技术类型。以下是一些最常见的最后一英里网络:
-
有线电视: 在这里使用了多种技术,通过客户安装的有线电视提供数据通信。最常用的是 DOCSIS,它在 2017 年升级到了版本 4。该解决方案使用一根共享给多个住宅的单电缆。
-
数字用户线路 (DSL): DSL 使用旧的电话线路进行数据通信。为此,有许多标准,其中最常见的是 VDSL 和 ADSL。DSL 解决方案不与有线电视共享相同的媒体,每个客户都有一个单独的电缆。
-
光纤到户 (FTTP): FTTP 是指光纤到达客户的住宅。就像有线电视一样,最常见的实现方式是单根光纤以可共享的方式跨越多个客户。最常见的技术是无源光网络 (PON),或者更具体地说,是吉比特以太网 PON (GPON)(或 G.984)。
重要注意事项
关于 GPON 网络的更多详细信息可以在 2010 年 11 月的论文《电信网络中的 GPON》中找到——2010 年国际超现代电信与控制系统大会(ICUMT)会议论文,2010 年。
-
Wi-Fi:通常,这项技术用于公司或家庭内部的私人用途,但一些互联网服务提供商(ISP)使用无线以太网标准(IEEE 802.11 系列)通过全向天线向客户提供最后一英里的服务。这种特定用途因国家而异,取决于政府的立法。它们通常被宣传为以太网热点(
en.wikipedia.org/wiki/Hotspot_(Wi-Fi))。 -
卫星:对于使用卫星进行数据通信,有两种方法:一种使用地球静止卫星,另一种使用星座卫星。它们之间的区别在于延迟,因为地球静止轨道离地球非常远。星座方法具有低延迟,但由于卫星不断移动,存在切换挑战,通常数据吞吐量非常低。最著名的使用地球静止轨道的技术是 VSAT。使用 VSAT 的互联网每次从地球到卫星的旅行都会增加大约 250 毫秒,因此往返时间为 500 毫秒。但随着 SpaceX 宣布他们最终解决了使用星座方法的切换问题,高延迟的黑暗时代可能已经结束。这项新服务被称为Starlink,并承诺使用低轨道卫星提供高容量、低延迟和高可用性。
重要提示
在卡林西亚应用科学大学 Research group ROADMAP-5G 的研究论文《Starlink 分析》中可以找到关于 Starlink 网络的良好讨论,日期为 2021 年 7 月 15 日。
-
电力线通信(PLC)或HomePlug:PLC,或电力线宽带(BoPL),使用电力线进行数据通信。这是通过在电线上调制高频来实现的。大多数变压器无法通过信息,因为它们充当低频截止滤波器,所以它必须包含在房屋内或变压器之间。这里最常见的技术是 HomePlug AV2 和 IEEE 1901-2010(
ieeexplore.ieee.org/document/5678772)。 -
移动网络:无疑是最后一英里中最受欢迎的网络。如今,它们使用 5G 技术,但其他旧网络仍在使用中,例如 4G(LTE)、3G 和 GPRS。
重要提示
更多关于移动技术的信息可以在 A. Agarwal、K. Agarwal、S. Agarwal 和 G. Misra 撰写的《面向 5G 网络的移动通信技术演变及其挑战》一文中找到,该文发表于 2019 年,美国电气和电子工程杂志,第 7 卷,第 2 期,第 34-37 页。
物理架构
物理架构有时并不一定是描述连接设备的电缆或光纤,而是网络作为 TCP/IP 堆栈中定义的物理层所使用的基础设施。这意味着我们可以将其他外国网络作为物理层来重用,即使它们有自己的协议栈。以下是架构中可能使用的某些物理技术:
-
暗光纤:当连接节点时,暗光纤这个术语意味着连接的节点将使用不含中继器或底层基础设施的光纤。在两个节点之间使用暗光纤连接的情况下,如果一个节点断电,另一个节点将不会从光纤中接收到任何光。在这种情况下,光纤切断会在两端立即感知到,接口会立即因光纤切断而关闭。当发生故障时,只有输出接口队列中的数据包会被丢弃。
-
同步传输模块(STM):STM 最初是为了复用数字电话线路而创建的,但后来开始用于数据通信。最常见的是 STM-1,其速率为 155 Mbps。路由器曾经有一个接口可以封装 STM 帧到 STM 网络。STM 网络只是将帧从一端切换到另一端。使用这种技术切断光纤可能不会迅速感知到,这可能导致大量数据包丢失。正如我们稍后将要描述的,双向转发检测(BFD)需要在这里使用以避免严重问题。
-
密集波分复用(DWDM):DWDM 是 STM 的一种演变。DWDM 网络是一个交换网络,它也为每个承载的数据包的帧、时间和波分提供了分路,类似于 STM 但有所增强。同样,BFD 是必要的,因为在这里光纤的切断可能不会迅速感知到,这可能导致大量数据包丢失。
-
端到端:如前所述,端到端这个术语通常用来指定直接连接而没有其他物理层(如中继器)在之间的节点。
-
网络隧道:网络隧道是网络中用于封装流量并在不同网络中传输的点。隧道可以是第 2 层或第 3 层,并实现以抽象所承载的网络。在某些网络架构中,它们旨在使用外国基础设施到达网络的遥远部分。
-
VPN 隧道:这些就像网络隧道。VPN 隧道通常会增加加密。
路由架构
定义网络中流量如何流动是很重要的。为此,我们需要在路由分配方面有一个适当的设计。这是必要的,以便实现故障恢复、冗余路径、负载均衡、路由策略和流量协议。如果连接到外部,架构必须包括内部路由协议和外部路由协议。以下是一个总结:
-
内部网关协议(IGP):IGP 是一种在限定区域或位置运行的路由协议,通常如名称所示,在同一个组织内部运行。在 IGP 域中,路由器通过宣布和接收拓扑更新来交换路径信息。最常用的 IGP 使用链路状态信息来构建路由路径拓扑。如果一个接口故障,更新必须传播到整个 IGP 域。孤立区域被用来避免更新过大的拓扑并引起不稳定。历史上,流行的 IGP 有 RIP 和 EIGRP,但今天,只有开放最短路径优先(OSPF)和中间系统到中间系统(IS-IS)被使用。
-
外部网关协议(EGP):EGP 是一种用于在组织之间交换路由信息的路由协议。它通常不包含链路状态信息,只有路径距离。最常用的 EGP 协议是边界网关协议(BGP)。
-
IS-IS: IS-IS 是由 ISO 设计的 IGP 协议,注册为 ISO 10589。它是一种基于称为迪杰斯特拉算法的最短路径算法的链路状态协议。它是第二常用的 IGP。
-
OSPF: OSPF 是由 IETF 设计的 IGP 协议,最初于 1989 年由 RFC1131 注册,后来更新了几次。版本 3 是最后在 RFC5340 中描述的版本。OSPF 也使用迪杰斯特拉算法来计算路径,是最流行和广泛使用的 IGP。OSPF 使用区域来扩展并提高路由数据库更新期间的稳定性。
-
BGP: BGP 是一种独特的协议,用于在组织之间交换路由信息。它首次在 1989 年的 RFC1105 中被引入。它也是 IETF 上更新和扩展最多的协议之一,可用于不同的目的,例如内部 BGP(iBGP)、在 RFC4760 中定义的多协议 BGP(MP-BGP)、MPLS(MP-BGP)以及最近在 2017 年定义的 BGPsec,定义在 RFC8205 中。BGP 是一种基于路径向量的协议,也称为距离向量协议,它不使用像 OSPF 那样的链路信息。
-
自治系统编号(ASN):与 IP 范围一样,ASN 是一个与组织在开始使用 BGP 交换路由表时关联的唯一数字。它由五个区域互联网注册机构控制:北美地区的ARIN,拉丁美洲的LACNIC,亚太地区的APNIC,欧洲的RIPE,以及非洲的AFRINIC。当使用 BGP 交换路由表时,ASN 会随路径传输。例如,Amazon.com 使用 ASN 16509 (
whois.arin.net/rest/asn/AS16509)。
让我们探讨从网络状态的角度来看网络是如何工作的。
失败类型
在计算机网络中,路由表、链路或节点故障引起的不稳定性是一个主要问题。例如,如果一个节点变得无响应,比如 CPU 冻结,其他节点必须快速检测到它,以便它们可以通过不同的路径重新分配流量。但如何检测到故障以便快速重新路由呢?让我们首先探讨故障类型:
-
链路故障:链路故障是指两个节点之间的连接停止接收或发送数据,因为路径上存在中断。故障可能由物理问题引起,例如光纤切割,环境条件,如大雨,或者由于中间件设备故障。节点通常通过输入信号缺失来检测链路是否断开,但在某些情况下,例如使用中继器或底层网络(如 DWDM),输入信号存在,但数据无法传输。因此,需要更高层次的协议来监控和检测通信中断,而不仅仅是接口输入信号;否则,数据将持续丢弃,直到节点决定重新路由流量,在某些情况下这可能需要几秒钟。
-
节点故障:节点可以通过多种方式发生故障;最常见的是电源丢失和操作系统冻结。软件故障可能导致路由器冻结数分钟甚至数小时,导致数据包丢失或不丢失,具体取决于冻结发生在转发平面还是控制平面。快速检测这种故障有点困难,因为所有接口信号仍然存在,转发平面可能仍在工作。
-
波动:接口波动是指接口在没有被检测到的情况下频繁地短暂断开。波动会导致数据丢失而未被检测到,通常没有特定设备来测量两端正常连接的介质是难以发现的。术语波动也用于描述路由表上路由频繁出现和消失的情况,称为路由波动。
故障检测技术
这里有一些检测故障的技术:
-
信号关闭:接口通过主信号或灯光的缺失来检测故障,这是一种非常简单的方式。在光纤的情况下,如果接收到的光强度太低,就会认为接口已关闭。请注意,这种检测是在输入接口上进行的。
-
协议保持活动状态和 hello 包:一些路由协议有保持活动状态(或hello)消息来检查它们的邻居是否仍然存活。在 OSPF 中,hello 包的默认周期对于 LAN 接口是 10 秒,对于 P2P 连接是 30 秒。BGP 的默认值是 30 秒。对于今天的网络速度,30 秒会导致大量数据丢失。一个 10 Gbps 的接口如果完全负载,就会丢弃总共 37 GB 的数据。在今天的协议实现中,发送这些消息的周期不能短于几秒,这仍然是一个很长的时间段,会导致大量数据丢失。
-
链路 BFD:2010 年,IETF 发布了 RFC5880,该文件描述了 BFD 协议,旨在允许路由器以微秒级顺序检测其接口上的故障。BFD 消息支持至少 1 毫秒的间隔。BFD 通常在接口硬件上实现,这使得它可以在不中断主 CPU 的情况下做出响应。
-
BFD 路由协议:链路 BFD 通常在网络的所有接口上启用,以快速检测故障,但在 OS 路由器冻结或控制平面故障的情况下,它不会有所帮助。为了避免在这些情况下丢失数据包,所有主要协议都具备 BFD 功能,包括 OSPF、IS-IS 和 BGP。尽管 BFD 协议消息支持微秒级间隔,但使用路由协议的实现通常以毫秒级顺序进行,并且受限于点的数量。原因是这些消息需要由主 CPU 处理,过多的消息可能会导致性能下降。
-
路由抖动检测:路由协议可以检测持续的路由抖动并在一段时间内抑制它。这有助于避免在路由实际上不稳定时重新计算路径。当抑制生效时,通常情况下,会采用默认路由。
控制平面和转发平面
理解转发平面和控制平面之间的区别非常重要,尤其是如果你正在从事网络自动化工作。让我们在本节中探讨它们。
转发平面,或数据平面,是一个抽象概念,其中一些进程、设备和硬件被用来通过网络转发流量。换句话说,转发平面定义了网络中所有负责接收数据、传输数据和交付数据的实体。
控制平面是一个抽象概念,指定了网络中所有负责构建数据路径、移除数据路径或更新数据路径的实体。
转发平面在数据从一个输入点,A,传输到另一个输出点,B时工作,但不需要控制平面工作。只有当从A到B不存在路径时,控制平面才会工作。在出现故障的情况下,控制平面也会工作,因为原始路径可能被中断,需要重新构建。
那么,为什么这在网络自动化中很重要呢?因为如果转发平面出现问题,控制平面必须更新转发路径,这可能导致数据包丢失、抖动和延迟。稳定的网络不需要任何路径更新,因此控制平面的工作量最小。网络自动化需要避免任何可能导致控制平面更新网络的特定自动化。
优雅重启
通常,当路由器重启时,所有路由对等体会检测到会话已断开,然后又重新建立。这种下/上转换导致控制平面重新计算所有路由路径,在整个网络中生成数千个更新,从而对转发平面造成波动。这种重新计算也可能导致路由震荡,可能创建瞬时的转发黑洞和瞬时的转发环路。这些瞬时问题也会消耗受影响路由器的控制平面的大量资源。
因此,为了在需要重启的情况下避免这种剧烈的变化,创建了一种优雅重启。
理念是我们可以在一个路由器上重启所有控制平面进程,而不影响转发平面和其他邻居路由器的控制平面。在实践中,优雅重启是一种重启路由进程而不影响转发平面的方法。
2003 年,IETF 发布了 RFC3623,以定义 OSPF 的优雅重启实现。今天,主要的控制平面协议都有某种形式的优雅重启,包括 BGP、IS-IS、MPLS、RSVP 和 LDP。
在构建网络自动化时,这种方法更倾向于更新软件。
在本节中,我们回顾了网络架构及其组件。我们获得了更多关于路由和物理架构组件的详细信息。我们还了解到控制平面和数据平面分离的重要性,以及故障类型。了解这些网络术语对于帮助网络自动化很重要。接下来,我们将回顾网络管理和其组件。
展示网络管理组件、网络堡垒等
在我们完成本章之前,让我们简要讨论一下网络管理和规划中使用的术语。
ACL
访问控制列表(ACL)几乎在网络的所有地方都用于通过基于 IP 或端口号过滤 IP 数据包来控制访问。
访问控制列表(ACL)可以在转发平面或控制平面实现。当在转发平面实现时,它们用于限制 IP 可达性到网络的一部分,并避免 IP 欺骗。当在控制平面实现时,它们用于保护路由协议和管理端口免受恶意连接。
在带内管理中,也使用 ACL 在管理接口中避免不希望的交通。
管理系统和被管理元素
管理系统是包括负责管理网络的软件和硬件的平台。它可以集中式或分布式。被管理元素是管理系统的目标,包括路由器、交换机、调制解调器、中继器和智能机架等。
注意,被管理元素不必是网络的一部分。它可以是一个支持系统,例如带有风扇的机架或空调单元。在编写网络管理代码时,适当地分类元素非常重要,以便可以相应地管理它们。
带内和带外管理
带内和带外(OOB)管理指的是管理系统流量如何到达管理元素。
对于带外管理,存在一个隔离的网络基础设施,它仅承载管理流量,以任何方式都不连接到主网络,仅连接到每个被管理元素的管理端口。换句话说,转发平面不承载任何管理流量。此外,带外网络应能够在主网络路由状态独立存在并传输管理流量,因为在灾难性场景中,即使其网络接口关闭,带外网络也应该足以到达被管理元素。重要的是要注意,这个网络通常不承载太多流量,并且其上的节点和接口吞吐量较低。一些带外网络使用移动网络实现。
在带内管理中,转发平面和管理流量之间没有物理网络隔离,因此承载客户数据的接口也承载管理流量。在这种情况下,广泛使用 ACL 来避免向被管理元素端口的不必要流量。此外,一些网络架构在接口上添加优先级队列,以便首先传输管理流量,避免在重载链路上丢弃。
一些管理系统同时使用带内和带外(OOB)网络与设备通信。通常,如操作系统升级和事件记录等重交通量通过带内网络,而元素控制台访问则通过带外网络。
网络遥测
遥测不是一个新术语,它指的是任何可以远程监控现场变量的设备,例如温度或湿度。这个术语后来被引入到计算机网络中,用来指代用于远程收集网络信息的一系列程序。
网络遥测指的是计算机网络中的一个区域,该区域负责涵盖各种程序和系统来定义、收集和分析网络数据。在某些情况下,它可能意味着通过使用网络设备中的流式传输方法来获取网络数据的新方法。
管理信息库
管理信息库(MIB)是使用简单网络管理协议(SNMP)可以管理的对象集合的正式描述。
MIB 可以是公共的或私有的。当它是公共的,其定义由 RFC 发布,例如在 RFC2863 上定义的接口组 MIB。当它是私有的,它必须由拥有 MIB 的供应商提供。
MIB 通常按数字组织成树状结构(图 1.8)。当描述 MIB 树中的对象时,它通常被称为对象标识符(OID)。例如,接口输出中看到的包数由一个名为IfOutUcastPkts的 OID 表示,其序列为 .1.3.6.1.2.1.2.17 (www.net-snmp.org/docs/mibs/interfaces.html)。
OID 通常包含一个值,该值是一个可以具有不同类型的变量,例如 COUNTER、GAUGE、INTEGER 和 OCTETSTR 等。

图 1.8 – SNMP MIB 树
网络堡垒
术语堡垒来自中世纪战争中用来保护加农炮的防御工事。当时,堡垒是外墙的角形部分,通常放置在堡垒的角落,以便在多个方向上进行防御射击。
就像中世纪时期的加农炮一样,网络元素需要层次结构来保护。网络堡垒或堡垒主机是具有内置防御机制并连接到两个或更多网络的物理设备,通常是计算机。
堡垒主机通常设计为在具有多个以太网端口的计算机上安装 Linux。每个以太网端口连接到网络的不同部分,在这些部分需要隔离或保护。
为了保护网络,堡垒主机不转发流量,通常情况下,IP 转发功能被禁用,如下例所示,这是在 Linux 中应用的:
# sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
一个没有 IP 转发的 Linux 盒子意味着流量不能在接口之间路由,因此流量必须从堡垒主机发起才能到达外部接口,并且没有其他流量会从不在盒子本地发起的以太网端口流出。因此,堡垒主机需要具有身份验证,例如用户名和密码,以允许用户或系统登录并本地运行 shell。从主机出发,用户可能能够生成指向其他以太网端口的 IP 数据包。
网络自动化将需要一个额外的机制来允许访问网络节点以进行配置。我们将在编写一些访问节点的代码时介绍这些机制。
图 1.9展示了堡垒主机的示例:

图 1.9 – 连接生产网络和公司网络的堡垒主机
FCAPS
FCAPS是由 ISO 定义的网络管理模型和框架。其首字母缩写代表故障、配置、计费、性能和安全。这些每一个都是 ISO 模型定义的网络管理任务的管理类别。让我们来看一下每个任务的非常简化的描述:
-
故障:故障管理的目标是识别、隔离、纠正和记录网络中发生的故障。
-
配置:这指的是从设备中存储配置、跟踪更改以及提供新的配置。
-
计费:这涉及跟踪网络使用情况,按业务、客户或用户进行。目标是能够适当地进行计费或收费。
-
性能:这侧重于通过监控网络延迟、数据包丢失、链路利用率、数据包丢弃、重传和错误率来确保网络在可接受的水平上运行。
-
安全:这指的是控制对网络中资产和协议的访问,这可能包括 AAA 系统、ACL 和防火墙。
为什么 FCAPS 对网络自动化很重要?因为用 FCAPS 将任务分离来编写网络自动化代码会更好,这样我们就可以根据管理系统的不同部分相应地放置自动化。
网络规划
扩展网络是困难的,因为如果你购买过多的资源,可能会损失金钱。然而,如果你购买得太少,可能会失去客户。网络规划用于确保性能和成本朝着正确的方向发展。换句话说,它包括几个活动,其最终目标是定义一个未来最优的成本效益网络设计。网络规划工程师使用预测和统计模型来绘制最可能未来的网络增长。
由于预测的性质,网络规划团队需要从网络中获取大量数据,其中大部分可能不容易收集。你可能需要收集瞬时数据,而这些数据在当前的管理系统中是不可用的,因此你可以使用你的网络自动化技能来完成这项工作。
网络安全
需要提到的一个重要观点是公司正在做的工作,以创建一个安全的环境让流量流动。如今,大多数公司都投资了独立的和专业的团队来处理安全问题,这些团队拥有只理解网络细微差别的工程师,他们负责在设计和运营计算机网络时制定安全规则。
你的自动化工作可能涉及处理在网络中应用的安全规则,并且可能因公司而异,这取决于技术和所需的安全级别。有关网络安全的良好概述可以在en.wikipedia.org/wiki/Network_security找到。
摘要
在本章中,我们回顾了网络的关键点。目的是突出主要概念并定义一些网络术语。到目前为止,你已经拥有了足够的网络背景知识,可以与任何网络工程师讨论自动化工作。我希望从现在开始进行的网络自动化编码工作将更有意义,并且你对网络术语更加熟悉。
在下一章中,我们将了解网络是如何演变成为可编程的。
第二章:可编程网络
最初,计算机网络是物理和静态的,由线和硬件组成,但随着高级计算、虚拟化和连接性的发展,网络变得更加灵活,可以通过软件进行配置。在本章中,我们将讨论软件是如何改变计算机网络图景的。我们将首先检查今天用于通过软件创建网络的几种不同技术,然后我们将检查当前的标准技术,称为软件定义网络(SDNs)。
正如我们在第一章中看到的,计算机网络可以非常复杂且难以维护。从路由器、交换机和 NAT 到负载均衡器等,有多种不同的设备。此外,在每一件设备中,还有多种不同的操作类型,例如核心或接入路由器。网络设备通常由各个供应商配置,接口之间差异很大。尽管有可以帮助集中配置的管理系统,但网络设备通常在单个级别上进行配置。这种操作意味着操作复杂性和新功能创新的缓慢。
在本章中,我们将涵盖以下主题:
-
探索可编程网络的历史并查看当前使用的那些
-
虚拟网络技术
-
SDNs 和 OpenFlow
-
理解云计算
-
使用 OpenStack 进行网络配置
探索可编程网络的历史并查看当前使用的那些
自从工程师最初构思可编程网络以来已经过去了几年,因此在我们深入了解当前技术之前,让我们简要回顾一些历史里程碑。
主动网络
国防高级研究计划局(DARPA)从 20 世纪 90 年代中期开始资助研究,旨在创建一个可以通过编程轻松更改和定制的网络,称为主动网络项目。该项目的主要目标是创建网络技术,与当时现有的网络相比,这些技术易于创新和演进,允许快速的应用程序和协议开发。
但在 20 世纪 90 年代创建这样一个灵活的网络并不容易,因为编程语言、信令和网络协议以及操作系统还不够成熟,无法容纳这样的创新想法。例如,操作系统是单块的,添加功能需要重新编译和重启。此外,服务 API 不存在,分布式编程语言仍处于早期开发阶段。
活跃的网络研究项目探索了通过 IP 提供的传统互联网堆栈服务的激进替代方案。这方面的例子可以在全球网络创新环境(GENI)项目中找到,该项目可以在www.geni.net/查看,国家科学基金会(NSF),未来互联网设计(FIND)可以在www.nets-find.net/查看,以及未来互联网研究和实验倡议(EU FIRE)。
当时,活跃的网络研究社区追求两种编程模型:
-
胶囊模型,其中节点上要执行的代码在数据包中带内传输
-
可编程路由器/交换机模型,其中节点上要执行的代码由带外机制建立
重要提示
在《主动网络——过去、现在和未来的一个视角》中可以找到更多阅读材料。作者:Jonathan M. Smith 和 Scott M. Nettles – 《IEEE 系统——应用和评论部分》第 34 卷第 1 期,2004 年 2 月。
让我们深入了解第一个尝试创建可编程节点的尝试,这个节点被称为NodeOS。
NodeOS
活跃网络项目的第一个目标之一是创建NodeOS。这是一个主要目的是支持主动网络中数据包转发的操作系统。NodeOS 在主动节点中运行在最低级别,并在穿越节点的数据包流之间复用节点资源,如内存和 CPU。NodeOS 为主动网络执行环境提供了几个重要的服务,包括资源调度和会计以及快速数据包输入/输出。NodeOS 的两个重要设计里程碑是创建应用程序编程接口(API)和资源管理。
重要提示
在《2001 年 4 月主动路由器操作系统接口》 – 《IEEE 选择通信领域杂志》第 19 卷第 3 期,第 473-487 页中可以找到关于 NodeOS 的更多阅读材料。
在此之后,我们现在将探索一些社区早期尝试 SDN 的项目。
数据平面和控制平面分离
可编程网络和SDN的一个重要步骤是将控制和数据平面分离。在第一章中,我们讨论了控制平面和数据平面的区别,在这里我们将讨论它们背后的历史。值得记住的是,数据平面也被称为转发平面。
到了 20 世纪 90 年代,这种分离已经在公共电话网络上存在,但尚未在计算机网络或互联网上实施。随着网络复杂性的增加和互联网服务开始成为几个骨干提供商的主要收入来源,可靠性、可预测性和性能成为网络运营商寻求管理网络更好方法的关键点。
在 2000 年代初,一群研究人员开始探索使用标准协议或其他即将部署的技术来采取实用方法的社区,他们要么为网络运营商工作,要么定期与他们互动。当时,路由器和交换机在控制和转发平面之间有紧密的集成。这种耦合使得各种网络管理任务变得困难,例如调试配置问题和控制路由行为。为了应对这些挑战,开始出现各种将转发和控制平面分离的努力。以下几节将探讨一些早期的努力。
IETF ForCES
转发与控制元素分离(IETF ForCES)工作组旨在创建一个框架,一个需求列表,一个解决方案协议,一个逻辑功能块库以及其他支持数据和控制元素分离的相关文档(datatracker.ietf.org/wg/forces/about/))。
NetLink 接口
NetLink可能是 Linux 内核中控制平面和数据平面最清晰的分离。2003 年,IETF 发布了RFC3549,描述了控制平面组件(CPCs)和转发引擎组件(FECs)的分离。图 2.1(来自原始 RFC)说明了 Linux 如何使用 Netlink 作为控制和数据平面之间的主要分隔符(datatracker.ietf.org/doc/html/rfc3549)。

图 2.1 – 如 RFC3549 所示的控制平面和数据平面分离
Netlink 首次出现在 Linux 内核的 2.0 系列中。
路由控制平台
路由控制平台(RCP)是一种实用的设计,用于分离控制和数据平面。想法是在创建一个集中式控制,其中收集所有路由信息,然后运行算法为网络中的每个路由器选择最佳路由路径。
RCP 通过从当前网络中的路由器收集外部和内部的边界网关协议(BGP)路由表来实现,以集中方式使用这些信息为每个路由器选择最佳路径。这种方法使得可以利用现有的网络设备并实现控制平面和数据平面的分离。
重要提示
在关于使用 BGP 的 RCP 的更多信息可以在论文《设计和实现一个路由控制平台》中找到——作者:马修·凯撒,唐纳德·卡尔多,尼克·费姆斯特,詹妮弗·雷克斯福德,阿曼·沙伊赫,雅各布斯·范德梅尔韦——NSDI’05:第二届网络化系统设计与实现研讨会论文集——第二卷,2005 年 5 月,第 15-28 页。
SoftRouter
SoftRouter的想法在 2004 年的一次会议上提出,并在 2005 年获得专利。同样,该架构在控制平面功能与数据平面功能之间有分离。
所有控制平面功能都实现在通用服务器上,称为控制元素(CEs),这些服务器可能距离转发元素(FEs)有多跳之远。SoftRouter 架构中有两种主要的网络实体,即 FEs 和 CEs。它们共同构成了一个网络元素(NE)路由器。与传统路由器的主要区别是本地没有运行任何控制逻辑(如 OSPF 或 BGP)。相反,控制逻辑是在远程托管。
重要提示
更多关于 SoftRouter 的细节可以在原始的 2004 年论文《The SoftRouter Architecture》中找到 – T. V. Lakshman, T. Nandagopal, R. Ramjee, K. Sabnani, T. Woo – Bell Laboratories, Lucent Technologies, ACM HOTNETS - January 2004.
路径计算元素架构
2006 年,IETF 网络工作组发布了一个 RFC,描述了一个集中控制的实体架构,用于做出路由路径决策,他们将此称为路径计算元素(PCE)架构。
最初,PCE 架构的发明是为了解决多协议标签交换(MPLS)中存在的问题,即每个路由器计算标签交换路径(LSP)变得越来越慢和繁重。它被设计为在内部或外部的服务器上执行计算。
重要提示
更多关于 PCE 的细节可以在 RFC4655 中找到:datatracker.ietf.org/doc/html/rfc4655
让我们现在看看最重要的项目,即 OpenFlow 和 SDN 的工作。
乙烷
乙烷是最重要的项目之一,最终导致了 OpenFlow 和 SDN 的创建。最初,它只是一个博士生的项目,定义了一个网络为一组数据流和网络策略来控制流量,这也是另一种看待数据平面和控制平面分离的方式。
Ethane 项目有集中所有网络策略在一个地方的想法。加入 Ethane 网络的新设备默认应该关闭所有通信。新设备在连接之前应该从集中服务器获得明确的权限,并且其数据流只能在允许的路径上传输。
重要提示
更多关于 Ethane 项目的细节可以在 2007 年的原始论文《Ethane: taking control of the enterprise》中找到 – 作者:M. Casado, M. J. Freedman, J. Pettit, J. Luo, N. McKeown, Scott Shenker – SIGCOMM ‘07: Proceedings of the 2007 conference, pages 1–12.
在本节中,我们探讨了可编程网络背后的部分历史。我们还探讨了几个导致控制平面和数据平面分离的主要项目,这是 SDN 的重要里程碑。你现在应该能够识别分离的重要性以及为什么会发生这种情况。
虚拟网络技术
网络虚拟化是指软件像网络硬件一样工作,这是通过使用逻辑上模拟的硬件平台来实现的。
网络虚拟化不是一个新概念,我们可以在 1970 年代中期在 X.25 网络上的虚拟电路中找到第一个实现。后来,其他技术也开始使用虚拟概念,如帧中继和 ATM,但它们现在已经过时。
回环接口基于电子学,其中回环用于创建电回路,以便信号返回其源进行测试。1981 年,IETF 将保留地址范围127.rrr.rrr.rrr(其中127.rrr.rrr.rrr正式称为127.0.0.1)。
网络虚拟化的另一个早期实现是虚拟****局域网(VLAN)。到 1981 年,David Sincoskie 正在测试分割语音以太网网络以实现容错,这与 VLAN 的作用类似。然而,直到 17 年后的 1998 年,IEEE 才将 VLAN 正式作为标准发布,命名为802.1Q。到 2000 年代,交换网络由交换机、中继器和桥接器主导,使得 VLAN 变得普遍。今天没有 VLAN 的局域网几乎是不可能的。
今天还有几种其他网络虚拟化技术被使用。接下来几节将探讨其中重要的技术。
虚拟专用网络
这是创建一个在网络上、服务提供商和互联网上实现的隔离安全网络覆盖的概念。
换句话说,虚拟专用网络(VPN)是一个通用术语,描述了使用公共或私人网络来创建与其他网络用户隔离的用户组,使他们能够像在私有网络中一样相互通信。
VPN 使用端到端流量加密来增强数据分离,尤其是在使用公共网络时,但这并不一定适用于所有实现。例如,在使用 MPLS 网络中的 VPN 时,由于流量在私有域中运行,因此不会进行加密,数据分离仅通过数据包封装存在。
VPN 是一个通用名称,但还可以找到更具体的名称,例如 L3VPN、L2VPN、VPLS、伪线(Pseudo Wires)和 VLLS 等。
重要提示
更多关于 VPN 及其相关家族的信息可以在datatracker.ietf.org/doc/html/rfc2764和datatracker.ietf.org/doc/html/rfc4026找到。
VLAN 可能是 L2 网络中创建的最重要的一种虚拟化。现在让我们看看为路由器网关创建的一个有趣的虚拟化。
虚拟路由器冗余协议
该协议最初由思科在 1998 年创建,命名为热备用路由器协议(HSRP),在RFC2281中定义。由于当时 HSRP 的使用非常流行,IETF 网络工作组创建了虚拟路由器冗余协议(VRRP)(RFC3768)。
概念很简单,通过自动使用 DHCP 或手动配置,在计算机的路由表中只给它们一个默认网关。为了冗余地使用两个路由器,你可能需要更新所有计算机或使用 VRRP。
VRRP 使用一个虚拟以太网地址来关联一个 IP 地址;这个 IP 地址是网络中所有计算机的默认网关。图 2.2展示了使用与两个路由器都关联的虚拟 MAC 地址的10.0.0.1。

图 2.2 – 使用虚拟以太网地址作为默认网关的 VRRP
重要提示
关于 VRRP 和 HSRP 的更多详细信息可以在datatracker.ietf.org/doc/html/rfc2281和datatracker.ietf.org/doc/html/rfc3768找到。
VLANs 很久以前就被创建了,但其概念被用来扩展到更灵活的使用,正如我们将在下一节中看到的。
虚拟可扩展局域网
在今天的虚拟化中,也许最重要的是虚拟可扩展局域网(VXLAN)。这个标准于 2014 年发布,在网络虚拟化中得到了广泛的应用,以提供连接性。使用 VXLAN,可以创建一个网络,其接口与路由器直接相连,就像它们是物理实体一样,但实际上它们是虚拟的。
VXLAN 封装了数据链路层以太网帧(第 2 层),在传输层使用 UDP 数据报(第 4 层)。终止 VXLAN 隧道且可能是虚拟或物理交换机端口的 VXLAN 端点被称为虚拟隧道端点(VTEPs)。
重要提示
关于 VXLAN 的更多信息可以在datatracker.ietf.org/doc/html/rfc7348找到。
现在我们来探索一个开源项目,该项目实现了包括 VLANs、VRRP 和 VXLANs 在内的多种虚拟网络技术。
Open vSwitch
这个开源项目可能是当今网络虚拟化中最重要的项目。Open vSwitch(OVS)运行在任何基于 Linux 的虚拟化平台(内核 3.10 及更高版本)上,并用于在虚拟和物理环境中创建连接。大部分代码是用 C 编写的,它支持包括 VXLAN、IPSEC 和 GRE 在内的多个协议。OVS 是 SDN 的 OpenStack 组件,也许是 OpenFlow 最受欢迎的实现。OVS 的工作基本架构可以在 图 2.3 中找到。

图 2.3 – 简化的 OVS 架构
更多关于 OVS 的详细信息可以在 github.com/openvswitch/ovs.git 找到。
Linux 容器
Linux 容器(LXC)通过使用 CPU、网络、内存和 I/O 空间隔离提供操作系统级别的虚拟化。它的首次实现是在 2008 年 1 月的 Linux 内核 2.6.24 上,但这个概念很古老,可以在 1999 年实现的名为 jails 的 FreeBSD 实现中找到,该实现于 2000 年 3 月在 FreeBSD 4.0 中发布(详情请见:docs.freebsd.org/en/books/handbook/jails/)。
今天,LXC 的实现越来越多,但 CPU、网络、内存和 I/O 空间隔离的概念是相同的。今天最受欢迎的 LXC 实现是 Docker。
使用 LXC 和 Open vSwitch,可以创建包含数百个路由器的整个虚拟网络拓扑。一个强大的例子是 Mininet(mininet.org/ 和 github.com/mininet/mininet)。
重要提示
更多关于 LXC 和 FreeBSD jail 的信息可以在 en.wikipedia.org/wiki/LXC 和 en.wikipedia.org/wiki/FreeBSD_jail 找到。
Linux 容器可以创建大多数虚拟化,然而由于它们使用相同的操作系统,因此受到限制,因为容器共享相同的内核。正如我们接下来将要看到的,虚拟机可以用来虚拟化广泛的其它操作系统。
虚拟机
LXC 在隔离操作系统部分方面非常强大;然而,它们无法运行需要不同 CPU 或硬件的应用程序。因此,虚拟机(VMs)存在是为了通过模拟物理硬件和 CPU 来添加额外的虚拟化。
虚拟机可以通过创建全新的 CPU、I/O、内存和网络层来进一步隔离操作系统。例如,在网络虚拟化中,可以运行使用不同 CPU 的不同操作系统,例如 Juniper JunOS 使用 Intel CPU,而 Cisco IOS 使用 MIPS CPU。
最受欢迎的开源虚拟机实现是 Xen(xenproject.org/)。
关于网络虚拟化,我们还有很多要讨论的,但这将是另一本书的主题。至少就目前而言,在本节中我们所探讨的已经足够用来识别可编程网络所使用的主要技术。此时,如果你遇到这些技术,你应该能够轻松地识别它们。
SDNs 和 OpenFlow
我们已经调查了一些可编程网络和网络虚拟化的历史里程碑,这些里程碑构成了我们今天所知道的 SDN 的基础。接下来,让我们谈谈 SDN 背后的细节。
为了使 SDN 成功,它们需要具有灵活性和可编程性,使得部署和控制流量以及管理其组件变得简单。如果没有控制平面和转发平面(数据平面)之间的分离,这一切都无法实现。
SDN 的实现是通过一个应用程序来完成的,该应用程序使用这两个平面的解耦来构建网络的数据流。该应用程序可以在网络服务器或虚拟机中运行,当可能时,使用 OpenFlow 协议向网络设备发送控制数据包。
OpenFlow 的历史
OpenFlow 是 SDN 中使用的标准协议。其起源可以追溯到 2006 年,本章前面提到的 Ethane 项目。最终,Ethane 项目在斯坦福大学和伯克利大学的研究团队共同努力下,导致了后来被称为 OpenFlow 的东西。最初的构想是使用基于流的网络和控制器集中管理策略,重点是网络安全;这就是为什么Flow在OpenFlow这个名字中的原因。
在伯克利和斯坦福的初步工作之后,尼克拉(Nicira)和 Big Switch Networks 等公司开始筹集大量的风险投资资金,以帮助推动基于流控制网络的产品理念,但当时还没有发布任何标准。需要一个协议将网络控制从专有网络交换机中移出,进入开源且本地管理的控制软件。这就是为什么OpenFlow这个名字中包含Open这个词的原因。
到 2011 年,开放网络基金会(ONF)已经成立,旨在标准化网络和数据中心管理的新兴技术。创始成员包括谷歌、Facebook 和微软,而思杰、思科、戴尔、惠普、F5 网络、IBM、NEC、华为、瞻博网络、甲骨文和 VMware 后来加入了。
ONF 工作组于 2009 年 12 月发布了 OpenFlow 协议的第一个版本,并在 2011 年 2 月发布了 1.1 版本。最新版本是 2015 年 3 月的 1.5.1 版本(opennetworking.org/wp-content/uploads/2014/10/openflow-switch-v1.5.1.pdf)。
SDN 架构
SDN 的简单架构在图 2.4中展示。SDN 控制器面向业务级应用有北向接口(NBIs),面向网络设备有南向接口(SBIs)。
为了与网络设备通信,SBI 需要控制协议。理想情况下,控制协议应该是 OpenFlow;然而,如果设备不支持它,可以使用其他协议,例如 Cisco OpFlex、SNMP,甚至通过 SSH 的 CLI(这将在下一章中介绍)。
NBI 用于从业务或为业务从网络收集信息(在图 2.4中,这表示为应用平面),例如,允许管理员访问 SDN 控制器以检索有关网络的信息。通常,通过 API 协议访问控制器。
通常,NBI 用于以下方面:
-
从设备获取信息
-
获取物理接口的状态
-
配置设备
-
在设备之间构建数据流
但是,NBI API 上可用的方法将取决于 SDN 应用以及厂商提供的功能。
重要提示
需要强调的是,SDN 的 NBI API 不负责管理网络设备,例如分配配置或进行软件更新。SDN NBI API 的主要责任是允许管理员和业务向 SDN 控制器下达指令,以便根据预定义的标准对网络设备中的流量流向做出决策。
现在,让我们看看 SDN 的简单架构:

图 2.4 – 基本 SDN 架构
尽管 OpenFlow 在 SDN 中被广泛应用,并在互联网社区中是一个广为人知的术语,但其未来可能并不那么光明。让我们来看看原因。
OpenFlow 及其未来
观察 OpenFlow 标准的更新情况以及厂商的实施情况,其未来看起来并不乐观。
OpenFlow 的第一个可用版本于 2011 年发布,称为版本 1.1。从那时起,更新一直持续到 2015 年的 1.5.1 版本。但是,已经过去了六年多,还没有发布任何更新。
OpenFlow 的 1.6 版本自 2016 年以来就已可用,但仅限于 ONF 的成员,这并不利于用户对 OpenFlow 未来的信心。
除了缺乏更新之外,思科(主要网络供应商之一)自 2014 年以来一直在开发自己的 OpenFlow 版本,称为 OpFlex,因为它看到了 OpenFlow 方法的局限性。思科还将 OpFlex 开源,允许他人无限制地使用,并开始着手制定 RFC 以发布 OpFlex(datatracker.ietf.org/doc/draft-smith-opflex/)。
因此,图 2.4中描述的 SBIs(服务化接口)不一定使用 OpenFlow。今天,SDN 的实施方式多种多样,可能使用与设备通信方法相关联的不同类型的 SBIs,以创建流量策略。
除了 OpenFlow 之外,还有其他方法和协议被用于 SDN 通信中,例如 OpenStack、OpFlex、通过 SSH 的 CLI、SNMP 和 NETCONF 等。
正如我们在本节中看到的,SDN(软件定义网络)是一个关于如何与可编程网络协同工作的非常明确的概念;然而,由于 OpenFlow 的采用不足,SDN 更多地成为一个概念,而不是一个标准。从现在起,你应该有足够的知识来决定你的网络自动化是否应该遵循 OpenFlow。
理解云计算
云计算的目标是让用户无需深入了解虚拟技术就能从中受益。云计算的目的是降低成本,并帮助用户专注于核心业务,而不是物理基础设施。
云计算倡导一切即服务(EaaS),包括基础设施即服务(IaaS),通过提供高级 API 来实现,这些 API 用于抽象底层网络基础设施的各种低级细节,例如物理计算资源、位置、数据分区、扩展、安全性和备份。
我们在这里的关注点将是云计算提供的网络服务,我们通常称之为云网络服务。
商业云计算
可能目前最受欢迎的云计算服务是 2002 年由亚马逊创建的子公司Amazon Web Services(AWS)。AWS 使用其专有 API 提供云服务;其中之一是通过使用AWS CloudFormation提供基础设施即代码的方式创建的。
2008 年,谷歌开始提供云服务;2010 年,微软开始提供微软 Azure;2011 年,IBM 宣布推出 IBM SmartCloud;2012 年,Oracle 开始提供 Oracle Cloud。
有数百个其他提供商,所有提供商的列表可以在以下链接中找到:www.intricately.com/industry/cloud-hosting。
OpenStack 基金会
OpenStack 基金会是 NASA 和 Rackspace 共同发起的第一个项目,旨在启动开源云服务软件。该基金会最终将其名称更改为OpenInfra 基金会,如今他们拥有超过 500 名成员。他们的工作非常出色,为云计算创建了一套优秀的开源代码。更多详情可以在openinfra.dev/about/找到。
Cloud Native Computing Foundation
这听起来有点令人困惑,但CloudStack 基金会和云原生计算基金会(CNCF)关注云计算的不同方面。CNCF 基本上是由 Kubernetes 作为一个基于 Linux 容器的理念创建的,而 CloudStack 则稍微早一些,基于虚拟机。
CNCF(云原生计算基金会)是一个成立于 2015 年的 Linux 基金会项目,旨在帮助推进 Linux 容器技术,并帮助技术行业围绕其发展进行协调。它与 Kubernetes 1.0 一同宣布,Kubernetes 是由谷歌赠予 Linux 基金会的种子技术。
我们已经涵盖了相当多的云计算内容,但关键要点是,尽管它最初是为了增加计算机的可编程性而设计的,但云计算在网络空间中也正在增长。这个空间中最可编程的网络之一是 OpenStack,我们将在下一部分对其进行探讨。
使用 OpenStack 进行网络
与 OpenFlow 相比,OpenStack 一直活跃且有前景。它始于 2010 年,是 NASA 和 Rackspace 之间的一个联合项目。Rackspace 希望重写其云服务器运行的基础设施代码,同时,Anso Labs(为 NASA 承包)发布了基于 Python 的云计算织物控制器 Nova 的 beta 代码。
到 2012 年,OpenStack 基金会成立,旨在推广 OpenStack 软件到云计算社区。到 2018 年,已有超过 500 家公司加入了 OpenStack 基金会。到 2020 年底,基金会宣布从 2021 年开始将其名称更改为开放基础设施基金会。原因是基金会开始将其他项目添加到 OpenStack 中,因此名称将不再反映他们的目标。
OpenStack 使用不同的名称跟踪其版本;2010 年的第一个版本被称为 Austin,包括两个组件(Nova 和 Swift)。到 2015 年,新的 OpenStack 版本到来,被称为 Kilo,有 12 个组件。到 2021 年 10 月,OpenStack Xena 已经发布,有 38 个服务组件(docs.openstack.org/xena/)。
对于我们来说,OpenStack 中重要的是那些将使我们能够自动化网络基础设施的组件。尽管不是为物理设备设计的,但网络 API 方法可能被扩展到物理设备,而不仅仅是用于云虚拟环境。
OpenStack Neutron
OpenStack 的目标是创建标准服务,使软件工程师能够将他们的应用程序与云计算服务集成。2021 年 10 月发布的 Xena 版本提供了 38 个服务。
网络服务中最重要的服务之一被称为Neutron(或OpenStack Networking),这是一个旨在在接口设备之间提供网络连接作为服务的 OpenStack 项目。它实现了 OpenStack 网络 API。
重要提示
Neutron API 定义可以在以下链接中找到:docs.openstack.org/api-ref/network/。
神经元管理所有虚拟网络基础设施(VNI)和物理网络基础设施(PNI)的访问层配置。它还使项目能够创建高级虚拟网络拓扑,这可能包括防火墙和 VPN 等服务。它提供网络、子网和路由器作为对象抽象。每个抽象都具有模拟其物理对应物的功能:网络包含子网,路由器在不同的子网和网络之间路由流量。
更多关于 Neutron 的详细信息,请访问以下链接:docs.openstack.org/neutron。
神经元 API
Neutron API 是一个使用 HTTP 协议所有方面的 RESTful HTTP 服务,包括方法、URI、媒体类型、响应代码等。API 客户端可以使用协议的现有功能,包括缓存、持久连接和内容压缩。
例如,让我们看看 HTTP GET BGP 对等体的方法。
要获取 BGP 对等体的列表,请使用 HTTP GET 请求到 /v2.0/bgp-peers。可能的响应如下:
-
正常响应代码:
200 -
错误响应代码:
400, 401, 403
可添加到 API 请求的字段:
| 名称 | 输入 | 类型 | 描述 |
|---|---|---|---|
fields (可选) |
查询 | 字符串 | 您希望服务器返回的字段。如果没有指定 fields 查询参数,网络 API 将返回策略设置允许的所有属性。通过使用 fields 参数,API 仅返回请求的属性集。fields 参数可以指定多次。例如,如果您在请求 URL 中指定 fields=id&fields=name,则仅返回 id 和 name 属性。 |
表 2.1 – API 请求字段
API 响应中返回的参数如下:
| 名称 | 输入 | 类型 | 描述 |
|---|---|---|---|
bgp_peers |
主体 | 数组 | bgp_peer 对象的列表。每个 bgp_peer 对象代表实际的 BGP 基础设施,例如路由器、路由反射器和路由服务器。 |
remote_as |
主体 | 字符串 | BGP 对等体的远程自治系统编号。 |
name |
主体 | 字符串 | BGP 对等体的更详细名称。 |
peer_ip |
主体 | 字符串 | 对等体的 IP 地址。 |
id |
主体 | 字符串 | BGP 对等体的 ID。 |
tenant_id |
主体 | 字符串 | 租户的 ID。 |
project_id |
主体 | 字符串 | 项目的 ID。 |
表 2.2 – API 响应字段
以下是一个 API 响应的示例:
{
"bgp_peers":[
{
"auth_type":"none",
"remote_as":1001,
"name":"bgp-peer",
"tenant_id":"34a6e17a48cf414ebc890367bf42266b",
"peer_ip":"10.0.0.3",
"id":"a7193581-a31c-4ea5-8218-b3052758461f"
}
]
}
API 在以下链接中有良好的文档:docs.openstack.org/api-ref/network/。
正如我们在本节中看到的,OpenStack 可能是与网络编程最接近的云计算平台,这由 CloudStack Neutron API 所证明。随着更多网络元素迁移到云端,可能还会添加更多功能。你现在应该熟悉 OpenStack 术语,并在必要时能够深入探索它们。
摘要
在本章中,我们讨论了可编程网络是如何发展到今天的。我们讨论了数据平面和控制平面分离的历史。我们看到了网络虚拟化是如何随着时间的推移而改进的。我们还审视了一些 SDN 和云网络的技术和标准,例如 OpenFlow 和 OpenStack。
你现在拥有了理解为什么今天使用某些技术来自动化和编写网络代码所需的知识。
在下一章中,我们将更深入地探讨用于配置和与网络设备通信的方法、协议和标准。
第三章:访问网络
在上一章中,我们探讨了可编程网络及其历史。我们探讨的一个想法是软件定义网络(SDN),我们看到了为什么数据平面和控制平面的分离很重要。SDN 的一个重要点是它的架构以及它是如何分离北向接口(NBI)和南向接口(SBI)的。在本章中,我们将探讨如何在 OpenFlow 不可用的情况下,访问可以解释为 SDN 的 SBI 的网络设备。
正如我们之前所看到的,OpenFlow 并不是一个广泛采用的协议,其可用性仅限于少数制造商和设备。因此,如果您计划使用 SDN,您可能需要使用可用的本地方法来配置设备。
网络访问不仅用于 SDN,还用于各种软件,例如网络配置、配置审计、升级工具和自动化等。此外,设备通常有多种方法或协议,其中一些可能比其他的好。
在本章中,我们将探讨访问网络设备的最常见方法和协议,以实现我们的网络自动化。由于设备有多种方法,我们的目标是提供足够的信息,以便您可以选择最适合您网络自动化代码的方法。
我们将要探讨以下主题:
-
使用 CLI
-
使用 SNMP
-
采用 NETCONF
-
采用 gRPC
-
使用 gNMI 操作
使用 CLI
命令行界面(CLI)可能是访问网络设备最广泛可用的方法。这是一个从计算机中引入的术语,它是电传打字机(TTY)机的替代品。CLI 通常通过在设备内部运行的程序来实现,该程序解释正在键入的键。CLI 程序的早期实现监控设备的串行端口,其中连接了带有键盘的终端以进行通信。
在 UNIX 系统中,CLI 程序被称为 shell,第一个 shell,称为V6 shell,是在 1971 年由贝尔实验室的 Ken Thompson 创建的。Bourne shell于 1977 年推出,作为 V6 shell 的替代品。尽管 UNIX shell 用作交互式命令解释器,但它也被设计为一种脚本语言,并包含大多数被认为是用于生成结构化程序的功能。
网络设备使用 shell 的简化版本作为它们的 CLI。让我们更深入地探讨 CLI 访问可以提供的内容。
命令提示符
在我们的网络自动化工作中使用 CLI 时,命令提示符是我们代码中最重要的部分,需要被解释。我们将看到设备使用命令提示符来指示它何时准备好接收新命令。
一个$、%、#、:、>或-。它还可以包括其他信息,例如当前时间、工作目录、用户名或主机名。在许多网络设备上,提示符通常以$或%结束,而对于特权 CLI 访问模式,它通常以#结束,这与 UNIX 超级用户root类似。
大多数提示符都可以由用户修改 – 然而,网络设备提示符中最常见的信息是主机名,有时还包括用于登录的用户名。
图 3.1中的示例显示了一个core-router和末尾的>字符,这意味着它正在等待将命令放置在光标所在的位置:

图 3.1 – 一个 FRR 路由器命令提示符示例
串行访问
网络设备通常有一个称为控制台或串行控制台的特殊端口。此端口通常配置为以较慢的速度运行,常见的配置将波特率设置为 9,600,有些设备能够接受高达每秒 115,200 位的速率。串行端口通常是一个DB9连接器或RJ45,具有RS232技术规范。图 3.2显示了用于 DB9 和 RJ45 串行控制台连接器的引脚示例。
处理串行端口的程序通常独立于设备的操作系统,允许在这种灾难性场景中使用此类端口:

图 3.2 – 设备串行端口引脚配置示例
重要的是要注意,串行端口在以下情况下使用:
-
网络设备不可靠的极端情况
-
在技术人员在场的情况下进行本地维护
-
带有崩溃或操作系统删除风险的临界升级
-
操作系统或硬件故障
网络自动化应避免使用串行端口来配置设备,因为没有并行性(只有一个端口)且其速度有限。
远程不安全访问
由于之前解释的原因,串行端口速度较慢,不能并行使用。因此,访问网络设备的最佳方式是通过远程访问,这可以通过Out-Of-Band(OOB)或带内管理(在*第一章**中描述)来实现。
如何确定访问是否安全将取决于所使用的协议以及用于传输此远程访问的网络类型。如果通过 OOB 网络访问,通常它是安全的,并且具有分离和隔离的基础设施,但如果它是带内的,则需要特别注意以避免一些常见的安全漏洞。
不安全的应用和协议
以下应用程序通常未加密,容易被窃听、代理或劫持。
Telnet
Telnet 是一个使用 TCP 端口23来访问远程设备的应用程序。数据未加密,且连接上没有身份验证。在使用 Telnet 时,TCP 劫持和窃听是最常见的安全问题。为了确保这些威胁不存在,承载访问的网络路径必须得到保护或隔离。
Telnet 通常需要密码,有时还需要用户名。
RSH
514且不提供加密或密码。由于使用 IP 地址进行身份验证,此协议极其不安全,容易受到 IP 欺骗攻击。
如果不是因为其缺乏安全性,RSH 将是一个运行命令和创建网络自动化的快速且简单选项。
远程安全访问
为了确保远程访问安全,数据必须加密,主机需要某种基于身份的认证,以确保连接的人被允许连接。
用于远程 CLI 访问的最常用的应用程序是默认的22。
基于身份的认证
SSH 上的基于身份的认证基于 SHA-256(一种加密算法)的密钥指纹。当 SSH 首次使用时,SSH 会要求你确认指纹密钥,以确保你连接到的主机是正确的。此指纹的一个示例如图 3.3 所示:

图 3.3 – SSH 指纹接受请求的示例
虽然 SSH 被认为是访问设备的加密应用程序协议,但基于身份的密钥指纹需要得到妥善管理,以避免最常见的攻击,即所谓的中间人攻击。这种攻击将流量重定向到另一个设备,假装它是终端设备,并使用此程序捕获最终目标网络设备的密码,如图 3.4 所示:

图 3.4 – 网络设备中间人攻击的示例
有几种方法可以避免中间人攻击:一种是确保网络设备有一个 ACL(在第一章中讨论,第一章),它可以过滤掉不属于管理的传入 IP。另一种是确保与请求远程访问的代理共享身份密钥管理。
为什么这很重要?因为网络自动化需要确保它使用一个安全的通道来修改网络,而密钥管理是网络操作员和网络自动化团队之间的共同责任。
这里是使用 CLI 进行网络自动化的优缺点:
优点:
-
几乎所有网络设备都支持
-
能够访问整个网络设备的操作系统
-
临界和特权访问
-
通过串行访问进行远程或本地访问,没有网络
缺点:
-
慢速
-
并行访问有限
-
如果使用不当,可能会允许黑客进行干扰
-
交互式,需要即时响应
-
数据信息没有结构化(如 JSON 或 XML),这使得它容易产生解释错误
总结本节,我们介绍了 CLI 的使用,CLI 是用于从网络设备收集信息的最老接口。我们还了解到,对于某些网络设备,CLI 是收集或配置设备的唯一方式。其中一个重要点是 CLI 始终存在于网络设备上。在下一节中,我们将看到如何改进网络设备的接口以收集网络信息,这被称为 SNMP。
使用 SNMP
简单网络管理协议(SNMP)可能是最古老和最常用的协议,用于从设备收集管理信息。SNMP 规范首次于 1998 年在 RFC 2261 中发布,旨在简单快捷。
SNMP 代理和管理器
SNMP 定义了两个身份:
-
管理器(或服务器)
-
代理
代理通常是一个网络设备,而管理器是一个网络管理系统。
SNMP 使用 UDP 和 TCP 作为传输协议,端口为161和162。UDP 端口161用于按需收集或设置信息,其中管理器向代理发送请求。UDP 端口162用于无需管理器请求的异步操作。代理在必要时向服务器发送 UDP 数据包。这种方法称为 SNMP 陷阱,用于发送未经请求的消息,如警报或阈值违规。
SNMP MIB
如第一章所述,管理信息库(MIB)用作访问网络信息变量的标识符。该标识符被称为对象标识符(OID),如图 1.8所示。
SNMP 版本
SNMP 包含版本 1、版本 2c 和版本 3。它们在不同的时间发布,差异在于可用方法、传输协议、MIB 变量和加密。SNMP 代理向后兼容——因此,支持版本 3 的代理可以与所有版本一起工作。以下各节将总结每个版本。
SNMPv1
这是第一个版本,包含了大多数 MIB 变量和方法。它基于 UDP,使用社区字符串作为身份验证。它只支持 32 位 MIB 计数器,这对于快速接口来说是一个问题,因为计数器会很快过期。
SNMPv2c
引入了 64 位 MIB 变量计数器和InformRequest以及GetBulkRequest方法。由于安全复杂性,版本 2 没有被采用。随后发布了简化后的版本 2c,并得到了广泛接受。
SNMPv3
引入了用于身份验证和隐私的加密。增加了Report方法。
SNMP 原始方法
SNMP 本质上使用以下原始方法。
-
GET:从代理获取信息的方法:-
GetRequest:给定一个 OID,返回与之关联的变量 -
GetNextRequest:给定 OID,返回与变量相关联的下一个 OID(用于 SNMP MIB 遍历功能) -
GetBulkRequest:给定 OID 基,返回此 OID 分支下的所有 OID 和变量(有时会导致无法中断的长响应)
-
-
SET:在代理程序上设置值的方 法:SetRequest:给定 OID 和值,在 MIB 上设置它
-
Response:SET和GET的所有响应 -
TRAP:从代理程序发送到管理器的异步信息,包含 OID 和变量 -
InformRequest:用于发送带有确认的异步信息
SNMP 安全问题
由于缺乏加密和认证,SNMPv1 和 v2 易受 IP 欺骗攻击,这允许黑客向代理发送 SET 请求,从而危害网络。历史上,由于这个安全问题,SNMP 不用于写入配置,而只使用 GET 方法或 TRAP 汇总配置。
这里列举了使用 SNMP 进行网络自动化的优缺点:
优点:
-
易于实现
-
快速
-
并行性容易
-
无特权访问
缺点:
-
需要汇总以频繁收集信息
-
通常不用于写入信息
-
与 CLI 相比,数据覆盖范围非常有限
-
写入时的安全问题
正如本节所述,SNMP 是网络管理中最古老且最健壮的协议。尽管它在写入配置方面存在安全和范围问题,但其协议轻量级、快速且易于阅读。下一节将介绍由 IETF 工作组开发的协议,以填补网络管理中配置方面的空白。它被称为 NETCONF。
使用 NETCONF
网络配置协议(NETCONF)是由 IETF 于 2006 年开发和标准化的网络管理协议。它提供了安装、操作和删除网络设备配置的机制。
NETCONF 操作是在 远程过程调用(RPC)层上实现的。NETCONF 协议使用基于 可扩展标记语言(XML)的数据编码来配置数据以及协议消息。协议消息也可以在安全的传输协议(如 SSH [RFC 6242])或使用 TLS [RFC 7589] 上进行交换。
动机
直到 21 世纪初的早期,IETF 可用的唯一管理协议是 SNMP,该协议于 20 世纪 80 年代末期开发。很明显,尽管最初的目的如此,但 SNMP 并未用于配置网络设备,而主要用于收集网络设备信息(如我们之前所见)。原因多种多样,但主要是由于与 CLI 相比,SNMP 不安全且范围有限。
2002 年 6 月,网络管理社区和互联网架构委员会与网络关键运营商一起讨论了网络管理协议和使用的实际情况。这次会议的结果记录在 RFC 3535 中 (datatracker.ietf.org/doc/html/rfc3535)。
结果表明,网络运营商并没有使用 SNMP,而是主要使用不同的专有 CLI 来配置他们的网络设备。原因多种多样,包括安全问题以及由于 SNMP 过于僵化而无法配置或编写配置的缺乏。
另一方面,在这个时候,Juniper Networks 已经开始使用基于 XML 的网络管理方法,这在 IETF 和网络运营商社区看来是一个结合力量的机会。这导致了 2003 年 5 月 NETCONF 工作组的成立。
2006 年 12 月,在 Juniper Networks 的大力帮助下,发布了第一个版本的基 NETCONF 协议,RFC 4741 (datatracker.ietf.org/doc/html/rfc4741)。此后,在随后的几年中发布了几个扩展(RFC 5277、RFC 5717、RFC 6243、RFC 6470 和 RFC 6536 等)。NETCONF 的最后修订版记录在 RFC 6241 中,于 2011 年 6 月发布(由 RFC 7803 和 RFC 8526 更新)。
OpenConfig
OpenConfig是一个由网络运营商组成的非正式工作组,他们共同的目标是通过采用 SDN 原则,如声明性配置和模型驱动的管理和操作,将我们的网络推向更动态、可编程的基础设施。
在 OpenConfig 中,我们的初步重点是编译一套一致的无厂商数据模型——用Yet Another Next Generation (YANG)编写——基于来自多个网络运营商的实际操作需求和需求。
YANG
YANG 是一种数据建模语言,它被 NETCONF 协议所使用。YANG 可以用来建模网络设备中的配置数据和状态数据。它是一种模块化语言,以 XML 格式表示数据结构,但也可以用其他格式表示。
对于每个网络设备功能,至少有一个 RFC 使用 YANG 描述数据模型——例如,VRRP(在第二章)中描述了 RFC 8347 中的 YANG 数据模型 (datatracker.ietf.org/doc/html/rfc8347)。另一个覆盖网络 ACLs 的努力(见第一章)描述了 RFC 8519 中的 YANG 模型 (datatracker.ietf.org/doc/html/rfc8519))。
让我们更仔细地考察 YANG 的特点和模型细节。
数据建模过程是困难的
需要理解的是,为路由器功能创建 YANG 数据模型不是一项容易的任务,因为它必须适应所有可能设备现有方法的可能场景。因此,这并不是从头开始,而是对已在多个供应商和设备中使用的功能进行建模的任务。让我们以一个例子为例 – 路由策略的 YANG 数据模型。如图 3.5 所示的时序图所示,这项工作始于 2015 年,经过 30 多个草案后,该标准终于在 2021 年 10 月发布,这意味着它几乎花了 7 年时间:

图 3.5 – 创建路由策略 YANG 数据模型的时序图
如果每个供应商都有自己的 YANG 数据模型会更容易,但那样就会消除通用依赖。
NETCONF
NETCONF 使用基于 RPC 的客户端-服务器通信。使用 NETCONF,服务器配置存储在遵循 YANG 数据格式规范的 NETCONF 配置数据存储中。要更改或更新数据,客户端通过一种安全传输方法发送基于 XML 的远程过程调用,服务器以 XML 编码的数据回复。
NETCONF 有四个层次,如图 3.6 所示,摘自原始 RFC 6241:

图 3.6 – RFC 6241 中描述的四个层次
让我们总结每一层:
-
内容层:由配置数据和通知数据组成。有效内容在 YANG 规范中定义。
-
get、get-config、edit-config、copy-config、delete-config、lock、unlock、close-session和kill-session。 -
来自客户端的
<rpc>请求或来自服务器的<rpc-reply>。RFC6241 还向此层添加了通知。 -
安全传输层:此层处理用于传输 NETCONF 消息的协议。SSH、TLS 和 HTTP 是与此层关联的几个协议。
RESTCONF
RESTCONF 协议是在 RFC 8040 中定义的提议标准(www.rfc-editor.org/rfc/rfc8040.html)。NETCONF 和 RESTCONF 在功能方面相似,但 RESTCONF 在 2017 年晚些时候出现,使用基于 HTTP 的 表示状态转移(REST)API 模型。它们都允许管理员使用客户端-服务器模型查询信息或修改设置。RESTCONF 在几个关键方面有所不同:
-
RESTCONF 使用 HTTP
-
RESTCONF 支持 JSON 和 XML
-
RESTCONF 没有事务的概念,因此没有像 NETCONF 那样的
lock概念。
RESTCONF 并不打算取代 NETCONF。相反,它被创建出来,以便可以使用 RESTful API,该 API 可以用于使用 NETCONF 或 YANG 配置数据存储查询和配置设备。
图 3.7 展示了一个从 RFC 8040 中提取的表格,展示了 RESTCONF 和 NETCONF 调用的重叠:

图 3.7 – 从 RFC 8040 中提取的 RESTCONF 和 NETCONF 方法的重叠
下面是使用 NETCONF 或 RESTCONF 的一些优缺点:
优点:
-
集成网络规范
-
IETF 标准
-
无特权访问
-
允许流事件通知
-
程序化设备配置
缺点:
-
YANG 没有涵盖所有设备功能
-
NETCONF 的采用速度非常慢
-
NETCONF 传输有限且实现过时
-
效率不高
本节总结了如何使用 NETCONF、RESTCONF 和 YANG 与网络设备进行交互。NETCONF 的事务状态使其成为网络配置的强大工具。尽管它基于良好的 IETF 标准,但 NETCONF 在处理一些我们想要的网络自动化任务(如以高频率收集数据)方面效率并不足够。在下一节中,我们将探讨一个名为 gRPC 的新协议。
采用 gRPC
gRPC于 2015 年作为一个开源 RPC 框架发布。由于它易于创建程序并添加方法以在网络上获取或设置配置,因此它是自动化中最有前途的协议之一。
gRPC 并不直接使用 TCP 进行传输,而是使用 HTTP/2,该协议于 2015 年发布,旨在克服 HTTP/1.1 的限制。虽然它与 HTTP/1.1 向后兼容,但 HTTP/2 带来了许多额外的先进功能,如下所示:
-
二进制帧层:请求和响应被划分为小消息并以二进制格式进行封装,使消息传输更高效
-
双向全双工流:在这里,客户端可以请求,服务器可以同时响应
-
流控制(用于 HTTP/2):允许对网络缓冲区使用的内存进行详细控制
-
头部压缩:在发送之前,HTTP/2 中的所有内容(包括头部)都会进行编码,这显著提高了性能
-
异步和同步处理:可用于执行不同类型的交互和流式 RPC
所有这些 HTTP/2 的特性使得 gRPC 能够使用更少的资源,从而减少了客户端和服务器之间的响应时间。
为了确保 gRPC 的安全性,可以使用端到端加密的 TLS,并且可以使用 SSL 或 TLS 进行身份验证,无论是否基于令牌的身份验证或需要通过扩展提供的代码来定义自己的身份验证系统(更多关于身份验证的信息可以在grpc.io/docs/guides/auth/找到)。
字母 g
在协议的 1.0 版本中,字母g是对 gRPC 名称的递归引用,但随着后续版本的发布,又添加了另一个单词,使得名称成为了一点代码娱乐。例如,在 1.1 版本中,这个单词是good,在 1.2 版本中,它是green,而在 1.42 版本中,它是granola。用于字母g的完整名称列表可以在 gRPC 源代码中找到:grpc.github.io/grpc/core/md_doc_g_stands_for.html.
动机
谷歌已经使用了一个名为Stubby的单个通用 RPC 基础设施,连接了在谷歌数据中心内部和跨数据中心运行的大量微服务超过十年。这促使谷歌发布并赞助了 gRPC 的创建。
2015 年 10 月 26 日星期一 gRPC 团队的信件
gRPC 团队很高兴地宣布 gRPC Beta 的立即可用性。这次发布代表了 API 稳定性的重大进步,未来的大多数 API 更改都是添加性的。它为 gRPC 在生产环境中的应用打开了大门。
我们更新了 grpc.io 文档以反映最新的变化,并发布了特定语言的参考文档。在 GitHub 上 Java、Go 和其他所有语言的发布说明中,您可以找到有关更改的信息。
我们感谢所有贡献代码、进行演示、采用这项技术并参与社区的人。我们期待在您的支持下推出 1.0 版本!
概述
gRPC 使用客户端和服务器应用程序的概念。客户端应用程序可以直接调用远程机器上的服务器应用程序,就像它们是本地对象一样。gRPC 基于定义一个服务和指定可以通过它们的参数和返回类型远程调用的方法的想法。服务器实现这个接口并运行 gRPC 服务器来处理客户端调用。客户端有一个存根(在某些语言中仅称为客户端),它提供了与服务器相同的方法。
在网络自动化的世界中,gRCP 客户端实际上是我们的自动化软件,而 gRPC 服务器是网络设备,如图图 3.8所示:

图 3.8 – gRPC 的基本请求和响应
gRPC 服务器和客户端不需要使用相同的编程语言。如今,有几种不同语言的实现,无论是 Go、Python、Java 还是 Ruby。支持的语言完整列表可以在这里找到:grpc.io/docs/languages/.
Protobuf
默认情况下,gRPC 使用协议缓冲区(Protobuf),这是由谷歌创建的另一种用于序列化数据的开源机制。尽管 Protobuf 是默认的,但 gRPC 也可以使用 JSON,但那效率较低,我们将在后面看到。
Protobuf 是一种语言和平台无关的数据序列化机制,类似于 JSON 或 XML,但更小、更快、更简单。数据结构一旦定义,就使用特别生成的源代码来轻松地从各种数据流中用任何编程语言写入和读取这种结构化数据。
更多关于 Protobuf 的信息可以在这里找到:developers.google.com/protocol-buffers。
gRPC 和网络遥测
在我们的网络自动化工作中,我们将遇到一系列限制,尤其是在有效收集网络信息方面。因此,让我们探讨以下示例。
想象一个有 500 台设备的网络,每台设备平均有 50 个接口。每个接口需要收集多个变量,例如当前状态、错误率、丢弃计数、入包计数或出包计数。如果我们考虑一种保守的方法,例如每个接口只收集 10 个变量,对于这个网络示例,我们将从 10 个变量 x 50 个接口 x 500 台设备的信息中收集数据,总计 250,000 个变量。
另一点需要考虑的是数据的频率。在 90 年代,网络管理需要每 5 分钟从网络获取信息,这对于处理故障和故障排除来说是足够的,但如今,间隔要小得多。我们希望以每分钟不到 1 秒的间隔收集信息,理想情况下是每 30 秒或 10 秒。原因是当快速检测到故障时,故障排除和故障解决可以更快地进行。
因此,在我们的示例中,每 10 秒收集 250,000 个变量,使用传统的轮询机制(如 SNMP)会产生大量数据。然而,这里的一个重要观点是,变量的大部分内容可能根本不会改变,例如在没有流量时接口的计数器、接口状态没有变化时的接口状态、没有流量时的接口丢弃计数器,或者接口完全正常时的错误率。因此,网络变量的内容不会经常改变,这意味着轮询机制效率低下,并且随着时间的推移积累冗余信息。什么比轮询更好?流式遥测。流式遥测允许设备在变化发生时立即连续发送增量更新。这样,网络信息的收集可以比轮询更有效地进行。
gRPC 支持双向流,这使得该协议在数据收集方面比我们之前看到的其他协议具有巨大优势。
使用 gRPC 的代码示例
为了使示例更符合网络自动化的实际情况,让我们在路由器上设置一个可以返回以下信息的服务:
-
返回内存利用率(百分比)
-
返回 CPU 利用率(百分比)
-
返回路由器运行时间(秒)
我们的示例将创建一个客户端 gRPC 存根以与路由器通信,该路由器将作为 gRPC 服务器,如图 3.8 所示。我们只将演示客户端,并假设路由器上的 gRPC 服务器已经实现。
Protobuf 文件
Protobuf 文件定义是代码的一个部分,它与任何语言无关。相同的文件定义在客户端和服务器上使用。它编译一次,为客户端和服务器程序提供解释用于生成 RPC 的数据。对于我们的示例,Protobuf 文件将如下所示:
service RouterStatus {
rpc GetStatus (StatusRequest) returns (StatusReply);
}
message StatusRequest {}
message StatusReply {
double memory = 1;
double cpu = 2;
int32 uptime = 3;
}
使用 Python 的示例
这里是一个使用 Python 的示例。导入名称r_grpc从 Protobuf 文件编译 Python 代码:
import grpc
import routerstatus_pb2
import routerstatus_pb2_grpc as r_grpc
def run():
address = "router:50051"
with grpc.insecure_channel(address) as channel:
stub = r_grpc.StatusStub(channel)
r = stub.GetStatus(r_grpc.StatusRequest())
print("Memory:{.2f}% CPU:{.2f}%, Uptime:{d}s\n".format(r.memory, r.cpu, r.uptime))
if __name__ == '__main__':
run()
使用 Go 的示例
这里是一个使用 Go 程序客户端的示例。请注意,pb(在导入中使用)是针对 Protobuf 编译的代码:
import (
"context"
"log"
"time"
"fmt"
"google.golang.org/grpc"
pb "example/routerstatus"
)
func main() {
address = "router:50051"
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
c := pb.NewStatusClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.GetStatus(ctx, &pb.StatusRequest{})
if err != nil {
log.Fatalf("could not get router status: %v", err)
}
fmt.Printf("CPU:%.2f%%, Memory:%.2f%%, Uptime:%ds", r.GetCpu(), r.GetMemory(), r.GetUptime())
}
使用 gRPC 进行网络自动化的优缺点如下:
优点:
-
安全
-
快速
-
并行处理很容易
-
无法进行特权访问
-
灵活,可以使用 gRPC 服务器暴露任何本地设备命令
缺点:
- 很少有网络设备具备 gRPC 功能
在本节中,我们了解到 gRPC 是一个用于网络自动化的强大协议。然而,它尚未很好地集成到网络设备中。大多数新的网络设备操作系统都自带这种功能。在下一节中,将使用一个名为 gNMI 的高级协议来更好地利用 gRPC 协议进行网络自动化。
使用 gNMI 操作
如前文所述,gRPC 可能是性能方面与设备协同工作的最合适的协议。然而,它实际上是一个通用的协议,可用于任何客户端和服务器交互——不仅限于网络设备,还包括计算机服务器。因此,gRPC 网络管理接口(gNMI)被创建出来。
gNMI 是由 OpenConfig 工作组创建的开源协议规范,用于使用 YANG(在NETCONF部分中讨论)与网络设备进行通信。换句话说,gNMI 是为了利用定义网络规范数据的人所做的良好工作而创建的,但使用的是更现代的协议 gRPC 而不是 NETCONF。
协议层
gNMI 使用 gRPC。为此,它必须将 YANG 数据描述转换为 Protobuf 以序列化通信,如图 3.9 所示。图的最底部是一个正常的基于 HTTP/2 和 TLS 的 gRPC 连接。gRPC 代码是从 gNMI Protobuf 模型自动生成的,gNMI 携带在 YANG 中建模的数据,可以支持像下面示例中的 JSON 编码。

图 3.9 – gNMI 协议层
数据模型
gNMI 使用一种名为 PathElem 的数据模型消息。每个 PathElem 包含一个编码为字符串的名称。一个元素名称必须编码为 PathElem 可以选择指定一组键,指定为 map<string,string>(字典或映射)。
根路径 / 编码为零长度的 PathElem 消息数组(切片)。以下是在 Go 和 Python 中的示例声明:
-
path := []*PathElem{} -
path = []
可以通过使用 / 分隔符连接前缀和路径的元素来形成可读的路径。
因此,让我们看看以下表示:/interfaces/interface[name=Ethernet1/2/3]/state.
这如下所示:
<elem: <name: "interfaces”>elem: <name: "interface"key: <key: "name”value: "Ethernet1/2/3">>elem: <name: "state">>
通信模型
通信模型使用目标客户端如下:
-
目标:在 gNMI 中作为正在操作或收集的数据的所有者的设备。通常,这是我们的网络设备。
-
客户端或收集器: 使用 gNMI 查询或修改目标上的数据或作为流数据的收集器的系统。通常,这是网络管理系统或我们的自动化代码。
与 gRPC 类似,服务器实际上位于网络设备上,如 图 3.10 所示:

图 3.10 – gNMI 目标和客户端通信
服务定义
gNMI 服务基于名为 Capabilities、Get、Set 和 Subscribe 的 RPC 调用,这些将在以下章节中详细介绍。
功能 RPC
客户端可以使用 CapabilityRequest 消息来发现目标的功能。客户端发送此消息以查询目标。然后,目标必须回复一个包含其 gNMI 服务版本、它支持的数据模型版本以及支持的数据编码的 CapabilityResponse 消息。这些信息用于客户端后续的 RPC 消息,以指示客户端将用于 Get 和 Subscribe RPC 调用的模型集以及用于数据的编码。
获取 RPC
向目标发送 GetRequest 消息,指定要检索的路径。在接收到 GetRequest 消息后,目标序列化请求的路径并返回一个 GetResponse 消息。此连接是短暂的,目标在传输 GetResponse 消息后关闭 Get RPC。
设置 RPC
通过向目标发送 SetRequest 消息来对目标的状态进行修改,以指示它希望进行的修改。
接收 SetRequest 消息的目标会处理其中指定的操作,这些操作被视为一个事务。作为对 SetRequest 消息的响应,目标必须回复一个 SetResponse 消息。对于 SetRequest 消息中指定的每个操作,必须在 SetResponse 的响应字段中包含一个 UpdateResult 消息。
订阅 RPC
这可能是 gNMI 上最重要的调用,因为它允许流式遥测,正如之前讨论的那样。
当客户端希望接收有关目标上数据实例状态更新的信息时,它通过 Subscribe RPC 创建一个订阅。订阅由一个或多个路径组成,并指定了订阅模式。每个订阅的模式决定了从目标发送到客户端的数据更新的触发器。
所有新的订阅请求都被封装在一个SubscribeRequest消息中,该消息本身有一个模式,描述了订阅的持久性。客户端可以创建一个订阅,该订阅有一个专门的流来返回一次性数据(ONCE);一个利用流来定期请求一组数据的订阅(POLL);或者一个长期存在的订阅,根据单个订阅模式中指定的触发器流式传输数据(STREAM)。对于流式遥测,模式设置为STREAM。
gNMI-gateway
gNMI-gateway 是由 Netflix 最初开发的开源软件,后来作为 OpenConfig 工作组的一部分发布,用于从网络设备收集和分发基于 OpenConfig 模式的 gNMI 数据。
创建 gNMI-gateway 的动机多种多样,如下所述:
-
首先,可用的开源服务不多,无法消费和分发基于 OpenConfig 模式的 gNMI 流式遥测数据。
-
第二,使用 gNMI 数据流进行客户端和目标连接的容错性不足,使得流式遥测容易受到威胁。当客户端崩溃时,流式数据会丢失,直到另一个订阅发生。
-
第三是缺乏支持多个消费者的功能。如果公司内的多个部门需要从网络设备或一组网络设备获取数据,那么所有这些部门都需要向目标发送订阅。在 gNMI-gateway 的集群功能和复制功能下,可以避免不必要地重复连接到目标,并向多个客户提供相同的数据。
-
第四,缺乏将 gNMI 客户端与非 gNMI 客户端统一的功能。gNMI-gateway 允许 gNMI 客户端或非 gNMI 客户端收集信息。
图 3.11 展示了 gNMI 客户端和非 gNMI 客户端(也称为导出器)的单个实例。Apache Kafka (kafka.apache.org/) 是可以用来作为导出器的一种软件;另一个已经实现的是 Prometheus (prometheus.io/):

图 3.11 – gNMI-gateway 的单个实例
通过使用多个 gNMI-gateway 实例来获得冗余,这些实例是通过使用Apache Zookeeper (zookeeper.apache.org/)实现的,如图图 3.12所示。如果只有一个实例正在运行,则不需要使用 Apache Zookeeper:

图 3.12 – gNMI-gateway 的多个实例
更多关于 gNMI-gateway 的信息可以在这里找到:github.com/openconfig/gnmi。
作为参考,以下是 2020 年 NANOG 网络广播中展示的关于 gNMI-gateway 的完整演示:nanog.org/news-stories/nanog-tv/nanog-80-webcast/gnmi-gateway/。
重要提示
gNMI 的完整规范可以在这里找到:github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md。
下面是使用 gNMI 进行网络自动化的优缺点:
优点:
-
安全
-
快速
-
并行化很容易
-
不可能获得特权访问
-
集成了网络 YANG 规范
-
允许轻松进行流式遥测
-
能够添加具有完全冗余的 gNMI-gateway
缺点:
- 并非许多网络设备都具备 gNMI 功能
摘要
在本章中,我们介绍了软件工程师用来与网络设备交互的主要方法。可用的方法并不多,所以我假设在写作时我们已经覆盖了大约 99.9%的所有现有方法。
使用本章提供的信息,您可以选择将哪种方法或哪些方法纳入您的网络自动化代码。在大多数情况下,您无法仅使用一种方法覆盖所有场景;您可能需要结合两种或更多方法。
下一章将探讨如何使用文件来定义网络。我们将讨论每种可用文件类型的优缺点。
第四章:与网络配置和定义一起工作
网络自动化中的一个重要点是配置的组织方式以及我们如何以可扩展的方式自动化我们的网络。在本章中,我们将探讨如何处理网络配置以及如何定义它以有效地用于网络自动化。我们希望构建可扩展且面向未来的解决方案。
我们为什么关心配置和网络定义?为什么使用哪个文件很重要?我们如何创建终身定义?我们如何利用这一点来帮助网络自动化?让我们在本章中探讨这些问题的答案。
我们将探讨以下内容:
-
描述配置问题
-
使用定义帮助网络自动化
-
创建网络定义
-
探索不同的文件类型
技术要求
本章中描述的源代码存储在 GitHub 仓库中,网址为github.com/PacktPublishing/Network-Programming-and-Automation-Essentials/tree/main/Chapter04。
描述配置问题
一些生产网络在将配置应用于网络设备时没有进行任何额外的外部定义。其中一些有网络图描述网络,但大多数都有过时或不完整的图。因此,在大多数情况下,你可能需要阅读运行设备配置来了解网络操作的细节。
在一些网络提供商中,图用于对网络或整体概述的初步理解。一旦工程师对他们的网络有足够的信心,这些图就会被忽略或不再使用。有些人会更新他们的图,但对于大多数工程师来说,这项任务不是优先事项,通常会被推迟。
对于一些工程师来说,将配置修复直接应用于生产设备以解决灾难性或紧急故障也是常见的。在其他情况下,额外的配置临时应用于故障排除,但从未被移除。在某些情况下,这些配置更改被遗忘,网络运行时配置差异未被察觉,直到需要软件更新。
让我们在以下子节中进一步讨论这些问题。
真相来源
真相来源是计算机网络中用来描述定义所在位置以及当咨询或使用该信息创建进一步定义时,所有其他系统必须依赖的术语。真相来源可以是文件、路由器配置、数据库、内存空间或网络图。
我们希望将真相来源尽可能稳定地定义;它不应在短时间内发生变化,并且应作为任何其他定义的参考。
大多数网络工程师依赖于路由器配置作为真相来源,因为路由器经常更新,并且有正确的定义来运行网络;然而,这并不帮助我们的网络自动化。
理想情况下,我们希望真相来源位于数据库中或存储在安全且未来可靠的环境中,以便任何系统都可以快速读取。
启动配置和运行配置
网络设备有两种配置状态,运行配置和启动配置。运行配置是设备内存中当前正在使用的配置,用于在当前时刻操作。启动配置用于将设备从关闭状态启动到开启状态。区别在于一个是易失性的,一旦设备关闭或失去电源,就会被删除,而另一个是永久的,无论是否有电源,它都会始终存在。
启动配置通常存储在非易失性存储器中,如 SSD、闪存驱动器或硬盘驱动器。
对于我们的自动化,我们希望运行配置与启动配置相同。当它们不同时,如果没有良好记录,可能会引起自动化问题。
配置状态和历史记录
一个好的网络自动化设计应该旨在拥有多个配置状态和历史记录,这将有助于确定网络当前是如何运行的,未来应该如何运行,以及过去是如何运行的。
对于部署和自动化,将配置分离成至少四个阶段非常有用:期望的、批准的、应用的和运行的,如图 4.1 所示:

图 4.1 – 配置阶段和控制层
我们将在以下子节中讨论这些内容。
想要的配置
期望配置用于记录即将应用在不久的将来的配置。它用于检查不一致性,并在测试网络(或网络模拟)中应用,以评估未来的部署、可能的语法错误和其他功能问题。
现代网络使用期望配置来向配置管道提供数据,该管道将执行一系列测试,包括模拟以验证和预测错误。它还用于测试部署顺序,并评估哪些设备可以并行部署,哪些设备需要等待某些设备完成部署。
批准配置
批准配置是已经通过配置管道中所有审批阶段的配置,无论是自动化审批阶段,如语法检查,还是人工审批阶段。
为了增强信心,一些配置管道会在网络模拟中应用所需的配置进行进一步的功能测试,如果一切顺利,配置管道将批准配置。
在所有配置阶段中,审批配置是耗时最长的,因为大型网络有成千上万的路由器,其中一些需要顺序功能测试而不是并行测试。完成所有工作后,配置就绪,可以部署。
应用的配置
应用的配置是保存在设备非易失性配置或启动配置中的配置。在这个阶段结束时,我们自信地知道配置已经保存在所有路由器中。
这个阶段也可能需要很长时间,因为部署不一定能并行进行。
运行配置
这个阶段用作未来部署和审批流程的安全保障。运行配置必须与应用配置相同;然而,它可能不是相同的配置,尤其是在需要配置干预的灾难性事件之后。
运行配置由触发点不断更新,如对设备的特权访问、故障硬件或任何配置更改。
使用配置管道要求只有管道能够更改路由器的启动配置。这意味着网络工程师无法在设备上保存配置更改,以避免这种情况发生。
对于需要快速更改配置的灾难性场景,通常绕过配置管道,由网络工程师手动干预进行配置更新。这些更改通常应用于运行配置,并且不会保存到设备中。如果运行配置审计,它将显示运行配置和应用的配置之间的差异。
配置历史
对于每个设备的所有配置阶段,都希望有一个配置历史记录。配置历史记录帮助我们通过比较旧配置与新配置来了解可能的故障或改进。它还用于在模拟中构建整个网络,以在配置更新期间解决部署中的故障。配置历史记录也可能对其他团队有益,例如安全审计和容量规划师。
部署管道
部署管道用于需要快速和可靠更改的大型网络。该管道使用现代的模拟和测试技术构建,包括广泛使用网络自动化编码。
网络部署管道的一个示例如下:

图 4.2 – 配置部署管道示例
在前面的图中,我们可以看到我们如何在部署管道中使用配置状态。管道的输入是生成的期望配置,而批准的配置只有在最终测试完成后才会更新。使用此工作流程,可以实现自动化部署,减少错误,并允许更快的并行部署。让我们描述管道过程每一步:
-
第一,自动检查配置语法。如果通过,则进入下一步。如果失败,管道过程停止,等待有效的配置出现。
-
第二,管道验证运行配置和应用的配置之间的任何差异,在某些情况下可能是由于紧急配置修复引起的。如果配置不同,则需要手动批准才能进入下一步。
-
第三,管道在配置更改范围内对路由器进行模拟,并将新配置部署到模拟网络中。
-
第四,管道在模拟中运行功能测试,以确认新配置不会破坏已存在的任何功能。如果通过,则进入下一步。如果失败,管道停止并等待拒绝或接受。
-
第五,该管道将新配置应用于网络,遵循所有并行依赖关系和时间限制。
网络图和自动化
网络图是网络的可视化、人性化的可读表示,但它们对机器来说并不容易阅读。大多数情况下,它们是由人类使用 Visio、Lucid Chart 和 Draw.io 等图形工具生成的。当由人类生成时,为了反映当前状态,图表的更新通常会被遗漏,导致图表上的信息过时。
要有一个准确的网络图,我们需要有一种图表生成器,它可以读取来自路由器配置或网络定义文件的数据。
在我们之前的部署管道示例中,图表生成器可以读取任何配置阶段的配置,并为运行配置和期望配置阶段生成不同的图表。
通过自动生成图表,可以为管道的任何阶段生成最新的图表。这些图表可以帮助网络工程师解决问题,或帮助网络设计师了解如何改进当前的网络功能。
在本节中,我们讨论了多个配置阶段如何帮助部署解决方案,以及当真相来源未定义或更新时可能发生的问题。接下来,我们将探讨如何从设备配置创建抽象定义,以创建更好的真相来源。
使用网络定义来辅助自动化
上一节探讨了配置阶段以及我们如何依赖它们来构建更好的网络部署管道。另一方面,我们没有涵盖另一个与路由器配置相关的问题,即与路由器软件版本和供应商相关。
如果您的网络不会更新、增长或更改供应商,使用路由器配置作为真相源具有优势。如果您的网络不打算更改,您可能根本不需要定义。然而,由于大多数网络都需要升级或增长,因此考虑摆脱特定供应商的解决方案并创建无供应商定义的网络是很重要的。
路由器供应商有不同的配置默认值,这意味着某些配置行可能在一个供应商中不是必需的,但在另一个供应商中却是必需的。对于网络自动化,我们希望避免这样的陷阱,并有一个明确说明配置所需内容的网络真相源。然后我们必须添加一个翻译层,以生成针对该供应商的特定路由器配置。
另一点是,一些供应商在相同操作系统的不同版本之间更改默认配置。一个版本可能包含其他版本中不存在的额外行。这也可能对我们的网络自动化造成问题。同样,我们希望添加一个供应商和版本特定的翻译层,因此生成适当的配置。
路由器配置渲染
路由器配置渲染是一个位于我们无差别的定义和之前描述的部署管道中所需配置之间的软件层。它的工作方式就像一个翻译者,需要了解供应商和路由器的版本以生成适当的配置。以下图显示了两个不同的配置渲染示例,一个用于 Juniper,一个用于 Cisco:

图 4.3 – Cisco 和 Juniper 的配置渲染示例
在为同一供应商生成配置行时,配置渲染可以是灵活的,但它必须了解每个供应商及其操作系统版本的配置默认值和差异。通常,一个渲染器可以覆盖同一供应商的一系列版本和平台。
使用配置模板
生成路由器配置的一个简单方法是使用配置模板。有了这些模板,可以轻松构建一个通用配置,然后通过添加关键文本词到需要更改的配置文本中来进行修改。
在我们的情况下,路由器配置渲染器将从文件中读取定义,然后读取一个配置模板以生成如以下图所示的路由器配置。它展示了 Juniper 路由器的示例,但也可以用于任何其他路由器:

图 4.4 – 使用模板进行路由器配置渲染
创建模板并不困难,它只需要你生成所需的路由器配置平台的样本;你只需用键字符串替换你想要的元素。前一个图中的示例使用了 {{ADDRESS}} 和 {{DESTINATION}}。
使用 Python 引擎模板
Jinja 是一个用于 Python 的模板引擎库,也常被称为 Jinja2 以反映最新发布版本。Jinja 用于创建 HTML、XML,以及对我们来说,路由器配置。它由 Armin Ronacher 创建,是开源的,采用 BSD 许可证。Jinja 与 Django 模板引擎类似,但具有使用 Python 表达式的优势。它使用文本模板,因此可以用于生成任何文本、标记,甚至源代码,在我们的情况下是路由器配置。
对于路由器配置,Jinja 很有用,因为它有一个一致的模板标签语法,并且路由器模板配置被提取为一个独立源,因此它可以作为其他代码库的依赖项使用。
以下是一个用于 Cisco 路由器的输入 Jinja 模板文件的示例(一个名为 cisco_template.txt 的文件):
hostname {{name}}
!
interface Loopback100
description {{name}} router loopback
ip address 100.100.100.{{id}} 255.255.255.255
!
interface GigabitEthernet1/0
description Connection to {{to_name}} router G0/1
!
interface GigabitEthernet1/0.1{{id}}
description Access to {{to_name}}
encapsulation dot1Q 1{{id}}
ip address 100.0.1{{id}}.2 255.255.255.252
ip ospf network point-to-point
ip ospf cost 100
!
router ospf 100
router-id 100.100.100.{{id}}
network 100.0.0.0 0.255.255.255 area 0
!
以下是用作向配置渲染提供输入的定义文件(一个名为 router_definitions.yaml 的文件):
- id: 11
name: Sydney
to_name: Melbourne
- id: 12
name: Brisbane
to_name: Melbourne
- id: 13
name: Adelaide
to_name: Melbourne
以下是用以生成路由器配置的 Python 代码:
from jinja2 import Environment, FileSystemLoader
import yaml
env = Environment(loader=FileSystemLoader('templates'))
template = env.get_template('cisco_template_python.txt')
with open('router_definitions.yaml') as f:
routers = yaml.safe_load(f)
for router in routers:
router_conf = router['name'] + '_router_config.txt'
with open(router_conf, 'w') as f:
f.write(template.render(router))
运行前面的 Python 脚本后,它将为悉尼、布里斯班和阿德莱德生成三个具有不同配置的文件。
这里是 Sydney_router_config.txt 输出文件的内容:
hostname Sydney
!
interface Loopback100
description Sydney router loopback
ip address 100.100.100.11 255.255.255.255
!
interface GigabitEthernet1/0
description Connection to Melbourne router G0/1
!
interface GigabitEthernet1/0.111
description Access to Melbourne
encapsulation dot1Q 111
ip address 100.0.111.2 255.255.255.252
ip ospf network point-to-point
ip ospf cost 100
!
router ospf 100
router-id 100.100.100.11
network 100.0.0.0 0.255.255.255 area 0
!
以下是为 Brisbane_router_config.txt 输出文件的内容,仅为了展示文件之间的细微差异:
hostname Brisbane
!
interface Loopback100
description Brisbane router loopback
ip address 100.100.100.12 255.255.255.255
!
interface GigabitEthernet1/0
description Connection to Melbourne router G0/1
!
interface GigabitEthernet1/0.112
description Access to Melbourne
encapsulation dot1Q 112
ip address 100.0.112.2 255.255.255.252
ip ospf network point-to-point
ip ospf cost 100
!
router ospf 100
router-id 100.100.100.12
network 100.0.0.0 0.255.255.255 area 0
!
与悉尼和布里斯班一样,阿德莱德的文件将创建所需字段已更改。前面的示例相当简单,只有三个键需要修改:{{id}}、{{name}} 和 {{to_name}}。更复杂的示例可以在 Jinja 文档中找到,链接为 jinja.palletsprojects.com/。
使用 Go 引擎模板
由于 Jinja 仅限于 Python,Go 有一个本地的文本模板引擎,可以用来生成路由器配置。
与 Python 不同,Go 模板通过将数据结构应用于模板文本来执行。模板有注释,这些注释引用数据结构,这通常是 Go 中 Struct 或 Map 的字段。
对于我们使用 Go 的示例,让我们使用与前面示例中 Jinja 使用的 Cisco 配置相似的模板,但进行一些小的修改以适应 Go 标准。让我们使用名为 cisco_template_go.txt 的文件:
hostname {{.Name}}
!
interface Loopback100
description {{.Name}} router loopback
ip address 100.100.100.{{.Id}} 255.255.255.255
!
interface GigabitEthernet1/0
description Connection to {{.Toname}} router G0/1
!
interface GigabitEthernet1/0.1{{.Id}}
description Access to {{.Toname}}
encapsulation dot1Q 1{{.Id}}
ip address 100.0.1{{.Id}}.2 255.255.255.252
ip ospf network point-to-point
ip ospf cost 100
!
router ospf 100
router-id 100.100.100.{{.Id}}
network 100.0.0.0 0.255.255.255 area 0
!
而对于路由器定义,router_definitions.yaml 文件与 Python 示例中使用的相同。
以下是用 Go 语言编写的生成与 Python 中创建的相同路由器配置的代码:
package main
import (
"io/ioutil"
"os"
"text/template"
"gopkg.in/yaml.v3"
)
type Router struct {
Id int `yaml:"id"`
Name string `yaml:"name"`
Toname string `yaml:"to_name"`
}
type RouterList []Router
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
var routers RouterList
yamlFile, err := ioutil.ReadFile("router_definitions.yaml")
check(err)
err = yaml.Unmarshal(yamlFile, &routers)
check(err)
templateFile, err := ioutil.ReadFile("cisco_template_go.txt")
check(err)
for _, router := range routers {
outFile, err := os.Create(router.Name + "_router_config.txt")
check(err)
tmpl, err := template.New("render").Parse(string(templateFile))
check(err)
err = tmpl.Execute(outFile, router)
check(err)
}
}
Go 语言中的代码与 Python 类似,但请注意,在 Go 语言中,你必须明确描述你将要从router_definitions.yaml文件中读取的所有字段。这通过代码中的Router和RouterList类型(前述代码的第 11 行和第 17 行)来完成。
在本节中,我们探讨了如何通过路由器配置渲染来提高网络自动化。我们还探讨了 Python 和 Go 中一些非常有用的库,用于路由器配置渲染。接下来,我们将探讨创建网络定义的细微差别。
创建网络定义
我们已经看到,模板引擎在通过网络自动化创建路由器配置方面极为有用。定义一个好的路由器定义也同样重要,这样我们就可以有更通用的模板,允许路由器定义文件决定路由器应该如何配置。此外,如果路由器定义创建得当,在需要更换供应商或升级路由器时,通常不需要更改它们。唯一需要更改的是路由器配置模板。
那么,我们如何创建一个可以持久存在并作为整个网络自动化真实来源的网络定义呢?让我们探讨一些有助于实现这一点的要点。
嵌套和分层定义
网络定义不一定要是一个扁平的唯一文件定义,而可以使用一组嵌套设置中的文件。原因是某些定义文件可能特定于所有设备中存在的特定特征,例如供应商、设备类型、设备规则、ACL 或设备功能。后续的文件可以包含更具体的细节,例如位置、名称、容量限制或大小。
使用嵌套或分层网络定义将有助于避免为每个设备创建大型定义文件,最重要的是,避免在不同文件中重复定义。
例如,假设你想控制一个允许登录你网络中所有路由器的 IP 地址列表。如果你不使用嵌套定义,你可能需要将 IP 列表添加到所有路由器定义中。但是,如果你有嵌套定义,你可能只需使用一个文件定义即可。
要使用层次结构和组,你必须创建一个自定义库,通过查看属于该路由器的所有分层定义文件来编译一个特定路由器的最终定义。然后,可以在这个路由器配置渲染中使用最终编译的定义来完成路由器模板并输出正确的路由器配置。
IP 分配考虑因素
在定义中需要注意的一个重要点是与网络设备上每个接口或协议关联的 IP 地址。除非使用网络地址转换器(NAT),否则 IP 地址通常是网络特有的,但大多数 IP 地址范围在每个地区和每个设备中都是唯一的。
为了创建更灵活且具有未来性的解决方案,IP 分配必须尽可能不固定,并且可以采用规则来配置渲染,以允许更好地使用 IP 分配。
结合使用 IP 分配引擎和嵌套定义,可以预留用于设备标识的基本 IP 地址,例如回环地址,并将其他 IP 地址范围留给网络接口关联。
当 IP 地址不固定时,可以进行的改进之一是拥有一个将 IP 地址转换为与网络定义文件关联的名称的服务。例如,可以使用 DNS 来完成这项工作。
在定义中使用较少固定的 IP 地址将允许更灵活的解决方案,并避免网络定义文件中的复杂性。
使用文件进行定义
在创建定义时,最佳实践是使用纯文本文件,而不是数据库或其他存储方法。这将允许工程师拥有一个完整的真实来源,而不依赖于任何系统或应用程序,因此即使在发生多系统故障的灾难性事件后,文件也可以被读取。
文件格式
网络自动化应跨所有网络定义仅使用一种文件格式。文件应该是基于文本的,但应使用一种众所周知且强制执行(包括但不限于)输入的格式。如果文件结构标准化且易于阅读,则在必要时将有助于工程师进行审查。
名称
在将名称与网络定义关联时,应避免使用缩写或任何形式的缩写。虽然缩短名称可以帮助工程师更快地输入,但可能会造成混淆,并在人类需要调查时引发问题。请记住,您始终可以在本地环境中创建快捷键或别名来更快地输入设备名称。
一些设备对名称的字符串大小有限制,因此请明智地使用名称,并尽可能详细地描述您命名的设备,以避免节省空间。
本节已展示了在创建网络定义时检查一些细节的重要性。现在,我们将回顾我们网络定义中最常见的几种数据表示类型。
探索不同的文件类型
我们在本章中看到的示例具有以.yaml结尾的文件,这是它们处于 YAML 格式的指示。但为什么是 YAML 而不是 XML 或 JSON 格式呢?让我们来探讨最常用文件格式的优缺点。
对于我们的网络定义,我们希望选择一种既易于人类阅读又易于系统解析,且存储空间小的格式。这样我们就可以在文件中编写大量内容,而不用担心性能、阅读难度或存储问题。
XML 文件
可扩展标记语言或XML是本节中描述的最古老的标记语言,其首次实现日期为 1996 年。其第一个标准发布是由万维网联盟在 1998 年创建的 1.0 规范。
初始 XML 设计的主要目标是创建一种简单、能覆盖通用情况且易于在互联网上使用的标记语言。尽管最初的想法是使用 XML 创建文档,但该语言已被用于客户端和服务器交互中的任意数据结构。
由于其模式系统,XML 可以使用多种媒体类型。2001 年,IETF 发布了 RFC3023,描述了所有可能的媒体类型,包括application/xml和text/xml。2014 年,IETF 发布了 RFC7303,细化了媒体类型的标准,使 RFC3023 过时。
这里有一个例子:
<router>
<interface>
<ip>
<unicast>
10.2.2.3
</unicast>
</ip>
</interface>
<loopback>
<ip>
<unicast>
100.1.1.1
</unicast>
</ip>
</loopback>
</router>
注意,在示例中,我们只有两个数据值(突出显示的 IP 地址);其余的都是标记开销。
这里是优缺点:
-
优点:
- 更灵活地表示通用数据
-
缺点:
-
由于其复杂性,处理速度慢
-
使用许多重复的结构标记带来的开销很大
-
不太易读
-
结构容易出现冗余
-
JSON 文件
JavaScript 对象表示法,或JSON,与 XML 相比,是一种较新的数据交换表示方法,原始规范由道格拉斯·克罗克福德在 21 世纪初制定。2004 年,IETF 发布了信息性 RFC4627,但直到 2014 年,IETF 才创建了 RFC7159 标准。现在最新的标准是 RFC8259。
JSON,就像 JavaScript 中的对象一样,具有原始类型,如字符串、布尔值、数字和 null。JSON 的结构由一对花括号包围的键名和值组成,如{"key": <value>}。键名始终是字符串。以下是一个例子:
{
"router": {
"interface": {
"ip": {
"unicast": "10.2.2.3"
},
"loopback": {
"ip": {
"unicast": "100.1.1.1"
}
}
}
注意,JSON 的开销比 XML 小得多。
上述表示也可以写在一行中,但不易阅读:
{"router": {"interface": {"ip": {"unicast": "10.2.2.3"}}, "loopback": {"ip": {"unicast": "100.1.1.1"}}}
这里是优缺点:
-
优点:
-
比 XML 简单且更快
-
更好的解析性能
-
避免加载截断的文件
-
-
缺点:
-
不支持注释
-
不允许别名
-
比其他格式有更多的开销
-
不可读,取决于格式
-
YAML 文件
YAML 最初是 Yet Another Markup Language 的缩写,因为它是在 1990 年代末 XML 和 HTML 等标记语言大量涌现之后创建的。创建者 Clark Evans 希望 YAML 听起来不同,因此将名称改为 YAML Ain’t Markup Language,这是一个递归缩写,用以区分 YAML 与其他标记语言的目的。标准版本 1.0 于 2004 年发布,最新版本 1.2.2 于 2021 年发布。
YAML 旨在易于人类阅读,其数据表示需要使用缩进和新行,这些用于界定和分组数据;这与 JSON 不同,JSON 实际上不需要新行或缩进。
YAML 还支持其他数据表示语言不支持的高级功能,例如锚点和引用,这对于避免重复和数据错误非常有用。YAML 本地编码标量(如字符串、整数和浮点数)、字典(或映射)和列表。
这里有一个例子:
router:
interface:
ip:
unicast: 10.2.2.3
loopback:
ip:
unicast: 100.1.1.1
如你所见,YAML 有更简短的表现形式,并且非常易于阅读。然而,与 JSON 不同,它不能仅用一行表示,因为格式改变了数据表示。
这里是优缺点:
-
优点:
-
更简单、更小
-
易于阅读
-
允许别名和锚点
-
允许注释
-
-
缺点:
- 不如 JSON 解析快
还有其他格式,例如 TOML、HOCON 和 HCL。每个都有其优缺点,但就我们的网络自动化和大多数网络定义而言,YAML 仍然是迄今为止的最佳选择。它也是网络定义中最常见的格式。
摘要
在本章中,我们探讨了网络自动化和工程师如何从拥有适当的配置和网络定义解决方案中受益。在没有最小化人工交互和降低对网络供应商和操作系统版本的依赖的情况下,增长网络并不容易。
你现在熟悉了网络配置问题及其解决方法。你能够区分部署管道的阶段。你也能够创建一个健壮的网络定义,以供自动路由器配置渲染,并选择最佳文件类型来表示网络定义。
下一章将通过网络编程的视角,探讨在编写网络代码时我们应该做什么和不应该做什么。
第二部分:自动化网络编程
书的第二部分更侧重于网络自动化的编程方面。这包括对流行库、运行时性能、扩展性、错误处理、日志记录等的描述。使用了 Go 和 Python,在某些情况下,还提供了类似示例来展示如何使用这两种语言进行更好的网络自动化工作。
本部分包含以下章节:
-
第五章,网络编程的做与不做
-
第六章, 使用 Go 和 Python 进行网络编程
-
第七章, 错误处理和日志记录
-
第八章, 代码扩展
第五章:网络编程的禁忌与注意事项
为网络编写代码是令人兴奋的,因为当它失败时,挑战重重,而当它成功时,则令人满意。如果你是一位经验丰富的程序员,那么这个过程会容易一些,但如果你是新手,那么可能会遇到风暴。让我们深入了解一些有助于你更容易度过这些风暴的编码实践。
我们将专注于本章中与网络编程相关的 Python 和 Go 的编码方面。这里涉及的主题也适用于任何类型的编程;然而,我们将专注于网络编程的编程,以下是我们将涵盖的主题:
-
编码主题
-
在编码中应用最佳实践
-
编码格式化
-
版本控制和并发开发
-
测试你的代码
在本章结束时,你应该熟悉社区使用的编码术语以及其中哪些最重要。你将能够理解编码最佳实践以及如何成为一名更好的网络代码开发者。如果你是一位经验丰富的编码者,这将是一个很好的复习。如果你是新手,那么这一章将成为你编写代码时的座右铭。
编码主题
编写代码曾经非常简单直接;它只需要理解程序的工作流程、性能和算法。但今天,情况略有不同。编码现在有一个在过去几十年中演变的文化。最重要的是代码的可重用性,因此是它的风格。为了可重用,代码必须易于理解,并且应该有很少或没有错误或安全问题。
如果你刚开始编码,或者对网络编码不熟悉,了解今天编码文化中使用的所有主题是很重要的。让我们简要讨论本节中最重要的几个主题。
同行评审
不建议在没有同行评审的情况下编写代码并发布。同行评审可以让编码者与团队保持一致,避免不希望出现的错误。然而,对于大多数组织来说,这个过程可能很慢,有时甚至很昂贵。一个替代方案是使用软件机器人,它可以执行以前由另一位软件工程师完成的许多同行评审工作。
同行评审是由另一位具有检查团队标准、语言标准和社区最佳实践、分享知识、验证代码重复和命名约定、以及将设计与实现对齐等目标的开发人员进行的自觉和客观的评审。由于列表很长,同行评审过程并不容易和直接;有时评审代码的时间比编写代码的时间还要长。
生命周期
通过测量和存储创建日期、编译(如果有)和分发日期来跟踪你的代码是很重要的。这是通过某种生命周期管理来完成的。有了生命周期,就可以触发操作回到源代码,并验证当前的建议是否仍然适用于源代码,并检查是否存在任何新的安全漏洞。有一些工具可以自动对你的代码库和应用程序进行生命周期管理;在可能的情况下使用它们。
重构
在计算机编程中,重构(或代码重构)是指在不改变其输出和输入的外部行为的情况下改变代码的术语。重构可以用来修复错误、提高性能、移除安全漏洞或遵守新的代码风格。这里重要的是重构不会改变代码的功能。换句话说,外部行为、输入和输出都得到了保留。
重要提示
更多关于代码重构的信息,请访问网站 refactoring.guru/。
代码复制和许可
在发布你的代码之前,你应该首先考虑版权和代码许可证。为什么?因为如果你的代码很好,它肯定会被其他人重用或被复制用于其他目的,除非你在许可证文件中严格指定复制的规则。此外,如果你正在导入或使用外部库,你可能会因为版权问题而受到损害。
因此,创建适当的许可证文件并阅读你使用或复制的代码的许可证文件是很重要的。这里列出了分配给软件的许可证类型:
-
商业秘密许可证:不公开信息;私人内部使用;未发表
-
专有许可证:拥有版权;没有公开许可证和源代码
-
非商业许可证:用于非商业用途且无源代码
-
版权许可证:授予使用权利;禁止重新许可和公开源代码
-
许可许可证:授予使用权利;允许重新许可和公开源代码
-
公有领域许可证:所有可能的授权
我们为什么关心这些类型的许可证?因为你不应该分发或发布你正在工作的软件。如果你违反了你在公司本地创建的代码的许可证协议,你可能会受到法律处罚和罚款。大多数情况下,你可能将重用他人的库或代码到主要软件中。在使用外部库或代码之前,最好的办法是咨询法律部门,检查是否有任何禁止内部使用的许可证。
因此,建议在编写代码时检查提供的许可证免责声明文档。它通常位于源代码的根目录中,用大写字母书写,名称如LICENSE或LICENSE.rst,但有时也使用COPYRIGHT、COPYING或类似名称来表达版权许可。
对于开源软件,代码也可以依赖于其他实体创建的定义中的规则;要做到这一点,你只需要阅读LICENSE文件并检查哪个被应用。在开源代码中最常用的一个是MIT 许可证。
在使用开源库或将其复制到你的代码之前,确保许可证不会对你的组织构成风险。在私有组织中重新使用时,风险最高的开源代码是GPL 2.0和GPL 3.0(GNU 通用公共许可证)。低风险的包括MIT 许可证、Apache 许可证 2.0和BSD 许可证 2.0。
以下表格显示了 GitHub 项目中使用最广泛的五个许可证:
| 排名 | 许可证 | 项目百分比 |
|---|---|---|
| 1 | MIT | 44.69 |
| 2 | 其他 | 15.68 |
| 3 | GLP 2.0 | 12.96 |
| 4 | Apache | 11.19 |
| 5 | GLP 3.0 | 8.88 |
表 5.1 – GitHub 项目中使用最广泛的许可证排名
以下截图显示了 GitHub 项目许可证从 2008 年到 2015 年的演变。注意 MIT 许可证的增长:

图 5.1 – GitHub 项目中的许可证使用情况(来源:github.blog)
现在你已经熟悉了最流行的代码许可证,让我们继续讨论代码质量和认知。
代码质量和认知
代码质量是软件工程师中常听到的术语,但为了非常清楚,这不是一个容易的主题,质量的定义通常是主观的,并容易受到个人偏好的影响。
为了避免这种含糊的定义,代码质量应侧重于团队或公司的指南和标准。如果你刚刚开始的公司有宽松或没有代码指南,代码的质量可能依赖于资深工程师。但这是必须的——并且应该避免。
应该使用标准和代码实践来衡量代码质量,而不是个人。如果你的团队没有标准或代码实践,使用本章来指导你创建一个。
在定义代码质量时,尽量使用可衡量的指标,这样软件工程师可以理解低质量代码,而无需将其个人化。在代码中寻找以下特征:
-
可靠性:衡量代码在无故障的情况下运行了多少次
-
可维护性:衡量代码如何容易更改,包括大小、复杂性和结构
-
可测试性:衡量代码如何被控制、观察和隔离以创建自动化测试,例如单元测试(将在本章后面的测试你的代码部分讨论)
-
可移植性:代码在不同环境中运行的容易程度
-
可重用性:衡量代码可以通过依赖关系或复制进行重用的程度
在可能的情况下,使用能够分类和格式化代码的自动化工具,这样可以避免个人主观分析。
架构和建模
软件架构和设计是一个非常广泛且漫长的主题。根据你项目的大小,在与网络自动化代码一起工作时,你可能不需要进行设计或架构。然而,了解它们的存在并且能够有效地使用它们来获得更好的代码结构和更好的组织是很重要的。
我建议软件架构最重要的部分是建模,这在代码的早期阶段验证结构和组织非常有帮助。与客户通过模型进行沟通很容易,即使你不是软件开发者也能理解。
最常用的模型是统一建模语言(UML),作为通用语言,系统建模语言(SysML),UML 的一个子集,面向服务的架构建模语言(SoaML),也是 UML 的一个子集,以及C4 模型。选择哪一个不重要;在开始编码之前使用任何模型都将帮助你编写正确的代码。
让我们以第四章中描述的部署管道为例,创建一个解决方案的软件模型。图 5.2说明了它将是一个非常简单的部署管道模型:

图 5.2 – 第四章中描述的部署管道软件模型示例 Chapter 4
上述图表使用箭头和方框说明了开发者如何处理构建部署管道的问题。输入是路由定义和模板,然后配置渲染创建配置并将其存储在所需状态。检查语法,然后验证运行配置是否等于 applied。如果不等于,则停止并等待人工干预;如果一切正常,则启动网络模拟。然后,进行所有测试。如果测试通过,则更新批准的配置。下一步是将配置应用到路由器上,如果正确应用,则更新 applied 配置。
在本节中,我们讨论了一些现在在开发社区中使用的代码文化话题。在下一节中,我们将深入探讨编写 Python 和 Go 代码的最佳实践。
应用编码最佳实践
这里有一些点可能不是标准,但在我们编写网络代码时很重要。
遵循标准
在 20 世纪 90 年代,互联网开始迅速增长,程序员开始在社区中更加活跃。Shell 脚本开始作为一种可行的工具帮助系统工程师处理他们的服务器。然后出现了Perl,作为一种强大的脚本语言来提供帮助。其他语言也变得成熟,如 C、C++和 Java。但每个开发者都以自己的风格编写代码,这不利于代码协作,使得代码共享成为一场灾难。当 Python 被创建时,编码风格问题已经存在。必须做些事情来避免在代码共享时产生混淆。
现在我们来看看 Python 和 Go 语言的编码标准是什么。
Python 标准风格
Python 可以用很多不同的方式编写。如果你是一位老程序员,你会记得 Perl,它就像 Python 一样是一种脚本语言,但没有特定的风格,这造成了一团糟。有一些文档可以帮助程序员在 Python 中编写更好的代码。这些文档是由 Python 社区创建的,它们被称为PEPs,代表Python 增强提案。
最初,PEP 流程是为了提出和讨论对新特性的共识而创建的,但它也被用来记录信息。PEP 类似于 Python 的互联网请求评论(RFC),在描述新特性时,它必须包含对该特性的详细描述以及创建它的理由。
当提出 Python 的新特性时,作者负责在社区内建立共识,描述不同的选项,收集社区的反馈,并对其进行记录。
一些 PEP 是信息性的,基本上用于指导 Python 编码风格。如果你打算成为一名 Python 代码开发者,一个 PEP 就像你的日常日记,用来检查和阅读。保持对最新 PEP 的更新很重要,并确保你的代码遵循这些建议。
到目前为止,有一些有用的 PEP(Python Enhancement Proposals)可以帮助社区在如何用 Python 编写代码方面获得指导和最佳实践。让我们来看看其中最重要的几个。
在 PEP-8 的指导下编写代码
PEP-8可能是如果你是 Python 新手的话必须阅读的最重要 PEP,因为它详细描述了在 Python 中编写代码时的所有风格和约定。尽管语言在编写代码时并不强迫你选择一种风格,但使用PEP-8(或PEP8)会使你的代码对 Python 社区中的任何人来说都更容易理解。
当你对 Python 还不太熟悉时,可能很难记住你几周或几天前写的代码的一部分原本是要做什么的。然而,如果你遵循PEP-8,你可以确信你已经很好地命名了变量,添加了足够的空白,并且很好地注释了代码。遵循这些指南将使你的代码对他人以及你自己在一段时间后回来阅读时更加易读。初学者在遵循PEP-8时也会更快、更轻松地学习 Python。
PEP-8 文档非常清晰;它展示了错误代码风格,然后是正确风格,并解释了为什么使用这种风格。
Go 标准风格
在描述 Go 语言的标准之前,重要的是要注意,Go 语言也被称为 golang.org。现在,Go 语言托管在 go.dev 上。
在代码格式和风格方面,Go 与 Python 相差甚远。Go 没有类似于 PEP 的文档来指出如何在 Go 中编写更好的代码。因此,风格标准主要依赖于内部格式化工具,称为 gofmt,它负责以可预测的方式格式化源代码。
虽然可以跳过 gofmt 格式化器,但强烈建议在更改代码后以及发布之前使用它。一些工具在运行或编译你的 Go 程序之前会自动运行格式化器。除了 gofmt 之外,还有 Go linters 可以指出风格错误或可疑结构;我们将在本章后面的 编码格式化器 部分描述这些。
此外,Go 社区已经发布了几个有助于编写更好代码的指南。以下是其中最好的几个:
仔细编写代码
大多数时候,我们将阅读代码而不是编写代码,因为编写代码的过程会驱使我们阅读代码的其他部分。所以,你知道你的代码去哪里了,你的代码是如何遵循现有设计的,以及现有代码的行为,这样它就不会崩溃。这里的重要点是,即使你没有打算与他人分享你的代码,你可能在几个月后就会忘记它,如果你可能需要更新它,你将很难理解它做了什么。所以,编写代码时始终要考虑到有人会阅读它,即使这仅仅是你自己。
因此,编写代码的过程必须比阅读代码的过程更加谨慎。当有人阅读代码的一部分时,它不应该产生任何疑问;它应该是直截了当的,易于理解,并且快速掌握。
使其极其易于阅读
编写代码时,不要节省单词或短语。如果你有机会在编写代码时选择错误,请选择一个过度解释你代码的错误。为什么?因为你的代码应该被任何人理解,包括几个月或几年后的你。因此,确保你的代码易于理解和跟踪,对于新手来说也是如此。
注释你的代码
代码应该易于理解;然而,有时,对代码进行注释以帮助读者和审阅者理解代码是必要的。当你想要强调代码中的细微差别、描述某些算法的细节或警告读者有关某些异常时,你应该在代码中添加注释。
注释标题
注释中的某些文本标题用于帮助读者避免在应该或可能需要更改的代码上浪费时间。当你开始与团队一起编码时,检查用于帮助代码阅读者和代码审阅者的术语。这里列出的注释标题不是固定的,它们在不同的组织和团队之间会有所变化。以下是最受欢迎的注释标题:
-
TODO: 可能最常用的注释是TODO,它用于指定需要添加或重构的代码部分。这个注释帮助审阅者避免在需要修改的代码块上浪费时间。 -
FIXME: 一些开发者在编写代码时也会使用FIXME注释标题,它指定了代码中错误、丑陋、性能问题或过于复杂的部分。当你知道某些东西是错误的并且应该尽快修复时,请使用FIXME。 -
HACK: 这个文本标题用于指定有解决错误或帮助性能的绕过方案的代码部分。HACK标题也是另一个重要的注释,向读者展示一个快速修复方案,稍后将其修复,帮助他们理解为什么代码的这一部分存在。 -
BUG: 这个标题用于显示代码中的问题,必须尽快修复。作者已经识别了问题,并决定将代码的这一部分作为错误进行注释。代码审阅者可以评估并决定是否需要接受带有错误的代码或要求更正。同样,这些注释节省了审阅者的时间,因为他们不需要深入评估代码的这一部分。当你看到代码中的问题时,请使用此功能。 -
注意: 这个标题是为了让作者与读者沟通一些值得注意的陷阱或特定细节,以帮助读者更快地理解代码。要明智地使用它们,以帮助审阅者理解,因为过多的注意标题也可能令人烦恼并造成干扰。
文档字符串
文档字符串指的是在 Python 中用于描述模块、函数、类或方法的注释字符串。文档字符串是 Python 特有的,在PEP-257中有详细说明。
一些集成开发环境(IDEs)如 PyCharm 和 IntelliJ 在你定义函数时将自动为你创建文档字符串。
这里是一个 Python 函数多行文档字符串的例子:
def router_connection(hostname, timeout):
"""Perform a connection to a router
:param string hostname:
:param int timeout:
:return: True | False
"""
print("Connecting to host", hostname)
.
.
.
return True
注意,文档字符串始终以三重双引号(""")开始和结束。前面的例子只是一个建议,其他样式也是可能的。要可视化所有可能的样式,请阅读PEP-257中的文档字符串约定。
注意
Docstring 文档可以在 peps.python.org/pep-0257/ 找到。
Godoc
在 Go 语言中,有 godoc,它比 docstring 简单,因为它不是一个语言构造或机器可读的语法;它只是字符串文本注释。惯例很简单:要注释变量、函数、包或常量,只需在其声明之前直接写注释。以下是在 Go math 包中用于 Asin 函数的注释示例:
// Asin returns the arcsine, in radians, of x.//
// Special cases are:
//
// Asin(±0) = ±0
// Asin(x) = NaN if x < -1 or x > 1
func Asin(x float64) float64 {
if haveArchAsin {
return archAsin(x)
}
return asin(x)
}
注意,在这个例子中,注释放在 Asin 函数之前,并详细描述了函数及其特殊情况。注释总是以双斜杠(//)开头的文本。
以下示例取自 Go 源代码 github.com/golang/go/blob/master/src/math/asin.go#L14-L25。
在 Go 中注释你的代码后,添加与前面示例中类似的功能说明。开发者可以使用 godoc 工具,该工具提取文本并生成 HTML 或 TXT 格式的文档。
注意
godoc 文档可在 pkg.go.dev/golang.org/x/tools/cmd/godoc 找到。
使用 IP 库
对于网络编程的新手来说,可能会认为 IP 地址可以像字符串或字符列表一样处理。但在处理网络时,使用相应的 IP 库从文本文件中加载 IP 或从循环中创建 IP 非常重要。你可能想避免使用字符串作为 IP 的几个原因如下:
-
避免超出 IP 范围的数字,例如数字 256
-
避免格式不正确的文本 IP 地址
-
确保不要与其他空间地址重叠
-
容易找到网络、掩码和广播地址
-
容易从 IP 版本 4 转换到 IP 版本 6
让我们来看看如何在 Python 和 Go 中使用这些 IP 库。
Python 中的 IP 库
在 Python 3.3 版本之前,Python 中的 IP 地址库不是原生存在的。在 3.3 版本之前,Python 程序员必须使用外部库来处理 IP 地址。Python 在意识到其重要性后,必须将此库内部化。这一过渡在 PEP-3144 中进行了记录(peps.python.org/pep-3144/)。
使用 Python 进行 IP 操作的示例
首先,让我们打印出子网中的所有有效 IP 地址:
import ipaddress
network = ipaddress.ip_network("10.8.8.0/30")
print(list(network.hosts()))
这里是输出结果:
[IPv4Address('10.8.8.1'), IPv4Address('10.8.8.2')]
注意,输出类型是 IPv4Address 而不是字符串。ipaddress.ip_network() 方法会自动检测它是一个 IPv4 还是 IPv6 地址。
现在,让我们打印出较大子网中存在的子网,排除一个子网:
import ipaddress
network = ipaddress.ip_network("10.8.8.0/24")
subnet_to_exclude = ipaddress.ip_network("10.8.8.64/26")
print(list(network.address_exclude(subnet_to_exclude)))
这里是输出结果:
[IPv4Network('10.8.8.128/25'), IPv4Network('10.8.8.0/26')]
注意,输出中的类型不再是之前的 IP 地址类型,而是一个名为 IPv4Network 的网络类型。
Go 中的 IP 库
Go 与 Python 不同,因为 IP 地址库是在 Go 语言的第一版之后出现的。Go 中的 IP 操作比 Python 中更快,因为 Go 不是像 Python 那样被解释,而是被编译。
使用 Go 进行 IP 操作的示例
在 Go 语言中,我们首先将检查一个 IP 地址是否属于一个子网:
import (
"fmt"
"net"
)
func main() {
_, subnet, _ := net.ParseCIDR("10.8.8.0/29")
ip := net.ParseIP("10.8.8.22")
fmt.Println(subnet.Contains(ip))
}
这里是输出:false。
IP 地址 10.8.8.22 不属于 10.8.8.0/29 子网,因为程序输出 false。
下一个示例检查一个子网是否属于另一个子网:
import (
"fmt"
"net/netip"
)
func main() {
first_subnet := netip.MustParsePrefix("10.8.8.8/30")
second_subnet := netip.MustParsePrefix("10.8.8.0/24")
fmt.Println(second_subnet.Overlaps(first_subnet))
}
这里是输出:true。
在这个例子中,first_subnet 属于 second_subnet,因此它们重叠。请注意,在这个例子中,我们使用了 "net/netip" 导入而不是 "net",这是 Go 语言中引入的一个较新的 IP 操作包。
在比较方面,Python 的内置库在 IP 操作方面比内置的 Go IP 包具有更多功能。另一方面,Go 操作 IP 地址的速度更快。
如果你正在寻找 net 或 net/netip 中不存在的特定 IP 操作功能,你可以使用一个新开发的社区包 inet.af/netaddr,详情请见 pkg.go.dev/inet.af/netaddr。
遵循命名规范
在开始发布任何代码以供审查之前,与你的团队核实本地开发命名规范是明智的。可能会有一些特定的差异,在你的组织中使用可能会出现问题。在这里,我们将描述最流行的命名规范,但请根据你当地的团队文化进行适当调整。
在开始描述命名规范之前,让我们定义三种书写多词名称的方式,如下所示:
-
myFirstTestName -
MyFirstTestName -
my_first_test_name
在定义常量的情况下,大多数编程语言中名称中的所有字符都必须是大写。因此,蛇形命名法始终使用小写,除非名称描述的是常量。以下是一个描述常量的名称示例,它将定义最大存储容量:MAX_STORAGE_CAPACITY。
Python 中的命名
对于 Python,编写变量名时有一些规则需要遵循。以下是一个简短的列表,说明应该做什么:
-
变量名只能以字母或下划线字符(
_)开头 -
变量名只能包含字母数字字符和下划线
-
变量名决不能以数字开头
-
破折号(
-)仅用于包和模块名称,决不用于变量 -
变量名开头的双下划线字符在 Python 中是保留的
-
类名使用帕斯卡命名法
-
模块名和函数使用蛇形命名法
-
Python 中的常量使用全部大写字母——例如,
MYCONSTANT
注意
Google 为 Python 创建了一个出色的命名规范指南,公开可访问于 google.github.io/styleguide/pyguide.html#316-naming。
Go 中的命名
对于 Go 语言,有一些规则需要遵循。以下是一些主要规则的列表:
-
变量名使用驼峰式(camel case)或帕斯卡式(pascal case)。
-
与变量名一样,函数名也使用驼峰式或帕斯卡式。
-
变量或函数名不以数字开头。
-
变量和函数名仅使用字母数字字符。
-
破折号或下划线通常不用于命名。
-
Go 允许使用蛇形命名法(snake case)来命名变量,但请检查您所在组织的命名规范是否允许这样做,因为在 Go 社区中不常用蛇形命名法。
-
Go 中的常量与 Python 中的类似:使用全部大写字母。
此外,Go 与其他语言有一个特别的不同之处,它使用大写字母在 Go 包(或模块)内部的变量中。因此,如果变量名以大写字母开头,通常是一个可以从 Go 包外部访问的变量。当变量以小写字母开头时,它通常是一个局部变量,只能在同一 Go 包内的代码中访问。
注意
可以在 go.dev/doc/effective_go 获取 Go 中命名的好参考。
不要缩短变量名。
在计算机编程的早期,变量名必须小写以在保存未编译代码时节省内存空间。但如今,这已不再是问题,因此在编写变量名时无需考虑内存节省。
如果您需要编写一个变量来表示飞翔的鸟的数量,请使用蛇形命名法写成 number_of_birds_flying 或驼峰式命名法写成 NumberOfBirdsFlying。
如果您在列表推导式或循环中的变量(如计数器或索引)中使用变量,则对此规则有一个例外;在这种情况下,使用缩写变量是可以接受的。
避免复杂的循环。
循环被添加到代码中有几个原因:增加、与列表交互、重复操作和读取流等。可以使用循环处理确定或非确定的大小,这在不知道将要交互的内容时非常有用。
但无论您在代码中使用循环的意图是什么,都要明智地使用它们,以免它们变得难以阅读或过于复杂。一个很好的经验法则是避免循环内部有太多的行。我会说 20 行应该是极限。另一个好的做法是避免嵌套循环或循环中的循环。如果您必须进行嵌套循环,请将层数限制在不超过两层。嵌套循环难以阅读,当它们内部有太多行时,情况会更糟。
那么,创建少于 20 行的循环的最佳方法是什么?建议是将几个操作组合成可以在循环内调用的函数。给函数命名,使其能够完成你想让它做的事情,并将操作添加到其中。当你阅读循环时,函数调用将很容易被发现,函数的名称将表明操作在做什么,这非常容易阅读和理解。此外,使用这种方法编写单元测试也更容易。
如何避免嵌套循环?使用函数来调用循环。相反,你也可以使用生成器、列表推导式或映射来避免循环。使用映射的优势在于,你不需要与之交互,因此可以使你的代码运行得更快。列表推导式在 Python 中使用。生成器可以在 Python 中使用,在 Golang 中通过 goroutines 实现。所以,每当你需要嵌套循环时,尽量不要使它过于复杂,如果可能的话,使用这些机制之一来避免它。
不要重复代码
在软件开发中,有一个术语称为 DRY 或 Don’t Repeat Yourself。开发者最糟糕的事情就是重复代码。是的,这是很糟糕——真的很糟糕。为什么?因为如果你在多个地方重复执行同一个任务的代码,那么每次你想修改这个任务时,你都需要在多个地方执行同样的更改。另一个问题是,你将不得不为同一个任务编写多个单元测试。
DRY 的反面是 WET,代表 Write Everything Twice。WET 解决方案可能发生在多层架构中,开发者有不同的任务,最终可能会出现重复。DRY 方法应该通过使用帮助开发者避免重复的框架来消除代码冗余。
在本节中,我们看到了一些示例,说明了我们如何在编写代码时使用最佳实践。在下一节中,我们将探讨如何使用工具帮助我们编写更好的代码。
注意
在编写代码的最佳实践方面还有许多建议。更详尽的列表可以在github.com/golang/go/wiki/CodeReviewComments和 https://go.dev/doc/effective_go 找到。
对于 Python,我推荐斯科特·迈耶斯的《Effective Python》。
代码格式化工具
现在我们可以看到,在 Go 或 Python 中编写代码时,有许多规则和约定需要遵循,而且列表很长,这对于初学者来说并不有帮助。但是,多亏了社区,有一些工具可以帮助你修复代码格式。这些工具可以自动修复你的代码或提出修改建议。让我们看看 Python 和 Go 的一些代码格式化工具。
Python Black
Black 是一个非常流行的 Python 代码格式化工具。它是符合 PEP-8 的,因此它会检查所有的 PEP-8 建议。默认运行会将代码重新格式化为 PEP-8 兼容,但你也可以运行它作为预览来识别代码更改建议。
使用 Black,可以自动修复 Python 代码。以下是一个为 Python 编写的错误代码示例:
if long_variable_name is not None and \
long_variable_name.field > 0 or long_variable_name.is_debug:
z = 'hello '+'world'
else:
world = 'world'
a = 'hello {}'.format(world)
f = rf'hello {world}'
运行 Black 后,代码将看起来像这样:
if (
long_variable_name is not None
and long_variable_name.field > 0
or long_variable_name.is_debug
):
z = "hello " + "world"
else:
world = "world"
a = "hello {}".format(world)
f = rf"hello {world}"
Black 确实应用了所有PEP-8的建议,并且也有自己的风格。在先前的例子中,Black 正确地更改了if语句并添加了正确的缩进。此外,它将字符串赋值从单引号更改为双引号。没有具体的 PEP 指定你必须用双引号或单引号编写字符串,但 Black 自己的风格使用双引号来表示字符串。
我个人认为,某些字符串使用单引号而其他字符串使用双引号的代码看起来非常丑陋。因此,我建议只使用双引号来表示字符串,就像 Black 一样。
注意
更多关于 Python Black 的信息,请参阅github.com/psf/black。
Python isort
一个实际上不容易解释的重要部分是 Python 代码中存在的导入行。有时有数十个导入,如果它们没有得到适当的组织,就很难意识到哪些被使用,哪些没有被使用。
感谢isort实用程序,可以修复import语句,正确分组它们,并自动排序。
这里有一个示例代码,它有一个松散且丑陋的import语句:
from central_lib import Path
import os
from central_lib import Path3
from central_lib import Path2
import sys
from my_lib import lib15, lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11, lib12, lib13, lib14
import sys
运行isort后,代码看起来像这样:
import os
import sys
from my_lib import (lib1, lib2, lib3, lib4, lib5, lib6,
lib7, lib8, lib9, lib10, lib11, lib12,
lib13, lib14, lib15)
from central_lib import Path, Path2, Path3
注意,isort将导入分组并按可预测的方式排序。如果你需要添加额外的导入,你不会重复或遗漏它们。
注意
更多关于isort的信息,请参阅pypi.org/project/isort/。
Python YAPF
虽然PEP-8指南可能允许你以其他开发者会欣赏的方式编写代码,但这并不一定意味着你的代码看起来很好。YAPF的缩写实际上在工具页面上没有解释,但它可能意味着另一个 Python 格式化工具。
在本质上,YAPF 与 Black 类似,除了遵循PEP-8建议外,它还有自己的风格,但一个主要区别是它可以配置以微调样式格式。
YAPF 基于由 Daniel Jasper 开发的clang-format,它使用 YAPF 风格指南对原始代码进行重新格式化,即使原始代码没有违反任何PEP-8指南。如果在整个代码中使用,则整个项目的风格保持一致,并且审查者不需要在代码审查期间讨论或争论风格。
注意
更多关于 YAPF 的信息,请参阅github.com/google/yapf。
YAPF、isort和 Black 是三大 Python 代码格式化工具,但 Python 格式化工具的列表非常广泛。其他格式化工具可以在github.com/life4/awesome-python-code-formatters找到。
Go gofmt
Go 语言自带一个格式化工具,它包含在 Go 语言包中,称为gofmt。其目标与 Python Black 和 Python YAPF 类似,即格式化源代码到最佳格式,并保持一致性。格式化工具与go命令行工具一起使用——例如,go fmt myprogram.go。
一些平台在每次你想构建或运行 Go 程序时都会自动运行gofmt。
Go golines
golines是一个格式化工具,除了修复gofmt所做的修复外,还可以缩短长行。这个格式化工具之所以被创建,是因为gofmt不会打断长行,当行太长时很难可视化。
在运行golines之前,这是一个示例:
myVariable := map[string]string{"a key": "a value", "b key": "b value", "c key": "c value", "d key": "d value", "e key": "e value"}
运行golines后,代码看起来像这样:
myVariable := map[string]string{
"a key": "a value",
"b key": "b value",
"c key": "c value",
"d key": "d value",
"e key": "e value"
}
关于golines如何工作的更多详细信息,请参阅yolken.net/blog/cleaner-go-code-golines。
Go golangci-lint
编程语言代码检查器或 linter 是一种用于指出源代码中的错误、bug、风格错误和可疑结构的软件。它不会自动修复这些问题,而是标记错误和警告以便修复。对于 Go 语言,最好的 linter 包是golangci-lint,详细信息请参阅golangci-lint.run/。最初用于 Go 语言的原始 linter 位于github.com/golang/lint,但由于缺乏贡献而被弃用并冻结。
golangci-lint可以执行几个检查。这里列出了默认最重要的检查:
-
govet:检查Printf调用中的格式字符串是否有正确对齐的参数 -
unused:检查未使用的变量、函数和类型 -
gosimple:指出可以简化的代码部分 -
structcheck:验证未使用的结构体字段 -
deadcode:验证未使用的任何代码
可以在golangci-lint.run/usage/linters/找到可用的完整 linter 列表。
注意
可以在github.com/life4/awesome-go-code-formatters获取额外的 Go 代码格式化工具列表。
有许多其他优秀的工具可以格式化你的代码,不仅限于命令行工具,还包括集成 IDE 格式化工具。本节创建的目的是介绍 Python 和 Go 使用的某些工具,并展示如何使用它们来帮助你的代码改进。在下一节中,我们将讨论如何在开发代码时使用工具来帮助并发开发和版本控制。
版本控制和并发开发
现在,编写代码不仅关乎你创建的代码行数,还关乎其他人编写的代码,以贡献到你的软件中。此外,有变更的代码需要一些关于变更原因的信息,以及一些版本标签来识别这些变更,以便开发者可以轻松地回滚更改或在不同版本测试或部署时使用。
在编写代码时,我们如何完成这样的任务?最好的答案是使用版本控制系统。如今,最受欢迎的免费工具是 Git、SVN、Mercurial 和 CVS。Git 无疑是其中最受欢迎的,这基本上是因为 Linux 和 GitHub 网站的增长。表 5.2 展示了这四种版本控制系统的快速比较。
| 版本控制系统 | 创建年份 | 著名于 | 网站 |
|---|---|---|---|
| Git | 2005 | Linux 和 github.com | www.git-scm.com |
| SVN | 2000 | FreeBSD 和 sourceforge.com | subversion.apache.org |
| CVS | 1986 | NetBSD 和 OpenBSD | www.nongnu.org/cvs/ |
| Mercurial | 2005 | Python 和 Mozilla | www.mercurial-scm.org |
表 5.2 – 最受欢迎的版本控制系统比较
编写代码时,建议使用源代码版本控制系统,这允许多个开发者以相同的方式编辑软件,从而避免在不同步更改的情况下向系统中添加代码。版本控制系统中使用了几个不同的术语;这些术语在不同的系统如 Git 或 SVN 中几乎相同。
让我们描述一下版本控制系统中最常用的命令。
clone
clone 是一个命令,用于将整个源代码(包括子目录和版本控制系统数据)复制到您的本地目录。
以下是在 Git 中使用该命令的示例:git clone https://github.com/brnuts/matrix.git。
checkout
checkout 是一个命令,用于将工作目录中的文件更新为与源树源代码匹配。它还用于切换到另一个代码分支。
以下是在 Git 中用于更新当前分支的命令示例:git checkout。
以下是一个在 Git 中用于检出另一个分支的示例:git checkout mybranch。
commit
在更改源代码时,建议更改小块代码,每个小块更改都应该附上描述更改的文本标签。commit 命令是一个将文本标签添加到代码和检查点编号的命令。随着代码的变化,您会对每个小块更改添加提交。当整个更改完成后,您将有一个包含多个提交文本说明的更改日志,帮助读者理解更改的原因和方法。
主分支
主分支通常是代码的主要 分支 的名称。代码的其他分支通常从主分支或其他分支分支出来,分支后有一个父分支。在这个意义上,主分支是一个没有父分支的分支。
主分支也被称为源代码树的主干,有时也称为 master 或 基线。
分支
您可以使用版本控制系统在代码中创建分支。一个 分支 通常是从另一个分支分叉出来的代码。拥有分支的目的是在不干扰父分支的情况下对代码进行特定的更改。
代码分支可以拥有所需的所有更改,当准备就绪时,可以使用 merge 命令将其添加到父分支。
使用分支有几个优点——其中之一是通过在分支上添加几个小的更改 提交 来阐明大改动的意图,并对每个更改进行解释。另一个优点是允许其他开发者继续向父分支添加更改。
没有父分支的分支是主分支或主分支。
合并
代码 merge 命令检查代码是否可以添加而不会干扰主分支代码。如果有干扰,它将创建代码 merge 命令也可以在单个分支之间进行,而不需要主分支。
合并技术是用于合并分支而不影响与其他代码不同的任何代码的最安全的技术。但有时,需要解决的冲突太多,这可能不可行。当这种情况发生时,建议开始一个新的分支,并尝试添加更改,最终逐步合并,或者使用 rebase 命令,如果需要,允许将提交交互式地应用于另一个分支。
看看下面的图:

图 5.3 – 主分支提交和分支的描述
图 5.3 展示了一个示例,其中包含了主分支和分支的版本控制系统的所有详细信息。您还可以看到为每个分支和主分支所做的提交和合并。请注意,分支 1(绿色)是为了添加两个文件而创建的,称为 LICENSE 和 COPYRIGHT,然后被合并到主分支。分支 2 是从 432384DC 提交创建的,尚未合并到主分支。
在本节中,我们探讨了版本控制工具在并发开发代码时如何大有裨益。尽可能使用版本控制系统来组织您的开发。在下一节中,我们将探讨为什么以及如何将测试添加到我们的代码中。
测试您的代码
测试代码是另一个在过去几十年中增长的主题。在 70 年代初,测试代码实际上并不是开发过程的一部分,而是在软件出现问题时作为调试过程的一部分。当美国计算机科学家 Glenford Myers 在 1979 年发表了经典书籍 软件测试的艺术 时,测试变得重要,他提出了将调试与测试分离。
从那时起,开发过程中的测试得到了很大的改进。如今,重点是防止错误,而使用测试来防止错误的代码开发过程被称为测试驱动开发或TDD。
在开发代码的过程中,开发者必须识别代码的两大部分:可测试和不可测试的代码。可测试的代码是指那些可以通过低级测试(如单元测试)轻松验证的代码片段。不可测试的代码(如库的导入)则是在较低级别上不易测试的代码片段,通常需要通过高级测试(如集成测试)进行测试。
因此,在编写代码时,开发者必须注意何时以及如何对该代码部分进行测试。开发者将很容易识别哪些代码部分可以使用低级或高级测试。有多个测试级别。以下小节中,我们将讨论其中的一些。
单元测试
单元测试是测试的较低级别,因为它们旨在覆盖代码的小部分,这是可以逻辑上隔离的最小代码片段。这些代码片段可以是子程序、函数、方法、循环或属性。单元测试的目标是证明代码的各个部分是正确的,并且当它们在方法或函数中使用时,确保它们按预期工作。
如果在未来,代码的库或模块进行了升级或重构,之前创建的单元测试能够识别代码是否仍然适用于这些变化,使其对升级过程中可能引入的最终错误具有鲁棒性。为此,必须编写单元测试来覆盖所有函数、模块、方法和属性;然后,无论发生什么可能导致故障的变化,都会被单元测试迅速识别。
单元测试可以以多种方式编写:有些人会先写一个函数,然后为这个函数编写单元测试;有些人会先编写单元测试,然后编写函数。当单元测试首先编写时,这通常被称为测试驱动开发(TDD)或极限编程(XP)。
由于某些代码无法通过单元测试进行测试,每个团队和组织都有一个用于评估每个软件包的测试覆盖率参数。这个数字各不相同,但通常软件包的测试覆盖率高于 50%,所有新引入的功能和方法则高于 80%。
当一个函数或方法有明确的输入参数和一些输出时,单元测试很容易创建,但当函数与外部元素(如数据库)交互时,创建单元测试就不那么容易了。为了处理这些外部环境,一些语言有模拟能力(或模拟)来模拟外部行为,而不实际执行。
如果代码的一部分不可进行单元测试(没有可用的模拟),它可能会被更高级别的测试覆盖,就像我们接下来要讨论的那样。
集成测试
集成测试用于填补无法通过单元测试执行的测试的空白。这些通常是涉及或需要外部环境,如数据库、外部文件或服务 API 的测试。
从测试顺序的角度来看,集成测试是在单元测试成功完成后和端到端测试(E2E)(如果需要)之前的第二步。集成测试通常在模块和功能方面使用更广泛的代码覆盖率。它将它们组合成一个大的聚合体,并应用测试计划来验证预期的输出。单个函数和模块使用单元测试进行测试。在集成测试期间,它们不一定单独测试,而是在一个更高级别的组中测试。
设置一个可以支持集成测试的测试环境,例如外部测试数据库,这是很重要的。在集成测试期间,单元测试中添加的模拟不再使用,而是使用真实的外部测试环境。外部测试环境通常与生产环境分离,并专门用于集成测试。
在执行集成测试时,测试环境应该以与生产环境相同或至少非常接近的方式表现。在集成测试通过后,具有类似的生产行为,我们可以更有信心地认为软件将在生产环境中运行。
单元测试可以在某人的电脑上运行;另一方面,集成测试不能运行,因为它们依赖于一个验证软件外部通信的测试环境。有人可能会说,在你的电脑上设置一个集成测试环境,但那样的话,就无法验证一些需要一致性的测试了。
端到端测试
大多数测试可以通过单元测试和集成测试来完成;然而,可能还有一些测试仍然缺失。端到端测试(E2E tests)在另一个测试级别中被添加,这将评估整体软件,包括性能,在一个类似生产环境下的表现。
如果可能并且允许的话,理想的方式是在生产环境中运行端到端测试。如果无法在生产环境中运行,可以使用一个隔离的类似生产环境。目标是模拟软件从开始到结束的真实场景,以便通过测试可以验证软件,并且可以在生产环境中无限制地使用。
在测试时间和周期方面,端到端测试应该最后运行,因为这些测试的运行时间比集成测试长得多。测试覆盖率应该覆盖更广泛的行为,不仅触及外部接口,还使用软件所属的整个工作流程过程。
为了总结单元测试、集成测试和端到端测试,让我们用一个例子来说明。想象一下,你的软件创建了一个可以喷漆的机器人。每个测试级别的责任是什么?以下是一个可能的实现:
-
单元测试:软件小型模块的底层测试:
-
移动x,y,和z方向
-
自我识别不匹配
-
识别容器具有哪种颜色
-
在所有方向上均匀喷涂油漆
-
不要泄漏油漆
-
识别容器中的剩余油漆
-
-
集成测试:与外部模块的高级测试:
-
可以涂装满一辆汽车
-
可以在空油漆容器中补充油漆
-
可以用不同颜色涂装汽车
-
汽车中不留空白颜色区域
-
-
端到端测试:这些将是更高层次、终极的测试:
-
汽车可以在X分钟内涂装
-
涂装当前汽车时,对下一辆或上一辆汽车无干扰
-
机器人每小时可以涂装Y辆汽车,包括油漆补充和颜色更换
-
如此例所示,测试数量通常在测试级别较低时更高。因此,单元测试会比集成或端到端测试有更多的测试。然而,随着测试级别的提高,运行测试的时间会变慢。端到端测试应该比集成或单元测试花费更长的时间来完成。
其他测试
还有其他几种测试分类。对于我们网络自动化,我们将只使用我们刚刚描述的三个级别:单元测试、集成测试和端到端测试。这里我们将解释的其他测试,仅供参考,如果您认为有必要,可以添加到您的自动化工作中。
让我们简要地谈谈它们。
系统测试
从技术上来说,所有测试都可以通过单元测试、集成测试和端到端测试来完成。系统测试是添加一个额外的测试阶段,可以用来评估系统是否符合指定的要求。因此,系统测试是正式程序中验证所有要求都已满足的另一种说法。
验收测试
当处理涉及最终客户的项目时,对软件行为预期的对齐很重要。验收测试是一种协商和与最终客户沟通系统正确行为的机制。验收测试是最终客户和开发者之间的一种合同,用于界定软件的最终交付。通常,在选择测试计划和测试时,最终客户会参与其中,以确保测试覆盖系统的行为和预期。
这里常用的术语是用户验收测试(或UAT)和操作验收测试(或OAT)。
如果您的网络自动化代码高度依赖于客户的使用,并有明确的要求,我强烈建议与客户一起编写测试用例进行 UAT 和 OAT。但如果您的工作是内部使用且没有最终用户需求,集成和单元测试就足够了。
安全测试
如果您的软件部署在受限或易受攻击的区域,或者触及敏感区域的网络设备,您可能需要运行安全测试。与其他测试不同,安全测试侧重于您所创建代码的漏洞,以及所有由您的代码导入的模块依赖项。所有模块的版本也会进行检查,某些版本可能因某些内部安全策略而被禁止。
另一个额外的测试称为渗透测试或pentest,它是在软件创建的系统上执行的。渗透测试的想法是对最终系统进行模拟网络攻击。测试的目标是识别弱点以及未经授权的人对数据或系统的潜在访问。
安全测试还验证了代码中的密码管理和所使用的任何加密技术。在使用密码时有一些最佳实践,例如加密和秘密,这些可以降低未经授权的人访问系统的风险。
对于我们的网络自动化,如果我们将要自动化的设备位于易受攻击的区域,或者软件可能暴露给外部各方或互联网,那么就需要进行安全测试。但一般来说,对于内部使用,不需要进行安全测试。
当然,在安全测试方面还有更多可讨论的内容,但这可能需要一本自己的书来详细说明。
破坏性测试
破坏性软件测试或DST是在软件中执行的,以评估添加代码后最终系统的鲁棒性。测试的目的是对系统施加压力或输入无效输入,使软件开始失败。测试的成功在于在正常和压力条件下暴露可能的设计弱点和性能限制。
在破坏性测试中,如果系统清楚地显示了它所使用的恢复或避免完全崩溃的方法,那么失败被视为良好的结果。在测试期间测量性能,并将系统资源压力最大化,直到出现退化或故障。可以使用在性能压力测试期间采取的测量结果,为当前实现添加输入系统限制,从而避免任何退化。
阿尔法和贝塔测试
当在投入生产之前进行测试时,有些人会称之为alpha或beta 测试。通常,alpha 测试是在 beta 测试之前进行的,仅仅是因为字母顺序。这些测试不一定与集成或端到端测试不同,但通常它们是在类似生产的环境中进行,有时甚至是在生产环境中进行。
一些 beta 测试还涉及了解正在测试的新功能的客户,他们可以在新软件投入生产环境中的其他客户之前帮助评估新软件。因此,beta 测试将是软件被认为足够可靠以全面部署在生产环境之前的最终阶段。beta 和 alpha 测试可以有从数小时到数天的测试窗口。
摘要
恭喜!到目前为止,你已经熟悉了当今使用的编码实践。你知道如何使用工具来帮助你的源代码在质量和并发开发方面。你也能够认识到在代码中添加单元测试和集成测试的重要性。
从现在起,作为一名自动化新代码开发者,你将更加熟悉软件开发社区中使用的术语和行话。你不仅能够提高你自己的代码质量,还能提高你团队的代码质量。在整个自动化开发生涯中,使用这里讨论的工具和主题。
在下一章中,我们将提供更多关于如何使用 Go 和 Python 进行网络自动化的实际示例。我们将探讨 Go 和 Python 中的自动化示例,并进行比较。
第六章:使用 Go 和 Python 进行网络编程
在本章中,我们将探讨 Python 和 Go 在网络编程中的强大功能和用途,但根据您的需求和您的环境,其中一个可能比另一个更适合您。我们将通过检查使用每种语言的优缺点来进行网络编程。
到本章结束时,您将能够确定哪种语言(Python 或 Go)更适合您的网络项目,以及使用哪个库。您将学习每种语言的不同之处和超能力,这些可能会在您的网络自动化工作中产生差异。
本章将要涵盖的主题如下:
-
查看语言运行时
-
使用第三方库
-
使用库访问网络设备
技术要求
本章中描述的源代码存储在本书的 GitHub 仓库中,网址为github.com/PacktPublishing/Network-Programming-and-Automation-Essentials/tree/main/Chapter06。
本章中的示例是使用一个简单的网络设备模拟器创建和测试的。如何下载和运行此模拟器的说明包含在Chapter06/Device-Simulator目录中。
查看语言运行时
在编写代码并保存后,您将在您的网络中的某个位置运行它。Go 和 Python 在运行前有不同的方式来组合您的源代码和所有导入的库。哪一个更适合您?是否存在任何重要的相关差异需要了解?我们将在本节中讨论这个问题。
什么是编译型和解释型语言?
在编写代码后,一些计算机语言需要编译才能在您的机器上运行,尽管有些不需要,因为它们在运行时逐行解释。
编译型语言必须有一个编译器,将源代码转换成一系列可以在您计算机的 CPU 架构上运行的比特和字节;它还必须链接所有静态和动态系统库。例如,配备苹果 M1 处理器的计算机将使用与配备英特尔 x86 处理器的苹果不同的编译器。编译后的结果是二进制程序,人类无法阅读,当它运行时,它将从磁盘加载到主内存中。
编译完成后,您不需要源代码来运行程序。运行代码的机器不需要编译器或源代码,只需要编译后的程序二进制文件,这增加了自由空间、代码隐私和代码安全性。
另一方面,解释语言(目前)由代码解释器解释,解释器在运行时逐行解释你的代码。这些解释语言也被称为 脚本语言。运行解释语言所需的机器需要拥有解释器和源代码,不幸的是,这暴露了源代码并需要额外的空间来存储。
编译语言的例子包括 Go、C、C++、Haskell 和 Rust。解释语言的例子包括 Python、PHP、Unix Shell、JavaScript 和 Ruby。
Java 是一个特殊情况,因为它有一个编译器,但它编译成自己的 Java 虚拟机 (JVM) 架构,这不是程序将运行的 CPU 架构。一旦编译,你可以在任何地方使用它,但需要为特定的 CPU 架构安装 JVM,这会增加额外的存储和运行时复杂性。
Python 解释器
Python 解释器有时被称为 Python 虚拟机,以参考 JVM,它可以在任何地方运行。但 Python 并不提供像 Java 那样的虚拟机 – 它提供的是一个解释器,这与其非常不同。
在 Java 中,这个虚拟机就像一个虚拟 CPU,为 Java 字节码编译程序提供运行环境。JVM 将 Java 编译的字节码转换为运行所在 CPU 架构的字节码。因此,Java 代码需要首先编译;然后,编译后的程序可以在安装了 JVM 的任何机器上运行。
相反,解释器更加复杂,因为它不像 JVM 那样翻译字节码,而是在其周围的环境中逐行解释。解释器读取整个代码并解析程序上下文中必须解码的语法。正因为这种复杂性,这可以是一个非常慢的过程。
让我们调查一些可用的 Python 解释器。
使用 CPython
CPython 是最常用的解释器程序之一,需要在 Python 代码将运行的机器上安装。CPython 用 C 语言编写,可能是 Python 解释器的第一个实现。
CPython 是新功能在暴露于 Python 之前被创建的地方。例如,当并发性被添加到 Python 中时,它最初是通过 CPython 解释器进程利用操作系统的多任务属性来实现的。
CPython 实现可以在传递给解释器之前编译成专有字节码。原因是基于堆栈机器指令集创建解释器更容易,尽管解释器不需要这样做。
以下是一个 CPython 堆栈机器指令集的例子:
$ pythonPython 3.9.4 [MSC v.1928 64 bit (AMD64)] on win32
>>> import dis
>>>
>>> def return_the_bigest(a, b):
... if a > b:
... return a
... if b > a:
... return b
... return None
...
>>> dis.dis(return_the_bigest)
2 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 COMPARE_OP 4 (>)
6 POP_JUMP_IF_FALSE 12
3 8 LOAD_FAST 0 (a)
10 RETURN_VALUE
4 >> 12 LOAD_FAST 1 (b)
14 LOAD_FAST 0 (a)
16 COMPARE_OP 4 (>)
18 POP_JUMP_IF_FALSE 24
5 20 LOAD_FAST 1 (b)
22 RETURN_VALUE
6 >> 24 LOAD_GLOBAL 0 (Null)
26 RETURN_VALUE
如您所见,return_the_bigest 函数被翻译成下面的 CPython 字节码,这将由 CPython 解释器在运行时使用。请注意,指令集执行与 return_the_bigest 函数相同的功能,这对人类来说更难阅读,但对 Python 解释器来说更容易。
更多关于反汇编 Python 字节码的信息可以在这里找到:docs.python.org/3/library/dis.html。
更多关于 CPython 的信息可以在这里找到:github.com/python/cpython。
使用 Jython
Jython 是另一种 Python 解释器,最初于 1997 年由 Jim Hugunin 创建,最初称为 JPython。1999 年,JPython 被更名为 Jython,正如今天所知。
Jython 用于将 Python 代码编译成可以在安装了 JVM 的任何硬件上运行的 Java 字节码虚拟机。有时,它运行得更快,因为它不需要像 CPython 那样进行解释。
尽管该项目一开始期望很高,但如今,它只支持 Python 2.7,对 Python 3.x 的支持仍在开发中。因此,只有当您在只支持 JVM 的机器上运行代码时,您才需要 Jython。由于它只支持 Python 2.7,而 Python 社区不再支持,且已于 2020 年 1 月弃用,因此它将存在许多限制。
更多关于 Jython 的信息可以在这里找到:github.com/jython/jython。
使用 PyPy
PyPy 是另一种 Python 解释器实现,声称比 CPython 运行 Python 代码更快。PyPy 还声称使用微线程可以更好地处理并发,并且最终它声称比 CPython 使用更少的内存。
尽管 PyPy 具有巨大的优势,但 CPython 仍然是使用最广泛的 Python 解释器,主要是因为人们不知道 PyPy,并且默认的 Python 安装使用的是 CPython。
PyPy 有一个专门用于比较其与其他解释器速度的网站,例如 CPython。该网站还提供了与其他 PyPy 版本的比较。图 6.1 展示了从 PyPy 速度网站获取的信息,CPython 和 PyPy 的比较。平均而言,PyPy 比 CPython 快 4 倍:

图 6.1 – 从 speed.pypy.org 获取的 CPython 和 PyPy 比较
前面的图表包含蓝色条形,这些条形表示特定基准在 PyPy 3.9 中与 CPython 3.7.6 的运行情况。例如,bm_dulwich_log 基准比 CPython 快一倍(0.5)。第六个蓝色条形显示 PyPy 在 bm_mdp 基准(1.3)上运行较慢,但对于第二个蓝色条形,它代表的是 chaos 基准,PyPy 的运行速度可高达 20 倍。有关每个基准的详细信息,可以在 speed.pypy.org/ 获取。
更多关于 PyPy 的信息,请访问其主页 www.pypy.org/。
使用 Cython
尽管有些人将 PyPy 与 Cython 进行比较,但 Cython 并不是像 PyPy 那样的 Python 解释器。Cython 是一个可以与 Python 和 Cython 语言一起使用的编译器,它基于 Pyrex,这是 Python 语言的超集。Cython 可以轻松地将 C 和 C++ 扩展添加到你的代码中。由于它是用 C 实现的,Cython 代码声称在使用 PyPy 时比 Python 快。
因此,如果你正在寻找编写高性能 Python 代码的方法,尝试使用 Cython。更多关于 Cython 的信息可以在 cython.org/ 找到。
使用 IPython 和 Jupyter
解释型语言的主要优势是它更容易进行交互式运行。Python 有一个名为 IPython 的解释器来实现这一点。
IPython 可以逐行运行你的代码,并检查内存中的变化情况。这在测试或尝试新的代码或函数时非常有用。在代码开发过程中,它也便于获取运行结果并调整代码以适应你期望的输出。
与 IPython 结合使用,你可以使用一个 Jupyter 笔记本,这是一个易于使用的网络界面,具有图形输出功能。
例如,假设你需要从 100 个网络节点收集关于网络 CPU 使用情况的信息,并为过去一小时制作一个图形报告。你如何快速完成这项工作,而不用担心构建或测试呢?你可以使用的最佳平台是带有 IPython 的 Jupyter 笔记本。
IPython 和 Jupyter 也常用于数据科学和机器学习,因为它们在交互方法和图形界面方面的优势。
对于我们的网络编程,IPython 是一个强大的工具,用于在 Python 中创建 概念验证(PoC)代码并测试新功能。
更多关于 IPython 的信息可以在 ipython.org/ 找到。更多关于 Jupyter 的信息可以在 jupyter.org/ 找到。
通过以上内容,我们已经了解了主要的 Python 解释器。现在,让我们看看 Go 是如何工作的。
Go 编译器
在 Go 语言开发中,没有像 Python 代码那样的代码解释;相反,是编译。这种编译由 Go 编译器完成,它通常包含在 Go 语言包中。编译器读取源代码,然后将其转换为程序将要执行的 CPU 架构的字节码。在执行编译后的代码时,不需要编译器或源代码,只需要编译后的二进制代码。
由于这种编译,Go 程序比基于 Python 的程序运行得更快——在某些情况下,它们可以快 30 到 100 倍,尤其是在处理并发时。例如,用于测试多核并行性的 fannkuch-redux 基准测试在 Go 中运行需要 8 秒,而在 Python 中运行则需要 5 分钟(来源:benchmarksgame-team.pages.debian.net/benchmarksgame/performance/fannkuchredux.html)。
尽管 Go 语言发行版提供了编译器,但 Go 社区已经开始启动其他项目作为 Go 编译器的替代方案。其中之一是名为 TinyGo 的项目,当编译的代码没有太多内存需要存储时使用,例如在小型微控制器或小型计算机中。因此,当运行目标计算机的内存空间有限时,会使用 TinyGo。更多关于 TinyGo 的信息可以在 github.com/tinygo-org/tinygo 找到。
现在,让我们从计算运行时的角度比较这两种语言。
编程运行时的优缺点
让我们探讨使用 Go 和 Python 进行编程的优缺点,同时关注在运行时将在机器上运行的代码。
使用 Python 运行时的优点:
-
通过使用交互式 Python(IPython 和 Jupyter 笔记本)易于创建 PoC 代码
-
在原型设计期间易于创建数据可视化
-
拥有庞大的社区,拥有不同的解释器、库和框架
使用 Python 运行时的缺点:
-
在目标运行机器上消耗更多空间和内存
-
相比 Go,消耗更多 CPU,完成任务速度较慢
-
代码在目标运行机器上可见,可能存在不安全因素
-
与 Go 相比,运行时并行实现较弱
使用 Go 运行时的优点:
-
在目标运行机器上消耗更少的内存和空间
-
消耗更少的 CPU,运行速度比 Python 快
-
代码被编译,这使其不可读,且难以解码
-
运行时并行实现的性能远优于 Python
使用 Go 运行时的缺点:
-
创建原型更困难
-
开发社区较小,运行时库较少
对于网络编程,与 Python 相比,Go 在性能方面具有更多优势。然而,由于 Python 是一种较老的语言,它拥有更广泛的社区,拥有更多的网络库和功能。
因此,选择使用哪种语言将取决于你正在处理的使用案例。如果你想要快速完成,并且希望通过重用他人的库来编写更少的代码,那么 Python 可能是最合适的选择。但如果你想要性能更高、更安全、占用内存更少,并且可以构建为一个二进制程序的东西,Go 就是你的最佳选择。随着 Go 社区的成长,它可能会随着时间的推移拥有更多帮助网络自动化的库,但就目前而言,Python 在网络自动化领域有更多的社区贡献。
在下一节中,我们将学习如何在 Go 和 Python 代码中添加外部库。
使用第三方库
在开发网络自动化时,总是值得研究社区中的工具和库,看看你是否可以整合一些外部代码,这将增加功能或加快你的开发过程。
为了解释如何使用第三方库,了解 Python 和 Go 中库的一般使用方式非常重要。在本节中,我们将详细说明 Python 和 Go 中的库添加过程。
添加 Python 库
在讨论如何将库添加到 Python 之前,重要的是要解释,在 Python 中,库也被称为包或模块。这三个术语在 Python 文档中广泛使用,这可能会让一些新的 Python 开发者感到困惑。所以,无论何时看到“Python 库”这个术语,它也可以指包或模块。
Python 中的库可以是外部的、内部的或内置的。这些外部库也被称为第三方模块、包或库。
要在 Python 中使用库,你只需在代码开头使用 import 语句。如果库未找到,它将引发一个名为 ModuleNotFoundError 的错误异常,如下所示:
$ python
Python 3.10.4 (main, Apr 8 2022, 17:35:13) on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import netmiko
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'netmiko'
在前面的示例中,Python 解释器抛出了一个名为 ModuleNotFoundError 的异常,这意味着包未安装或不在搜索路径中。搜索路径通常包含在 sys 包内的 path 变量中,如下面的示例所示:
$ python3.10
Python 3.10.4 (main, Apr 8 2022, 17:35:13) on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path
['', '/usr/lib/python3.10', '/usr/lib/python3.10/lib-dynload', '/usr/local/lib/python3.10/dist-packages', '/usr/lib/python3/dist-packages']
注意,在前面的示例中,sys.path 变量已经被预填充了一个系统路径列表,但如果你需要,可以追加更多。
现在,让我们讨论如何在 Python 中使用内置的、标准的和外部模块。
使用 Python 内置库
内置模块(或库)是可以导入的模块,但它们包含在 Python 运行时程序中。它们不是需要添加的外部文件,因此不需要在 sys.path 变量中找到。还有内置函数,如 print,但内置模块是在使用之前需要显式导入的,例如众所周知的 sys,或其他如 array 和 time 的模块。这些内置模块不是像 Python 标准库中的那样是外部程序,而是包含在解释器的二进制代码中,就像 CPython 一样。
使用 Python 标准库
这些是与 Python 发行版一起提供的模块,但它们是当你开始在 Python 代码中声明 import 时添加的独立 Python 文件;它们需要在 sys.path 变量中找到。这些模块是 Python 程序,可以在 Python 库安装目录中找到,例如下面的示例中的 socket 库:
$ python3.10
Python 3.10.4 (main, Apr 8 2022, 17:35:13) on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import inspect
>>> import socket
>>> inspect.getfile(socket)
'/usr/lib/python3.10/socket.py'
注意,/usr/lib/pytho3.10/socket.py 的位置将取决于操作系统和 Python 版本。在上面的例子中,使用了 Linux Ubuntu 和 Python 3.10。Python 3.10.x 的所有标准库的列表可以在 github.com/python/cpython/tree/3.10/Lib 找到。有关 Python 3.x 中每个内置和标准库的更多信息,可以在 docs.python.org/3/library/ 找到。
使用第三方 Python 库
外部库或第三方库是 Python 中不包含在 Python 发行版中的模块(或包),在使用之前需要手动安装。这些模块通常不是由 Python 库团队维护,而是由 Python 社区的开发者维护,这些开发者与 Python 主发行版不一定相关。
正如我们在 第五章 中讨论的,在将外部模块添加到代码之前,查看 LICENSE 文件,并检查是否有任何限制可能会影响该模块在您的代码或组织中的使用。
Python 社区已经组织了一个名为 pip 的团队。
PyPA 还负责维护 pypi.org,其中记录了所有可以与 pip 一起使用的包。在这个网站上,有一个用于查找包的搜索引擎,还有为想要贡献或分享他们的包的开发者提供的文档。请注意,包的源代码不在 pypi.org 网站上,而是在如 GitHub 这样的仓库中。
现在,让我们通过一个例子来了解在代码中使用第三方包的过程。我们作为例子添加的包名为 netmiko:
- 根据 PyPA 检查包是否包含在 pypi.org 上。如果是,我们可以使用
pip将包添加到我们的本地 Python 环境中。
是的,就是这样的:pypi.org/project/netmiko/.
- 阅读许可证文件并检查是否允许在您的组织中使用。
本许可证基于 MIT 协议,限制较少,因此我们可以使用它:github.com/ktbyers/netmiko/blob/develop/LICENSE.
-
使用
pip工具将包安装到您的本地 Python 环境中,如下例所示:$ pip install netmiko Installing collected packages: netmiko Successfully installed netmiko-4.1.0 -
检查您是否可以导入并定位
netmiko库的安装位置:>>> import netmiko >>> import inspect >>> inspect.getfile(netmiko) '/home/claus/.local/lib/python3.10/site-packages/netmiko/__init__.py'
在前面的示例中,netmiko 包已使用 pip 工具安装,并且库位于我的家目录下,/home/claus。然而,这取决于 Python 的版本和使用的操作系统。在 Linux 中,它也取决于发行版,如 Debian、Ubuntu 或 Red Hat。
只需记住,第三方 Python 库将在名为 site-packages 的目录下正常安装。以下是每个操作系统可能的位置示例:
-
对于 macOS:
/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages -
对于 Windows:
C:\Users\username\AppData\Local\Programs\Python\Python39\lib\site-packages -
对于 Linux Ubuntu:
/usr/lib/python3.9/site-packages
要查找您在系统上安装的所有包的完整列表,请输入以下命令:
pip list -v
现在,让我们探索如何在 Go 中使用库和第三方库。
添加 Go 库
与 Python 相比,Go 只有两种类型的库:标准库和第三方库。对于 Go,没有内置库的概念,因为它没有解释器;相反,它有一个编译器。原因是解释器可以在其二进制文件中包含一些库,这些库被称为内置库。
要将库添加到您的 Go 代码中,您需要使用 import 语句,这与 Python 中的用法相同,但语法略有不同。
与 Python 类似,在 Go 中,一个库也被称为 包。
让我们看看如何在 Go 中添加标准库和第三方库。
在 Go 中使用标准库
Go 的每个版本都包含一组与 Go 发行版一起安装的标准库。这些标准库也是 Go 程序,当导入时,将在编译期间与您的代码结合。
标准库的列表可以在 pkg.go.dev/std 找到。该网站非常有用,因为它包括每个库(或包)的解释。
标准库也可以在您的本地开发文件系统中找到。Go 的安装包括所有标准库,例如 fmt 和 math 包。这些标准库的位置因操作系统而异,但可以通过查看 GOROOT 环境变量来找到。
大多数操作系统不会设置GOROOT变量,因此它们将使用 Go 语言的默认位置。要找出您的默认位置,您可以运行go env命令,如下所示:
$ go env GOROOT
/usr/lib/go-1.19
在前面的示例中,GOROOT位于/usr/lib/go-1.19。
要了解库的定位方式,让我们使用fmt标准库将字符串打印到计算机终端:
package main
import "fmt"
func main() {
fmt.Println("My code is awesome")
}
在前面的示例中,import语句告诉 Go 编译器需要添加fmt包,在这种情况下是一个标准库包。在这个示例中,Go 编译器将从这个包开始搜索,首先查找/usr/lib/go-1.19目录。
更具体地说,fmt包位于/usr/lib/go-1.19/src/fmt。在这个示例中使用的Println函数在/usr/lib/go-1.19/src/fmt/print.go文件中有描述。同样,math目录中的所有文件在其第一行上都有package math语句。
在 Go 语言中,所有程序都必须属于一个包,包名在代码的第一行中描述。在fmt目录(/usr/lib/go-1.19/src/fmt)中,该目录下所有包含的文件的第一行都包含package fmt语句。这包括位于fmt目录中的scan.go、format.go和print.go文件。
Go 语言中的其他标准库示例可以在您的本地 Go 安装中找到,通常位于src目录下。在前面的示例中,它位于/usr/lib/go-1.19/src。其他示例包括位于/usr/lib/go-1.19/src/math/的math包和位于/usr/lib/go-1.19/src/time/的time包。
这里还有一个示例,它使用了math标准库:
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println("Square root of 2 is", math.Sqrt(2))
}
在前面的示例中,math库使用了Sqrt函数,该函数在/usr/lib/go-1.19/src/math/sqrt.go文件中有描述。同样,math目录中的所有文件在其第一行上都有package math语句。
现在,让我们学习如何将第三方库添加到 Go 中。
在 Go 中使用第三方库
Go 语言中的第三方库包的添加方式与标准库类似——通过在代码中使用import语句。然而,将这些包添加到 Go 开发环境中的底层过程在几个特性上略有不同。
对于第三方包,编译器需要在不同的路径中搜索包含该包的新文件,这个路径由GOPATH环境变量设置。与GOROOT一样,您不需要设置GOPATH,因为 Go 编译器为GOPATH变量有一个默认位置。
在我们的 Go 示例中,让我们通过运行以下命令来检查GOPATH的默认位置:
$ go env GOPATH
/home/claus/go
如我们所见,GOPATH的默认位置是我家目录中的go目录(/home/claus)。
在 Go 中,可以通过在go命令行上调用get子命令来添加第三方库,如下所示:
$ go get golang.org/x/crypto
go: downloading golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
go: downloading golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1
go: added golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
在先前的例子中,我们添加了x/crypto包。请注意,该包的所有其他依赖项也被添加;在这个例子中,其他依赖项是x/sys。get子命令还存储了包的版本——在这种情况下,v0.0.0-20220622213112-05595931fe9d。
然后,该包被保存在GOPATH目录下。在先前的例子中,它被保存在/home/claus/go/pkg/mod/golang.org/x/crypto@<VERSION>。
每次你运行go get并且有新版本可用时,Go 都会存储新版本,并将旧版本保存在不同的目录中。
在添加第三方库时,不需要运行go get。如果包尚未下载或缓存,则在运行go build时有时会调用get命令,如下所示:
$ go build
go: downloading golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
在先前的例子中,Go 程序有一个import语句:
import (
"golang.org/x/crypto/ssh"
)
在添加第三方库后,Go 也会更新go.mod文件。此文件用于跟踪添加到你的 Go 程序中的包版本。以下是为先前列举的示例的go.mod文件内容:
$ cat go.mod
module connect
go 1.19
require (
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
)
require golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
注意,go.mod文件还存储了所有依赖项的包版本。在先前的例子中,x/sys包的版本也存储在该文件中。
现在你已经熟悉了将第三方库添加到 Go 和 Python 中,让我们深入下一节,看看一些可以用来访问网络的库。
使用库访问网络设备
到目前为止,我们已经讨论了如何在 Python 和 Go 中运行和使用库。现在,让我们专注于如何使用 Python 和 Go 来访问网络设备,这是我们网络自动化工作中最重要的一个点。
在第三章中,我们讨论了几种访问网络设备的方法。其中最受欢迎的一种是使用命令行界面(CLI)。我们还讨论了 SNMP、NETCONF、gRPC 和 gNMI。在本节中,我们将探讨一些使用库来访问网络设备的示例,主要使用 CLI。稍后,我们将解释并展示使用其他方法访问网络的库。
通过 CLI 访问网络的库
互联网上有许多可以访问网络设备的库,其中一些可能已经过时或不再使用。在这里,我们将按时间顺序介绍最受欢迎的库,从旧到新。
以下示例将通过 SSH 连接在 CLI 上发送uptime命令来收集网络设备的运行时间。
使用 Python Paramiko
Paramiko 可能是通过 CLI 访问网络设备的最早实现之一。它的第一个版本发布于 2003 年。如今,它拥有超过 300 位贡献者,并且有近 2000 个包依赖于 Paramiko(github.com/paramiko/paramiko/network/dependents)。
正如我们在 第三章 中讨论的那样,正确使用 CLI 库的方式是通过 Secure Shell (SSH)。Paramiko 使用名为 PyCrypto 的底层库实现 SSH 的安全加密(pycrypto.org/)。
让我们看看一个简单的示例,以获取网络主机的 uptime:
import paramiko
TARGET = {
"hostname": "10.0.4.1",
"username": "netlab",
"password": "netlab",
}
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(**TARGET)
stdin, stdout, stderr = ssh.exec_command("uptime")
stdin.close()
print(stdout.read().decode("ascii"))
如果连接成功,前面程序的输出将是网络主机的 uptime。
13:22:57 up 59 min, 0 users, load average: 0.02, 0.07, 0.08
使用 Python Netmiko
Netmiko 也是另一个流行的库,用于通过 CLI 访问网络设备。它的第一个版本发布于 2014 年,建立在 Paramiko 之上,以简化对网络设备的连接。总的来说,Netmiko 比 Paramiko 更简单,更专注于网络设备。
让我们看看当使用 Netmiko 时,与 Paramiko 相同的示例会是什么样子:
import netmiko
host = {
"host": "10.0.4.1",
"username": "netlab",
"password": "netlab",
"device_type": "linux_ssh",
}
with netmiko.ConnectHandler(**host) as netcon:
output = netcon.send_command(command)
print(output)
如您所见,Netmiko 代码的实现只有 2 行,与 Paramiko 相比,代码更小、更简单。Netmiko 库的巨大优势在于它自动处理设备的命令提示符,并在更改特权模式下的配置时轻松工作,因为提示符通常会改变。在上一个示例中,设备类型是 linux_ssh,因为我们的目标主机是 Linux 设备。
Netmiko 可以支持数十种网络设备,包括 Cisco、华为、Juniper 和 Alcatel。设备列表的完整列表可以在以下链接找到:github.com/ktbyers/netmiko/blob/develop/PLATFORMS.md。
使用 Python AsyncSSH
AsyncSSH 是 Python AsyncIO 的现代实现,用于 SSH。Python AsyncIO 在 Python 3.4 版本中引入,允许 Python 使用 async/await 语法进行并发工作,并为网络访问提供高性能。要使用 AsyncSSH,您需要 Python 3.6 或更高版本。
总结来说,AsyncSSH 为多个主机提供了更好的性能,但它是一个更底层的实现,与 Netmiko 相比,在处理网络设备时需要更多的代码复杂性。
获取 AsyncSSH 的 uptime 的相同示例可以写成这样:
import asyncio, asyncssh, sys
TARGET = {
"host": "10.0.4.1",
"username": "netlab",
"password": "netlab",
"known_hosts": None,
}
async def run_client() -> None:
async with asyncssh.connect(**TARGET) as conn:
result = await conn.run("uptime", check=True)
print(result.stdout, end="")
try:
asyncio.get_event_loop().run_until_complete( run_client() )
except (OSError, asyncssh.Error) as execrr:
sys.exit("Connection failed:" + str(execrr))
注意,当使用 AsyncSSH 时,您将不得不按照 Python AsyncIO 和前面示例的指示与例程和事件一起工作。如果您想深入了解,可以在 https://realpython.com/async-io-python/ 找到一些优秀的文档。
使用 Python Scrapli
与 Netmiko 相比,Scrapli 是较新的。它的第一个版本发布于 2019 年,也是建立在 Paramiko 之上,但具有使用 AsyncSSH 的能力,声称在访问多个设备时可以提高性能。Scrapli 这个名字的灵感来自 scrape cli,类似于某人刮擦屏幕。这是因为 Scrapli 的主要目标是使用 CLI 从网络终端解释文本。
Scrapli 被构建出来,允许其用户以减少的平台集来解释来自多厂商网络设备(如 Netmiko)的提示。2022.7.30 版本支持 Cisco、Juniper 和 Arista。使用 Scrapli 的一个优点是它还支持 NETCONF。
以下是一个使用 Scrapli 获取运行时间的相同示例:
from scrapli.driver import GenericDriver
TARGET = {
"host": "10.0.4.1",
"auth_username": "netlab",
"auth_password": "netlab",
"auth_strict_key": False,
}
with GenericDriver(**TARGET) as con:
command_return = con.send_command("uptime")
print(command_return.result)
到目前为止,我们已经了解了使用 Python 访问网络终端最流行的库。现在,让我们学习如何使用 Go 语言来做这件事。
使用 Go ssh
与 Python 相比,Go 没有处理网络设备的库的大社区。因此,在某些特定情况下,您可能必须编写自己的抓取机制来通过 CLI 解释远程网络终端。自己这样做的问题在于,它将因每个网络供应商而异,并且随着添加不同的网络设备,将需要更多的时间进行编码。
让我们看看从远程网络主机获取运行时间的示例:
package main
import (
"bytes"
"fmt"
"log"
"golang.org/x/crypto/ssh"
)
func main() {
host := "10.0.4.1"
config := &ssh.ClientConfig{
User: "netlab",
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Auth: []ssh.AuthMethod{
ssh.Password("netlab"),
},
}
conn, err := ssh.Dial("tcp", host+":22", config)
if err != nil {
log.Fatalf("Dial failed: %s", err)
}
session, err := conn.NewSession()
if err != nil {
log.Fatalf("NewSession failed: %s", err)
}
var buff bytes.Buffer
session.Stdout = &buff
if err := session.Run("uptime"); err != nil {
log.Fatalf("Run failed: %s", err)
}
fmt.Println(buff.String())
}
如您所见,前一个示例比一些高级库包含更多的代码。
使用 Go vSSH
Go 语言中有一个名为 vSSH 的库,它是由雅虎的工程师基于golang.org/x/crypto构建的。它为访问网络设备上的远程终端创建了一个抽象层,从而避免了我们在之前的 SSH 示例中看到的代码。
vSSH 的主要主张之一是它能够以高性能处理对多个目标的访问,这是通过使用 Go 协程(可以在go.dev/tour/concurrency/1找到关于 Go 协程的优秀入门指南)实现的。
尽管 vSSH 可以有效地处理多个目标,但让我们先从一个只使用一个目标的示例开始:
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/yahoo/vssh"
)
func main() {
vs := vssh.New().Start()
config := vssh.GetConfigUserPass("netlab", "netlab")
vs.AddClient(
"10.0.4.1:22", config, vssh.SetMaxSessions(1),
)
vs.Wait()
ctx, cancel := context.WithCancel(
context.Background()
)
defer cancel()
timeout, _ := time.ParseDuration("4s")
rChannel := vs.Run(ctx, "uptime", timeout)
for resp := range rChannel {
if err := resp.Err(); err != nil {
log.Println(err)
continue
}
outTxt, _, _ := resp.GetText(vs)
fmt.Println(outTxt)
}
}
在本例中使用了 Go 协程。在调用vs.Wait()之前,可以使用vs.AddClient()添加更多目标。在我们的示例中,只添加了一个目标来获取远程网络主机的运行时间。末尾的循环对于单个目标来说不是必需的,但我将其留下以演示如何与多个目标一起使用。
当使用多个目标时,我们可能有一个可能存在故障或速度较慢的主机,因此使用了解析超时,在我们的示例中是 4 秒。前面的示例通过rChannel变量使用通道来获取每个目标的 Go 协程的结果。
使用 Go Scrapligo
Scrapligo 是成功的 Python 库 Scrapli 的 Go 版本。它支持相同的网络平台,也支持 NETCONF。使用 Scrapligo 而不是 Scrapli 的优点是我们在本章前面讨论的与 Go 和 Python 运行时性能相关的优点。
在 Scrapligo 中的前一个示例看起来是这样的:
package main
import (
"fmt"
"log"
"github.com/scrapli/scrapligo/driver/generic"
"github.com/scrapli/scrapligo/driver/options"
)
func main() {
target, err := generic.NewDriver(
"10.0.4.1",
options.WithAuthNoStrictKey(),
options.WithAuthUsername("netlab"),
options.WithAuthPassword("netlab"),
)
if err != nil {
log.Fatalf("Failed to create target: %+v\n", err)
}
if err = target.Open(); err != nil {
log.Fatalf("Failed to open: %+v\n", err)
}
output, err := target.Channel.SendInput("uptime")
if err != nil {
log.Fatalf("Failed to send command: %+v\n", err)
}
fmt.Println(string(output))
}
我们刚刚讨论了如何使用 Python 和 Go 库通过 CLI 访问网络设备。现在,让我们学习如何使用其他方法通过库访问网络。
使用 SNMP 访问网络的库
除了 CLI 之外,第二种最受欢迎的网络设备访问方法是 SNMP。但正如我们在 第三章 中讨论的那样,SNMP 仅用于从网络设备读取信息。SNMP 写入方法不使用,原因如 第三章 中所述。
对于 SNMP 示例,我们将从 Python 和 Go 中各选择一个库。这些是目前使用 SNMP 方法最受欢迎的库。
在前一个子节中,我们使用了 CLI 方法来收集网络设备的运行时间。现在我们将演示,我们也可以通过使用 SNMP 方法来获取远程网络设备的运行时间。为此,我们需要收集 SNMPv2-MIB::sysUpTime MIB 变量。
使用 Python PySNMP
PySNMP 是 Python 中用于 SNMP 方法的最受欢迎的库。它支持从版本 1 到版本 3 的所有 SNMP 版本。以下是一个使用 SNMP 方法获取网络设备运行时间的示例:
from pysnmp.hlapi import *
snmpIt = getCmd(SnmpEngine(),
CommunityData("public"),
UdpTransportTarget(("10.0.4.1", 161)),
ContextData(),
ObjectType(ObjectIdentity("SNMPv2-MIB", "sysUpTime", 0)))
errEngine, errAgent, errorIndex, vars = next(snmpIt)
if errEngine:
print("Got engine error:", errEngine)
elif errAgent:
print("Got agent error:", errAgent.prettyPrint())
else:
for var in vars:
print(' = '.join([x.prettyPrint() for x in var]))
输出将是 SNMPv2-MIB::sysUpTime.0 = 72515。
更多关于 PySNMP 的信息可以在 pysnmp.readthedocs.io/en/latest/ 找到。
使用 gosnmp
对于 Go 语言,用于 SNMP 方法的最受欢迎的库是 gosnmp。它也支持 SNMP 协议的 1 到 3 版本。与 PySNMP 相比,gosnmp 更新,但拥有更多的开发者和用户,使其在未来的开发中更加可靠。以下是一个使用 Go 语言通过 SNMP 方法从网络设备收集运行时间的示例。在这个例子中,OID 号码 (1.3.6.1.2.1.1.3.0) 与 SNMPv2-MIB::sysUpTime 相同:
package main
import (
"fmt"
"log"
snmp "github.com/gosnmp/gosnmp"
)
func main() {
snmp.Default.Target = "10.0.4.1"
if err := snmp.Default.Connect(); err != nil {
log.Fatalf("Failed Connect: %v", err)
}
defer snmp.Default.Conn.Close()
//SNMPv2-MIB::sysUpTime
oid := []string{"1.3.6.1.2.1.1.3.0"}
result, err := snmp.Default.Get(oid)
if err != nil {
log.Fatalf("Failed Get: %v", err)
}
for _, variable := range result.Variables {
fmt.Printf("oid: %s ", variable.Name)
fmt.Printf(": %d\n", snmp.ToBigInt(variable.Value))
}
}
输出将是 oid: .1.3.6.1.2.1.1.3.0 : 438678。
更多关于 gosnmp 的信息可以在 pkg.go.dev/github.com/gosnmp/gosnmp 找到。
使用 NETCONF 或 RESTCONF 访问网络的库示例
当向网络设备写入配置时,首选的方法将是 NETCONF 或 RESTCONF。然而,某些设备或设备的某些功能可能尚未实现。在这种情况下,最合适的方法是通过 CLI,因为 SNMP 不会用于在设备上写入数据。
与 CLI 或 SNMP 相比,RESTCONF/NETCONF 方法是较新的网络设备访问方法。因此,可用的库不多。今天,在 Python 中使用 NETCONF 的最佳库是 Scrapli;对于 Go,这将是在 Scrapligo。
使用 Python Scrapli 与 NETCONF 的示例可以在 scrapli.github.io/scrapli_netconf/ 找到。
使用 Go Scrapligo 与 NETCONF 的示例可以在 github.com/scrapli/scrapligo/tree/main/examples/netconf_driver/basics 找到。
您还可以使用纯 HTTP 库通过 RESTCONF 收集信息,如下面的 Python 示例所示:
import requests
from requests.auth import HTTPBasicAuth
import json
requests.packages.urllib3.disable_warnings()
headers = {"Accept": "application/yang-data+json"}
rest_call = "https://10.0.4.1:6060/data/interfaces/state"
result = requests.get(rest_call, auth=HTTPBasicAuth("netlab", "netlab"), headers=headers, verify=False)
print(result.content)
现在,让我们学习如何使用 Python 和 Go 通过 gRPC 和 gNMI 访问网络。
使用 gRPC 和 gNMI 访问网络的库
与其他方法相比,gRPC 相对较新,网络设备供应商在近年来添加了这项功能。因此,如果您网络中有旧设备,您可能无法使用 gRPC 或 gNMI。
如我们在第三章中讨论的,gRPC 是一种更通用的方法,而 gNMI 更适用于网络接口。gNMI 的主要用途是通过调用底层 gRPC 流式订阅功能来进行网络遥测。使用 gNMI 允许您的网络代码轻松扩展,并且与 SNMP 相比,可以收集更多的网络管理数据。gNMI 库是建立在 gRPC 协议之上的。
所有主要网络设备供应商在其较新的网络操作系统上都有某种 gRPC 和/或 gNMI 实现。其中包括 Cisco、Juniper、Arista、Nokia、Broadcom 以及其他公司。
在 Python 中使用 gRPC
只有 Python 的新版本支持 gRPC,并且 Python 版本必须为 3.7 或更高。要使用它,您需要安装 grpcio (pypi.org/project/grpcio/)。
在这里可以找到使用 Python 的 gRPC 示例:grpc.io/docs/languages/python/quickstart/.
在 Go 中使用 gRPC
在 Go 中,gRPC 可以运行在任何主要版本上。它有很好的文档,可以在 github.com/grpc/grpc-go 找到几个示例。
在 Python 中使用 gNMI
Python 对 gNMI 的支持特别罕见。Python 社区中可用的库不多。以下列表描述了主要的一些:
-
Cisco-gnmi-python于 2018 年创建,最初由 Cisco Networks 支持。这个库是由 Cisco 创建的,旨在促进在 Cisco 设备上使用 gNMI,可能不适合多供应商支持。更多详情可以在github.com/cisco-ie/cisco-gnmi-python找到。 -
gnmi-py于 2019 年创建,由 Arista Networks 赞助。这个库不支持多供应商平台,只能用于 Arista 设备。更多详情可以在github.com/arista-northwest/gnmi-py找到。 -
pyygnmi于 2020 年创建。这个库可以使用pip导入,并在 Cisco、Arista、Juniper 和 Nokia 设备上进行了测试。这将是在多供应商平台支持下的首选选择。更多详情可以在github.com/akarneliuk/pygnmi找到。
在 Go 中使用 gNMI
对于 Go,gNMI 比 Python 的 gNMI 实现更为成熟,支持也更好。Go 中只有一个库可以使用 gNMI,称为 openconfig-gnmi。
openconfig-gnmi 库由 Google 在 2016/2017 年创建,现在在 GitHub openconfig 组下得到支持。更多关于这个库的信息可以在 pkg.go.dev/github.com/openconfig/gnmi 找到。
除了 openconfig-gnmi 库之外,还有其他与 gNMI 相关的 Go 库可能对你有用。以下是主要的一些:
-
google-gnxi是一个可以与 gNMI 和 gNOI 一起使用的工具组合。详细信息可以在github.com/google/gnxi找到。 -
openconfig-gnmi-gateway是一个库,可用于通过多个客户端进行高可用性流式传输以收集网络数据。详细信息可以在github.com/openconfig/gnmi-gateway找到。 -
openconfig-gnmic是一个用 Go 编写的 CLI 工具,你可以用它来测试 gNMI 功能。CLI 实现了所有 gNMI 客户端功能。更多详细信息可以在 https://github.com/openconfig/gnmic 找到。
到目前为止,我们已经涵盖了通过几种不同的方法访问网络设备的主要和最受欢迎的库。关于这个主题的进一步讨论可以在 Slack 等聊天社区中找到。例如,包括 devopschat.slack.com/ 和 https://alldaydevops.slack.com/。
摘要
在本章中,我们深入探讨了 Python 和 Go 的运行时行为,研究了如何将库添加到这两种语言中,并展示了在访问网络设备时可以使用的一些网络库的示例。
本章提供了足够的信息,帮助你区分 Python 和 Go 的运行方式以及它们如何与标准库和第三方库一起使用。现在,你应该能够根据性能、安全性、可维护性和可靠性要求选择合适的语言用于网络自动化。你也应该能够选择合适的方法和库来访问你的网络设备,无论是用于配置还是收集网络数据。
在下一章中,我们将探讨如何在 Go 和 Python 中处理错误,以及我们如何编写代码来正确处理网络自动化中的异常。
第七章:错误处理和日志记录
在上一章中,我们描述了 Python 和 Go 的运行方式以及它们如何访问网络;然而,在构建我们的网络自动化解决方案时,我们遗漏了两个重要的点:如何报告程序执行事件以及如何处理错误。
这两个主题并不像它们看起来那么简单,而且它们大多数时候在系统中都实现得不好。一些网络开发者可能由于知识不足而没有正确地做到这一点,但也有一些开发者由于时间限制和编码所需额外时间而没有正确地做到这一点。
但这些活动真的重要吗?让我们在本章中探讨这些问题。首先,让我们研究我们如何以及为什么处理错误,然后研究我们为什么以及如何进行事件记录。
本章我们将涵盖以下主题:
-
编写错误处理代码
-
记录事件
-
在你的代码中添加日志记录
在阅读本章之后,你将能够有效地添加代码来处理错误并在你的网络开发中记录事件。
技术要求
本章中描述的源代码存储在 GitHub 仓库github.com/PacktPublishing/Network-Programming-and-Automation-Essentials/tree/main/Chapter07中。
编写错误处理代码
要了解处理错误的重要性,我们必须将我们的系统作为一个整体来考虑,包括输入和输出。我们的代码本身可能永远不会遇到错误;然而,当与其他系统集成时,它可能会产生不可预测的输出,或者可能只是崩溃并停止工作。
因此,处理错误对于应对输入的不确定性以及保护你的代码以避免错误输出或崩溃至关重要。但我们如何做到这一点呢?
首先,我们需要确定我们的输入,然后我们创建一系列不同的值组合,这些值被发送到我们的输入。然后通过运行我们的代码来评估这些输入组合的行为。对于一个函数,我们通过添加单元测试来实现,正如我们在第五章中讨论的那样。对于系统,我们添加集成测试和端到端测试。在第五章中也讨论了其他一些技术。
但编写处理错误的代码的正确方法是什么?这取决于语言。
在 Go 中编写处理错误的代码与 Python 相比有很大不同。让我们看看我们如何在 Go 中有效地做到这一点,然后再在 Python 中做到这一点。
在 Go 中添加错误处理
Go 语言的设计要求在发生错误时显式检查错误,这与 Python 中抛出异常然后捕获它们的方式不同。在 Go 中,错误只是函数返回的值,这使得 Go 编码更加冗长,也许更加重复。在 Python 中,你不需要检查错误,因为会抛出异常,但在 Go 中,你必须检查错误。但另一方面,与 Python 相比,Go 的错误处理要简单得多。
Go 中的错误是通过使用error类型接口创建的,如下所示:
type error interface {
Error() string
}
如前述代码所示,Go 中的错误实现通过使用名为Error()的方法来返回错误消息作为字符串,相当简单。
在你的代码中构建错误的正确方式是使用errors或fmt标准库。
下面是两个使用每个函数进行除以零函数的示例。
使用errors库的示例如下:
func divide(q int, d int) (int, error) {
if d == 0 {
return 0, errors.New("division by zero not valid")
}
return q / d, nil
}
使用fmt库的示例如下:
func divide(q int, d int) (int, error) {
if d == 0 {
return 0, fmt.Errorf("divided by zero not valid")
}
return q / d, nil
}
前两个示例产生相同的结果。例如,如果你调用这两个函数中的任何一个并使用fmt.Println(divide(10, 0)),它将打印以下输出:0 divided by zero not valid。
fmt.Errorf和errors.New之间的主要区别在于格式化字符串和添加值的可能性。另一个点是errors.New更快,因为它没有调用格式化器。
如果你想要创建自定义错误、堆栈跟踪和更高级的错误功能,请考虑使用errors库或第三方库,如流行的pkg/errors (pkg.go.dev/github.com/pkg/errors) 或 golang.org/x/xerrors (pkg.go.dev/golang.org/x/xerrors)。
让我们现在关注编写 Go 中错误处理代码的最佳实践。以下是一些最佳实践。
最后返回错误,并将值设为 0
当创建一个返回多个值的函数时,错误应该放在返回值的最后一个参数。当返回带有错误的值时,如果是数字则使用 0,如果是字符串则使用empty string,如下例所示:
func findNameCount(text string) (string, int, error) {
if len(text) < 5 {
return "", 0, fmt.Errorf("text too small")
}
. . .
}
在前面的例子中,返回的字符串值为空,返回的int值为 0。这些值只是建议,因为当返回错误时,调用语句会首先检查是否存在错误,然后再分配返回的变量。因此,带有错误的返回值是不相关的。
只添加调用者没有的信息
在创建错误消息时,不要添加调用者已经知道的信息。以下示例说明了这个问题:
func divide(q int, d int) (int, error) {
if d == 0 {
return 0, fmt.Errorf("%d can't be divided by zero", q)
}
return q / d, nil
}
如前述示例所示,q的值被返回在错误消息中。但这不是必要的,因为divide函数的调用者已经有了这个值。
在您的函数中创建要返回的错误时,不要包含任何传递给函数的参数,因为这是调用者所知道的。这可能导致信息重复。
使用小写且不要以标点符号结尾
总是使用小写,因为错误信息将在返回时与其他消息连接。大多数时候,您也不应使用任何标点符号,因为错误信息可能会链接在一起,标点符号最终会在错误信息中间看起来很奇怪。
小写规则的一个例外是当您引用已经具有大写字母的函数或方法名称时。
在错误信息中添加冒号
冒号(:)用于您想要添加来自代码内部调用产生的另一个错误信息的任何信息时。以下代码将作为示例:
func connect(host string, conf ssh.ClientConfig) error {
conn, err := ssh.Dial("tcp", host+":22", conf)
if err != nil {
return fmt.Errorf("ssh.Dial: %v", err)
}
. . .
在前例中,connect 函数封装了对 ssh.Dial 的调用。我们可以通过添加调用名称或有关 ssh.Dial 的信息来将错误上下文添加到错误信息中,如果需要,使用冒号分隔。请注意,config 和 host 参数由 connect 函数的调用者知道,因此不应添加到错误信息中。
使用 defer、panic 和 recover
Go 有重要的机制来控制程序在错误发生时的流程。这主要用于使用 goroutines,因为一个错误可能会导致程序崩溃,您可能需要格外小心以避免未关闭的软件管道和软件缓存;以及避免未释放的内存和未关闭的文件描述符。
defer
Go 的 defer 用于将执行推送到一个列表,该列表仅在周围函数返回或崩溃后执行。defer 的主要目的是执行清理。考虑以下示例,该示例将数据从文件复制到另一个文件,然后删除它:
func moveFile(srcFile, dstFile string) error {
src, err := os.Open(srcFile)
if err != nil {
return fmt.Errorf("os.Open: %v", err)
}
dst, err := os.Create(dstFile)
if err != nil {
return fmt.Errorf("os.Create: %v", err)
}
_, err = io.Copy(dst, src)
if err != nil {
return fmt.Errorf("io.Copy: %v", err)
}
dst.Close()
src.Close()
err = os.Remove(srcFile)
if err != nil {
return fmt.Errorf("os.Remove: %v", err)
}
return nil
}
在前例中,如果 os.Create 发生错误,函数会在调用 src.Close() 之前返回,这意味着文件没有被正确关闭。
避免在代码中重复添加 close 语句的方法是使用 defer,如下例所示:
func moveFile(srcFile, dstFile string) error {
src, err := os.Open(srcFile)
if err != nil {
return fmt.Errorf("os.Open: %v", err)
}
defer src.Close()
dst, err := os.Create(dstFile)
if err != nil {
return fmt.Errorf("os.Create: %v", err)
}
defer dst.Close()
_, err = io.Copy(dst, src)
if err != nil {
return fmt.Errorf("io.Copy: %v", err)
}
err = os.Remove(srcFile)
if err != nil {
return fmt.Errorf("os.Remove: %v", err)
}
return nil
}
如前例所示,defer 在成功的 os.Open 和成功的 os.Create 之后使用。因此,如果发生错误或函数结束,它将首先调用 dst.Close(),然后以相反的顺序调用 src.Close(),就像一个 后进先出(LIFO)队列一样。
现在我们来看看如何使用 panic。
panic
在编写代码时,如果您不想处理错误,可以使用 panic 来立即停止。在 Go 中,可以通过显式编写来调用 panic,但在运行时如果发生错误,它也会自动调用。以下是可以发生的重大运行时错误列表:
-
越界内存访问,包括数组
-
错误类型断言
-
尝试使用
nil指针调用函数 -
向已关闭的通道或文件描述符发送数据
-
零除
因此,panic仅在你不打算处理错误或处理尚未理解的错误时才用于你的代码中。
重要的是要注意,在退出函数并传递panic消息之前,程序仍然会运行在函数中之前堆叠的所有defer语句。
这里是一个使用panic在接收到负值作为参数后退出程序示例:
import (
"fmt"
"math"
)
func squareRoot(value float64) float64 {
if value < 0 {
panic("negative values are not allowed")
}
return math.Sqrt(value)
}
func main() {
fmt.Println(squareRoot(-2))
fmt.Println("done")
}
让我们运行这个程序并检查输出:
$ go run panic-example.go
panic: negative values are not allowed
goroutine 1 [running]:
main.squareRoot(...)
Dev/Chapter07/Go/panic-example.go:10
main.main()
Dev/Chapter07/Go/panic-example.go:17 +0x45
exit status 2
注意,输出没有打印done,因为panic是在squareRoot函数中调用的,在打印指令之前。
假设我们按照以下方式将defer添加到函数中:
func squareRoot(value float64) float64 {
defer fmt.Println("ending the function")
if value < 0 {
panic("negative values are not allowed")
}
return math.Sqrt(value)
}
输出将如下所示:
$ go run panic-example.go
ending the function
panic: negative values are not allowed
goroutine 1 [running]:
main.squareRoot(…)
Dev/Chapter07/Go/panic-example.go:10
main.main()
Dev/Chapter07/Go/panic-example.go:17 +0x45
exit status 2
注意,ending the function打印语句是在发送panic消息之前放置的。这是因为,正如我们解释的,defer栈在panic返回函数之前执行。
现在让我们看看我们如何使用recover。
recover
在 Go 中,recover是处理错误所需的最后一块错误流控制。它用于处理panic情况并恢复控制。它应该只在使用defer函数调用时使用。在正常调用中,recover将返回一个nil值,但在panic情况下,它将返回panic给出的值。
例如,让我们考虑以下程序:
import "fmt"
func divide(q, d int) int {
fmt.Println("Dividing it now")
return q / d
}
func main() {
fmt.Println("the division is:", divide(4, 0))
}
如果你运行前面的程序,你会得到以下panic消息:
$ go run division-by-zero-panic.go
Dividing it now
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.divide(...)
Dev/Chapter07/Go/division-by-zero-panic.go:7
main.main()
Dev/Chapter07/Go/division-by-zero-panic.go:11 +0x85
exit status 2
如从这个输出中可以看出,你对panic情况没有控制权。它基本上会崩溃程序,没有机会正确处理错误。这在大多数生产软件中是不理想的,尤其是在使用多个 goroutine 时。
因此,为了正确处理panic情况,你应该添加一个defer函数来测试是否是panic情况,使用recover,如下例所示:
import "fmt"
func divide(q, d int) int {
fmt.Println("Dividing it now")
return q / d
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Got a panic:", r)
}
}()
fmt.Println("the division is:", divide(4, 0))
}
在添加了与前面示例相同的defer函数之后,输出将如下所示:
$ go run division-by-zero-panic-recover.go
Dividing it now
Got a panic: runtime error: integer divide by zero
如你所见,在defer函数内添加一个recover测试将允许你处理意外的panic情况,避免程序意外崩溃,没有进行适当的清理或修复错误。
现在我们已经研究了如何处理 Go 错误,让我们看看 Python 的错误处理。
在 Python 中添加错误处理
Python 处理错误的方式与 Go 不同。Python 不需要你的函数返回错误值。在 Python 中,错误在运行时抛出,它们被称为异常。为了处理异常,你的代码必须正确捕获它们并避免引发它们。
捕获异常
在 Python 中,有许多运行时错误会引发内置异常。内置异常的列表相当长,可以在以下位置找到:docs.python.org/3/library/exceptions.html。例如,除以零错误被称为ZeroDivisionError异常。
为了处理错误,您需要捕获异常,然后使用try、except、else和finally Python 语句来处理它。为了创建一个处理除以零异常的示例,让我们首先运行以下程序而不捕获异常:
def division(q, d):
return q/d
print(division(1, 0))
如果您运行前面的程序,它将生成以下输出:
$ python catching-division-by-zero-exception.py
Traceback (most recent call last):
File "Chapter07/Python/catching-division-by-zero-exception.py", line 7, in <module>
print(division(1, 0))
File "Chapter07/Python/catching-division-by-zero-exception.py", line 4, in division
return q/d
ZeroDivisionError: division by zero
如您所见,程序崩溃并在屏幕上显示错误消息作为Traceback,其中包含错误发生的位置和异常名称的详细信息,在这种情况下是ZeroDivisionError。
现在,让我们更新 Python 代码以捕获此异常并更优雅地处理错误,如下所示:
def division(q, d):
return q/d
try:
print(division(1, 0))
except ZeroDivisionError:
print("Error: We should not divide by zero")
现在,如果您运行程序,它将优雅地打印错误而不会崩溃,如下所示:
$ python catching-division-by-zero-exception.py
Error: We should not divide by zero
因此,每当您认为函数可能会通过错误引发异常时,请使用try和except语句,正如前一个示例所示。
除了try和except语句之外,Python 还允许使用else和finally语句来添加更多的错误处理流程控制。它们不是强制的,因为流程可以在try/except语句之外控制,但有时它们很有用。以下是在相同示例中添加else和finally语句的代码:
def division(q, d):
return q/d
try:
result = division(10, 1)
except ZeroDivisionError:
print("Error: We should not divide by zero")
else:
print("Division succeded, result is:", result)
finally:
print("done")
如果您运行此程序,它将生成以下输出:
$ python catch-else-finally-division-by-zero.py
Division succeded, result is: 10.0
done
注意,只有当try子句中没有引发异常时,else语句才会执行。finally语句总是执行,无论try子句中是否引发了异常。
现在我们已经看到了如何在 Python 中捕获异常,让我们讨论如何选择我们想要捕获的异常。
选择更具体的异常
在 Python 中,异常是有层次的,并且始终以名为BaseException的异常开始。例如,除以零展示了以下层次结构:
BaseException -> Exception -> ArithmeticError-> ZeroDivisionError
异常层次结构非常有用,因为您的代码可以捕获更高层次的异常或更具体的异常。对于除以零的情况,您可以捕获ArithmeticError异常而不是ZeroDivisionError。然而,有时捕获更具体的异常而不是更高层次的异常是一种好的实践。
更具体的异常更希望在函数和库内部捕获,因为如果您在函数内部捕获通用异常,那么当代码的另一个部分调用您的函数时,可能会掩盖问题。因此,这取决于您在哪里捕获以及如何处理它。
我们现在对如何在 Go 和 Python 中处理错误有了很好的了解。让我们讨论如何将日志记录添加到我们的代码中。
记录事件
在计算机软件中,日志记录是一种众所周知的技巧,用于帮助调试问题、记录里程碑、理解行为、检索信息和检查历史事件,以及其他有用的操作。尽管有这些优势,但许多开发人员并没有在他们的代码中添加适当的日志记录。事实上,一些开发人员什么都不做,只有在程序出现问题时才添加日志记录以进行调试。
在网络自动化中,日志记录甚至更为重要,因为网络元素通常是分布式的,并且严重依赖日志记录以便在出现问题时或需要改进时进行审计。将日志添加到您的代码中是一种良好的实践,将受到多个工程级别的赞赏,例如网络操作员、网络规划师、网络安全和网络设计师等。
但这里有一个重要观点必须注意,那就是网络元素之间的时间同步是强制性的,以便使日志变得有用。必须在整个网络中使用诸如网络时间协议(NTP)或精确时间协议(PTP)之类的协议。
使用日志的一个良好实践是使用名为syslog的 Unix 日志参考,它最初作为 RFC3164 的信息性 RFC 发布,后来作为 RFC5424 的标准文档。(www.rfc-editor.org/rfc/rfc3164)
对于我们的网络自动化代码,我们不需要遵循syslog协议标准的所有细节,但我们将根据严重程度级别将其用作记录有用信息的指南。
让我们谈谈在记录事件时我们希望记录的一些信息级别。
严重程度级别
RFC5424 的syslog协议定义了八个严重程度级别,这些级别在 RFC5424 的第 6.2.1 节中描述。以下列表中提到了这些级别,并简要说明了打算添加到每个级别的信息消息类型:
-
紧急: 系统无法运行,且无法恢复。 -
警报: 需要立即关注。 -
关键: 发生了不好的事情,需要快速关注以修复它。 -
错误: 正在发生故障,但不需要紧急关注。 -
警告或 Warn: 表明有问题,可能会在未来导致错误,例如软件未更新。 -
通知: 已达到一个重要里程碑,可能表明未来的警告,例如配置未保存或资源利用率限制未设置。 -
信息性或 Info: 正常操作里程碑消息。用于以后的审计和调查。 -
调试: 由开发人员用于调试问题或调查可能的改进。
虽然这八个级别在 syslog 协议中定义,但它们相当模糊,容易产生不同的解释。例如,Alert 和 Emergency 对于不同的开发者在编写代码时可能会有所不同,其他级别如 Notice 和 Informational 也是如此。因此,一些网络开发者更喜欢使用较少的级别,这些级别更容易理解。具体数量将取决于网络的运行方式,但通常在三个到五个级别之间。对于 Go 和 Python,级别的数量将取决于你用来创建日志消息的库。有些库可能比其他库有更多的级别。
现在,让我们研究如何使用 Go 和 Python 将日志事件添加到你的代码中。
在你的代码中添加日志
在你的代码中添加事件日志在 Go 和 Python 中会有所不同,并且会根据你代码中使用的库而变化。但两种语言的想法都是将信息划分为严重性级别,就像在 syslog 中做的那样。
严重性日志级别也会根据所使用的库而有所不同。Python 和 Go 都有标准的日志库,但你也可以使用第三方库来在两种语言中记录事件。
这里的一个重要点是,在编写代码时,你将决定是否需要添加一个日志事件行。添加的日志行必须携带一些信息,这将向程序发出信号,表明消息的严重性级别。因此,像失败这样重要的消息将比像调试这样不太重要的消息有更高的优先级。理想情况下,关于应该公开哪个日志级别的决定通常是通过向程序添加允许设置日志级别的输入参数来做出的。所以,如果你在调试程序运行时,它将生成比正常操作多得多的信息。
现在我们来看看如何在 Go 代码中添加日志事件,然后我们检查如何在 Python 中实现。
在 Go 中添加事件日志
Go 语言有一个标准的日志库,它随 Go 安装一起提供,但它相当有限。如果你想在 Go 中实现更高级的日志功能,你可能需要使用第三方日志库。
让我们看看我们如何使用标准库,然后检查其他流行的第三方库。
使用标准的 Go 日志
Go 的标准日志库可以通过在 import 语句中使用 log 来导入。默认情况下,Go 标准日志不提供任何严重性级别,但它有一些辅助函数可以帮助创建日志。辅助函数在此列出:
-
Print、Printf和Println:这些函数使用stderr在终端打印传递给它们的消息 -
Panic、Panicf和Panicln:这些像Print一样工作,但在打印日志消息后会调用Panic -
Fatal、Fatalf和Fatalln:这些也像Print一样工作,但在打印日志消息后会调用os.Exit(1)
以下是一个使用标准 Go 日志库的简单示例:
import (
"log"
"os/user"
)
func main() {
user, err := user.Current()
if err != nil {
log.Fatalf("Failed with error: %v", err)
}
log.Printf("Current user is %s", user.Username)
}
运行此程序将无错误地打印以下输出:
% go run standard-logging.go
2022/11/08 18:53:24 Current user is claus
如果由于任何原因无法检索当前用户,它将调用 Fatalf,在打印失败信息后,将调用 os.Exit(1)。
现在,让我们展示一个更复杂的示例,说明如何使用标准日志库创建严重性级别并将其保存到文件中:
import (
"log"
"os"
)
var criticalLog, errorLog, warnLog, infoLog, debugLog *log.Logger
func init() {
file, err := os.Create("log-file.txt")
if err != nil {
log.Fatal(err)
}
flags := log.Ldate | log.Ltime
criticalLog = log.New(file, "CRITICAL: ", flags)
errorLog = log.New(file, "ERROR: ", flags)
warnLog = log.New(file, "WARNING: ", flags)
infoLog = log.New(file, "INFO: ", flags)
debugLog = log.New(file, "DEBUG: ", flags)
}
func main() {
infoLog.Print("That is a milestone")
errorLog.Print("Got an error here")
debugLog.Print("Extra information for a debug")
warnLog.Print("You should be warned about this")
}
在前面的示例中,我们创建了五个严重性级别,可以根据需要将它们写入文件。请注意,在 Go 中,init() 函数在 main() 函数之前执行。如果您想在其他包中使用这些日志定义,请记住使用变量的大写;否则,变量将仅限于此包;例如,errorLog 应该是 ErrorLog。
此外,如果您想设置日志级别以避免 Debug 或 Info 消息,您必须向程序传递一个参数,并根据设置的级别抑制较低级别的严重性。使用 Go 标准日志库,您必须自己这样做。
现在,让我们调查一个在 Go 开发者中非常受欢迎的第三方日志库。
使用 logrus
也许在 Go 中最受欢迎的日志库之一是 logrus,它是一个具有多个日志功能的结构化日志库。logrus 有七个日志级别,并且与标准日志库兼容。默认情况下,该库允许您设置日志级别,因此如果您不想看到调试信息,它不会产生噪音。
这里是一个使用 logrus 并将日志级别设置为 Error 的简单示例,这意味着不会显示低级别的日志,例如 Warning、Info 或 Debug:
import (
log "github.com/sirupsen/logrus"
)
func init() {
log.SetFormatter(&log.TextFormatter{
DisableColors: true,
FullTimestamp: true,
})
log.SetLevel(log.ErrorLevel)
}
func main() {
log.Debug("Debug is suppressed in error level")
log.Info("This info won't show in error level")
log.Error("Got an error here")
}
运行前面的示例将在终端中仅显示以下输出:
% go run logrus-logging.go
time="2022-11-09T11:16:48-03:00" level=error msg="Got an error here"
由于严重性级别设置为 ErrorLevel,不会显示任何不太重要的日志消息——在示例中,对 log.Info 和 log.Debug 的调用。
logrus 非常灵活且功能强大,互联网上有许多使用示例。有关 logrus 的更多详细信息,请参阅 github.com/sirupsen/logrus。
如果您想在 Go 中使用更多日志库,这里有一个第三方日志库的编译列表:awesome-go.com/logging/。
现在,让我们检查如何使用 Python 将日志添加到我们的代码中。
在 Python 中添加事件日志
与 Go 相比,Python 为标准日志库添加了许多更多功能。尽管支持更好,但 Python 社区也开发了多个第三方日志库。
让我们看看 Python 的标准库和流行的第三方库。
使用标准日志库进行 Python
标准日志库包含五个严重级别和一个额外的级别,用于指示级别未由记录器设置。每个级别都与一个数字相关联,可以用来解释优先级级别,其中数字越小,优先级越低。级别包括 CRITICAL(50)、ERROR(40)、WARNING(30)、INFO(20)、DEBUG(10)和 NOTSET(0)。
NOTSET 级别在使用日志层次结构时很有用,允许非根记录器将级别委派给其父记录器。
以下是一个使用 Python 标准日志的示例:
import logging
logging.basicConfig(
filename='file-log.txt',
level=logging.ERROR,
format='%(asctime)s.%(msecs)03d %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
)
logging.debug("This won't show, level is set to info")
logging.info("Info is not that important as well")
logging.warning("Warning will not show as well")
logging.error("This is an error")
运行前面的程序将在名为 file-log.txt 的输出文件中产生以下行:
2022-11-09 14:48:52.920 ERROR: This is an error
如前述代码所示,将级别设置为 logging.ERROR 将不允许在文件中写入低级别的日志消息。程序只是忽略了 logging.debug()、logging.info() 和 logging.warning() 调用。
另一个重要点是展示在 Python 中使用标准日志的简便性。前述示例表明,您只需调用一次 logging.basicConfig 就可以设置几乎您需要的所有内容,从格式化程序到严重级别。
除了易于使用之外,Python 社区还为标准日志库创建了优秀的教程和文档。以下是文档和高级使用信息的三个主要参考:
从本质上讲,Python 标准日志库非常完整,您在大多数工作中不需要使用第三方库。然而,有一个名为 loguru 的流行第三方库提供了许多有趣和有用的功能。让我们看看如何使用它。
使用 Python loguru
Python loguru 提供了比标准 Python 日志库更多的功能,并旨在使其更容易使用和配置。例如,使用 loguru,您将能够设置日志文件的文件轮换,使用更高级的字符串格式化程序,并使用装饰器在函数上捕获异常,并且它是线程和进程安全的。
它还具有一些有趣的功能,允许您通过使用 patch 方法(更多关于 patch 方法的信息请参阅 loguru.readthedocs.io/en/stable/api/logger.html#loguru._logger.Logger.patch)来添加额外的日志信息。
以下是一个使用 loguru 的简单示例:
from loguru import logger
logger.add(
"file-log-{time}.txt",
rotation="1 MB",
colorize=False,
level="ERROR",
)
logger.debug("That's not going to show")
logger.warning("This will not show")
logger.error("Got an error")
运行前面的示例将创建一个包含日期和时间的文件,其中包含日志消息,如果文件大小达到 1 MB,则将进行轮换。文件中的输出将如下所示:
% cat file-log-2022-11-09_15-53-58_056790.txt
2022-11-09 15:53:58.063 | ERROR | __main__:<module>:13 - Got an error
更详细的文档可以在loguru.readthedocs.io找到,源代码在github.com/Delgan/loguru。
摘要
在阅读本章之后,你可能更加清楚为什么我们需要处理错误以及为什么我们需要创建适当的事件记录。你也应该更加熟悉 Go 和 Python 在处理错误方面的差异。此外,你还看到了在使用标准库和第三方库进行事件记录方面的差异。从现在开始,你的网络自动化代码设计将包含一个专门关于日志记录和错误处理的章节。
在下一章中,我们将讨论如何扩展我们的代码以及我们的网络自动化解决方案如何与大型网络交互。
第八章:规模化你的代码
现在我们已经知道了如何与网络设备交互,我们应该开始考虑构建一个可扩展的解决方案。但我们为什么需要扩展我们的代码呢?你可能认为答案很明显,那就是因为它将允许你的解决方案随着网络的增长而轻松增长。但扩展不仅仅是关于向上扩展,还包括向下扩展。因此,扩展你的代码意味着你将构建一个能够轻松跟随需求的解决方案,在不需要时节省资源,在需要时使用更多资源。
在编写代码之前,你应该考虑在你的网络自动化解决方案中添加扩展功能。它应该在设计时间规划,并在开发时间执行。扩展功能必须是构建解决方案的要求之一。它还应该在实施和测试期间成为一个明确的里程碑。
在本章中,我们将检查一些今天用来有效扩展和缩小代码的技术。这将使你的解决方案能够轻松适应网络增长,并在必要时轻松缩小规模以节省资源。
我们将在本章中涵盖以下主题:
-
处理多任务、线程和协程
-
添加调度器和作业分配器
-
使用微服务和容器
到本章结束时,你应该有足够的信息来为你的代码选择最佳的扩展解决方案。
技术要求
本章中描述的源代码存储在 GitHub 仓库github.com/PacktPublishing/Network-Programming-and-Automation-Essentials/tree/main/Chapter08。
处理多任务、线程和协程
多任务,正如其名所示,是指同时执行多个任务的能力。在计算机中,任务也被称为作业或进程,并且有不同技术在同时运行任务。
同时运行代码的能力允许你的系统在必要时进行扩展和缩小。如果你需要与更多的网络设备通信,只需并行运行更多的代码;如果你需要较少的设备,只需运行较少的代码。这将使你的系统能够进行扩展和缩小。
但是并行运行代码将对可用的机器资源产生影响,其中一些资源将受到你的代码如何消耗它们的影响而受限。例如,如果你的代码正在使用网络接口下载文件,并且运行一行代码就已经消耗了网络接口的 50 Mbps(即 100 Mbps 接口的 50%),那么不建议并行运行多行代码以增加速度,因为限制在于网络接口,而不是 CPU。
在并行运行代码时,还需要考虑其他因素,即除了网络之外的共享资源,例如 CPU、磁盘和内存。在某些情况下,磁盘的瓶颈可能会对代码并行性造成比 CPU 更多的限制,尤其是在使用通过网络挂载的磁盘时。在其他情况下,一个消耗大量内存的大程序可能会因为缺乏可用内存而阻塞任何其他并行运行的程序。因此,你的进程将接触到的资源以及它们之间的交互将影响并行化的程度。
在这里我们需要明确一点,即术语 I/O,它是计算机 输入/输出 的缩写。I/O 用于指定机器的 CPU 与外部世界之间的任何通信,例如访问磁盘、写入内存或将数据发送到网络。如果你的代码需要大量的外部访问,并且大多数时间都在等待外部通信的响应,我们通常说代码是 I/O 密集型。当访问远程网络和在某些情况下远程磁盘时,可以找到慢速 I/O 的例子。另一方面,如果你的代码需要的 CPU 计算比 I/O 更多,我们通常说代码是 CPU 密集型。大多数网络自动化系统将是 I/O 密集型的,因为网络设备访问。
现在我们来探讨一些在 Go 和 Python 中同时运行代码的技术。
多进程
在计算机中,当一个程序被加载到内存中运行时,它被称为进程。程序可以是脚本或二进制文件,但通常由一个单独的文件表示。这个文件将被加载到内存中,并且操作系统将其视为一个进程。同时运行多个进程的能力称为多进程,这通常由操作系统管理。
运行进程的硬件的 CPU 数量与多进程能力无关。操作系统负责为所有已加载到内存中并准备运行的进程分配 CPU 时间。然而,由于 CPU 数量、CPU 速度和内存有限,可以同时运行的进程数量也将有限。通常,这取决于进程的大小以及它消耗的 CPU 资源。
在大多数计算机语言中,多进程是通过操作系统实现的 fork() 系统调用来创建当前运行进程的完整副本来实现的。
让我们研究一下如何在 Go 和 Python 中使用多进程。
Python 中的多进程
在 Python 中,多进程是通过标准库 multiprocessing 实现的。Python multiprocessing 的完整文档可以在 docs.python.org/3/library/multiprocessing 找到。
在第一个示例中,我们将使用操作系统的程序ping来针对一个网络节点。然后,我们将使其对多个目标并行化。
以下是一个针对单个目标网络节点的示例:
import subprocess
TARGET = "yahoo.com"
command = ["ping", "-c", "1", TARGET]
response = subprocess.call(
command,
stdout=subprocess.DEVNULL,
)
if response == 0:
print(TARGET, "OK")
else:
print(TARGET, "FAILED")
需要注意的是,从 Python 调用ping效率不高。它将导致更多的开销,因为 Python 将不得不调用一个位于文件系统中的外部程序。为了使示例更高效,我们需要使用 ICMP 的echo request并从 Python 网络套接字接收 ICMP 的echo reply,而不是调用外部程序,如ping。一个解决方案是使用名为pythonping的 Python 第三方库(pypi.org/project/pythonping/)。但有一个注意事项:ping程序被设置为setuid,允许非特权用户发送 ICMP 数据包。因此,为了使用pythonping运行,你需要管理员/根权限(在 Linux 中使用sudo完成)。
以下是一个使用pythonping针对单个目标网络节点的相同示例:
import pythonping
TARGET = "yahoo.com"
response = pythonping.ping(TARGET, count=1)
if response.success:
print(TARGET, "OK")
else:
print(TARGET, "FAILED")
运行此程序应该会生成以下输出:
% sudo python3 single-pyping-example.py
yahoo.com OK
如果你想要向多个目标发送 ICMP 请求,你必须一个接一个地顺序发送。然而,一个更好的解决方案是使用multiprocessing Python 库并行运行它们。以下是一个使用multiprocessing的四个目标的示例:
from pythonping import ping
from multiprocessing import Process
TARGETS = ["yahoo.com", "google.com", "cisco.com", "cern.ch"]
def myping(host):
response = ping(host, count=1)
if response.success:
print("%s OK, latency is %.2fms" % (host, response.rtt_avg_ms))
else:
print(host, "FAILED")
def main():
for host in TARGETS:
Process(target=myping, args=(host,)).start()
if __name__ == "__main__":
main()
如果你运行先前的程序,你应该得到类似于以下输出的结果:
% sudo python3 multiple-pyping-example.py
google.com OK, latency is 45.31ms
yahoo.com OK, latency is 192.17ms
cisco.com OK, latency is 195.44ms
cern.ch OK, latency is 272.97ms
注意,每个目标的响应不依赖于其他目标的响应。因此,输出应该始终按从低延迟到高延迟的顺序排列。在先前的示例中,google.com首先完成,显示延迟仅为 45.31 毫秒。
重要提示
在main()函数或从main()函数调用的函数中调用multiprocessing是很重要的。同时,确保main()可以被 Python 解释器安全导入(使用__name__)。你可以在docs.python.org/3/library/multiprocessing.html#multiprocessing-programming找到更多关于为什么需要这样做的细节。
除了先前的使用Process()的示例之外,Python 还有其他方法可以调用代码并行性,称为multiprocessing.Pool()和multiprocessing.Queue()。Pool()类用于实例化一个工作池,它可以执行工作而无需相互通信。当需要进程间通信时使用Queue()类。更多关于这方面的信息可以在docs.python.org/3/library/multiprocessing.html找到。
让我们看看如何在 Go 语言中使用多进程。
Go 语言中的多进程
要从程序中创建进程,你需要将当前运行程序的数据复制到一个新进程中。这就是 Python 的multiprocessing所做的事情。然而,Go 实现并行化的方式非常不同。Go 被设计成与协程类似的工作方式,它们被称为 goroutines,它们在运行时管理并行性。由于 goroutines 效率更高,因此不需要在 Go 中原生实现多进程。
注意,使用exec库,通过调用exec.Command()然后Cmd.Start()和Cmd.Wait(),可以同时创建多个进程,但这是对操作系统的调用以执行外部程序。因此,它不被视为原生多进程,并且效率不高。
由于这些原因,我们在 Go 中没有多进程的示例。
让我们看看我们是如何进行多线程的。
多线程
在计算机语言中,线程是进程的一个较小部分,它可以有一个或多个线程。在同一个进程中的线程之间共享内存,与不与其他进程共享内存的进程相对。因此,线程被称为轻量级进程,因为它需要的内存更少,并且进程内线程之间的通信更快。因此,与创建新进程相比,创建新线程要快得多。
具有多线程能力的 CPU 是能够通过提供指令级并行性或线程级并行性,在单个核心中运行多个线程的 CPU。这种能力也被称为同时 多线程 (SMT)。
SMT 的一个例子是 Intel CPU i9-10900K,它有 10 个核心,每个核心可以同时运行 2 个线程,这允许最多 20 个同时线程。Intel 为 SMT 创建了一个商标名称,他们称之为超线程。通常,AMD 和 Intel x86 CPU 架构每个核心可以运行多达两个线程。
相比之下,Oracle SPARC M8 处理器有 32 个核心,每个核心可以运行 8 个线程,允许惊人的 256 个同时线程。更多关于这个惊人的 CPU 的信息可以在www.oracle.com/us/products/servers-storage/sparc-m8-processor-ds-3864282.pdf找到。
但要使 CPU 通过线程发挥最佳性能,还需要两个其他要求,一个是允许 CPU 级多线程的操作系统,以及一个允许创建同时线程的计算机语言。
让我们看看我们如何在 Python 中使用多线程。
Python 中的多线程
多线程是 Python 的阿喀琉斯之踵。主要原因在于 Python 解释器 CPython(在第六章中讨论)使用全局解释器锁(GIL)来确保线程安全。这导致的结果是在多线程 CPU 上不允许 Python 代码同时运行多个线程。GIL 还增加了开销,并且当需要更多的 CPU 工作时,使用多线程可能比使用多进程慢。
因此,在 Python 中,对于 CPU 密集型的程序,不建议使用多线程。对于网络和其他 I/O 密集型的程序,多线程可能更快地启动,更容易通信,并且可以节省运行时内存。但需要注意的是,使用 CPython 解释器时,一次只能运行一个线程,所以如果你需要真正的并行性,请使用multiprocessing库。
在 Python 中,标准库通过使用名为threading的库提供了多线程功能。因此,让我们通过使用与上一节代码示例中相同的 ICMP 测试目标,在 Python 中创建一个使用多线程的示例。以下是一个使用 ICMP 但使用线程的相同示例:
from pythonping import ping
import threading
TARGETS = ["yahoo.com", "google.com", "cisco.com", "cern.ch"]
class myPing(threading.Thread):
def __init__(self, host):
threading.Thread.__init__(self)
self.host = host
def run(self):
response = ping(self.host)
if response.success:
print("%s OK, latency is %.2fms" % (self.host, response.rtt_avg_ms))
else:
print(self.host, "FAILED")
def main():
for host in TARGETS:
myPing(host).start()
if __name__ == "__main__":
main()
运行前一个程序的输出将如下所示:
% sudo python3 threads-pyping-example.py
google.com OK, latency is 36.21ms
yahoo.com OK, latency is 136.16ms
cisco.com OK, latency is 144.67ms
cern.ch OK, latency is 215.81ms
如你所见,使用threading和multiprocessing库的输出相当相似,但哪一个运行得更快?
现在我们运行一个测试程序来比较使用threading和multiprocessing进行 ICMP 测试的速度。这个程序的源代码包含在本章的 GitHub 仓库中。程序的名字是performance-thread-process-example.py。
这是这个程序运行 10、20、50 和 100 个 ICMP 探测的结果:
% sudo python3 performance-thread-process-example.py 10
Multi-threading test --- duration 0.015 seconds
Multi-processing test--- duration 0.193 seconds
% sudo python3 performance-thread-process-example.py 20
Multi-threading test --- duration 0.030 seconds
Multi-processing test--- duration 0.315 seconds
% sudo python3 performance-thread-process-example.py 50
Multi-threading test --- duration 2.095 seconds
Multi-processing test--- duration 0.765 seconds
% sudo python3 performance-thread-process-example.py 100
Multi-threading test --- duration 2.273 seconds
Multi-processing test--- duration 1.507 seconds
如前所述的输出所示,在 Python 中运行多线程对于一定数量的线程来说可能更快。然而,当我们接近数字 50 时,它变得不那么有效,运行速度也大大减慢。重要的是要注意,这取决于你运行代码的位置。在 Windows 上运行的 Python 解释器与 Linux 或 macOS 上的不同,但总体思路是相同的:更多的线程意味着 GIL(全局解释器锁)有更多的开销。
建议除非你正在启动少量线程并且不是 CPU 密集型的,否则不要使用 Python 多线程。
重要提示
由于 CPython GIL 的存在,Python 中无法运行并行线程。因此,如果你的程序是 CPU 密集型的并且需要 CPU 并行性,那么应该使用multiprocessing库而不是threading库。更多详情可以在docs.python.org/3/library/threading中找到。
但如果你仍然想使用 Python 进行多线程,还有一些其他 Python 解释器可能提供一些功能。一个例子是threading模块。使用 PyPy-STM,可以同时运行线程,但你将不得不使用transaction模块,特别是TransactionQueue类。有关使用 PyPy-STM 进行多线程的更多信息,请参阅doc.pypy.org/en/latest/stm.html#user-guide。
现在,让我们看看如何在 Go 中实现多线程。
Go 中的多线程
在 Go 中编写可扩展的代码不需要创建线程或进程。Go 通过 goroutines 实现了并行性,goroutines 被 Go 运行时作为线程呈现给操作系统。Goroutines 将在下一节中更详细地解释,该节将讨论协程。
我们还将看到如何使用协程同时运行多行代码。
协程
术语协程最早在 1958 年由 Melvin Conway 和 Joel Erdwinn 提出。然后,这个想法在 1963 年发表在ACM杂志上的一篇论文中被正式介绍。
尽管这个术语非常古老,但它的采用是在一些现代计算机语言中后来的事情。协程本质上是可以挂起的代码。这个概念类似于线程(在多线程中),因为它是一小部分代码,有自己的局部变量和栈。但多任务系统中线程和协程的主要区别在于线程可以并行运行,而协程是协作的。有些人喜欢将这种区别描述为任务并发和任务并行之间的区别。
这里是来自Oracle Multithreaded Programming Guide的定义:
在单处理器上的多线程进程中,处理器可以在线程之间切换执行资源,从而实现并发执行。并发表示多个线程正在取得进展,但实际上线程并不是同时运行的。线程之间的切换发生得足够快,以至于线程可能看起来是同时运行的。在共享内存的多处理器环境中的相同多线程进程中,进程中的每个线程都可以在不同的处理器上并发运行,从而实现并行执行,这是真正的同时执行。
源代码可以在docs.oracle.com/cd/E36784_01/html/E36868/mtintro-6.html找到。
那么,现在让我们来看看如何在 Python 和 Go 中使用协程。
在 Python 中添加协程
Python 最近将协程添加到标准库中。它们是名为asyncio的模块的一部分。因此,你不会在 Python 的旧版本中找到这种功能;你需要至少 Python 版本 3.7。
但我们何时在 Python 中使用协程呢?最适合的情况是需要大量 I/O 绑定的并行任务,例如网络。对于 CPU 密集型应用程序,始终建议使用multiprocessing。
与threading相比,asyncio对我们的网络自动化工作更有用,因为它与 I/O 绑定,并且比使用threading扩展得更好。此外,它甚至比线程和进程更轻量。
然后,让我们使用 Python 中的协程创建相同的 ICMP 探测测试。以下是在之前示例中使用的相同网络目标的代码示例(你可以在本书的 GitHub 仓库中的Chapter08/Python/asyncio-example.py找到此代码):
from pythonping import ping
import asyncio
TARGETS = ["yahoo.com", "google.com", "cisco.com", "cern.ch"]
async def myping(host):
response = ping(host)
if response.success:
print("%s OK, latency is %.3fms" % (host, response.rtt_avg_ms))
else:
print(host, "FAILED")
async def main():
coroutines = []
for target in TARGETS:
coroutines.append(
asyncio.ensure_future(myping(target)))
for coroutine in coroutines:
await coroutine
if __name__ == "__main__":
asyncio.run(main())
运行前面的程序示例将生成以下输出:
% sudo python3 asyncio-example.py
yahoo.com OK, latency is 192.75ms
google.com OK, latency is 29.93ms
cisco.com OK, latency is 162.89ms
cern.ch OK, latency is 339.76ms
注意,现在打印出的第一个 ping 回复实际上并不是延迟最低的那个,这表明程序是顺序运行的,遵循循环中TARGETS变量的顺序。这意味着当协程被阻塞时,并没有挂起以允许其他协程运行。因此,如果我们想要扩展,这不是一个很好的使用协程的例子。这是因为示例中使用的库是pythonping,它不是asyncio兼容的,并且在等待网络 ICMP 响应时不会挂起协程。
我们添加这个例子是为了展示使用与asyncio不兼容的代码的协程是多么糟糕。为了解决这个问题,现在让我们使用一个与asyncio兼容的第三方库来进行 ICMP 探测,这个库叫做aioping。
以下代码仅显示了导入更改,将pythonping替换为aioping,以及myping()函数的更改,我们在ping()函数前添加了一个await语句。另一个区别是aioping使用异常TimeoutError来检测 ICMP 请求的非响应:
from aioping import ping
async def myping(host):
try:
delay = await ping(host)
print("%s OK, latency is %.3f ms" % (host, delay * 1000))
except TimeoutError:
print(host, "FAILED")
之前显示的修复后的完整程序可以在本书的 GitHub 仓库中找到,位于Chapter08/Python/asyncio-example-fixed.py。
如果你现在运行这个代码并修复了它,它应该会显示类似以下输出:
% sudo python3 asyncio-example-fixed.py
google.com OK, latency is 40.175 ms
cisco.com OK, latency is 170.222 ms
yahoo.com OK, latency is 181.696 ms
cern.ch OK, latency is 281.662 ms
注意,现在,输出是基于目标响应 ICMP 请求的速度,输出不遵循之前示例中的TARGETS列表顺序。
之前代码中的重要区别是在ping之前使用await,这向 Python asyncio模块指示协程可能会停止并允许另一个协程运行。
现在,你可能想知道,你是否可以在不使用新库aioping的情况下,只需在pythonping库中的ping语句前添加await。但这是不会工作的,并会生成以下异常:
TypeError: object ResponseList can't be used in 'await' expression
这是因为pythonping库与asyncio模块不兼容。
当你需要运行大量任务时,请使用asyncio,因为将协程用作任务非常便宜,比进程和线程更快更轻量。然而,要利用协程的并发性,你的应用程序必须是 I/O 密集型的。访问网络设备就是一个慢速 I/O 密集型应用程序的例子,可能非常适合我们的网络自动化案例。
重要提示
为了在 Python 中高效使用协程,你必须确保当 I/O(如网络)等待时,协程会挂起执行,以便其他协程可以运行。这通常通过名为await的asyncio语句来指示。确实,在你的协程中使用第三方库需要与asyncio兼容。由于asyncio模块相当新,与asyncio兼容的第三方库并不多。如果没有这种兼容性,你的代码将顺序而不是并发地运行协程,使用asyncio将不是一个好主意。
让我们看看协程在 Go 语言中的应用。
Go 中的协程
Go 语言很特别,在需要代码与性能一起扩展时表现得最好,而在 Go 中,这是通过 goroutine 实现的。
Goroutine 与协程不同,因为它们可以像线程一样并行运行。但它们也不像线程,因为它们更小(从 Go 版本 1.4 开始,仅占用 8 KB)并且使用通道进行通信。这可能会让人一开始感到困惑,但我向你保证,goroutine 并不难理解和使用。实际上,与 Python 中的协程相比,它们更容易理解和使用。
自 Go 版本 1.14 以来,goroutine 是通过异步可抢占式调度实现的。这意味着任务不再受开发者的控制,而是完全由 Go 的运行时管理(你可以在go.dev/doc/go1.14#runtime找到详细信息)。Go 的运行时负责向操作系统展示将要运行的线程,在某些情况下,这些线程可以同时运行。
Go 的运行时负责创建和销毁与 goroutine 对应的线程。当使用原生多线程语言由操作系统实现时,这些操作会更重,但在 Go 中,它们很轻,因为 Go 的运行时维护了一个线程池供 goroutine 使用。Go 的运行时控制 goroutine 和线程之间的映射,使得操作系统完全不知道 goroutine 的存在。
总结来说,Go 不使用协程,而是使用 goroutine,它们与协程不同,更像是协程和线程的结合,性能优于两者。
现在我们通过一个使用 goroutine 的简单 ICMP 探测示例来了解一下:
import (
"fmt"
"time"
"github.com/go-ping/ping"
)
func myPing(host string) {
p, err := ping.NewPinger(host)
if err != nil {
panic(err)
}
p.Count = 1
p.SetPrivileged(true)
if err = p.Run(); err != nil {
panic(err)
}
stats := p.Statistics()
fmt.Println(host, "OK, latency is", stats.AvgRtt)
}
func main() {
targets := []string{"yahoo.com", "google.com", "cisco.com", "cern.ch"}
for _, target := range targets {
go myPing(target)
}
time.Sleep(time.Second * 3) //Wait 3 seconds
}
如果你运行这个程序,它应该输出类似以下内容:
$ go run goroutine-icmp-probe.go
google.com OK, latency is 15.9587ms
cisco.com OK, latency is 163.6334ms
yahoo.com OK, latency is 136.3522ms
cern.ch OK, latency is 225.0571ms
要使用 goroutine,你只需要在你想作为 goroutine 调用的函数前加上go。go语句表示该函数可以在后台执行,拥有自己的栈和变量。然后程序执行go语句之后的行,并正常继续流程,不需要等待 goroutine 返回任何内容。由于 ICMP 探测请求需要几毫秒来接收 ICMP 响应,程序会在 goroutine 打印任何内容之前退出。因此,我们需要在程序结束前添加 3 秒的睡眠时间,以确保所有发送 ICMP 请求的 goroutine 都已经接收并打印了结果。否则,你将看不到任何输出,因为程序会在 goroutine 完成打印结果之前结束。
如果你想要等待 goroutines 结束,Go 有机制来进行通信并等待它们结束。其中一个简单的方法是使用sync.WaitGroup。现在让我们重写之前的例子,移除睡眠时间并添加WaitGroup来等待 goroutines 完成。以下是一个等待所有 goroutines 结束的相同例子:
import (
"fmt"
"sync"
"github.com/go-ping/ping"
)
func myping(host string, wg *sync.WaitGroup) {
defer wg.Done()
p, err := ping.NewPinger(host)
if err != nil {
panic(err)
}
p.Count = 1
p.SetPrivileged(true)
if err = p.Run(); err != nil {
panic(err)
}
stats := p.Statistics()
fmt.Println(host, "OK, latency is", stats.AvgRtt)
}
func main() {
var targets = []string{"yahoo.com", "google.com", "cisco.com", "cern.ch"}
var wg sync.WaitGroup
wg.Add(len(targets))
for _, target := range targets {
go myping(target, &wg)
}
wg.Wait()
}
如果你运行前面的代码,它应该比之前的代码结束得更快,因为它不需要等待 3 秒;它只等待所有 goroutines 结束,这应该不到半秒。
要允许sync.WaitGroup工作,你必须在开始时使用Add()给它设置一个值。在前面的例子中,它添加了4,这是将要运行的 goroutine 数量。然后,你将变量的指针传递给每个 goroutine 函数(&wg),该函数将使用defer(在第七章中解释)标记为Done()。
在前面的例子中,我们没有在 goroutines 之间生成任何通信,因为它们使用终端进行打印。我们只传递了工作组变量的指针,称为wg。如果你想 goroutines 之间进行通信,你可以通过使用channel来实现,它可以是单向的或双向的。
更多关于 goroutines 的信息可以在以下链接中找到:
-
Google I/O 2012 - Go 并发 模式:
www.youtube.com/watch?v=f6kdp27TYZs -
Google I/O 2013 – 高级 Go 并发 模式:
www.youtube.com/watch?v=QDDwwePbDtw -
更多关于 Goroutines 的文档可以在go.dev/doc/effective_go#concurrency找到
在进入下一节之前,让我们总结一下如何在 Python 和 Go 中进行扩展。在 Python 中,为了做出正确的选择,使用图 8**.1:

图 8.1 – Python 代码扩展的决策
图 8.1中的图示显示了在扩展你的代码时应该使用哪个 Python 库。如果你是 CPU 密集型的,使用multiprocessing。如果你有太多慢速 I/O 的连接,使用asyncio,如果连接数量较少,使用threading。
对于 Go 语言来说,只有一个选项,那就是 goroutine。简单明了的答案!
现在我们来检查如何使用调度器和派发器来扩展系统。
添加调度器和任务派发器
调度器是一个选择要运行的任务的系统,其中任务可以理解为需要运行的一个程序或代码的一部分。另一方面,派发器是接收任务并将其放入机器执行队列的系统。它们是互补的,在某些情况下,它们被视为同一个系统。因此,为了本节的目的,我们将讨论一些可以同时进行调度和派发任务的系统。
使用能够调度和派发任务的系统的主要目标是通过对能够并行运行更多任务的机器进行添加来获得规模。这有点类似于单个程序使用多进程,但不同之处在于新进程是在另一台机器上执行的。
你可以做一些工作来提高你程序的性能,但最终,你将受到机器限制的束缚,如果你的应用程序是 CPU 密集型的,它将受到核心数量、并发线程数量和所用 CPU 速度的限制。你可以努力提高代码的性能,但为了进一步增长,唯一的解决方案是添加更多的 CPU 硬件,这可以通过添加机器来实现。专门用于为调度器和派发器运行任务的机器组通常被称为集群。
一组准备并行运行任务的机器可以本地安装,也可以在单独的位置安装。集群中机器之间的距离会增加机器间通信的延迟,并延迟数据同步。快速同步机器可能或可能不相关,这取决于所需结果的快慢以及它们在时间上的依赖性。如果需要更快的结果,可能需要一个本地集群。如果对结果的时间框架较为宽松,集群可以位于更远的位置。
现在我们来讨论如何使用经典的调度器和派发器。
使用经典的调度器和派发器
经典的调度器和派发器可以是任何一种系统,它接受一个任务,将其部署到集群中的某台机器上,并执行它。在经典的情况下,任务只是一个准备在机器上运行的程序。程序可以用任何语言编写;然而,Python 和 Go 在安装方式上的差异是存在的。
让我们调查一下使用准备运行 Python 脚本的集群和准备运行 Go 编译代码的集群之间的区别。
使用 Go 和 Python 的注意事项
如果程序是用 Python 编写的,集群中的所有机器都必须安装与 Python 代码兼容的 Python 解释器版本。例如,如果代码是为 Python 3.10 编写的,则要安装的 CPython 解释器版本必须至少为 3.10。另一个重要的点是,Python 脚本中使用的所有第三方库也必须安装到所有机器上。每个第三方库的版本必须与 Python 脚本兼容,因为特定第三方库的新版本可能会破坏你代码的执行。你可能需要在某处维护一个包含每个第三方库版本的表格,以避免错误的机器更新。总之,使用 Python 使得你的集群安装、管理和更新变得非常复杂。
另一方面,使用 Go 语言在集群中部署要简单得多。你只需将 Go 程序编译成代码将要运行的相同 CPU 架构。所有使用的第三方库都将添加到编译后的代码中。你程序中使用的每个第三方库的版本将由你的本地开发环境通过 go.sum 和 go.mod 文件自动控制。总之,你不需要在机器上安装解释器,也不需要担心安装或更新任何第三方库,这要简单得多。
现在我们来看一些机器集群的调度器和派发器的例子。
使用 Nomad
Nomad 是一个使用 Go 语言编写的作业调度程序实现,由名为 HashiCorp 的公司支持(www.nomadproject.io/)。Nomad 在机器集群中进行调度和启动 Docker 容器方面也非常受欢迎,我们将在下一节中看到这一点。
使用 Nomad,你可以通过编写一个描述作业及其运行方式的配置文件来定义一个作业。作业描述可以编写为任何格式化的文件,例如 YAML 或 TOML,但默认支持的格式是 HCL。一旦完成作业描述,它就会被转换为 JSON 格式,这将用于 Nomad API(更多详情请参阅 developer.hashicorp.com/nomad/docs/job-specification)。
Nomad 支持多种任务驱动程序,这允许你调度不同类型的程序。如果你使用的是 Go 编译程序,你必须使用 Fork/Exec 驱动程序(更多详情请参阅 developer.hashicorp.com/nomad/docs/drivers/exec)。使用 Fork/Exec 驱动程序,你可以执行任何程序,包括 Python 脚本,但前提是在集群的所有机器上预先安装了所有第三方库和 Python 解释器,这不由 Nomad 管理,必须由你自己单独完成。
以下是一个 ICMP 探测程序的作业规范示例:
job "probe-icmp" {
region = "us"
datacenters = ["us-1", "us-12"]
type = "service"
update {
stagger = "60s"
max_parallel = 4
}
task "probe" {
driver = "exec"
config {
command = "/usr/local/bin/icmp-probe"
}
env {
TARGETS = "cisco.com,yahoo.com,google.com"
}
resources {
cpu = 700 # approximated in MHz
memory = 16 # in MBytes
}
}
注意,前面的程序示例名为 icmp-probe,必须接受操作系统环境变量作为输入。在我们的示例中,该变量名为 TARGETS。
一旦您定义了作业,您可以通过发出 nomad job dispatch <job-description-file> 命令来调度它(更多详情请见 developer.hashicorp.com/nomad/docs/commands/job/dispatch)。
现在我们来检查我们如何使用另一个流行的调度器。
使用 Cronsun
Cronsun 是另一个调度器和分配器,它的工作方式与流行的 Unix cron 类似,但适用于多台机器。Cronsun 的目标是使管理大量机器上的作业变得简单和直观。它是用 Go 语言开发的,但也可以通过在远程机器上调用 shell 来启动任何语言的作业,例如在 Nomad 的 Fork/Exec 驱动程序中(更多详情请见 github.com/shunfei/cronsun)。它还提供了一个图形界面,可以轻松可视化正在运行的作业。Cronsun 是基于另一个名为 robfig/cron 的 Go 第三方包构建和设计的(更多详情请见 github.com/robfig/cron)。
使用 Cronsun,您将能够在多台机器上启动多个作业,但没有像 Nomad 那样的机器集群管理。另一个重要点是 Cronsun 不与 Linux 容器一起工作,因此它纯粹专注于通过进程分叉在远程机器上执行 Unix shell 程序。
现在我们来看一个更复杂的调度器。
使用 DolphinScheduler
DolphinScheduler 是一个由 Apache 软件基金会支持的完整作业调度和分配系统。与 Nomad 和 Cronsun 相比,它具有更多功能,具有工作流功能,允许作业在执行前等待来自另一个作业的输入。它还提供了一个图形界面,有助于可视化正在运行的作业和依赖关系(更多详情请见 dolphinscheduler.apache.org/)。
虽然 DolphinScheduler 主要用 Java 编写,但它可以在 Python 和 Go 中调度作业。它更加复杂,具有许多可能对您的扩展需求不是必需的功能。
有几种其他作业调度器和分配器可供使用,但其中一些是为特定语言设计的,例如用于 .NET 应用程序的 Quartz.NET (www.quartz-scheduler.net/) 和用于 Node.js 应用程序的 Bree (github.com/breejs/bree)。
现在我们来看看如何使用大数据调度器和分配器在网络自动化中进行大规模计算。
与大数据一起工作
有一些特定的应用需要大量的 CPU 进行数据处理。这些应用需要一个允许运行专注于数据分析的非常专业算法的系统的系统。这些通常被称为大数据系统和应用。
大数据是数据集的集合,太大以至于无法仅在一台计算机上进行分析。这是一个由数据科学家、数据工程师和人工智能工程师主导的领域。原因是他们通常分析大量数据以提取信息,并且他们的工作需要一个在 CPU 处理方面大量扩展的系统。这种规模只能通过使用可以在集群中的多台计算机上调度和分配任务的系统来实现。
用于大数据的算法模型称为MapReduce。使用在集群中的多台机器上运行的算法对大型数据集进行分析的 MapReduce 编程模型被用来实现分析。最初,MapReduce 术语与谷歌产品相关,但现在它是一个用于处理大数据的程序术语。
由 Jeffrey Dean 和 Sanjay Ghemawat 发表的原始论文《MapReduce:在大型集群上的简化数据处理》是深入了解该主题的好参考和好读物。该论文是公开的,可以从谷歌研究页面static.googleusercontent.com/media/research.google.com/en//archive/mapreduce-osdi04.pdf下载。
让我们看看我们如何在网络自动化中使用大数据。
大数据和网络自动化
大数据在网络自动化中用于帮助进行流量工程和优化。MapReduce 用于计算基于流量需求和路由路径组合的更好流量路径。流量需求通过 IP 源和 IP 目的地址收集和存储,然后使用 MapReduce 计算流量需求矩阵。对于这项工作,使用 BGP、SNMP 和基于流的收集(如sflow、ipfix或netflow)从所有网络设备收集路由和流量信息。收集的数据通常是大量的,需要实时结果以允许及时进行适当的网络优化和流量工程。
一个例子就是从转接路由器和对等路由器(在第第一章中讨论)收集的 IP 数据流。然后,将这些流信息与从路由器获取的路由信息一起分析。然后,可以实时应用更好的路由策略,以选择更少拥堵的外部路径或网络接口。
现在我们来调查一些可以用于大数据的流行系统。
使用大数据系统
两个最受欢迎的大数据开源系统是 Apache Hadoop(hadoop.apache.org/)和 Apache Spark(spark.apache.org/)。这两个系统都由 Apache 软件基金会(www.apache.org/)支持和维护,用于构建大型集群系统以运行大数据。
Hadoop 和 Spark 之间的区别与它们如何执行大数据分析有关。Hadoop 用于批量作业调度,没有实时需求。它使用更多的磁盘容量,响应时间更宽松,因此集群机器不需要本地化,机器需要有大容量硬盘。另一方面,Spark 使用更多的内存和更少的磁盘空间,机器需要更靠近,响应时间更可预测,因此它用于实时应用。
对于我们的流量分析网络自动化,可以使用任一选项,但为了获得更快和更规律的结果,Spark 会更受欢迎。Hadoop 将用于生成月度和日报,但不用于与实时路由策略交互。
现在我们来看一下拥有自己的集群的一个常见问题。
资源分配和云服务
使用 Hadoop 和 Spark 的问题之一是,你需要创建自己的机器集群。这意味着安装和维护硬件和操作系统软件。但这不是主要问题。主要问题是资源利用率将在一天和一年中变化。
例如,想象一下你正在使用公司的大数据系统来计算白天特定组路由器的最佳路径。问题是待分析收集的数据将发生变化;在繁忙时段,与空闲时段相比,你需要更多的 CPU 处理能力来计算。差异可能达到数百个 CPU,这将导致月底有大量的空闲 CPU 小时。
你如何解决这个问题?通过使用基于云的服务提供商为你的集群分配机器。有了它,你可以在一天之内和整个星期内添加和删除机器,在需要时增长,在不需要时释放计算能力。一个例子是使用 AWS 的产品 Elastic MapReduce(EMR),它可以用于轻松地为你的集群分配机器,通过软件进行扩展和缩减(更多详情请见 aws.amazon.com/emr/)。其他云服务提供商,如 Google、Oracle 或 Microsoft,也可以获得类似的服务。
一个需要注意的重要点是,大数据系统不允许运行任何程序或语言,但只有具有 MapReduce 概念能力的代码。因此,它比 Nomad 或 Cronsun 更具体,并且只关注数据分析。
现在我们来检查如何使用微服务和 Linux 容器进行扩展。
使用微服务和容器
当软件基于小型、独立服务的组合构建时,我们通常说该软件是使用微服务架构构建的。微服务架构是通过组合可能属于或不属于同一软件开发团队的小型服务来开发应用程序的一种方式。
这种方法的成功归功于每个服务之间的隔离,这是通过使用 Linux 容器(在第第二章中描述)实现的。使用 Linux 容器是隔离内存、CPU、网络和磁盘的好方法。除非建立了预定义的通信通道,否则每个 Linux 容器都无法与同一主机上的其他 Linux 容器交互。服务的通信通道必须使用经过良好文档化的 API。
运行微服务的机器通常被称为容器主机或简称主机。一个主机可以有多个微服务,这些微服务可能相互通信,也可能不通信。主机的组合称为容器主机集群。一些编排软件能够在单个主机或不同主机上生成服务的多个副本。使用微服务架构是扩展您系统的好方法。
构建和发布微服务的一个非常流行的场所是Docker (www.docker.com/)。Docker 容器通常指的是使用 Linux 容器构建的服务。Docker 主机是 Docker 容器可以运行的地方,同样地,Docker 集群是一组可以运行 Docker 容器的主机。
现在我们来看看如何使用 Docker 容器来扩展我们的代码。
通过示例构建可扩展的解决方案
让我们通过创建自己的 Docker 容器并多次启动它来构建一个使用微服务架构的解决方案。我们的服务有一些要求,如下:
-
需要有一个 API 来接受请求
-
API 需要接受目标列表
-
将向每个目标发送 ICMP 探测以并发验证延迟
-
API 将以 HTTP 纯文本格式响应
-
每个服务可以接受多达 1,000 个目标
-
每个 ICMP 探测的超时时间必须为 2 秒
根据这些要求,让我们编写一些将在我们的服务中使用的代码。
编写服务代码
根据前面的要求,让我们用 Go 编写一些代码来构建我们的服务。我们将使用本章之前使用的 Go 第三方 ICMP 包go-ping/ping,以及sync.WaitGroup来等待 goroutines 结束。
让我们将代码分成两个部分。以下是第二个代码块,描述probeTargets()和main()函数:
func probeTargets(w http.ResponseWriter, r *http.Request) {
httpTargets := r.URL.Query().Get("targets")
targets := strings.Split(httpTargets, ",")
if len(httpTargets) == 0 || len(targets) > 1000{
fmt.Fprintf(w, "error: 0 < targets < 1000\n")
return
}
var wg sync.WaitGroup
wg.Add(len(targets))
for _, target := range targets {
log.Println("requested ICMP probe for", target)
go probe(target, w, &wg)
}
wg.Wait()
}
func main() {
http.HandleFunc("/latency", probeTargets)
log.Fatal(http.ListenAndServe(":9900", nil))
}
前面的代码块代表我们服务的最后两个函数。在main()函数中,我们只需要调用http.HandleFunc,传递用于GET方法的 API 引用和将被调用的函数名称。然后,使用端口9900调用http.ListenAndServe以监听 API 请求。注意,log.Fatal与ListenAndServe一起使用,因为它不应该在没有问题的情况下结束。以下是一个 API GET客户端请求示例:
GET /latency?targets=google.com,cisco.com HTTP/1.0
前面的 API 请求将调用probeTargets(),这将运行循环调用 goroutines(称为probe()),两次发送 ICMP 请求到google.com和cisco.com。
现在我们来看看包含probe()函数的最后一段代码:
func probe(host string, w http.ResponseWriter, wg *sync.WaitGroup) {
defer wg.Done()
p, err := ping.NewPinger(host)
if err != nil {
fmt.Fprintf(w, "error ping creation: %v\n", err)
return
}
p.Count = 1
p.Timeout = time.Second * 2
p.SetPrivileged(true)
if err = p.Run(); err != nil {
fmt.Fprintf(w, "error ping sent: %v\n", err)
return
}
stats := p.Statistics()
if stats.PacketLoss == 0 {
fmt.Fprintf(w, "%s latency is %s\n", host, stats.AvgRtt)
} else {
fmt.Fprintf(w, "%s no response timeout\n", host)
}
}
注意,probe()函数不返回任何值、日志消息或打印消息。所有消息,包括错误,都返回给请求 ICMP 探测的 HTTP 客户端。为了允许消息返回给客户端,我们必须使用fmt.Fprintf()函数,传递引用w,它指向一个http.ResponseWriter类型。
在我们继续示例之前,让我们修改一下main()函数,以便从操作系统环境变量中读取端口号。这样,当服务被调用时,可以使用不同的端口号,只需更改名为PORT的操作系统环境变量,如下所示:
func main() {
listen := ":9900"
if port, ok := os.LookupEnv("PORT"); ok {
listen = ":" + port
http.HandleFunc("/latency", probeTargets)
log.Fatal(http.ListenAndServe(listen, nil))
}
现在我们使用 Dockerfile 构建我们的 Docker 容器。
构建我们的 Docker 容器
构建 Docker 容器时,我们将使用 Dockerfile 定义。然后,我们只需运行docker build来创建我们的容器。在你将 Docker 引擎安装到你的环境中之前,请检查有关如何安装它的文档,docs.docker.com/engine/install/。
以下是我们示例中使用的 ICMP 探测服务的 Dockerfile:
FROM golang:1.19-alpine
WORKDIR /usr/src/app
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY icmp-probe-service.go ./
RUN go build -v -o /usr/local/bin/probe-service
CMD ["/usr/local/bin/probe-service"]
要构建 Docker 容器,你只需运行docker build . –t probe-service。构建完成后,你应该可以使用docker image命令看到镜像,如下所示:
% docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
probe-service latest e9c2 About a minute ago 438MB
Docker 容器的名称是probe-service,你可以使用以下命令运行服务:
docker run -p 9900:9900 probe-service
要监听不同的端口,你需要设置PORT环境变量。端口7700的示例如下:
docker run -e PORT=7700 -p 7700:7700 probe-service
注意,如果你想在同一主机上运行多个服务而不更改容器监听的端口,你可以将不同的主机端口映射到端口9900。你只需在映射时指定不同的主机端口,如下例所示,在同一台机器上运行三个服务:
% docker run -d -p 9001:9900 probe-service
% docker run -d -p 9002:9900 probe-service
% docker run -d -p 9003:9900 probe-service
运行前面的三个命令将在主机端口上启动三个服务:9001、9002和9003。容器内的服务仍然使用端口9900。要检查主机上运行的服务,请使用docker ps命令,如下所示:
% docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6266c895f11a probe-service "/usr/local/bin/prob…" 2 minutes ago Up 2 minutes 0.0.0.0:9003->9900/tcp gallant_heisenberg
270d73163d19 probe-service "/usr/local/bin/prob…" 2 minutes ago Up 2 minutes 0.0.0.0:9002->9900/tcp intelligent_clarke
4acc6162e821 probe-service "/usr/local/bin/prob…" 2 minutes ago Up 2 minutes 0.0.0.0:9001->9900/tcp hardcore_bhabha
前面的输出显示,在主机上运行了三个服务,监听端口9001、9002和9003。你可以访问每个服务的 API,并对高达 3,000 个目标进行探测,每个服务 1,000 个。
现在让我们看看如何使用 Docker Compose 自动化启动多个服务。
使用 Docker Compose 进行扩展
使用 Docker Compose 可以帮助你添加同时运行的服务,而无需调用docker run命令。在我们的示例中,我们将使用 Docker Compose 来启动五个 ICMP 探测服务。以下是一个 YAML 格式的 Docker Compose 文件示例(在第四章中描述):
version: "1.0"
services:
probe1:
image: "probe-service:latest"
ports: ["9001:9900"]
probe2:
image: "probe-service:latest"
ports: ["9002:9900"]
probe3:
image: "probe-service:latest"
ports: ["9003:9900"]
probe4:
image: "probe-service:latest"
ports: ["9004:9900"]
probe5:
image: "probe-service:latest"
ports: ["9005:9900"]
要运行服务,只需输入docker compose up –d,要停止它们,只需运行docker compose down。以下是一个命令输出的示例:
% docker compose up –d
[+] Running 6/6
⠿ Network probe-service_default Created 1.4s
⠿ Container probe-service-probe5-1 Started 1.1s
⠿ Container probe-service-probe4-1 Started 1.3s
⠿ Container probe-service-probe3-1 Started 1.5s
⠿ Container probe-service-probe1-1 Started 1.1s
⠿ Container probe-service-probe2-1 Started
现在,让我们看看如何使用多个机器和 Docker 容器进行扩展。
使用集群进行扩展
为了进一步扩展规模,你可以设置一个 Docker 主机容器的集群。这将允许你启动成千上万的服务,使得我们的 ICMP 探测服务能够扩展到数百万的目标。你可以通过管理一组机器并运行服务来自己构建集群,或者你可以使用一个系统为你完成所有这些工作。
现在让我们调查一些用于管理和启动运行容器服务的机器集群服务的系统。
使用 Docker Swarm
使用Docker Swarm,你能够在多台机器上启动容器。它很容易使用,因为它只需要安装 Docker。一旦安装了它,就很容易创建一个 Docker Swarm 集群。你只需运行以下命令:
Host-1$ docker swarm init
Swarm initialized: current node (9f2777swvj1gmqegbxabahxm3) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-1gdb6i88ubq5drnigbwq2rh51fmyordkkpljjtwefwo2nk3ddx-6nwz531o6lqtkun4gagvrl7ws 192.168.86.158:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
一旦你启动了第一个 Docker Swarm 主机,它将接管领导位置,要添加另一个主机,你只需使用docker swarm join命令。为了避免任何主机加入 Docker Swarm 集群,需要使用一个令牌。前面的示例以SWMTKN-1开始。请注意,Docker Swarm 集群中的主机也称为节点。所以,让我们向我们的集群添加更多节点:
host-2$ docker swarm join --token SWMTKN-1-1gdb6i88ubq5drnigbwq2rh51fmyordkkpljjtwefwo2nk3ddx-6nwz531o6lqtkun4gagvrl7ws 192.168.86.158:2377
host-3$ docker swarm join --token SWMTKN-1-1gdb6i88ubq5drnigbwq2rh51fmyordkkpljjtwefwo2nk3ddx-6nwz531o6lqtkun4gagvrl7ws 192.168.86.158:2377
host-4$ docker swarm join --token SWMTKN-1-1gdb6i88ubq5drnigbwq2rh51fmyordkkpljjtwefwo2nk3ddx-6nwz531o6lqtkun4gagvrl7ws 192.168.86.158:2377
现在,集群中有四个节点,host-1是领导者。你可以通过输入以下命令来检查集群节点的状态:
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER
9f2777swvj* host-1 Ready Active Leader
a34f25affg* host-2 Ready Active
7fdd77wvgf* host-4 Ready Active
8ad531vabj* host-3 Ready Active
一旦你有了集群,你可以通过运行以下命令来启动一个服务:
$ docker service create --replicas 1 --name probe probe-service
7sv66ytzq0te92dkndz5pg5q2
overall progress: 1 out of 1 tasks
1/1: running
[==================================================>]
verify: Service converged
在前面的示例中,我们只是使用probe-service镜像启动了一个名为probe的 Swarm 服务,与之前的示例中使用的相同镜像。请注意,我们只启动了一个副本,以展示扩展起来的简单性。现在让我们通过运行以下命令来检查服务是如何安装的:
$ docker service ls
ID NAME MODE REPLICAS IMAGE
7sv66ytzq0 probe replicated 1/1 probe-service:latest
现在让我们通过运行以下命令来扩展到 10 个探测:
$ docker service scale probe=10
probe scaled to 10
overall progress: 10 out of 10 tasks
1/10: running
[==================================================>]
2/10: running
[==================================================>]
3/10: running
[==================================================>]
4/10: running
[==================================================>]
5/10: running
[==================================================>]
6/10: running
[==================================================>]
7/10: running
[==================================================>]
8/10: running
[==================================================>]
9/10: running
[==================================================>]
10/10: running
[==================================================>]
verify: Service converged
现在,如果你检查服务,它将显示 10 个副本,如下面的命令所示:
$ docker service ls
ID NAME MODE REPLICAS IMAGE
7sv66ytzq0 probe replicated 10/10 probe-service:latest
你也可以通过运行以下命令来检查每个副本的运行位置:
$ docker service ps probe
ID NAME IMAGE NODE DESIRED STATE
y38830 probe.1 probe-service:latest host-1 Running Running v4493s probe.2 probe-service:latest host-2 Running Running
zhzbnj probe.3 probe-service:latest host-3 Running Running
i84s4g probe.4 probe-service:latest host-3 Running Running 3emx3f probe.5 probe-service:latest host-1 Running Running
rd1vp1 probe.6 probe-service:latest host-2 Running Running
p1oq0w probe.7 probe-service:latest host-3 Running Running ro0foo probe.8 probe-service:latest host-4 Running Running
l6prr4 probe.9 probe-service:latest host-4 Running Running
dwdr43 probe.10 probe-service:latest host-1 Running Running
如您在前一个命令的输出中看到的,有 10 个探针作为副本在节点host-1、host-2、host-3和host-4上运行。您还可以指定副本运行的位置,以及其他参数。在这个例子中,我们能够通过使用 4 个主机将我们的 ICMP 探针服务扩展到 10,000 个目标。
我们在这些命令中遗漏的一个重要点是分配监听副本的端口。由于副本可以在同一主机上运行,它们不能使用相同的端口。因此,我们需要确保每个副本都被分配了不同的端口号来监听。在连接进行请求之前,访问我们的probe-service集群的客户端需要知道主机的 IP 地址和监听的端口号。
使用 YAML 配置文件部署 Docker Swarm 是一个更好、更受控的方法,就像我们使用 Docker Compose 时做的那样。有关 Docker Swarm 配置文件的更多详细信息可以在github.com/docker/labs/blob/master/beginner/chapters/votingapp.md找到。
更多关于 Docker Swarm 的文档可以在docs.docker.com/engine/swarm/swarm-mode/找到。
现在我们来探讨如何使用 Kubernetes 来使用多个主机。
使用 Kubernetes
Kubernetes 可能是管理集群中微服务架构最受欢迎的系统之一。它的流行也得益于它由云原生计算基金会([www.cncf.io/](https://www.cncf.io/))支持,该基金会是Linux 基金会([https://www.linuxfoundation.org/](https://www.linuxfoundation.org/))的一部分。像亚马逊、谷歌、苹果、思科和华为等大型公司都在使用 Kubernetes。
Kubernetes 提供了比 Docker Swarm 更多的功能,例如服务编排、负载均衡、服务监控、自我修复以及通过流量进行自动扩展等。尽管社区庞大且功能强大,但如果您的需求简单且需要大量扩展,您可能不想使用 Kubernetes。Kubernetes 提供了许多可能对您的开发造成负担的功能。对于我们的probe-service,我不建议使用 Kubernetes,因为它对我们进行 ICMP 探针目标的目的来说过于复杂。
您还可以使用 Docker Compose 文件来配置 Kubernetes,这可以通过使用 Kompose([https://kompose.io/](https://kompose.io/))等服务转换器来完成。更多详细信息可以在https://kubernetes.io/docs/tasks/configure-pod-container/translate-compose-kubernetes/找到。
如果您想开始使用 Kubernetes,您可以在互联网上找到大量的示例和文档。开始的最佳地方是kubernetes.io/docs/home/。
让我们现在看看如何使用基于 Nomad 的另一个集群。
使用 Nomad
Nomad 也被用于实现 Docker 集群(www.nomadproject.io/)。Nomad 还具有与 Kubernetes 相当的一些功能,例如监控、自我修复和自动扩展。然而,其功能列表并不像 Kubernetes 那样长且完整。
那么,为什么我们会选择使用 Nomad 而不是 Kubernetes 呢?这里列出了三个主要的原因,你可能会想使用 Nomad:
-
相比 Kubernetes,部署更简单,配置也更方便。
-
Kubernetes 可以扩展到 5,000 个节点和 300,000 个容器。另一方面,Nomad 能够扩展到 10,000 个节点以及超过 2 百万个容器(
www.javelynn.com/cloud/the-two-million-container-challenge/)。 -
除了 Linux 容器外,还可以支持其他服务,例如QEMU虚拟机、Java、Unix 进程和 Windows 容器。
在Nomad的更多文档可以在developer.hashicorp.com/nomad/docs找到。
让我们现在简要地看看如何使用云服务提供商提供的微服务架构。
使用云服务提供商
云服务提供商还提供了一些专有解决方案,例如Azure 容器实例、Google Kubernetes Engine(GKE)和Amazon 弹性容器服务(ECS)。使用云服务提供商的优势是你不需要在基础设施中拥有物理机器来创建集群。还有一些产品,你甚至不需要关心集群及其中的节点,例如亚马逊的一个产品叫做 AWS Fargate。使用 AWS Fargate,你只需要在 Docker 注册表中发布 Docker 容器和一个服务规范,无需指定节点或集群。
希望这一部分能给你一个很好的想法,了解如何通过使用 Linux 容器和主机集群来扩展你的代码。微服务架构是近年来开发者和服务提供商都非常关注的热门话题。可能会使用几个缩写来描述这项技术,但在这里我们已经涵盖了基础知识。你现在有足够的知识可以更深入地研究这个主题。
摘要
本章向您展示了如何改进和使用系统来扩展你的代码的简要总结。我们还演示了如何使用标准和第三方库来为我们的代码添加功能以实现扩展。
现在,你可能对可以用来与大型网络交互的技术更加熟悉了。你现在处于更好的位置来选择一种语言、一个库和一个系统,这将支持你的网络自动化以扩展处理数千甚至数百万的网络设备。
在下一章中,我们将介绍如何测试你的代码和系统,这将使你能够构建更少出现故障的网络自动化解决方案。
第三部分:测试、动手实践和前进
书的第三部分将讨论在构建测试代码的框架时需要考虑什么,以及如何进行测试,我们将进行一些实际的动手测试,最后描述在网络自动化领域前进时应该做什么。我们将提供创建测试框架的详细信息,并使用模拟网络进行实际操作,这将有助于将之前部分学到的所有信息付诸实践。
本部分包含以下章节:
-
第九章, 网络代码测试框架
-
第十章, 动手实践和前进
第九章:网络代码测试框架
在开发代码时,一个重要的方面是添加测试;我们在第五章,“网络编程的优缺点”中讨论和回顾了一些代码测试策略。但我们还没有研究网络自动化特有的技术,例如构建一个网络测试环境,我们可以使用网络自动化代码进行一些实际测试。
本章将重点介绍构建网络测试框架的技术,该框架可用于测试您的网络自动化代码。我们还将探讨可以添加到测试框架中以使其更加有用和可靠的先进技术。
下面是本章将要涉及的主题:
-
使用软件进行测试
-
使用设备仿真
-
连接设备进行测试
-
使用高级测试技术
到本章结束时,您应该有足够的信息来构建和使用一个测试框架,这将为您网络自动化项目带来显著的价值。
技术要求
本章中描述的源代码存储在本书的 GitHub 仓库中,网址为github.com/PacktPublishing/Network-Programming-and-Automation-Essentials/tree/main/Chapter09。
使用软件进行测试
一些公司在购买网络设备时,会额外购买一些设备用于测试目的。这些额外的设备通常安装在独立的环境中,以复制生产网络的一部分进行测试。在进行测试之前,设备会被连接并配置,以复制网络的一部分。一旦测试完成,设置就会被移除,然后进行不同的配置,以对网络的其他部分进行测试。进行这些测试的原因有很多,例如测试新软件、验证新配置、验证更新、检查性能、评估新的网络设计以及测试新功能等。
但主要问题是测试环境成本高昂,设置缓慢,不能由多个测试工程师并行使用。它还要求有专业技术人员在场,最终需要整理新的电缆连接、执行硬件更新、添加或移除网络卡,有时还需要更新设备的操作系统。
物理测试环境最终是不可避免的,但可以通过使用软件而不是物理硬件来执行一些测试。软件可以执行的测试将取决于测试的要求。评估软件配置、验证设计概念、验证路由行为、验证新功能以及可能的路由稳定性测试可能由软件执行。
此外,软件可以用来在网络设备之间建立连接,这也会加快设置过程。但有一个区域无法进行测试,那就是网络压力和性能,例如测量最大吞吐量或容量。
我们可以使用几种技术来使用软件进行网络测试,其中大部分将使用模拟和仿真来完成。但仿真和模拟之间的区别是什么?现在让我们来讨论这个问题。
仿真和模拟之间的区别
模拟和仿真的含义常常被混淆。虽然弄混它们并不是那么重要,但了解它们的含义是好的,这样你就可以在使用时理解它们的局限性和能力。
在仿真的情况下,是指你使用软件来模拟你想要测试的实体的物理方面。因此,在我们的网络自动化中,它可能是一个路由器、交换机或网络连接(链路)。
因此,使用路由器仿真意味着所有必要的硬件,如网络端口、控制台端口、CPU 和内存,必须由软件模拟,以便路由器的操作系统可以无缝运行,就像它在真实硬件上运行一样。一个路由器仿真器的例子是Dynamips(更多详情请见github.com/GNS3/dynamips)。
另一方面,仿真器是为了模拟你想要测试的实体的某些功能而构建的。在路由器的例子中,通常只模拟特定的功能,而不是路由器的所有功能。由于仿真器体积更小,它可以更快地完成结果,并且与仿真器相比,可以扩展到数千个。两个用于模拟网络的流行软件示例是ns-3 (www.nsnam.org/)和NetworkX (networkx.org/)。
既然我们已经了解了仿真和模拟之间的区别,那么让我们更深入地探讨一下仿真。
使用设备仿真
在我们的网络自动化中使用仿真的最佳用例可能是路由器。通过路由器仿真,我们可以在不实际拥有它的情况下测试路由器的几个功能。但路由器仿真可能是最难以实现且在资源方面成本最高的。作为一个例子,让我们探讨一个流行的 Cisco 路由器仿真器的工作原理,称为 Dynamips。图 9.1展示了使用 Dynamips 在 Linux 主机上仿真的 Cisco 路由器:

图 9.1 – Cisco 路由器仿真
如上图所示,Dynamips 是一个软件层,用于模拟 Cisco 路由器的硬件。Dynamips 可以模拟一些 Cisco 硬件,例如网络端口、CPU、内存、辅助端口和控制台端口。Dynamips 由 Christophe Fillot 在 2005 年创建,用于模拟 Cisco 路由器的 MIPS 处理器架构。如今,Dynamips 由 GNS 网络模拟团队支持和维护,更多详情可以在 github.com/GNS3/dynamips 找到。
Dynamips 的工作方式类似于虚拟机,它只会运行 Cisco 操作系统。为了模拟 MIPS 处理器,Dynamips 会消耗大量的 CPU 和内存。例如,要运行传统的 Cisco 路由器 7200,Dynamips 至少需要分配 256 MB 的 RAM,以及 16 MB 的缓存。CPU 也被大量使用来运行路由器,通过逐条指令进行翻译。Dynamips 的早期版本过度使用了 CPU 主机,但随着名为 idle-PC 的功能的引入,CPU 消耗量显著减少。
其他路由器也可以进行模拟,但需要提供所需 CPU 平台的必要硬件模拟的模拟器。可以使用 Juniper Olive 来模拟 Juniper 路由器。Juniper Olive 是修改过的 FreeBSD,用于加载 Juniper 路由器操作系统 JunOS。通过模拟,您还可以使用能够提供传统 CPU 架构的硬件模拟器来运行传统路由器。
下图展示了运行四个模拟的系统,包括两个 Cisco 路由器、一个 Juniper 路由器和一个传统 OpenWRT 路由器:

图 9.2 – 单个系统上的四个模拟
前图中这些路由器之间的连接是在操作系统主机上创建的。主机可以提供更复杂的软件链路模拟以提供连接,或者只是从一端复制流量并发送到另一端以实现点对点连接。关于这些连接的更多内容将在本章的 连接设备进行测试 部分中解释。
注意,通过模拟,可以完全隔离路由器,从而提供完全不同的架构。在 图 9.2 中,Dynamips 提供了 MIPS CPU 架构来模拟 Cisco 7200,Qemu 提供了 32 位 CPU 架构来模拟传统 OpenWRT 路由器,VMware 提供了 64 位 x86 CPU 架构来模拟 Juniper 路由器,而 Qemu 提供了 64 位 x86 CPU 架构来模拟 Cisco XRv 9000。
用于仿真这些路由器的宿主操作系统是 Linux,内核版本为 6.1.4,但也可以是其他内核或其他操作系统,例如能够运行仿真器的 Windows。图 9.2的 CPU 和内存消耗相当高——Cisco 9000 至少需要 4 个 vCPU 和 16GB 的 RAM,Juniper 至少需要 2 个 vCPU 和 512MB 的 RAM,Legacy OpenWRT 至少需要 1 个 vCPU,而 Cisco 7200 至少需要 2 个 vCPU 和大约 300MB。
因此,使用路由器仿真创建大型网络是困难的,也许由于资源有限,甚至是不可能的。扩展仿真的一个方法是通过某种操作系统隔离来共享硬件驱动程序、内存和 CPU,例如使用 Linux 容器或 FreeBSD 监狱。但是,在容器设置中,您必须为所有路由器使用相同的内核版本和相同的 CPU 架构。因此,如果您的路由器运行在 ARM 处理器上,而您的宿主机是 x86 处理器,Linux 容器将无法工作。为了工作,容器和宿主机必须使用相同的 CPU 架构。
现在,让我们看看如何使用容器扩展仿真。
使用容器扩展仿真
如果您动态地从宿主机共享资源,仿真可以扩展。然而,这要求您的路由器作为程序在 Linux 宿主机上运行,并且可以作为一个容器进行隔离。这或许是一个很大的限制,因为大多数商业路由器不运行在 Linux 上,也不能进行容器化。选择开源路由器可以让你享受到它易于迁移到基于容器架构的优势。一些大型公司已经选择放弃商业操作系统路由器,并迁移到基于 Linux 的路由架构,这有助于创建仿真的网络。
尽管存在商业限制,一些供应商提供了可容器化的版本,例如 Arista、Cisco、Juniper 和 Nokia。这包括 Cisco XRv 和 CSRv 版本、Juniper vMX 和 vQFX、Arista vEOS 和 Nokia VSR。一个探索这些功能的项目是vrnetlab(更多详情请见github.com/vrnetlab/vrnetlab)。
即使路由器与生产环境不同,也可以进行一系列网络测试,例如网络设计测试、拓扑迁移测试、基于 IP 过滤器的测试和拓扑故障转移测试等。原因是大多数拓扑运行标准协议,可以转换为开源网络平台。如果您使用 SDN 和 OpenFlow,这也同样适用。
以下图表说明了您如何使用容器运行四个仿真的路由器:

图 9.3 – 使用容器运行仿真
如前图所示,容器与 Linux 主机(绿色矩形)共享 CPU、内存和网络端口,但在容器内部是隔离的。每个容器将共享资源与其他容器隔离,但它们使用相同的 Linux 内核、相同的驱动程序和相同的 CPU 架构。可以使用不同的运行时库,但内核和 CPU 架构必须相同。每个容器将有自己的路由表,运行在相同容器上的程序将共享相同的路由表,但除非使用路由协议,否则不会共享容器之间的路由表。
你也可以在容器内运行虚拟机,但这样你并没有节省资源,之前显示的限制仍然是相同的。所以,如果你想扩展,你必须与所有容器共享硬件资源,而不是像图 9.2中那样模拟另一层。
在图 9.3的示例中,有四个路由器——一个 FRRouting,一个 OpenWRT,一个 Quagga 和一个 DD-WRT。所有这些路由器都是开源的,并且可以被容器化。但它们不一定是一个程序在运行,而是一组程序。Quagga 和 FRRouting 运行了几个执行不同任务的程序,例如bgpd、ospfd和zebra。这些开源路由器的参考资料可以从以下来源获得:
-
FRRouting:
frrouting.org/ -
Quagga:
www.nongnu.org/quagga/ -
DD-WRT:
dd-wrt.com/
你将需要一些连接能力来连接模拟的路由器。现在,让我们讨论我们可以用来连接网络设备的技术。
测试设备连接
确保我们的设备在测试中的连通性对于获得适当的测试网络环境非常重要。有几种不同的方式可以连接测试设备,例如物理电缆和软件。物理电缆总是有两个缺点——它们需要现场的技术人员,并且需要时间来实现。通过软件,只有一个限制——最大数据吞吐量,这通常是一个物理电缆的一部分。因此,如果你的测试需要高数据吞吐量,你可能需要使用物理电缆。我们将在此章的后面部分解释这个限制的解决方案,当我们查看高级技术时。
使用设备进行测试的环境也被称为网络测试实验室,或者简称为网络实验室。为了解释我们如何在实验室中连接设备,让我们描述在实验室中连接设备的三种可能方式。
使用物理电缆连接
在测试环境中,物理连接通常由连接网络设备端口的线缆组成。它们通常是光纤电缆、同轴电缆或双绞线电缆。如果您有两个设备,电缆很简单,将从一个设备传递到另一个设备。然而,如果您计划拥有一个包含几个机架和数十个设备的实验室,您可能希望使用配线线缆和配线面板,而不是通过机架传递线缆。使用配线面板的想法是,技术人员只需使用配线线缆来连接设备,这使得连接设置更快,并且以后更容易移除。
理解配线面板和配线线缆在物理实验室中的工作原理非常重要,因为它将帮助我们理解软件版本。以下图表展示了连接两个机架(每个机架有四个路由器)的配线面板:

图 9.4 – 使用配线面板连接路由器
注意,在前面的图中,橙色和蓝色线代表永久线,永远不会被移除。红色和绿色线代表用于连接设备的配线线缆,但可以轻松移除和重新连接以进行不同的拓扑配置。机架 1 中的蓝色线将路由器 R0、R1、R2 和 R3 连接到配线面板 P1,类似于机架 2,它将路由器 R10、R11、R12 和 R13 连接到配线面板 P11。橙色线代表连接配线面板 P0 到配线面板 P10 的永久线。
每当需要拓扑配置时,技术人员只需使用配线线缆来设置路由器之间的连接。每个配线面板上的端口数量将取决于每个路由器上可用的网络端口数量。例如,对于图 9.4,假设机架 1 中的每个路由器都有五个网络端口可用。因此,配线面板 P1 至少需要 20 个端口,以便连接机架 1 上的所有路由器。
在图 9.4中,有三条配线线缆。机架 1 中的绿色线缆连接机架 1 内的两个设备,例如 R0 和 R1。其他两条红色配线线缆用于连接机架 1 和机架 2 之间的设备,例如 R0 和 R10 之间。
现在,让我们看看如何使用软件连接来连接设备。
使用软件连接
在本小节的解释中,我们将假设所有路由器都是软件模拟的路由器。软件和真实设备混合配置将在下一小节中解释。
可以使用几种软件技术来互连模拟路由器,并且它们也将取决于作为主机的操作系统。在我们的例子中,我们将使用 Linux 作为主机。对于 Windows、FreeBSD 或 macOS,您可能需要不同的技术。
连接仿真路由器的方法也将取决于你使用的仿真类型。它们可能因你使用的是 Dynamips、VirtualBox、VMware、Qemu 还是 Linux 容器而有所不同。
让我们探索一些使用 Linux 连接仿真路由器的方法。
使用 TUN/TAP 接口
在 Linux 中,TUN/TAP 接口是用于接收和发送网络流量的软件接口,但它们没有连接到任何网络。该接口被称为 TUN/TAP,因为设备可以被配置为仅工作在第三层,这被称为 TUN 模式,或者工作在第二层,这被称为 TAP 接口模式。两种模式都使用相同的 Linux 设备驱动程序(通过/dev/net/tun访问),只是使用了不同的标志。使用 TAP 模式的标志是IFF_TAP,而使用 TUN 的标志是IFF_TUN。有关 TUN/TAP 的内核驱动程序的更多详细信息,请参阅www.kernel.org/doc/html/v5.8/networking/tuntap.html。
Linux 提供了一个简单的接口来创建和删除 TUN/TAP 接口;你可以使用ip tuntap命令来完成此操作。以下是一个创建 tap 接口的示例:
claus@dev:~$ sudo ip tuntap add dev tap0 mode tap
claus@dev:~$ sudo ip link set tap0 up
claus@dev:~$ ip link show tap0
4: tap0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN mode DEFAULT group default qlen 1000
link/ether b2:2e:f2:67:48:ff brd ff:ff:ff:ff:ff:ff
与 TUN 接口相比,TAP 接口更可取,因为它们工作在第二层,像真实以太网接口一样接收和发送数据包。
现在,让我们看看我们如何使用veth接口。
使用 veth 接口
Linux 容器中的网络是隔离的,并且与一个与之关联的命名空间编号。要连接到它们,你需要使用 veth 接口。veth 接口可以与命名空间关联,可以独立创建,也可以在点对点配置中与对等接口一起创建。当创建带有对等接口的 veth 时,你需要将两个命名空间关联起来,每个 veth 对等的一侧一个。一旦设置了对等接口,任何写入 veth 对等一侧的信息都将发送到另一侧,这是在 Linux 容器中使用时快速且简单地将仿真路由器互连的方法。在我们的示例中,我们将大量使用它们。以下是如何创建 veth 对等接口的示例:
claus@dev:~$ sudo ip link add A type veth peer name B
claus@dev:~$ sudo ip link set A netns 41784
claus@dev:~$ sudo ip link set B netns 41634
claus@dev:~$ sudo nsenter -t 41784 -n ip link show A
11: A@if10: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 9a:fa:1e:7f:0c:34 brd ff:ff:ff:ff:ff:ff link-netnsid 1
claus@dev:~$ sudo nsenter -t 41634 -n ip link show B
10: B@if11: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether d6:de:78:9c:e9:73 brd ff:ff:ff:ff:ff:ff link-netnsid 1
在这个示例中,使用了两个容器,它们分别由41784和41634网络命名空间标识。创建了一个带有接口名称A和B的对等接口,但容器之间的通信只有在使用ip link set <ifname> netns <namespace>命令将接口名称与网络命名空间关联后才会成为可能,就像这个示例中一样。接口名称可以相同,但需要在与命名空间关联后重命名。这是因为,在关联之前,veth 接口位于主机上,因此处于相同的命名空间,这不会允许创建具有相同名称的多个接口。
现在,让我们学习如何使用软件桥接。
使用软件桥接
软件桥接用于连接软件和硬件网络端口,这些端口可以像真实网络交换机一样添加和删除。Linux 内核有一个本地的软件桥接,可以通过使用bridge命令或添加bridge-utils包并使用brctl命令来使用。当创建一个软件桥接时,它需要一个名称,这个名称也分配给一个将或不会分配 IP 地址的网络接口。以下是一个创建桥接并将其与三个接口关联的示例:
claus@dev:~$ sudo brctl addbr Mybridge
claus@dev:~$ sudo brctl addif Mybridge tap0
claus@dev:~$ sudo brctl addif Mybridge tap1
claus@dev:~$ sudo brctl addif Mybridge enp0s3
claus@dev-sdwan:~$ brctl show Mybridge
bridge name bridge id STP enabled interfaces
Mybridge 8000.f65.. no enp0s3
tap0
tap1
如前所述,Mybridge桥接也与 Linux 主机上的一个网络接口相关联。可以通过运行ip link命令来查看,如下所示:
claus@dev-sdwan:~$ ip link show Mybridge
12: Mybridge: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default qlen 1000
link/ether f6:78:c6:1a:1c:65 brd ff:ff:ff:ff:ff:ff
Linux 原生桥接快速简单,但有一些高级配置无法由它们执行。为了能够使用更高级的命令,建议使用OpenvSwitch,也称为OvS(更多详情请见www.openvswitch.org/)。
使用 VXLAN
Linux 桥接、TAP 和 veth 接口在 Linux 主机内部本地使用,用于在仿真路由器之间建立连接,但将运行在不同主机上的仿真路由器相互连接是不行的。有一些技术可以用来在主机之间连接仿真路由器,例如伪线、L2TP 和第二层 VPN 等,但最好的选择是 VXLAN。
VXLAN 作为一个第二层隧道,将本地桥接扩展到另一个远程设备,这可以是另一个 Linux 主机、网络交换机或路由器。使用 VXLAN,还可以将仿真路由器连接到真实路由器,就像它们通过背对背连接的线缆连接一样。正如我们将在本节后面看到的那样,VXLAN 将被用于混合实验室,其中使用真实路由器和仿真路由器之间的连接。
VLAN 是一个众所周知的协议,在第二章中进行了解释。原始 VLAN 有 12 位标识符,允许最多有 4,096 个 VLAN ID。但 VLAN 标记(IEEE 802.1Q)增加了额外的 12 位,因此在正常以太网第二层帧中使用 VLAN 标记时,VLAN 的标识符可达 24 位。
VXLAN 独立于 VLAN 或 VLAN 标记,使用 24 位的头部标识符,并使用 UDP 作为传输协议,端口号为4789。使用 VXLAN 隧道的一个原始以太网帧将需要额外的 54 字节开销。因此,如果您的网络 MTU 为 1,500 字节,隧道内可以携带的最大有效载荷 MTU 将减少 54 字节。建议在使用 VXLAN 时增加 MTU。以下图示显示了 VXLAN 的协议封装示例:

图 9.5 – VXLAN 封装
VXLAN 连接端点,称为虚拟隧道端点(VTEPs)。当在 VTEP 上接收到以太网帧时,与 VXLAN 一起工作的设备将添加 VXLAN 头部、UDP 和 IP,并将其发送到另一个 VTEP 目标。
现在,让我们看看我们如何使用软件连接和物理线缆设置混合实验室。
构建混合实验室
每当你想要将模拟路由器与物理路由器结合使用时,混合实验室是必要的。你需要这种配置的主要原因是当测试真实路由器的性能并添加复杂性,例如在 OSPF 骨干中额外添加 500 个路由器时。测试与不同复杂拓扑结构的连接,例如具有波动路由的外部 BGP,也非常有用。所有额外的异常都可以通过模拟环境自动化和添加,从而帮助测试获得敏捷性和准确性。
使用混合实验室,你可以将少量真实路由器连接到无限数量的模拟路由器,也许可以构建一个可以连接到真实路由器进行更接近生产环境测试的整个网络仿真。再次强调,异常可以自动引入仿真中,包括数据包丢失、延迟和抖动。因此,你的网络自动化技能将成为混合实验室成功的关键。
以下图表显示了混合实验室连接四个模拟路由器到两个真实路由器的示例:

图 9.6 – 混合实验室的连接示例
注意,在前面的图中,紫色线条代表连接 Linux 主机 1、Linux 主机 2、路由器 A 和路由器 B 到网络交换机 1 的物理电缆。VXLAN 用于这些设备之间,以允许在这些设备之间设置任何连接。Linux 主机 2 仅使用容器模拟路由器,因此使用 veth 接口。Linux 主机 1 使用 tap 接口连接模拟路由器 2,例如 Dynamips 与 Cisco 模拟路由器。
以下图表显示了更复杂的混合设置:

图 9.7 – 更复杂的混合实验室设置
如前图所示,有 3,000 个模拟路由器和 6 个物理路由器通过 VXLAN 交换机和软件 VXLAN 桥连接。设置看起来干净利落,但它可以创建非常复杂的连接和拓扑。VXLAN 交换机充当可由软件配置的跳线面板。还需要一个真实路由器,其所有接口都连接到 VXLAN 交换机,以便它可以作为该路由器的跳线面板,例如路由器 A 连接到 VXLAN 交换机 2。
现在,让我们讨论我们如何添加一个 OOB 网络。
在你的实验室中添加一个 OOB 网络
我们需要关注的一个重要问题是,我们如何在不需要任何网络连接的情况下访问设备。这个问题的答案是使用 OOB 网络,即带外管理网络,正如我们在第一章中讨论的那样。
为那些不需要任何网络连接的设备添加某种形式的访问,有助于进行灾难性测试,例如当路由器必须被移除或关闭时。有几种方法可以访问模拟路由器,这通过访问运行模拟的主机来完成。对于真实路由器,访问它们的方式是通过控制台或辅助端口,这通常是通过串行通信来完成的。因此,为了允许所有设备的自动化,您将需要一个串行端口服务器设备,它将允许通过 IP 和 SSH 进行远程访问。一旦通过 SSH 连接到串行端口服务器,您将能够通过端口服务器通过串行端口访问路由器。这个端口服务器的一个例子是 Avocent ACS8000 产品,它有 32 个串行端口,可以通过以太网端口或 4G 移动网络通过 IP 访问(www.amazon.com/Avocent-ACS8000-Management-Cellular-ACS8032-NA-DAC-400/dp/B099XFB39R)。
现在,让我们使用一些高级技术来增强我们的网络代码测试。
使用高级测试技术
我创建了这个部分来探讨一些可以用于测试但不太常见但可能以某种方式有用的方法。这些技术可能现在不太常用,但未来可能会成为主流,所以请密切关注事情的发展。
首先,让我们看看我们如何在网络代码测试中使用时间膨胀。
使用时间膨胀
在构建您的测试环境时,您可能会遇到在实验室中使用模拟路由器物理上无法完成的测试要求,例如测量协议收敛时间或在不同设备之间发送大量数据。这些高性能测试在模拟中物理上不可能实现的原因是,模拟路由器上的 CPU 和 I/O 比真实路由器小且有限。克服这种限制的一种方法就是使用时间膨胀。
时间膨胀是一种技术,它以这种方式改变模拟环境中的 CPU 时钟,使得模拟路由器相对于没有时间膨胀的模拟运行得更慢。从主机的角度来看,具有时间膨胀的模拟路由器将使用更少的资源,因为它运行的速度不如没有时间膨胀的模拟路由器快。但从使用时间膨胀的模拟路由器的角度来看,一切似乎都以正常速度运行,但实际上要慢得多。
假设你想测试使用网络仿真来复制文件所需的时间。在真实网络中,这些设备将具有 10 GE 接口,可以达到 10 Gbps。但在仿真环境中,它们可能只有 100 Mbps 可用,甚至更少。为了克服这些限制,一种技术是将所有仿真,包括应用程序,放入一个时间膨胀环境,该环境具有 1,000 或更高的时间膨胀因子(或TDF)。具有 1,000 的 TDF,CPU 和 I/O,包括网络接口,将能够从仿真的网络和应用程序的角度执行更多的工作。
对于网络测试,通常 TDF 大于 1,但也可以使用小于 1 的 TDF,这意味着仿真将比主机运行得更快。使用小于 1 TDF 的应用程序通常用于测试需要更快进行且不存在 CPU 限制的情况。例如,TDF 为 0.1 将运行 10 倍更快,但实现通常不是微不足道的,有时甚至不可能,因为它依赖于缩短等待时间。
圣地亚哥大学基于一篇名为《无限与超越:时间扭曲网络仿真》的论文进行了一些关于时间膨胀的研究,这篇论文确实为 Xen 和 Linux 内核提供了一些实现代码。详细信息可以在www.sysnet.ucsd.edu/projects/time-dilation找到。其他时间膨胀的实现专注于虚拟机,并使用 Qemu 来操作时间;一个实现来自北卡罗来纳大学,他们创建了一个名为 自适应时间膨胀 的项目。详细信息可以在research.ece.ncsu.edu/wireless/MadeInWALAN/AdaptiveTimeDilation找到。
使用时间膨胀的一个优点是,每次你在环境中进行测试时,TDF 都可以进行调整。低 TDF 将会施加更多的 CPU 和 I/O 限制,这可能有助于测试应用程序和网络在低性能条件下的表现,从而得到一个下限测试结果。高 TDF 将提供足够的 CPU 和 I/O 资源,以便在没有资源限制的理想世界中测试应用程序和网络,从而得到一个上限测试结果。将 TDF 调整到某个中间值将为你提供与真实网络和真实应用程序兼容的资源,从而得到一个更接近现实的测试结果。
现在,让我们看看我们如何可以在网络代码测试中使用猴子测试。
使用猴子测试
在软件开发中,当应用程序需要测试但没有正式的定义要测试什么时,会使用猴子测试。猴子测试生成随机输入并评估输出。
猴子测试的思路是找出软件开发者没有预料到的代码问题。在我们的情况下,我们可以使用猴子测试来评估网络自动化工具和网络本身。在某种程度上,渗透测试会被认为是猴子测试的一部分,但更多地与发现安全漏洞有关,而不是失败。
一个很好的猴子测试例子是 Netflix 的Chaos Monkey实现(更多详情请访问github.com/Netflix/chaosmonkey)。Chaos Monkey 是用 Go 编写的,用于随机终止容器和虚拟机。经过一些修改,它可以用来关闭接口和网络链接。它基于混沌工程(更多详情请访问principlesofchaos.org/)。Chaos Monkey 是猴子测试的一个好例子,可以添加到我们的网络自动化测试基础设施中。
现在,让我们探讨如何使用人工智能(AI)进行网络代码测试。
使用人工智能
使用 AI 进行软件开发的产品还处于早期阶段,但它们很快将成为现实。今天,通过 AI 平台如ChatGPT(更多信息请访问chat.openai.com/auth/login)可以生成代码。通常,AI 生成的代码简单且不一定优化,但它是在 AI 编写代码的必要里程碑。以下是用 ChatGPT 编写单元测试的输出示例:
Question: Write a unit test for the following code:
from paramiko import SSHClient
client = SSHClient()
client.connect('10.1.1.1', username='user', password='pw')
AI: Here is the answer:
import unittest
import mock
from paramiko import SSHClient
class TestSSHClient(unittest.TestCase):
@mock.patch('paramiko.SSHClient.connect')
def test_connect(self, mock_connect):
client = SSHClient()
client.connect('10.1.1.1', username='user', password='pw')
mock_connect.assert_called_with('10.1.1.1', username='user', password='pw')
client.close()
if __name__ == '__main__':
unittest.main()
注意,这个问题非常简单,并没有指定语言或包,例如 Python 或使用unittest和mock包。AI 自己推断出了语言和要导入的包。然而,它引入了一个小的过时导入。自从 Python 3.3 以来,mock必须导入为from unittest import mock,而不是import mock。
尽管之前编写的单元测试并不完整且包含一个小错误,但它是在开发过程中让计算机自动化测试迈出的巨大一步。它今天就可以用来帮助开发者改进他们的代码。
一些商业产品已经提供 AI 软件开发的解决方案。例如,GitHub 有一个名为Copilot的产品,提供与 AI 的配对编程(更多详情请访问github.com/features/copilot)。Copilot 声称它可以为你编写单元测试,这是一个惊人的成就。
越来越多的公司将开始提供代码开发的解决方案,并且可以肯定的是,编写单元测试将是 AI 平台要实现的第一里程碑。单元测试消耗了开发者大量的时间,在大多数情况下,编写单元测试的时间甚至比编写代码本身还要多。关注市场上使用 AI 的网络自动化测试工具——这将使开发更加稳健和快速。
现在,让我们看看如何添加网络模拟以增强网络代码测试。
使用网络模拟
网络模拟与网络仿真的不同之处在于,它使用软件来模拟网络部分行为。大多数网络模拟器要么用于模拟网络协议行为,要么用于预测和计算流量需求和网络路径。它还可以用于计算资源,例如设备上的内存和网络容量,但除此之外并不多。
在网络模拟中,Python 中非常流行的 Python 包之一是NetworkX(更多详情请见networkx.org/),这是一个图操作库。使用 NetworkX,可以创建一个包含数千个节点和数百万个链接的大型网络,所需资源比使用网络仿真少得多。如果您希望运行多个测试,这些测试将比使用仿真运行得更快,那么使用 NetworkX 模拟大型网络是可能的。然而,这些测试将评估网络由于链路和节点故障的行为,而不是控制平面(路由协议)或路由器的操作系统。
网络模拟的另一个有用应用是测试特定 IP 前缀的网络访问列表路径。一旦网络模拟构建完成,就可以确定某个 IP 数据包在正常和故障条件下的流向。模拟必须使用网络路由器配置表来构建,并且可能需要定期更新以匹配生产环境。请注意,此类测试将需要为每个要测试的 IP 前缀创建一个网络图,并且每个接口的访问列表将决定链路是否包含在 IP 前缀图中。
以下图示展示了我们将使用 NetworkX 构建的拓扑结构,作为示例:

图 9.8 – 使用 NetworkX 的拓扑
此拓扑也在本书 GitHub 仓库中的Chapter09/NetworkX/topology.yaml文件中描述,以下代码读取此文件并使用此拓扑创建一个 NetworkX 图:
import networkx as nx
import yaml
G = nx.Graph()
devices = {}
with open("topology.yaml", "r") as file:
yfile = yaml.safe_load(file)
for i, x in enumerate(yfile["devices"]):
devices[x] = i
G.add_node(i, name=x)
for link in yfile["links"]:
G.add_edge(devices[link[0]], devices[link[1]])
在加载拓扑后,可以进行一系列测试以评估网络的行为。例如,我们可以移除cpe-a和acc-a之间的链路,并查看pc-a和internet之间是否存在连接性。由于添加和删除边的操作更加交互式,使用 NetworkX 进行测试的最佳平台将是 Jupyter 笔记本(如第六章所述)。以下截图显示了 Jupyter 笔记本的输出,显示了移除链路并测试pc-a和internet之间连接性的测试结果:

图 9.9 – Jupyter 笔记本输出示例
如您所见,如果您移除 cpe-a 和 acc-a(边缘 1,2)之间的链接,pc-a 将失去与 internet 的连接。node_connectivity() 方法返回一个 整数,如果大于零,表示节点之间存在连接性(有关此方法和其他连接性算法的更多详细信息,请参阅networkx.org/documentation/stable/reference/algorithms/connectivity.html)。一系列附加测试可以在 Chapter09/NetworkX/example.ipynb 文件中找到。
网络模拟和网络仿真的组合可以用来提高代码测试的容量和速度。必须包含一个机制来构建仿真和创建使用相同配置的模拟。此外,一些测试可以先在模拟中进行,如果需要,可以在仿真中重复以进行验证。
使用流量控制
通过使用流量整形(或流量控制),我们可以通过添加一些真实链路和多对多点网络中存在的物理特性来增加我们的仿真的复杂性。使用流量整形,我们可以向特定连接添加延迟,引入丢包,添加随机限制,添加网络拥塞,添加抖动等。在 Linux 上,可以通过使用内置的 tc 命令轻松实现。
TC 通过在 Linux 中使用调度器或 tc 实现,qdiscs 可以在tldp.org/HOWTO/Traffic-Control-HOWTO找到。
以下是一个使用名为 netem 的 qdisc 添加 10 毫秒延迟到环回接口的示例:
$ sudo fping -q -c 100 -p 100 localhost
localhost : xmt/rcv/%loss = 100/100/0%, min/avg/max = 0.030/0.044/0.069
$ sudo tc qdisc add dev lo root netem delay 10ms
$ sudo fping -q -c 100 -p 100 localhost
localhost : xmt/rcv/%loss = 100/100/0%, min/avg/max = 20.4/21.4/24.8
此示例在每个方向上添加了 10 毫秒以达到 lo 接口,因此往返时间是双倍,这在结果中表现为 21.4 毫秒的平均值。
这里是另一个示例,展示了如何使用 netem 添加 5%的丢包率:
$ sudo fping -q -c 100 -p 100 localhost
localhost : xmt/rcv/%loss = 100/100/0%, min/avg/max = 0.031/0.044/0.101
$ sudo tc qdisc add dev lo root netem loss 5%
$ sudo fping -q -c 100 -p 100 localhost
localhost : xmt/rcv/%loss = 100/96/4%, min/avg/max = 0.032/0.056/0.197
在这个测试示例中,结果是 4%的丢包率,而不是配置的 5%。这是因为 netem 使用随机选择来获取丢包率,需要更大的测试样本才能接近 5% - 例如,1000 个数据包而不是前一个测试中使用的 100 个。
使用 netem 可以在接口中添加更复杂的网络行为,例如突发控制、最大容量、网络拥塞和随机延迟变化等。更多详细信息可以在wiki.linuxfoundation.org/networking/netem找到。
除了 netem 之外,还有很多其他的调度器,如 choke、codel、hhf 和 ATM。所有无类和有类 qdiscs 的列表可以在 tc 手册页上找到,只需输入 man tc 即可查看(HTML 版本可以在manpages.debian.org/buster/iproute2/tc.8.en.html找到)。
希望您已经充分利用了这一部分,并开始思考是否可以将这些高级技术添加到您的项目中。添加其中之一可能会使您的项目更加可靠,并更接近真实的生产环境。
摘要
本章的目标是向您介绍如何使用软件构建和使用适当的基础设施来测试您的自动化代码。您学习了如何有效地使用软件来测试您的自动化代码,如何使用模拟和仿真,如何连接真实和仿真设备,以及最后,如何融入高级技术。
在您的网络自动化代码项目中添加本章描述的一些技术将赋予您的项目超能力。从现在开始,它将无与伦比。
在下一章中,我们将亲自动手在网络实验室中进行操作,并以一些额外的评论结束这本书。
第十章:实践和前进
恭喜你,你已经到达了这本书的最后一章,没有什么比用一些使用网络自动化的真实例子来巩固所学的所有知识更好的了。我们可能无法为这本书涵盖的所有主题都编写例子,但目的是至少为进一步的实验和学习打下基础。
在本章中,我们将使用我们的网络自动化技能和模拟路由器从头开始构建一个网络。构建完成的模拟网络将包含足够的组件,使我们能够实验本书中描述的几种技术。无论何时你需要,你都可以用它来进行自己的实验。
我们还将添加一些关于未来学习和工作的备注和指导,这应该足以结束本书。
在本章结束时,你将能够构建自己的网络模拟,并在自己的计算机上实验自己的网络自动化项目,这将为你未来的实验和学习提供一个很好的环境基础。
本章我们将涵盖以下主题:
-
使用网络实验室
-
构建我们的网络实验室
-
连接设备
-
添加自动化
-
前进和进一步学习
技术要求
本章中描述的源代码存储在 GitHub 仓库github.com/PacktPublishing/Network-Programming-and-Automation-Essentials/tree/main/Chapter10。
使用网络实验室
是时候在我们虚拟环境中进行一些真正的代码自动化测试了。某些开源和商业产品可以在你的环境中用于测试网络自动化。商业和开源解决方案之间的区别在于支持的设备类型数量以及如何扩展。使用开源解决方案,你可能能够扩展数千台设备,但在模拟的设备类型方面可能会有所限制。商业和开源网络的组合可能更有用。
Cisco 有一个项目允许公众访问虚拟实验室中的模拟路由器;它们被称为沙盒。Cisco 提供免费 24/7 远程访问其沙盒,但设备数量有限。有关 Cisco 沙盒的更多信息,请参阅developer.cisco.com/site/sandbox/。
例如,第六章 [B18165_06.xhtml#_idTextAnchor166] 中描述的 scrapligo 项目使用 Cisco 沙盒;有关使用详情,请参阅 github.com/scrapli/scrapligo/blob/main/examples/generic_driver/interactive_prompts/main.go#L14。还有其他商业产品,例如 Cisco Packet Tracer,它是 Cisco 网络学院的一部分(https://www.netacad.com/courses/packet-tracer),以及 EVE-NG (www.eve-ng.net/)。
在开源方面,最受欢迎的是 GNS3 (gns3.com/) 和 Mininet (mininet.org/)。Mininet 使用 Linux 容器来扩展网络,而 GNS3 则更专注于虚拟机,如 Dynamips。因此,GNS3 可以运行多种不同的路由器类型,但在规模上有限。另一方面,Mininet 可以扩展到数千个,但只有一种路由器类型,这更适合测试网络概念和网络拓扑,而不是功能。
对于我们来说,从零开始构建自己的网络实验室将更有趣,这将让我们更深入地了解如何使用它以及我们如何使用网络自动化工具,实际上,这将在真实网络中有所帮助。我们网络实验室的基础将是 Linux 容器。因此,我们需要使用易于容器化的路由器,并且由于许可问题,我们应该坚持使用如 FRRouting、Quagga、OpenWRT 或 DD-WRT 等开源解决方案,如 第九章 [B18165_09.xhtml#_idTextAnchor209] 中所述。
我们的网络实验室将使用 FRRouting 作为路由器的基础,它具有接近 Cisco 路由器的接口配置,可以通过 vtysh 命令访问。有关 FRRouting 设置和配置的更多详细信息,请参阅 docs.frrouting.org/en/latest/basic.html。
现在让我们构建自己的网络实验室。
构建我们的网络实验室
在我们的网络实验室中,我们将为所有设备使用 Linux 容器。基本上会有两种类型的设备,一种是运行路由器,另一种运行 Linux。那些不作为路由器运行的 Linux 容器将被用来生成流量或接收流量;它们将模拟用户的 PC 和互联网上的服务器。
目前的拓扑结构在以下图中描述:

图 10.1 – 网络实验室拓扑
将作为路由器运行的容器是 图 10.1 中的白色矩形,黄色矩形将作为用户 PC 运行,绿色矩形将模拟互联网上的服务器。
总的来说,网络实验室将拥有 16 台路由器、3 台 PC 和 1 台服务器。网络实验室中使用的容器镜像使用 Docker 创建并存储在 Docker Hub (hub.docker.com/),这些是公开可用的,可以用于镜像下载。路由器是基于 FRRouting Docker 镜像(hub.docker.com/r/frrouting/frr)创建的,PC 和服务器是基于 Alpine Linux Docker 镜像(hub.docker.com/_/alpine)创建的。
原始镜像经过了一些工具和配置更改的轻微修改,创建了三个新的镜像。路由器的镜像为hub.docker.com/r/brnuts/routerlab,PC 的镜像为hub.docker.com/r/brnuts/pclab,互联网的镜像为hub.docker.com/r/brnuts/internetlab。
现在我们来看看如何启动我们的实验室主机。
启动实验室主机
Linux 容器需要一个主机来运行。因此,你首先需要启动路由器将运行在其中的 Linux 主机。我准备了两个预构建镜像以帮助,一个是用于 VirtualBox 的,另一个是用于 Qemu 的。你可以在 GitHub 上找到如何下载和启动它们的说明:Chapter10/NetworkLab/README.md。这些虚拟机镜像使用 Debian Linux。
然而,如果你不想使用预构建的虚拟机,我还包括了如何构建你自己的主机的说明,这基本上是任何带有额外包和一些配置更改的 Linux 发行版。如果你构建自己的镜像,你需要自己启动所有容器。我添加了一个 Shell 脚本,应该能够做到这一点,叫做start-containers.sh。
一旦启动了主机,让我们看看如何检查它是否正确启动。
检查实验室主机
在解压缩并启动预构建镜像后,一旦主机完成引导序列,你应该能够看到所有设备正在运行。原因是我已经更新了容器,以便它们会自动重启,除非明确停止。
要验证代表设备的容器是否正在运行,你只需使用docker ps命令,如下面的截图所示:

图 10.2 – 显示网络实验室上所有设备运行的输出
docker ps命令的输出应该显示所有正在运行的设备,总共应该是 20 个容器,其中 16 个代表路由器(使用brnuts/routerlab镜像),3 个代表 PC(使用brnuts/pclab镜像),1 个代表互联网(使用brnuts/internetlab镜像)。
我还向所有容器添加了卷,并将它们作为持久存储附加,因此即使在重启容器时配置更改也不会被删除。要查看卷,只需输入docker volume list即可。
现在,检查/etc/hosts文件是否已更新为容器的 IP 地址。你应该能在# BEGIN DOCKER CONTAINERS之后看到几行,就像以下截图中的示例一样。此文件由systemctl中包含的update-hosts.sh脚本更新:

图 10.3 – 检查/etc/hosts 文件
我们将在下一节解释为什么我们需要 LLDP,但现在,让我们使用systemctl status lldpd.service命令来检查主机上是否正在运行lldpd:

图 10.4 – 检查主机上 lldpd 是否正在运行
如果lldpd守护进程运行正确,你应该能在绿色中看到active (running),就像前面的截图所示。
现在,我们应该准备好开始进行一些网络自动化,以完成构建我们的实验室。现在让我们看看我们如何连接设备。
连接设备
在我们的网络实验室中连接设备将通过使用veth对等接口来完成,正如在第九章中所述。如果我们需要从两个不同的主机连接两个不同的实验室,我们可以使用 VXLAN,但在这个部分的练习中,我们只在本主机上建立连接。因此,veth对等接口将完成这项工作。
我在预构建的虚拟机镜像中包含了一个协议,这对我们来说非常重要,那就是链路层发现协议(LLDP)。LLDP 是 IETF 标准之一,它是在成功实施思科的专有协议Cisco Discovery Protocol(CDP)之后出现的。它通过发送特定的以太网帧来获取关于 2 层连接另一侧的信息。我们将使用它来验证我们网络实验室中设备之间的连接。
在我们进行连接之前,让我们检查 Docker 是如何创建我们的带外(OOB)管理网络的。
带外管理网络
默认情况下,Docker 创建了一个连接所有容器的网络,我们将使用它作为我们的带外管理网络(在第第一章中描述)。为此,Docker 在容器和主机之间创建veth接口对。在容器一侧,Docker 将eth0作为名称,而在另一侧,使用veth后跟一些十六进制字符以使其唯一 – 例如,veth089f94f。
主机上的所有veth接口都连接到一个名为docker0的软件桥。要使用brctl命令,你可能需要通过执行sudo apt install bridge-utils来安装bridge-utils包:

图 10.5 – 检查 docker0 桥接上的接口
为了验证哪个veth接口属于哪个容器,你可能需要执行两个命令,如下例所示:

图 10.6 – 检查容器在主机上的 veth 对端名称
如前一个截图的输出所示,为了确定哪个veth对等接口属于border路由器,你需要在容器内执行一个命令以获取eth0接口的索引,在这种情况下,是23。一旦你有了索引,你可以通过在所有文件上使用grep命令来检查主机上的哪个veth接口具有该索引。
我们也可以通过在路由器上直接执行命令来使用 LLDP 找出veth接口名称:

图 10.7 – 显示边界路由器内的 LLDP 邻居
前面的截图显示了lldpctl命令的成功输出,显示了eth0接口的邻居,在这种情况下,是主机 Debian Linux,SysName为netlab。与边界路由器上的eth0对等的接口在PortDescr字段中描述为veth089f94f——这正是我们使用图 10.6中的命令发现的接口。
然而,为什么不使用图 10.6中描述的第一种方法来找出连接而不是使用 LLDP 呢?因为在实际网络中,LLDP 用于识别设备之间的连接。因此,编写一个自动化代码来验证实验室中所有网络连接使用 LLDP 也可以在生产中使用。我们的实验室将作为测试我们自动化代码的第一个地方——在这种情况下,检查 LLDP 拓扑。
到目前为止,你可能已经注意到我们只需使用docker exec <router 名称>命令就可以访问路由器,那么为什么我们还需要 OOB 管理网络来访问设备呢?答案是像 LLDP 的情况一样——通过 OOB 网络访问,设备可以通过 SSH 访问,这正是我们将在生产中要做的。因此,为实验室开发的任何代码都可以在生产中使用。
为了测试我们的实验室 OOB 管理网络,我们只需使用ping或ssh命令通过 IP 访问设备——例如,使用ping cpe-a命令:

图 10.8 – 使用 OOB 从主机测试连接到路由器
你也应该能够使用netlab作为用户名和密码 SSH 到任何容器:

图 10.9 – 测试是否可以通过 OOB 使用 SSH 访问设备
现在我们已经知道了我们的实验室中 OOB 管理网络的工作方式,让我们使用 OOB 网络连接设备。
查看拓扑
图 10.1 展示了我们旨在创建的拓扑结构。我们实验室中的设备正在运行并连接到一个 OOB 网络,但它们没有像图 10.1中描述的拓扑那样的连接。
除了图表之外,GitHub 上的一个文件中也包含了一个正式的拓扑描述,可以通过Chapter10/NetworkLab/topology.yaml访问。该文件描述了拓扑中的路由器和它们的连接。这是我们之前在第四章中讨论的 YAML 格式的网络定义的简单版本。
拓扑文件基本上有两个主要键,devices和links。这些键应该描述与图 10.1中显示相同的连接。以下是devices和links键的文件样本:
devices:
- name: acc-a
type: router_acc
image: brnuts/routerlab
- name: acc-b
type: router_acc
image: brnuts/routerlab
links:
- name: [pc, cpe]
connection: [pc-a, cpe-a]
- name: [cpe, acc]
connection: [cpe-a, acc-a]
该文件应包含图 10.1中描绘的所有链接。理想情况下,图 10.1中的图表应该由一个读取topology.yaml的工具自动创建。在我们的例子中,图表和 YAML 文件是相同的,但我自己手动构建了图表,并且对于任何拓扑变化,我需要更新topology.yaml文件和图表。这个问题也在第四章中讨论过,随着拓扑变得更加复杂,文件和图表之间的更新同步往往会出错。但是,对于使用这个小拓扑的示例,自动图表构建器不是必需的。
创建设备之间的连接
要连接设备,就像在拓扑中一样,我们必须使用veth对等接口,并且正如我们在第九章中讨论的那样,我们需要对等接口每边的命名空间编号以及我们将要使用的接口名称。图 10.1中的大多数连接都是设备之间的点对点连接,除了连接到骨干或WAN。
下面的图表显示了我们必须配置的所有veth对等接口;大多数是在两个容器之间以点对点配置连接。然而,核心路由器将使用,比如说,骨干veth或 WANveth,因为它们在多对多点的环境中连接,类似于 WAN。为此,我们将在主机上使用一个软件桥来提供骨干veth之间的连接性;如果需要进行网络降级测试,可以在桥接口上添加延迟和丢包:

图 10.10 – 实验室拓扑,显示所有veth对等接口
当我们开始创建用于连接所有核心路由器的骨干veth接口时,我们将在主机上使用一个命名空间,在核心路由器上使用另一个命名空间。这与所有其他veth不同,它们将有两个命名空间。以下是如何手动在pc-a和cpe-a之间创建连接的一个示例:
netlab@netlab:~$ docker inspect -f '{{.State.Pid}}' pc-a
1069
netlab@netlab:~$ docker inspect -f '{{.State.Pid}}' cpe-a
1063
netlab@netlab:~$ sudo ip link add pc-cpe type veth peer name cpe-pc
netlab@netlab:~$ sudo ip link set pc-cpe netns 1069
netlab@netlab:~$ sudo ip link set cpe-pc netns 1063
netlab@netlab:~$ docker exec pc-a ip link set pc-cpe up
netlab@netlab:~$ docker exec cpe-a ip link set cpe-pc up
如我们在这组命令中所见,首先,我们需要获取我们想要连接的每个路由器的网络命名空间 ID,然后我们可以创建 veth 对等端,并将对等端的每一侧分配给一个命名空间 ID。最后,我们在每个路由器上启动接口。请注意,pc-a 上的接口名称是 pc-cpe,而 cpe-a 上的接口名称是 cpe-pc,这有助于识别接口的方向。
为了验证我们之间路由器的连接是否创建正确,我们可以运行以下命令:

图 10.11 – 检查 pc-a 和 cpe-a 之间的连接
现在,我们可以通过查看 图 10.11 中的 lldpctl 命令输出,确认 pc-a 是否连接到 cpe-a。输出显示了 SysName 的名称为 cpe-a,确认了连接。我们还可以看到另一侧的接口名称,它是 cpe-pc。
现在,让我们看看我们如何自动化设备连接。
自动化连接
我们的网络实验室现在所有设备都在运行,我们将使用一个程序连接所有设备。您可以在 Chapter10/NetworkLab/AUTOMATION.md 获取该程序。
要在您的计算机上安装它,只需使用以下命令克隆它:
claus@dev % git clone https://github.com/brnuts/netlab.git
Cloning into 'netlab'…
remote: Enumerating objects: 103, done.
remote: Counting objects: 100% (103/103), done.
remote: Compressing objects: 100% (79/79), done.
remote: Total 103, reused 44 (delta 12), pack-reused 0
Receiving objects: 100% (103/103), 58 KiB | .9 MiB/s, done.
Resolving deltas: 100% (36/36), done.
然后,您需要构建 Go 程序:
claus@dev % go build
claus@dev % ls -lah netlab
-rwxr-xr-x 1 user staff 5.3M Feb 8 11:54 netlab
如果您正在使用预构建的 VirtualBox 映像,您可能正在通过本地主机端口 22 使用 SSH 访问网络实验室。然后,您只需像这样运行它:
claus@dev % ./netlab
如果您使用 QEMU 或带有网络实验室的自己的 Linux 虚拟机,您可以按照以下方式传递用户名、密码和主机 IP:
claus@dev % ./netlab -host 10.0.4.1 -user oper -pw secret
通过添加 -help,可以访问一个小型帮助指南,如下所示:
claus@dev % ./netlab -help
Usage of ./netlab:
-host string
Host IP for netlab (default "localhost")
-port uint
SSH port to access Host IP for netlab (default 22)
-pw string
Password to access netlab host (default "netlab")
-topo string
Topology yaml file (default "topology.yaml")
-user string
Username to access netlab host (default "netlab")
程序在输出中显示了一些日志,成功运行应该显示类似于以下类似的行:

图 10.12 – 运行 Go 自动化程序连接设备
如您在前面的屏幕截图中所见,程序运行大约需要 12 秒,并且在结束时应该显示 all done successfully。
让我们看看这个程序以及它在做什么。
查看自动化程序
在我们的例子中,程序是用 Go 语言编写的,它所在的目录包含九个文件,其中六个是带有 .go 扩展名的 Go 源代码,如下所示:
claus@dev % ls –1
go.mod
go.sum
hostconnect.go
netlab.go
readtopology.go
runcommand.go
topology.yaml
types.go
vethcommands.go
让我们讨论每个文件。
go.mod 和 go.sum
这些文件被 Go 构建器用于包依赖管理;它们包含将第三方库添加到我们程序所需的所有必要信息。每次我们导入一个包时,它都会自动更新这些文件。更多关于这些文件的信息可以在 go.dev/ref/mod 获取。
topology.yaml
这包含了 图 10.1 中显示的拓扑描述。
types.go
这部分包含了程序中使用的所有数据结构定义,包括变量类型和 YAML 拓扑数据结构。与 Python 不同,在 Go 中,最好指定要从 YAML 文件中读取的数据结构。在我们的案例中,使用TopologyConfType结构类型来定义 YAML 文件结构,如下所示:
type DeviceTopologyType struct {
Name string
Type string
Image string
}
type LinkTopologyType struct {
Name []string
Connection []string
}
type TopologyConfType struct {
Devices []DeviceTopologyType
Links []LinkTopologyType
}
readtopology.go
这部分包含了用于读取topology.yaml文件的函数。该文件的 数据结构定义在TopologyConfType结构类型中。
runcommand.go
这部分包含了封装在实验室主机上运行的命令的通用函数。如果运行命令时发生错误,输出将与错误消息结合,以错误消息的形式返回,如下例所示:
fmt.Errorf("failed '%s', out: %s ,err: %v", cmd, out, err)
将输出添加到错误信息中的想法是因为当通过 SSH 和 shell 运行远程命令时,没有stdout消息,错误可能不容易解释。
veth.go
这部分包含了形成将在主机上运行的命令字符串的函数,这些命令字符串用于创建或操作veth接口。它还包含了所有用于填充conf.Veths列表的函数,例如loadVeth()和createPeerVeths()。
hostconnect.go
该文件包含了用于连接我们实验室的函数。在我们的案例中,我们使用了一个名为melbahja/goph的第三方包,它是一个 SSH 客户端,允许执行命令并立即输出。为了获得更快和更好的性能,我们应该使用 vSSH,如第六章中所述。
netlab.go
这是主程序文件,其中包含了main()和init()函数。flags库用于在 shell 中命令执行时传递参数。默认情况下,它们在init()函数中初始化,如果没有传递参数,则使用默认值。
main()函数还描述了整个过程的流程,它由五个主要调用组成 – readTopologyFile、connectToHost、loadVeths、createVeths和addVethsToBackbone。
现在我们已经将所有设备连接起来,并且我们了解了自动化是如何工作的,让我们进行一些手动检查,以验证连接是否已正确创建。
手动检查连接
要创建检查连接的自动化,我们首先需要了解检查连接的过程。一旦我们知道了手动检查是如何工作的,我们就可以稍后自动化它。
一旦netlab程序运行无误,它应该已经创建了连接和名为backbone的软件桥,并将 WAN 接口连接到它。让我们使用以下图作为我们手动验证连接的指导:

图 10.13 – 显示我们手动检查连接的示意图
该图显示了表示我们将要进行的手动检查的位置的数字。让我们首先开始检查由1表示的连接。我们将使用 LLDP 在所有情况下验证连接。
下面的截图显示了 sudo lldpctl cpe-acc 命令的输出,该命令通过 SSH 在 cpe-a 路由器内部运行。请注意,在示例中,我们从 netlab 主机开始:

图 10.14 – 验证 cpe-a 连接到 acc-a 的 lldpctl 命令输出
如您所见,cpe-a 路由器中的 cpe-acc 接口连接到 acc-a 路由器中的 acc-cpe 接口。
要验证 core-a1 的情况:

图 10.15 – 验证 core-a1 连接到骨干的 lldpctl 命令输出
如您所见,core-a1 路由器的接口 core-a1-wan 通过 wan-core-a1 连接到 netlab 主机。为了验证 wan-core-a1 接口是否属于 backbone 交换机,我们需要执行以下额外命令之一:

图 10.16 – 检查 wan-core-a1 是否属于 backbone 桥接的命令
图 10.16 中显示的任何命令都可以确认 wan-core-a1 属于 backbone。区别在于第二个命令以 JSON 格式显示输出,这对于软件解析更容易。lldpctl 也支持使用 lldpctl -f json 的 JSON 输出。
现在,让我们讨论如何添加更多自动化。
添加自动化
对于您可能想要创建自动化的过程,有无限的可能性。大多数操作程序都是重复的,如果手动操作,则容易出错。因此,我们需要尽可能自动化我们的网络。
然后让我们描述一些简单的自动化形式,这些形式可以帮助我们的网络操作。
链路连接检查自动化
其中一个非常重要且需要大量注意的程序是物理网络的构建和施工,特别是物理机架及其电缆。其复杂性会根据是否使用星型拓扑配置或 Clos 拓扑配置而有所不同,我们已在第一章中讨论过。
结合所有可能拓扑的混合拓扑配置更加复杂,其复杂性会增加构建网络错误的机会。例如,如图图 10.17所示的 Clos 网络共有 32 个连接,如果再增加三个路由器,其复杂性将增加。

图 10.17 – Clos 网络连接
包含了 E5、S5 和 L5 后,Clos 网络现在将有 50 个连接。因此,对我们来说,连接检查自动化非常重要,可以避免网络设置中的操作失败。
最重要的是,我们的网络实验室可以用来测试链路连接检查的自动化,这可以在生产中后期使用。
在生产环境中,通常需要首先访问堡垒主机,然后才能通过 OOB 网络访问设备。在我们的网络实验室中,堡垒主机与网络实验室主机相同。一旦登录到堡垒主机,自动化代码就可以通过 OOB 网络访问路由器,这与网络实验室中的情况相同。
现在我们编写一些代码来自动化这个过程
链路检查示例代码
我添加了一个 Python 脚本,该脚本返回一个包含所有接口及其连接的特定设备的 JSON 列表格式。Python 代码可以在 Chapter10/NetworkLab/AUTOMATION.md 中访问。
让我们运行几个示例来查看 Python 脚本的工作方式;以下截图显示了 internet 设备的结果:

图 10.18 – 检查连接到互联网设备的输出
如您所见,internet 设备只有两个接口,一个通过 OOB 网络使用 eth0 接口连接到 netlab 设备,另一个名为 internet-border 的接口连接到 border 设备,这证实了 图 10.1 中的连接。
现在我们检查 border 设备的连接情况。

图 10.19 – 检查边界设备连接的输出
如 图 10.19 所示,border 设备连接到 internet 设备和三个核心路由器,core-i1、core-i2 和 core-i3,正如 图 10.1 所示。
如果您为所有设备运行此操作,您应该确认所有连接。但我们可以自动化确认所有路由器的连接吗?当然可以,但为此,我们需要读取 topology.yaml 文件,然后创建一个循环,该循环将在每个设备上运行以确认连接。我将把这个任务留给你作为练习。
现在我们来解释一下 show-connections.py 代码的一些部分。
查看代码
show-connections.py Python 代码使用 paramiko 第三方库作为 SSH 连接的基础,我们已在 第六章 中讨论过(可以使用 pip install paramiko 安装 paramiko)。
Paramiko 是一个低级别的 SSH 连接库,它允许我们在 SSH 会话内创建一个 SSH 会话,因为我们正在使用堡垒机连接到网络实验室中的设备,即实验室主机。通过NetLab()类描述的代码,这些堆叠的 SSH 连接的细节被描述,该类有一个名为connectBastion()的方法用于连接堡垒机,还有一个名为connectDevice()的方法用于连接设备。请注意,这些方法使用一个名为self.transport的类属性将堡垒机的paramiko处理程序传递给代码中描述的设备通道,如下所示:
device_channel = self.transport.open_channel(
"direct-tcpip", target_socket, source_socket
)
使用堡垒机还有其他方法,例如使用 SSH 代理或 SSH 代理。然而,在我们的例子中,我想展示如何原生地创建一个 SSH 堆叠连接。因为如果您在连接设备之前有两个堡垒机,也有可能使用paramiko,但可能不会那么容易使用 SSH 代理和代理。
在 Python 代码中,我们使用argparser向我们的命令行添加参数,这样您就可以更改堡垒机的地址或用户名和密码。参数和默认值位于parse_arguments()中。如果您输入--help,还会自动生成一个帮助指南。

图 10.20 – Python 代码 show-connections.py 的帮助输出
我将让您改进这个 Python 脚本,使其能够读取topology.yaml文件,然后验证网络实验室中图 10.1所示的所有连接。
现在,让我们看看我们如何自动化接口的 IP 配置。
IP 配置自动化
在我们能够使用我们的网络进行 IP 流量之前,我们需要将 IP 分配给我们的网络接口,这可以通过手动为每个接口添加 IP 来完成,或者我们可以创建一个自动化的代码来分配 IP 并在网络实验室的设备上配置它们。
对于我们的网络实验室,基本上有两种 IP 分配方式,一种是在设备之间点对点的,另一种是在骨干接口上的多点 WAN。让我们举一个 WAN 接口自动化的例子。名为configure-ip-wan.py的 Python 代码位于Chapter10/NetworkLab/AUTOMATION.md。
以下截图显示了运行configure-ip-wan.py程序后的输出:

图 10.21 – Python 代码 configure-ip-wan.py 的输出
注意,IP 是在设备上使用 Paramiko 配置的,就像前面的例子一样。代码使用ipaddress Python 库,通过创建一个 IP 列表来分配将在 WAN 接口中使用的 IP,如下面的命令所示:
network = ipaddress.ip_network(args.subnet)
valid_ips = list(network.hosts())
然后,每个 IP 都是通过在valid_ips列表中使用pop()方法获得的,如下面的循环所示:
prefix = network.prefixlen
for device, interface in wan_interfaces.items():
ip = valid_ips.pop(0)
cmd = "sudo ip addr add {}/{} dev {}".format(ip, prefix, interface)
run_device_command(args, device, cmd)
现在,我们可以使用包含在Chapter10/NetworkLab/AUTOMATION.md中的 Python 脚本来测试 WAN 中设备之间的 IP 连通性:

图 10.22 – 通过 WAN 测试 core-a1 和 core-b1 之间的 IP 连通性
从图 10.21所示的输出中,我们可以假设core-b1接口的 IP 地址是10.200.200.4。因此,在core-a1上执行的测试是测试core-a1和core-b1之间通过骨干网桥的 IP 连通性。
完成 IP 配置后,您还需要为所有其他接口配置 IP。我将把为其他网络实验室接口添加 IP 作为练习,但就目前而言,这个例子足以指导您继续操作。
网络实验室自动化补充
让我们简要讨论一下我们可以添加到实验室中的其他可能的自动化。
添加和移除设备
我们可以添加可以读取topology.yaml文件的代码,然后根据正在运行的内容确定是否需要某些修改,例如添加或移除设备。我会说,对于我们来说,简单地拆除网络实验室并从头开始构建另一个实验室比移除和添加设备要容易得多,因为在我们的网络仿真中,关闭和启动都很迅速。
因此,添加代码以移除和添加设备在我们的网络实验室中更像是一项练习,而不是真正的实用工具。
使用 gRPC 进行自动化
我们也可以使用 gRPC 进行一些自动化,因为 FRRouting 支持此接口。有了这个,我们就消除了通过 SSH 访问设备的必要性。您可以在docs.frrouting.org/en/latest/grpc.html上找到有关 FRRouting 的 gRPC 的更多信息。
使用 NETCONF 进行自动化
要使用 NETCONF 进行自动化,您需要在路由器映像中安装libyang,在我们的网络实验室中,这是在 Alpine Linux 上运行的 FRRouting。要添加libyang,只需在路由器设备上输入sudo apk add libyang命令。将 FRRouting 和 NETCONF 结合使用并不是一个很好的文档化选项,所以祝您好运。
添加网络退化
您可以向您的网络实验室添加延迟、抖动、丢包、拥塞和其他退化,这些可以是永久的或随时间变化。要移除和添加这些退化,最好的做法是编写可以应用必要的流量整形机制并随后移除它们的自动化代码。我们已在第九章中讨论了这些退化方法。
例如,我们可以通过使用 Linux tc来在我们的网络实验室的骨干接口上添加一些延迟,如下所示:
sudo tc qdisc add dev wan-core-a1 root netem delay 100ms
此命令应在实验室主机上运行,并将向连接core-a1到骨干网的wan-core-a1接口添加 100 毫秒的延迟。
图 10.23显示了与图 10.22中相同的测试,但增加了 WAN 延迟。

图 10.23 – 在 WAN 中添加 100 毫秒延迟后与图 10.22 中相同的测试
随意将其他网络退化添加到你的网络实验室中,通过自动化 Linux 流量控制在网络实验室接口中的使用。
配置路由
在这个阶段,我们的网络实验室不提供 IP 流量功能,因为它不知道如何路由 IP 数据包。可以通过配置接口使用静态路由或为设备添加动态协议来实现两种路由方式,以进行路由表的通信和交换。
在我们的网络实验室中,所有路由器都使用 FRRouting;可用的协议包括 EIGRP、OSPF、ISIS、RIP 或 BGP。完整的列表和更多详细信息可以在docs.frrouting.org/找到。
可能还有更多种类的自动化。有些可能只适用于网络实验室,但有些可能在生产网络中使用。希望你能利用网络实验室来提高你的网络自动化代码技能,并在构建生产网络解决方案时更有信心。
让我们现在讨论接下来要做什么以及进一步学习。
前进和进一步学习
你可能现在正在思考接下来要做什么,以及你如何可以进步到网络自动化的下一个主题。我在这里提供了一些建议,我建议你遵循这些建议,但请记住,可能还有许多其他路径可以遵循。所以,这只是一个谦逊的建议,希望你能享受这个过程。
检查流行的平台和工具
你可以使用许多自动化平台,也许可以调查它们是如何工作的。这个列表将会是动态的,可能每年都会有所变化。所以,请记住如何搜索它们以及如何评估它们。
我对一些平台进行了初步研究,但如果你想要提高你的知识并更深入地了解如何自动化网络,我建议你深入研究。你可能会有一些想法,也许还能改进你今天正在做的事情。
这里是一个你可能想要查看的、最受欢迎的自动化平台和工具的小列表,不分先后顺序:
-
Salt 项目:
-
简短描述:一个远程执行管理器
-
贡献者:超过 2K
-
仓库创建日期:2011 年 2 月
-
主要语言:Python 98%
-
许可证:Apache 2.0
-
赞助商:VMware/public
-
人气:13K 星标,5.4K 分叉,544 关注
-
-
Ansible 项目:
-
简短描述:一个用于配置管理、部署和编排的简单自动化系统
-
贡献者:超过 5K
-
仓库创建日期:2012 年 3 月
-
主要语言:Python 88%,PowerShell 6.9%
-
许可证:GPL 3.0
-
赞助商:红帽
-
人气:56K 星标,23K 分叉,2K 关注
-
-
Puppet 项目:
-
简短描述:一个旨在配置、更新和安装系统的通用管理控制系统
-
贡献者:超过 1K
-
仓库创建日期:2010 年 9 月
-
主要语言:Ruby 99%
-
许可证:Apache 2.0
-
赞助商/所有者:Puppet by Perforce
-
人气:6.8K 星标,2.3K 分支,475 关注
-
-
Chef 项目:
-
简短描述:一款旨在涵盖所有 IT 基础设施自动化的配置管理工具
-
贡献者:超过 1K
-
仓库创建日期:2009 年 1 月
-
主要语言:Ruby 98%
-
许可证:Apache 2.0
-
赞助商/所有者:Progress Software Corporation
-
人气:7.1K 星标,2.6K 分支,374 关注
-
-
Stackstorm 项目:
-
简短描述:一个事件驱动的自动化工具
-
贡献者:超过 300
-
仓库创建日期:2014 年 4 月
-
主要语言:Python 94%
-
许可证:Apache 2.0
-
赞助商/所有者:Linux 基金会
-
人气:5.4K 星标,696 分支,168 关注
-
-
eNMS 自动化项目:
-
简短描述:一个创建基于工作流程的网络自动化解决方案的高级管理系统
-
贡献者:30
-
仓库创建日期:2017 年 10 月
-
主要语言:Python 53%,JavaScript 26%,HTML 16%
-
许可证:GLP 3.0
-
赞助商:N/A
-
人气:700 星标,148 分支,73 关注
-
-
NetBrain 产品:
-
简短描述:NetBrain 开发了多个网络自动化产品,包括问题诊断自动化系统(PDAS)
-
所有者:NetBrain Automation
-
-
SolarWinds 网络自动化管理器:
-
简短描述:SolarWinds 为网络自动化开发的专有产品
-
所有者:SolarWinds
-
除了你可以调查的工具和平台,你还可以参与与网络自动化相关的工作组。让我们看看其中的一些。
加入网络自动化社区
提高你的知识和跟上新技术的一个策略是参与社区。以下是一些你可能感兴趣关注或参与的团体的小列表:
-
IETF netmgmt 工作组:
-
简短描述:一个专注于制定自动化网络管理标准(如 RESTCONF、NETCONF 和 YANG)的团体
-
-
聚会小组:
-
简短描述:一个很好的建议是加入一个定期举行会议的本地聚会小组。这样,你可以与同一领域的专业人士交谈,并提升你的网络和知识。
www.meetup.com/是一个人们可以组织和聚会的网站。
-
-
北美网络运营商小组(NANOG):
-
简短描述:NANOG 拥有大量的文档和演示文稿,还组织会议,在那里你可以找到关于网络自动化的多个主题。
-
-
全球网络进步小组(GNA-G):
-
简短描述:GNA-G 是一个来自世界各地的网络专业人士的社区,包括研究、运营和教育。他们组织会议并有一些文档资源。
-
网站:
www.gna-g.net/.
-
-
网络到代码公司社区:
-
简短描述:网络到代码是一家维护 GitHub 存储库和讨论网络自动化的slack.com群组的咨询公司,可以免费加入。
-
Slack 群组:networktocode.slack.com
-
-
IP Fabric 公司社区:
-
简短描述:IP Fabric 公司也维护 GitHub 存储库,并有一个对任何人开放的slack.com群组。
-
GitHub:
github.com/community-fabric -
Slack 群组:ipfabric-community.slack.com
-
其他社区也可以在 IBM、Oracle、VMware、Google 和 Amazon 等一些私营公司中找到。他们甚至可能使用像 Slack、LinkedIn 或 GitHub 这样的公共工具进行沟通,并且可能更专注于这些公司提供的产品,而不是通用的讨论。它们值得一看,因为它们可能有一些可以补充的内容。
提高知识和技能的另一个想法是作为开发者为已经存在的平台做出贡献,或者如果你敢的话,建立自己的平台。
我希望这一部分能给你提供前进的思路。
摘要
本章的重点是在网络实验室中亲自动手,检查一些代码自动化在 Go 和 Python 中的工作情况,并最终探索如何继续进行网络自动化的可能性。
到目前为止,你应该对自己如何构建自己的网络实验室、如何改进你的网络自动化代码以及下一步如何继续改进非常有信心。
代表所有为这本书辛勤工作的人们,我们想感谢您抽出时间阅读这本书。收集如此多的信息并以简单愉快的方式传递给他人是一项艰巨的成就。我希望您通过阅读它并发现网络自动化领域的新技术和技巧,已经充分利用了它。
现在,你可以开始接受进一步的挑战,这将使你更深入地了解网络自动化,你将在构建完整解决方案并实践你所学的所有内容时发现这一点。


浙公网安备 33010602011771号