銀狐NetDevOps-網絡運維Python初篇(六)協程-gevent,10秒併發備份上百臺華爲網絡配置

銀狐DevNet系列會持續將網絡運維工作中python的應用進行場景化的分享,因爲每個單獨的模塊網上都有詳細的教學,這裏就不深入講解模塊基礎了,內容主要以思路和示例爲主,並將碰到的問題彙總提出注意事項。

主要是因爲網絡工程師和網絡運維工作者編程基礎不強,加上網上對於這個領域的python資料又少,傳統的分享方式(每個章節僅單純分享一個知識點)對於很多網工來說各個知識點相對獨立且割裂的,很難進行一個知識的融合,現實工作中也很難直接應用,大家學習的難度就會很大,也會導致大部分人剛入門就放棄。所以我將這些內容進行場景化,根據特定場景由淺入深不斷優化,從而帶出更多知識點,希望對大家有所幫助。

這些分享都是我本人真實的學習路徑,一方面是幫助自己梳理網絡自動化相關知識,另一方面也希望可以通過分享我微不足道的學習過程和實戰經驗,幫助更多想要了解NetDevOps而苦於沒有資料和環境被勸退的人,進而找到同行之人互相交流、互相提升。


1、使用場景:

前面5個小節由淺入深分享了調用excel內容,通過netmiko批量抓取華爲網絡設備配置,並存入本地。目前來看已可以正常在企業內進行配置備份工作,但現實情況下有個不可避免的問題,如果要批量備份大量設備,加入1臺設備需要10秒,那100臺設備豈不是需要1000秒,那如果有上千、上萬臺設備怎麼辦呢?雖然自動化方式比人爲備份強很多,但效率還是太低了,所以在現實網絡運維場景中,批量操作一般需要結合併發操作,這也是本小節的核心內容。

2、知識講解

高併發操作網上有很多內容,但都說的很官方,看完並不知道再說些什麼。所以我儘可能用大白話簡單解釋一下核心概念,什麼是進程、線程、協程,同步、異步、併發、並行?有什麼區別?

模式一:試想工廠裏有一條生產線做手機,第一個人負責裝電池,第二個人負責裝芯片,第三個人負責裝屏幕。每個人只負責自己的一項任務,這樣分工明確的方式肯定比一個人乾的效率高,這樣的模式就是單進程、多線程方式。

模式二:這時工廠增加了一條生產線,還是同樣的配置,第一個人負責裝電池,第二個人負責裝芯片,第三個人負責裝屏幕。兩條線同時工作生產手機,這就叫多進程、多線程方式。

模式三:讓我們把視角重新拉回單條生產線,這裏會發現一個問題,如果第一個人工作沒完成,第二個人是沒法工作的,他必須處於等待狀態,就算第一個人工作速度足夠快,第二個人也會有等待時間,只是很短暫而已,這就叫同步。也就是處理任務必須一個一個來,單個任務處理的再快,也是有等待時間的。如果我們不讓工人閒着,第一個人處理任務的時候,第二個人不閒着,而是處理其他事情(工人只要進入等待狀態就幹其他任務),最後會發現同樣的時間有更多的產出,這種方式就叫協程,也就是我們說的異步

進程就是我們CPU的核數,有多少核理論上就可以有多少進程,2個進程同時工作就是2條生產線,相互之間沒有依賴關係,你幹你的,我幹我的,這種模式就是並行,2個CPU可以實現真正的同時處理任務

當我們啓動py腳本的時候,就會啓動一個進程,進程會啓動線程,線程是依賴進程的,單進程多線程工作是有前後依賴關係的,第一個工作做完工作,第二個工作才能繼續,同一個任務項無論他們操作的速度有多快,他們永遠無法並行工作的,而是有個前後等待關係,這就叫併發。協程只是線程的優化,把等待時間利用上,但也無法實現真正意義上同時任務,所以也是併發。

總結:

進程是種資源的分配單位,實際上做事情的是線程,線程進行優化就變成了協程。多進程可實現真正意義上的並行,但成本很高,換句話說就是需要資源很多,所以網絡運維一般使用協程就好。經過測試抓取100臺華爲設備配置並進行本地測試需要的時間12秒,幾百臺的時間也不會超過20秒,所以簡單的批量操作使用協程就好,如果是比較複雜的跑批操作可以使用進程+協程的組合方式,大部分企業用不上的。

3、實驗環境:

操作系統:Linux CentOS 7.4

python版本:python 3.8

網絡設備:華爲CE 6865

編輯器:vscode(pycharm、sublime均可,推薦vscode)

excel格式:初次使用簡單一些,excel中只加入IP地址

4、思路分析

批量操作直接使用協程就好,不用考慮進程和線程,線程有很多庫asyncio 、gevent之類的,個人不推薦什麼模塊都研究,很浪費時間,不是開發人員不用研究太深,哪個方便好用就用哪個,個人推薦gevent,方便好用而且不用改源代碼。有些研發人員會將gevent的一些缺陷,但其實運維場景中沒什麼影響,不用在意。

gevent也有很多使用方式,可以查看文檔,這裏就推薦我常用的方式,gevent.map,這種方式可以控制併發量,比較靈活,防止CPU動不動幹到100%。

gevent批量備份配置的思路,比如我們要備份100臺設備,其實是登錄100臺設備,然後輸出命令抓取配置,並將回顯內容寫入本地文件。當我們SSH第一臺設備時,其實會有一個等待時間,這時不要讓程序等待,而是直接SSH第二臺設備,以此類推。可能SSH第10臺網絡設備時,第一臺設備配置回顯了,這些任務流雖然有前後順序,但切換的速度非常非常塊,所以會給我們一種同時連接100臺設備並寫入文件的錯覺,所以我們操作1臺設備的時間和操作100臺設備的時間,其實差距不會很大。這就是爲什麼我說100臺設備12秒,500臺也不會超過20秒。

