天天看點

openstack nova 基礎知識——eventlet

激勵自己的話:

站在nova的源碼面前,不知該從何處開始,木有資料,木有人指導,隻能自己去摸索。nova中,除了mysql是我以前就熟知的之外,其它全是陌生,這對我來說,真是一個不小的挑戰。另外,就是看英文文檔,就好像幹嚼饅頭一樣,嚼得久了,才會覺得它甜,這也實在是沒有辦法。

登山的樂趣不在于到達山頂,而在于到達山頂的過程中,在此記錄這個過程,希望我可以走到最後!

申明:

這裡所寫的,隻是我對自己學習過程的一個記錄,而且隻是一個初步的學習,其中難免會有了解不到位,或者是有錯誤的地方,如果有高人飄過,還請不吝賜教。

正文:

萬事開頭難啊,翻看了nova的源碼,隻有一個感覺:完全看不懂!在官網上也沒有找到關于在源代碼層面上的架構圖,隻有幾個元件之間的關系的高層結構圖,這又增加了一個難度,好在網上還流傳了一份"Hacking on Nova",對nova的源碼結構有一個大概的介紹,也有一定的幫助。但是,這對從0起步的新手來說,還是過于高層,因為源碼中用到了很多第三方的包,如果對這些包不熟悉的話,那是寸步難行啊!是以我決定從這些包開始學習,其中之一,就是今天的主題:eventlet,因為這個包在很多地方都用到了,深入了解了eventlet之後,發現這絕對是進入nova世界的關鍵,因為它涉及一個很重要的概念,就是“協程(coroutines)”。

這是eventlet的官方網站:http://eventlet.net/

1. 首先說一下什麼是協程

協同程式與線程差不多,也就是一條執行序列,擁有自己獨立的棧,局部變量和指令指針,同時又與其它協同程式共享全局變量和其它大部分東西。線程與協同程式的主要差別在于,一個具有多線程的程式可以同時運作幾個線程,而協同程式卻需要彼此協作地運作。就是說,一個具有多個協同程式的程式在任何時刻隻能運作一個協同程式,并且正在運作的協同程式隻會在其顯示地挂起時,它的執行才會暫停。

2. 協程有什麼好處呢?

(1)每個coroutine有自己私有的stack及局部變量。

(2)同一時間隻有一個coroutine在執行,無需對全局變量加鎖。

(3)順序可控,完全由程式控制執行的順序。而通常的多線程一旦啟動,它的運作時序是沒法預測的,是以通常會給測試所有的情況帶來困難。是以能用coroutine解決的場合應當優先使用coroutine。

3. eventlet是什麼,它用來做什麼的?

這是我現階段的了解:eventlet是一個用來處理和網絡相關的python庫函數,而且可以通過協程來實作并發,在eventlet裡,把“協程”叫做greenthread(綠色線程)。所謂并發,就是開啟了多個greenthread,并且對這些greenthread進行管理,以實作非阻塞式的I/O。比如說用eventlet可以很友善的寫一個性能很好的web伺服器,或者是一個效率很高的網頁爬蟲,這都歸功于eventlet的“綠色線程”,以及對“綠色線程”的管理機制。更讓人不可思議的是,eventlet為了實作“綠色線程”,竟然對python的和網絡相關的幾個标準庫函數進行了改寫,并且可以以更新檔(patch)的方式導入到程式中,因為python的庫函數隻支援普通的線程,而不支援協程,eventlet稱之為“綠化”。

4. 幾個主要API的了解

這裡可以直接看源碼,因為官方的文檔也是從源碼中的注釋得來的。

(1)Greenthread Spawn(spawn,孵化的意思,即如何産生greenthread)

       主要有3個函數可以建立綠色線程:

       1)spawn(func, *args, **kwargs):

            建立一個綠色線程去運作func這個函數,後面的參數是傳遞給這個函數的參數。傳回值是一個eventlet.GreenThread對象,這個對象可以用來接受func函數運作的傳回值。在綠色線程池還沒有滿的情況下,這個綠色線程一被建立就立刻被執行。其實,用這種方法去建立線程也是可以了解的,線程被建立出來,肯定是有一定的任務要去執行,這裡直接把函數當作參數傳遞進去,去執行一定的任務,就好像标準庫中的線程用run()方法去執行任務一樣。

      2)spawn_n(func, *args, **kwargs):

           這個函數和spawn()類似,不同的就是它沒有傳回值,因而更加高效,這種特性,使它也有存在的價值。

      3)spawn_after(seconds, func, *args, **kwargs)

           這個函數和spawn()基本上一樣,都有一樣的傳回值,不同的是它可以限定在什麼時候執行這個綠色線程,即在seconds秒之後,啟動這個綠色線程。

