最近在學習eventlet這個強悍的東東,看到我同事的一些整理。故貼出來,大家一起分享~
motivation
114.113.199.11伺服器上nova服務中基於python eventlet實現的定時任務(periodic_task)和 心跳任務(report_state)都是eventlet的一個greenthread執行個體.
目前伺服器上出現了nova定時任務中某些任務執行時間過長而導致心跳任務不能準時啟動並執行問題.
如果eventlet是一個完全意義上的類似線程/進程的並發庫的話, 不應該出現這個問題, 需要研究 eventlet的並發實現, 瞭解它的並發實現原理, 避免以後出現類似的問題. 分析
經過閱讀eventlet原始碼, 可以知道eventlet主要依賴另外2個python package: 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連接埠 的串連, 並返回結果.
需要注意的是程式會在
connectiontoclient, address = serversocket.accept()
這一行block住, 直到擷取到新的串連, 程式才會繼續往下運行.
同時, 這個程式同一個時間內只能處理一個串連, 如果有很多使用者同時訪問8080連接埠, 必須要按先後 順序依次處理這些串連, 前面一個串連成功返回後, 才會處理後面的串連.
下面的例子將用epoll將這個簡單的web伺服器改寫為非同步方式 non-blocking IO by using epoll
import socket, selectEOL1 = 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()