龙空技术网

网络工程师的Python之路——netdev(异步并行)

弈心 7270

前言:

如今你们对“python多线程异步”都比较着重,小伙伴们都需要分析一些“python多线程异步”的相关资讯。那么小编也在网络上收集了一些关于“python多线程异步””的相关知识,希望小伙伴们能喜欢,同学们快快来学习一下吧!

弈心:从事计算机网络工作11年(新加坡7年,沙特4年),2013年考取CCIE,在新加坡先后任职于AT&T,新加坡交通部,苹果,Equinix,苏格兰皇家银行等大型企业、银行和政府部门。 目前供职于“世界第一土豪大学“沙特阿卜杜拉国王科技大学(KAUST),担任Senior Network Engineer,为KAUST校史上第一位也是唯一一位华人IT部门高级职员。2019年6月在知乎发布了华语圈第一本专门为编程零基础的网络工程师量身打造的Python教程《网络工程师的Python之路》。

对网工来说,我们通常必须借助paramiko,netmiko,NAPALM或者pyntc等这些第三方开源模块才能通过SSH或者各种API来登录、操作、管理各种网络设备。很遗憾的是,由于异步在Python中引入较晚(Python 3.4过后才支持异步),上述所有这些模块都不支持异步。也就意味着在不使用多线程的情况下,运行Python脚本的主机必须一台一台地登录设备执行代码。假设登录一台交换机执行配置需耗时5秒的话,那么在拥有1000台交换机的大型企业网里就要耗时5000秒才能执行完脚本,效率太低。

2019年4月,受netmiko项目的启发,俄罗斯网络运维开发工程师Sergey Yakovlev在netmiko的基础上开发了一个叫做netdev的开源模块,该模块依赖于netmiko,并且需要至少Python3.5以上才能运行,最大的特点是支持对网络设备进行异步登录和操作。

在讲解netdev的用法前,首先我们需要知道什么是“同步”(Synchronous),什么是“异步”(Asynchronous),以及为什么使用异步能够帮助我们提升我们创建的Python脚本的工作效率。

1. 同步vs异步

所谓同步,可以理解为每当系统执行完一段代码或者函数后,系统将一直等待该段代码或函数返回的值或消息,直到系统接收到返回的值或消息后才继续往下执行下一段代码或者函数,在等待返回值或消息的期间,程序处于阻塞状态,系统将不做任何事情。

本文前面所有涉及到管理多个设备的实验中,我们都是将设备的IP地址预先写入一个名为“ip_list.txt”的文本文件,然后在脚本里使用open()函数将其打开,然后调用readlines()函数并配合for循环读取每个设备的IP地址,然后通过paramiko或者netmiko一台设备接一台地完成SSH登录。像这样Python一次只能登录一台设备,只有在完成一台设备的配置后才能继续登录下一台设备继续配置的方式就是一种典型的“同步”。

而异步则恰恰相反,系统在执行完一段代码或者函数后,不用阻塞性地等待返回的值或消息,而是继续执行下一段代码或函数,在同一时间段里执行多个任务(而不是傻傻的等着一件事情做完并且直到结果出来了以后才去做下件事情),将多个任务并行,从而提高程序的执行效率。如果你有读过数学家华罗庚的《统筹方法》,一定不会对其中所举的例子感到陌生:同样是沏茶的步骤,因为烧水需要一段时间,你不用等水煮沸了过后才来洗茶杯、倒茶叶(类似“同步”),而是在等待烧水的过程中就把茶杯洗好,把茶叶倒好,等水烧开了就能直接泡茶喝了,这里烧水、洗茶杯、倒茶叶三个任务是在同一个时间段内并行完成的,这就是一种典型的“异步”。

2. 单线程vs多线程