(2)Greenthread Control

      1)sleep(seconds=0)

           中止目前的綠色線程,以允許其它的綠色線程執行。

      2)eventlet.GreenPool

           這是一個類,在這個類中用set集合來容納所建立的綠色線程,并且可以指定容納線程的最大數量(預設是1000個),它的内部是用Semaphore和Event這兩個類來對池進行控制的,這樣就構成了線程池。其中,有幾個比較重要的方法:

           running(self):傳回目前池中的綠色線程數

           free():傳回目前池中仍可容納的綠色線程數

           spawn()、spawn_n():建立新的綠色線程

           starmap(self, function, iterable)和imap(self, function, *iterables):

            這兩個函數和标準的庫函數中的這兩個函數實作的功能是一樣的,所不同的是這裡将這兩個函數的執行放到了綠色線程中。前者實作的是從iterable中取出每一項作為function的參數來執行,後者則是分别從iterables中各取一項,作為function的參數去執行。

            如:imap(pow, (2,3,10), (5,2,3)) --> 32 9 1000

        starmap(pow, [(2,5), (3,2), (10,3)]) --> 32 9 1000

      3)eventlet.GreenPile

            這也是一個類,而且是一個很有用的類,在它内部維護了一個GreenPool對象和一個Queue對象。這個GreenPool對象可以是從外部傳遞進來的,也可以是在類内部建立的,GreenPool對象主要是用來建立綠色線程的,即在GreenPile内部調用了GreenPool.spawn()方法。而Queue對象則是用來儲存spawn()方法的傳回值的,即Queue中儲存的是GreenThread對象。并且它還實作了next()方法,也就意味着GreenPile對象具有了疊代器的性質。是以如果我們要對綠色線程的傳回值進行操作的話,用這個類是再好不過的了。

      4)eventlet.Queue

     說到隊列就不得不畫個類圖了,基類是LightQueue,它實作了大部分的隊列的常用方法。它是用collections做為實作隊列的基本資料結構的。而且這個LightQueue的實作,不單單實作了存取操作,我覺得在本質上它實作了一個生産者和消費者問題,定義了兩個set()類型的成員變量putters和getters,前者用來存放在隊列滿時,被阻塞的綠色線程,後者用來存放當隊列空時,被阻塞的綠色線程。類中的putting()和getting()方法就是分别得到被阻塞的綠色線程的數量。

     Queue繼承了LightQueue,并且又增加了它自己的兩個方法:task_done()和join()。task_done()是被消費者的綠色線程所調用的,表示在這個項上的所有工作都做完了,join()是阻塞,直到隊列中所有的任務都完成。LifoQueue和PriorityQueue是存放資料的兩種不同的方式。

openstack nova 基礎知識——eventlet

(3)Patching Functions(更新檔方法)

      這裡就是之前所說的“綠化”,經過eventlet“綠化”過的子產品都在eventlet.green中,導入他們主要有兩種方法:

      1)  from eventlet.green import ...  +  import_patched(module_name,*additional_modules,**kw_additional_modules),如:

from eventlet.green import socket
from eventlet.green import SocketServer
BaseHTTPServer = eventlet.import_patched('BaseHTTPServer',
                        ('socket', socket),
                        ('SocketServer', SocketServer))
BaseHTTPServer = eventlet.import_patched('BaseHTTPServer',
                        socket=socket, SocketServer=SocketServer)
           

           這種方法有個缺陷就是不支援“延遲綁定”(late binding),比如在運作時導入子產品。

      2)monkey_patch(all=True,os=None, select=None, socket=None,thread=None,time=None,psycopg=None),如:

import eventlet
eventlet.monkey_patch(socket=True, select=True)
           

           不知道老外是怎麼想的,竟然叫做monkey,這個monkey是作何解呢?可能是因為它導入子產品是零星導入的吧,就好像猴撓一樣。這種方法就沒有上面所說的那個“延遲綁定”的缺陷,想什麼時候導入就什麼時候導入,just like monkey !

