gunicorn workers 差別
以後都在 github 更新,請戳 gunicorn workers 差別
我們在 第一篇 裡已經了解過 gunicorn 的
SyncWorker
原理, 現在我們來看下其他的 workers 是如何工作的
目錄
- eventlet
- gevent
- thread
- tornado
- 更多資料
Eventlet
如果你打開 eventlet 的官網
Eventlet 是一個 Python 網絡庫, 支援并發通路, 使用這個庫可以在不改變代碼寫法的情況下更改代碼的運作方式
- 它使用了 epoll/kqueue/libevent , 這樣可以支援 可擴充的非阻塞式 I/O
- 協程 的支援可以讓開發者像使用線程一樣編寫順序性代碼, 但是運作時又提供了非阻塞IO的運作方式
- 對于事件的派發/回調是內建在庫中的, 開發者不需要關注這部分邏輯, 是以你可以很友善地在 Python 解釋器中使用 Eventlet, 或者在一個大型應用的一個子產品中使用
EventletWorker
繼承自
AsyncWorker
, 它覆寫了
init_process
方法和
run
方法
def patch(self):
hubs.use_hub()
eventlet.monkey_patch()
patch_sendfile()
def init_process(self):
self.patch()
super().init_process()
在從主程序
fork
之後,
init_process
方法會調用
eventlet.monkey_patch()
, 這個方法會預設把下面的子產品替換成
eventlet
提供的對應的子產品
for name, modules_function in [
('os', _green_os_modules),
('select', _green_select_modules),
('socket', _green_socket_modules),
('thread', _green_thread_modules),
('time', _green_time_modules),
('MySQLdb', _green_MySQLdb),
('builtins', _green_builtins),
('subprocess', _green_subprocess_modules),
]
Eventlet 把預設的 IO 子產品替換成自己的子產品, 當你調用
socket
方法時, 你實際上調用的是實作了非阻塞式IO的
_green_socket_modules
子產品中的方法
對于每一個
socket
的讀寫操作, 或者
time.sleep
操作, 實際上
eventlet
把目前的上下文儲存起來, 并把目前的
gthread
加到等待清單種, 之後調用 pool 去等待下一個可讀/可寫的 IO 事件
整體的調用流程和 python3 中的
async
方法類似, 但是這種方式幾乎沒有代碼入侵
如果你用
eventlet
模式運作你的應用
gunicorn --workers 2 --worker-class eventlet mysite.wsgi
EventletWorker
會生成一個新的
gthread
, 新生成的
gthread
負責從監聽的描述符中接收新的 socket, 在接收到一個新的 socket 之後,
gthread
會把 socket 對象和 django 處理函數一起傳給
greenpool
,
greenpool
負責調用對應的 django 函數
在
eventlet
的幫助下, 我們簡單的更改
--worker-class
就可以讓我們的 django 應用從阻塞式 IO 模式變成 非阻塞式 IO 模式
比起直接定義
async
函數, 用
eventlet
的好處是你的代碼可以以阻塞式的模式啟動, 也可以以非阻塞式的模式啟動, 調試起來更佳友善
直接定義
async
函數, 需要從頭到尾以
async
的方式去設計你的代碼, 你可以進行更細粒度的異步控制, 打個比方,
eventlet
可以控制兩個不同的 django 請求并發執行, 而
async
函數可以在同一個 django 請求中, 并發執行多個 IO 操作
Gevent
如果你通路 gevent 的官網
gevent 是一個基于 協程 的 Python 網絡庫, 它通過 greenlet 提供進階的同步調用方法, 底層是通過 libev 或 libuv 的事件循環來實作的
gevent 的 靈感來源于 eventlet, 但是提供更加一緻性的 API, 更簡單的實作以及更好的性能
他們的差別有這些
- gevent 底層基于 libevent(1.0 版本之後, gevent 基礎基于 libev 和 c-ares.)
- 信号處理和事件循環(event loop) 綁定
- 其它基于 libevent 編寫的庫可以和你的應用通過同個事件循環(event loop)進行綁定
- DNS 查詢是通過原生異步調用完成, 而不是開啟一個線程池之通過阻塞式調用完成
- WSGI 服務是通過 libevent 的内置 HTTP 服務搭建, 速度 非常快.
- gevent 的接口和标準庫的常用接口保持一緻
- Eventlet 提供的有些功能 gevent 不包含
如果你有其他的庫(用C編寫)用到了 libevent 的事件循環(event loop) 并且想要把它和你的Python程式內建在同個程序中, gevent 支援但是 eventlet 不支援
讓我們回到
gunicorn
GeventWorker
繼承自
AsyncWorker
, 它也覆寫了
init_process
方法和
run
方法
def patch(self):
monkey.patch_all()
def init_process(self):
self.patch()
hub.reinit()
super().init_process()
從主程序
fork
出子程序之後, 子程序調用
init_process
, 間接調用了
gevent.monkey()
,這個方法把下面的子產品替換成對應的
gevent
支援的子產品
def patch_all(socket=True, dns=True, time=True, select=True, thread=True, os=True, ssl=True,
subprocess=True, sys=False, aggressive=True, Event=True,
builtins=True, signal=True,
queue=True, contextvars=True,
**kwargs):
pass
整個調用模式和 eventlet 的模式相似, 但是由于底層庫提供的接口是不相同的, 在
run
函數中進行調用的函數也會有一些差別
# gunicorn/workers/ggevent.py
from gevent.pool import Pool
from gevent.server import StreamServer
def run(self):
# ...
pool = Pool(self.worker_connections)
# ...
server = StreamServer(s, handle=hfun, spawn=pool, **ssl_args)
# ...
server.start()
如果你運作
gunicorn --workers 2 --worker-class eventlet mysite.wsgi
使用
gevent
的優缺點和使用
eventlet
的優缺點基本相同, 我們不在這裡重複了
如果你更關注的是性能, 或者你有一個外部庫(C 庫)使用的是 libevent(或libev) 的事件循環, 并且你想在Python中同一個程序内使用同個事件循環, 你可以選擇
gevent
如果你需要一些
eventlet
才具有的特定的功能, 比如
eventlet.db_pool
/
eventlet.processes
, 你可以選擇使用
eventlet
thread
預設情況下
gunicorn
使用的是
sync
的 模式, 它預先 fork
workers
個程序, 每個程序同一時刻隻能處理一個請求
ThreadWorker
繼承自
Worker
, 它也覆寫了
init_process
方法和
run
方法
def init_process(self):
self.tpool = self.get_thread_pool()
self.poller = selectors.DefaultSelector()
self._lock = RLock()
super().init_process()
def enqueue_req(self, conn):
conn.init()
# submit the connection to a worker
fs = self.tpool.submit(self.handle, conn)
self._wrap_future(fs, conn)
def accept(self, server, listener):
try:
sock, client = listener.accept()
# initialize the connection object
conn = TConn(self.cfg, sock, client, server)
self.nr_conns += 1
# enqueue the job
self.enqueue_req(conn)
except EnvironmentError as e:
if e.errno not in (errno.EAGAIN, errno.ECONNABORTED,
errno.EWOULDBLOCK):
raise
def run(self):
# ....
我們可以看到
init_process
建立了一個線程池,
accept
隻是把新接收到的連接配接放到
ThreadPool
的隊列中就結束了
- 如果你的應用對記憶體覆寫區有需求, 用
模式(gthread worker class)而不是其他的模式能獲得更好的性能, 因為每個 worker 都預先加載了你的應用, worker 中的不同線程共享相同的記憶體空間, 這裡的代價就是會有額外的 CPU 消耗
threads
我們來看一個示例
gunicorn --workers 1 --worker-class gthread --threads 2 mysite.wsgi
這個
--threads
參數隻會影響到
gthread
worker class, 其他的 worker 是不受這個參數影響的
每一個 worker 都會初始化一個大小為
--threads
的線程池 (
ThreadPool
), 每當主線程接收到一個 socket 對象時, 這個 socket 被推倒隊列中, 之後
ThreadPool
中的線程會從隊列中取出對應的 socket, 并從 socket 接收資訊并調用 django 應用中的對應的接口函數
tornado
最後一個 worker class 是
tornado
, 代碼比較簡潔
# gunicorn/gunicorn/workers/gtornado.py
def init_process(self):
# IOLoop 在 fork 之後就不能使用了
# 開啟多程序的情況下, 每個程序都應該有自己的 IOLoop
# 如果在 fork 之前就存在了 IOLoop, 我們應該清理掉它
IOLoop.clear_current()
super().init_process()
def run(self):
# ...
run
方法初始化了
gunicorn
所需的一些監控函數 , 并啟動了一個 tornado 服務執行個體, 把之前監聽的端口綁定到新啟動的 tornado 執行個體中, 之後就啟動事件循環 (
IOLoop
)
更多資料
- what are you using gevent for?
- Comparing gevent to eventlet
- Better performance by optimizing Gunicorn config