eventlet 模塊搭建 WEB 服務器

eventlet這個強悍的東東,看到我同事的一些整理。故貼出來,你們一塊兒分享~python

motivation

114.113.199.11服務器上nova服務中基於python eventlet實現的定時任務(periodic_task)和 心跳任務(report_state)都是eventlet的一個greenthread實例.linux

目前服務器上出現了nova定時任務中某些任務執行時間過長而致使心跳任務不能準時運行的問題.git

若是eventlet是一個徹底意義上的相似線程/進程的併發庫的話, 不該該出現這個問題, 須要研究 eventlet的併發實現, 瞭解它的併發實現原理, 避免之後出現相似的問題.github

分析

通過閱讀eventlet源代碼, 能夠知道eventlet主要依賴另外2個python package:web

  • greenlet
  • python-epoll (或其餘相似的異步IO庫, 如poll/select等)

主要作了3個工做:服務器

  • 封裝greenlet
  • 封裝epoll
  • 改寫python標準庫中相關的module, 以便支持epoll

epoll

epoll是linux實現的一個基於事件的異步IO庫, 在以前相似的異步IO庫poll上改進而來.併發

下面兩個例子會演示如何用epoll將阻塞的IO操做用epoll改寫爲異步非阻塞. (取自官方文檔)異步

blocking IO

    import socket EOL1 = b'\n\n' EOL2 = b'\n\r\n' response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n' response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n' response += b'Hello, world!' serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) serversocket.bind(('0.0.0.0', 8080)) serversocket.listen(1) try: while True: connectiontoclient, address = serversocket.accept() request = b'' while EOL1 not in request and EOL2 not in request: request += connectiontoclient.recv(1024) print('-'*40 + '\n' + request.decode()[:-2]) connectiontoclient.send(response) connectiontoclient.close() finally: serversocket.close() 

這個例子實現了一個簡單的監聽在8080端口的web服務器. 經過一個死循環不停的接收來自8080端口 的鏈接, 並返回結果.socket

須要注意的是程序會在ide

connectiontoclient, address = serversocket.accept()

這一行block住, 直到獲取到新的鏈接, 程序纔會繼續往下運行.

同時, 這個程序同一個時間內只能處理一個鏈接, 若是有不少用戶同時訪問8080端口, 必需要按前後 順序依次處理這些鏈接, 前面一個鏈接成功返回後, 纔會處理後面的鏈接.

下面的例子將用epoll將這個簡單的web服務器改寫爲異步的方式

non-blocking IO by using epoll

import socket, select EOL1 = b'\n\n' EOL2 = b'\n\r\n' response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n' response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n' response += b'Hello, world!' serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) serversocket.bind(('0.0.0.0', 8080)) serversocket.listen(1) serversocket.setblocking(0) epoll = select.epoll() epoll.register(serversocket.fileno(), select.EPOLLIN) try: connections = {}; requests = {}; responses = {} while True: events = epoll.poll(1) for fileno, event in events: if fileno == serversocket.fileno(): connection, address = serversocket.accept() connection.setblocking(0) epoll.register(connection.fileno(), select.EPOLLIN) connections[connection.fileno()] = connection requests[connection.fileno()] = b'' responses[connection.fileno()] = response elif event & select.EPOLLIN: requests[fileno] += connections[fileno].recv(1024) if EOL1 in requests[fileno] or EOL2 in requests[fileno]: epoll.modify(fileno, select.EPOLLOUT) print('-'*40 + '\n' + requests[fileno].decode()[:-2]) elif event & select.EPOLLOUT: byteswritten = connections[fileno].send(responses[fileno]) responses[fileno] = responses[fileno][byteswritten:] if len(responses[fileno]) == 0: epoll.modify(fileno, 0) connections[fileno].shutdown(socket.SHUT_RDWR) elif event & select.EPOLLHUP: epoll.unregister(fileno) connections[fileno].close() del connections[fileno] finally: epoll.unregister(serversocket.fileno()) epoll.close() serversocket.close() 

能夠看到, 例子中首先使用serversocket.setblocking(0)將socket設爲異步的模式, 而後 用select.epoll()新建了一個epoll, 接着用epoll.register(serversocket.fileno(), select.EPOLLIN) 將該socket上的IO輸入事件(select.EPOLLIN)註冊到epoll裏. 這樣作了之後, 就能夠將 上面例子中會在socket.accept()這步阻塞的Main Loop改寫爲基於異步IO事件的epoll循環了.

events = epoll.poll(1)

簡單的說, 若是有不少用戶同時鏈接到8080端口, 這個程序會同時accept()全部的socket鏈接, 而後經過這行代碼將發生IO事件socket放到events中, 並在後面循環中處理. 沒有發生IO事件的 socket不會在loop中作處理. 這樣使用epoll就實現了一個簡單的併發web服務器.

注意, 這裏提到的併發, 和咱們一般所理解線程/進程的併發並不太同樣, 更準確的說, 是 IO多路複用 .

greenlet

greentlet是python中實現咱們所謂的"Coroutine(協程)"的一個基礎庫.

看了下面的例子就明白了.

    from greenlet import greenlet def test1(): print 12 gr2.switch() print 34 def test2(): print 56 gr1.switch() print 78 gr1 = greenlet(test1) gr2 = greenlet(test2) gr1.switch() 

輸出

  1.  
    12
  2.  
    56
  3.  
    34

程序先分別爲兩個函數定義了2個greenlet: gr1和gr2.