(4)Network Convenience Functions(和網絡相關的函數)

     這些函數定義在convenience.py檔案中,對和socket相關的網絡通信進行了包裝,注意,這裡用的socket是經過修改後的socket,以使它使用綠色線程,主要有以下一個函數:

      1)connect(addr, family=socket.AF_INET, bind=None)

            主要執行了以下幾個步驟:建立了一個TCP類型的socket,綁定本地的ip和端口,和遠端的位址進行連接配接,源碼如下:

def connect(addr, family=socket.AF_INET, bind=None):
    sock = socket.socket(family, socket.SOCK_STREAM)
    if bind is not None:
        sock.bind(bind)
    sock.connect(addr)
    return sock
           

      2)listen(addr, family=socket.AF_INET, backlog=50)

            過程和connect()類似,隻是把connect()換成了listen(),backlog指定了最大的連接配接數量,源碼如下:

def listen(addr, family=socket.AF_INET, backlog=50):
    sock = socket.socket(family, socket.SOCK_STREAM)
    if sys.platform[:3]=="win":
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  #這段不知道具體是做什麼的
    sock.bind(addr)
    sock.listen(backlog)
    return sock
           

      3)serve(sock, handle, concurrency=1000)

            這個函數直接建立了一個socket伺服器,在它内部建立了一個GreenPool對象,預設的最大綠色線程數是1000,然後是一個循環來接受連接配接,源碼如下:

def serve(sock, handle, concurrency=1000):
    pool = greenpool.GreenPool(concurrency)
    server_gt = greenthread.getcurrent()
 
    while True:
        try:
            conn, addr = sock.accept()
            gt = pool.spawn(handle, conn, addr)
            gt.link(_stop_checker, server_gt, conn)
            conn, addr, gt = None, None, None
        except StopServe:
            return
           

      4)wrap_ssl(sock, *a, **kw)

           給socket加上ssl(安全套接層),對資料進行加密。

還有幾個比較重要的API這裡就不羅列了,等以後用到了再進行分析吧,下面看幾個官方的例子:

5. 舉例:

有了上面的基礎知識,官方的例子就比較容易了解了。

(1)官方上引以為傲的“網頁爬蟲”,用到了綠色線程池和imap()函數

urls = ["http://www.google.com/intl/en_ALL/images/logo.gif",
     "https://wiki.secondlife.com/w/images/secondlife.jpg",
     "http://us.i1.yimg.com/us.yimg.com/i/ww/beta/y3.gif"]

import eventlet
from eventlet.green import urllib2  

def fetch(url):
  print "opening", url
  body = urllib2.urlopen(url).read()
  print "done with", url
  return url, body

pool = eventlet.GreenPool(200)
for url, body in pool.imap(fetch, urls):
  print "got body from", url, "of length", len(body)
           

(2)socket伺服器

import eventlet

def handle(fd):
    print "client connected"
    while True:
        # pass through every non-eof line
        x = fd.readline()
        if not x: break
        fd.write(x)
        fd.flush()
        print "echoed", x,
    print "client disconnected"

print "server socket listening on port 6000"
server = eventlet.listen(('0.0.0.0', 6000))
pool = eventlet.GreenPool()
while True:
    try:
        new_sock, address = server.accept()
        print "accepted", address
        pool.spawn_n(handle, new_sock.makefile('rw'))
    except (SystemExit, KeyboardInterrupt):
        break
           

(3)使用GreenPile的例子

import eventlet
from eventlet.green import socket

def geturl(url):
    c = socket.socket()
    ip = socket.gethostbyname(url)
    c.connect((ip, 80))
    print '%s connected' % url
    c.sendall('GET /\r\n\r\n')
    return c.recv(1024)

urls = ['www.google.com', 'www.yandex.ru', 'www.python.org']
pile = eventlet.GreenPile()
for x in urls:
    pile.spawn(geturl, x)

# note that the pile acts as a collection of return values from the functions
# if any exceptions are raised by the function they'll get raised here
for url, result in zip(urls, pile):
    print '%s: %s' % (url, repr(result)[:50])
           

參考資料:

http://eventlet.net/doc/index.html

http://docs.python.org/library/itertools.html

http://timyang.net/lua/lua-coroutine/

繼續閱讀