过去的Python并不支持异步,因为不管是同步还是异步,它们都是在单线程下完成的。而之前在Python中已经有了多线程(Multithreading)的存在,程序可以启动多个线程同时完成多个任务,如果一个线程阻塞,其他线程并不受影响,程序并不会卡死。后来Python在3.4.x版本中开始加入了异步,为什么要加入呢?因为多线程虽然效率很高,但是程序在切换线程的时候会占用系统资源,产生额外的开销。另外因为异步只用了一个线程,不用担心多线程复杂的锁机制(Lock Mechanism),这也是异步被加入进Python的原因之一。关于多线程及其锁机制的话题超出了本文的讨论范围,读者可以自己参阅其他的材料深入学习。

3. 异步在Python中的应用

自从Python在3.4.x版本起开始支持异步后,关于异步的Python语法几经更改,在Python3.4、Python3.5、Python3.7中异步的实现方式有很大的不同,本文后面的例子中都将基于Python 3.8.2来讲解异步的使用。

要了解异步在Python中的应用,必须知道什么是协程(Corountine),什么是任务(Task),什么是可等待对象(Awaitable Object)。

协程可以理解为线程的优化,我们可以把协程看成一种微线程。它是一种比线程更节省资源、效率更高的系统调度机制。而异步就是基于协程实现的。在Python中实现协程的模块主要有asyncio,gevent和tornado,使用较多的是asyncio。首先来看下面的例子:

 #coding=utf-8 import asyncio import time   async def main():  print('hello')  await asyncio.sleep(1)  print('world')   print (f"程序于 {time.strftime('%X')} 开始执行") asyncio.run(main()) print (f"程序于 {time.strftime('%X')} 执行结束")  
在Python中,我们通过在def语句前加上async语句来将一个函数定义为协程函数,在上面的例子中,main()现在被定义为了协程函数。这里的“await asyncio.sleep(1)”表示临时中断当前的函数一秒钟,如果程序里还有其他函数的话,继续执行下一个函数,直到下一个函数执行完毕后,再返回来执行这个main()程序,因为这里我们除了一个main()函数之外没有其他的函数了,所以在print('hello')后,main()函数休眠了1秒钟,然后继续print(‘world’)。协程函数不是普通的函数,这里我们不能直接用“main()”来调用它,我们需要使用“asyncio.run(main())”才能执行该协程函数。这里我们配合time模块的strftime()函数来记录程序开始前的时间和程序结束后的时间,可以看到总共耗时确实是1秒钟。

这里需要注意的是不要把 “await asyncio.sleep(1)”和“time.sleep(1)”弄混,后者是在“同步”中使用的休眠操作,前者是在“异步”中使用的,因为这里我们只有main()一个函数需要执行,所以你暂时感受不到这两者有什么区别,不用着急,继续来看下面的两个例子你就知道了:

 #coding=utf-8 import asyncio import time   async def say_after(what, delay):   print(what)  await asyncio.sleep(delay)   async def main():  print (f"程序于 {time.strftime('%X')} 执行结束")  await say_after('hello',1)  await say_after('world',2)  print (f"程序于 {time.strftime('%X')} 执行结束")   asyncio.run(main())
这里我们在协程函数main()的基础上加入了另外一个函数say_after(),同样的,我们用async将它定义为了协程函数。我们在main()函数中两次调用say_after()函数,因为say_after()是一个协程函数,因此在调用它时,前面必须加上await。当main()第一次调用say_after()函数时,我们首先打印出hello,然后休眠1秒,第二次调用say_after()函数时,我们打印出world,然后再休眠两秒,两次调用完毕后总共花费了3秒钟来运行完整个程序, 如下:

这里你会说,第一次花了1秒,第二次花了2秒,总共3秒时间,这没节省时间啊,两此调用的say_after()函数并没有并行啊,这和同步有什么区别?别急,继续往下看:

 #coding=utf-8 import asyncio import time   async def say_after(what, delay):  await asyncio.sleep(delay)  print(what)   async def main(): task1 = asyncio.create_task(say_after('hello',1))  task2 = asyncio.create_task(say_after('world',2))  print (f"程序于 {time.strftime('%X')} 开始执行")  await task1  await task2  print (f"程序于 {time.strftime('%X')} 执行结束")   asyncio.run(main())