gr1.switch()顯式切換到gr1上執行, gr1中輸出"12"後gr2.switch()顯式切換到gr2上執行 輸出56, 又gr1.switch()顯式切換到gr1上, 輸出34. test1()執行結束, gr1 die. 因而 test2()裏的78不會輸出.

能夠發現greenlet僅僅是實現了一個最簡單的"coroutine", 而eventlet中的greenthread是在 greenlet的基礎上封裝了一些更high-level的功能, 好比greenlet的調度等.

eventlet.green

從epoll的運行機制能夠看出, 要使用異步IO, 必需要將相關IO操做改寫成non-blocking的方式. 可是咱們用eventlet.spawn()的函數, 並無針對epoll作任何改寫, 那eventlet是怎麼實現 異步IO的呢?

這也是eventlet這個package最兇殘的地方, 它本身重寫了python標準庫中IO相關的操做, 將它們 改寫成支持epoll的模式, 放在eventlet.green中.

好比說, socket.accept()被改爲了這樣

  1.  
    def accept(self):
  2.  
    if self.act_non_blocking:
  3.  
    return self.fd.accept()
  4.  
    fd = self.fd
  5.  
    while True:
  6.  
    res = socket_accept(fd)
  7.  
    if res is not None:
  8.  
    client, addr = res
  9.  
    set_nonblocking(client)
  10.  
    return type(self)(client), addr
  11.  
    trampoline(fd, read=True, timeout= self.gettimeout(),
  12.  
    timeout_exc=socket.timeout( "timed out"))

而後在eventlet.spawn()的時候, 經過 一些高階魔法和"huge hack", 將這些改寫過得模塊"patch"到spawn出的greenthread上, 從而 實現epoll的IO多路複用, 至關兇殘.

eventlet併發機制分析

前面說了這麼多, 這裏能夠分析一下eventlet的併發機制了.

eventlet的結構以下圖所示

  1.  
    ______________________________________ _
  2.  
    | python process |
  3.  
    | _________________________________ |
  4.  
    | | python thread | |
  5.  
    | | _____ ___________________ | |
  6.  
    | | | hub | | pool | | |
  7.  
    | | |_____| | _____________ | | |
  8.  
    | | | | greenthread | | | |
  9.  
    | | | |_____________| | | |
  10.  
    | | | _____________ | | |
  11.  
    | | | | greenthread | | | |
  12.  
    | | | |_____________| | | |
  13.  
    | | | _____________ | | |
  14.  
    | | | | greenthread | | | |
  15.  
    | | | |_____________| | | |
  16.  
    | | | | | |
  17.  
    | | | ... | | |
  18.  
    | | |___________________| | |
  19.  
    | | | |
  20.  
    | |_________________________________| |
  21.  
    | |
  22.  
    | _________________________________ |
  23.  
    | | python thread | |
  24.  
    | |_________________________________| |
  25.  
    | _________________________________ |
  26.  
    | | python thread | |
  27.  
    | |_________________________________| |
  28.  
    | |
  29.  
    | ... |
  30.  
    |_______________________________________|

eventlet arch

其中的hub和greenthread分別對應eventlet.hubs.hub和eventlet.greenthread, 本質都是 一個greenlet的實例.

hub中封裝前面提到的epoll, epoll的事件循環是由hub.run()這個方法裏實現. 每當用戶調用 eventlet.spawn(), 就會在當前python線程的pool裏產生一個新的greenthread. 因爲greenthread 裏的IO相關的python標準庫被改寫成non-blocking的模式(參考上面的socket.accept()).

每當greenthread裏作IO相關的操做時, 最終都會返回到hub中的epoll循環, 而後根據epoll中的 IO事件, 調用響應的函數. 具體以下面所示.

greenthread.sleep(), 實際上也是將CPU控制權交給hub, 而後由hub調度下一個須要運行的 greenthread.

    # in eventlet.hubs.poll.Hub

    def wait(self, seconds=None): readers = self.listeners[READ] writers = self.listeners[WRITE] if not readers and not writers: if seconds: sleep(seconds) return try: presult = self.poll.poll(int(seconds * self.WAIT_MULTIPLIER)) except select.error, e: if get_errno(e) == errno.EINTR: return raise SYSTEM_EXCEPTIONS = self.SYSTEM_EXCEPTIONS for fileno, event in presult: try: if event & READ_MASK: readers.get(fileno, noop).cb(fileno) if event & WRITE_MASK: writers.get(fileno, noop).cb(fileno) if event & select.POLLNVAL: self.remove_descriptor(fileno) continue if event & EXC_MASK: readers.get(fileno, noop).cb(fileno) writers.get(fileno, noop).cb(fileno) except SYSTEM_EXCEPTIONS: raise except: self.squelch_exception(fileno, sys.exc_info()) clear_sys_exc_info() 

總結

eventlet實現的併發和咱們理解的一般意義上相似線程/進程的併發是不一樣的, eventlet實現的"併發" 更準確的講, 是 IO多路複用 . 只有在被eventlet.spawn()的函數中存在能夠 支持異步IO 相關的操做, 好比說讀寫socket/named pipe等時, 才能不用對被調用的函數作任何修改而實現 所謂的"併發".

若是被eventlet.spawn()的函數中存在大量的CPU計算或者讀寫普通文件, eventlet是沒法對其 實現併發操做的. 若是想要在這樣的greenthread間實現相似"併發"運行的效果, 須要手動的在函數 中插入greenthread.sleep().

相關文章
相關標籤/搜索