5、整體代碼

#!/usr/bin/env python
#coding: utf-8

import os
from time import time
from datetime import datetime
from netmiko import ConnectHandler
from openpyxl import Workbook
from openpyxl import load_workbook
import gevent
from gevent import spawn
from gevent import monkey;monkey.patch_all()
from gevent.pool import Pool
from netmiko.ssh_exception import NetMikoTimeoutException
from netmiko.ssh_exception import AuthenticationException
from paramiko.ssh_exception import SSHException

def read_device_excel( ):

    ip_list = []

    wb1 = load_workbook('/home/netops/venv/cs_lab.xlsx')
    ws1 = wb1.get_sheet_by_name("Sheet1")

    for cow_num in range(2,ws1.max_row+1):

        ipaddr = ws1["a"+str(cow_num)].value
        ip_list.append(ipaddr)

    return ip_list

def get_config(ipaddr):

    session = ConnectHandler(device_type="huawei",
                            ip=ipaddr,
                            username="root",
                            password="root",
                            banner_timeout=300)

    print("connecting to "+ ipaddr)
    print ("---- Getting HUAWEI configuration from {}-----------".format(ipaddr))

    config_data = session.send_command("dis cu")

    session.disconnect()

    return config_data

def write_config_to_file(config_data,ipaddr):
    now = datetime.now()
    date= "%s-%s-%s"%(now.year,now.month,now.day)
    time_now = "%s-%s"%(now.hour,now.minute)

    #---- Write out configuration information to file
    config_path = '/home/netops/linsy_env/devconfig/' +date
    verify_path = os.path.exists(config_path)
    if not verify_path:
        os.makedirs(config_path)

    config_filename = config_path+"/"+'config_' + ipaddr +"_"+date+"_" + time_now # Important - create unique configuration file name

    print ('---- Writing configuration: ', config_filename)
    with open( config_filename, "w",encoding='utf-8' ) as config_out:  
        config_out.write( config_data )

    return

def write_issue_device(issue_device):
    now = datetime.now()
    date= "%s-%s-%s"%(now.year,now.month,now.day)
    time_now = "%s-%s"%(now.hour,now.minute)

    config_path = '/home/netops/venv/' + "issue_" + date
    verify_path = os.path.exists(config_path)
    if not verify_path:
        os.makedirs(config_path)

    config_filename = config_path+"/"+'issue_'+date+"_" + time_now
    print ('---- Writing issue: ', config_filename)
    with open (config_filename, "w", encoding='utf-8') as issue_facts:
        issue_facts.write('\n'.join(issue_device))

def run_gevent(ipaddr):

    issue_device = []

    try:
        hwconfig = get_config(ipaddr)
        write_config_to_file(hwconfig,ipaddr)

    except (AuthenticationException):
        issue_message = (ipaddr + ': 認證錯誤 ')
        issue_device.append(issue_message)

    except NetMikoTimeoutException:
        issue_message = (ipaddr + ': 網絡不可達 ')
        issue_device.append(issue_message)

    except (SSHException):
        issue_message = (ipaddr +': SSH端口異常 ')
        issue_device.append(issue_message)

    except Exception as unknown_error:
        issue_message = (ipaddr +': 發生未知錯誤: ')
        issue_device.append(issue_message+str(unknown_error))

    finally:
        write_issue_device(issue_device)                  #異常處理信息寫入文件

def main():

    starting_time = time()   
    ip_list = read_device_excel()

    pool = Pool(100)
    pool.map(run_gevent,ip_list)                                 #map(func, iterable)
    pool.join()
    print ('\n---- End get config threading, elapsed time=', time() - starting_time)

#========================================
# Get config of HUAWEI
#========================================
if __name__ == '__main__':
    main()

6、代碼詳解

大部分內容都是迭代上一小節的內容,只講解新增的協程部分。

from gevent import spawn
from gevent import monkey;monkey.patch_all()
from gevent.pool import Pool

導入模塊複製粘貼就好,monkey.patch_all()是爲了不修改源代碼直接使用協程功能的,這麼理解比較簡單。

def main():

    starting_time = time()   
    ip_list = read_device_excel()

    pool = Pool(100)
    pool.map(run_gevent,ip_list)                                 #map(func, iterable)
    pool.join()
    print ('\n---- End get config threading, elapsed time=', time() - starting_time)

首先定義一個pool,我一般使用50或者100,主要是爲了控制下併發數,別給本地機器帶來太大壓力。

pool.map(第一個參數是執行的函數,第二個參數是可迭代的數據)

run_gevent函數也是上一個小節的內容,只是我把需要執行的函數從main函數中剝離,重新定義了一個函數。可迭代的數據就是能被for循環的,list和tuple都屬於可迭代。

這裏我們用pool.map調用run_gevent函數(真正的主程序任務),並把IP地址列表傳遞進去,就會自動迭代每個IP地址到run_gevent函數的任務裏。


到此爲止,抓取設備配置這個小場景就可以在現實環境中應用了,但還遠遠不止於此,還有很多內容可以優化和使用,我也會在後續持續分享。比如如何加入類和方法,什麼是封裝和繼承,如何一次性批量抓取Juniper、華爲、H3C、Cisco不同廠商的配置,有的廠商不支持netmiko怎麼辦?跑批1000臺設備靠協程,複雜任務跑批1萬臺設備時如何使用進程+協程?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章