要实现异步并行,需要将协程函数打包成一个任务(Task),这里我们使用asyncio的create_task()函数将say_after()打包了两次,并分别赋值给task1和task2两个变量。然后使用await来调用task1和task2两个任务。运行脚本后可以看到,因为task1和task2是并行执行的,所以程序总共只耗时两秒钟即告完成(06:57:04到06:57:06)!

最后来说说什么是可等待对象(Awaitable Object),可等待对象的定义很简单:如果一个对象可以在await 语句中使用,那么它就是可等待对象。可等待对象主要有三种类型:协程、任务以及Future。协程和任务前面已经讲到了,Future不在本文的讨论范围内,读者可以自己参阅其他的材料深入学习。

4. netdev的安装和应用

截至2020年5月,netdev支持7家厂商的12种操作系统:

Cisco IOSCisco IOS XECisco IOS XRCisco ASACisco NX-OSHP Comware (like V1910 too)Fujitsu Blade SwitchesMikrotik RouterOSArista EOSJuniper JunOSAruba AOS 6.XAruba AOS 8.XTerminal

Netdev可以通过pip直接下载安装(因为netdev依赖于netmiko,下载netdev前请确认你的Python主机已经安装了netmiko):

下载完成后进入Python编辑器,如果import netdev没有报错则说明安装成功:

在使用netdev进行异步操作之前,我们先来做个“同步”和“异步”的对比试验,首先我们用传统的“同步”方式,通过netmiko对5台交换机(192.168.2.11--192.168.2.15)下的“line vty 5 15”配置“login local”,并统计从脚本开始运行到脚本执行完成所耗费的时间,然后我们再使用netdev,通过“异步”的方式对同样的5台交换机做同样的配置,并计时,最后将两次耗时相比较,看看孰优孰劣。

传统的“同步”方案的脚本如下(交换机192.168.2.11 – 192.168.2.15的IP的地址被保存在一个名叫ip_list.txt的文件里):

 from netmiko import ConnectHandler import time   f = open('ip_list.txt') start_time = time.time()   for ips in f.readlines():  ip = ips.strip()  SW = {   'device_type': 'cisco_ios',   'ip': ip,   'username': 'python',   'password': '123',  }  connect = ConnectHandler(**SW)  print ("Sucessfully connected to " + SW['ip'])  config_commands = ['line vty 5 15','login local','exit']  output = connect.send_config_set(config_commands)  print (output)   print ('Time elapsed: %.2f'%(time.time()-start_time))

执行脚本看效果:

可以看到,通过“同步”方式一台一台登录5台交换机完成配置总共耗时了45.02秒。

接下来我们再来看netdev的表现,代码如下:

 import asyncio import netdev import time   async def task(dev):  async with netdev.create(**dev) as ios:  commands = ["line vty 5 15", "login local","exit"]  out = await ios.send_config_set(commands)  print(out)   async def run():  devices = []  f = open('ip_list.txt')  for ips in f.readlines():  ip = ips.strip()  dev = { 'username' : 'python',  'password' : '123',  'device_type': 'cisco_ios',  'host': ip  }  devices.append(dev)  tasks = [task(dev) for dev in devices]  await asyncio.wait(tasks)   start_time = time.time() asyncio.run(run()) print ('Time elapsed: %.2f'%(time.time()-start_time))

执行代码看效果:

可以看到通过netdev提供的“异步”方式,我们仅仅耗时5.15秒便跑完了脚本,对5台交换机完成了同样的配置!

另外,除了netdev外,pexpect模块也支持异步并行,有兴趣的读者可以自行参阅其他资料了解。

标签: #python多线程异步