天天看點

gunicorn workers 差別(sync/eventlet/gevent/thread/tornado)gunicorn workers 差別目錄

gunicorn workers 差別

以後都在 github 更新,請戳 gunicorn workers 差別

我們在 第一篇 裡已經了解過 gunicorn 的

SyncWorker

原理, 現在我們來看下其他的 workers 是如何工作的

gunicorn workers 差別(sync/eventlet/gevent/thread/tornado)gunicorn 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
           
gunicorn workers 差別(sync/eventlet/gevent/thread/tornado)gunicorn workers 差別目錄

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, 更簡單的實作以及更好的性能

他們的差別有這些

  1. gevent 底層基于 libevent(1.0 版本之後, gevent 基礎基于 libev 和 c-ares.)
    • 信号處理和事件循環(event loop) 綁定
    • 其它基于 libevent 編寫的庫可以和你的應用通過同個事件循環(event loop)進行綁定
    • DNS 查詢是通過原生異步調用完成, 而不是開啟一個線程池之通過阻塞式調用完成
    • WSGI 服務是通過 libevent 的内置 HTTP 服務搭建, 速度 非常快.
  2. gevent 的接口和标準庫的常用接口保持一緻
  3. 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
           
gunicorn workers 差別(sync/eventlet/gevent/thread/tornado)gunicorn workers 差別目錄

使用

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

的隊列中就結束了

  1. 如果你的應用對記憶體覆寫區有需求, 用

    threads

    模式(gthread worker class)而不是其他的模式能獲得更好的性能, 因為每個 worker 都預先加載了你的應用, worker 中的不同線程共享相同的記憶體空間, 這裡的代價就是會有額外的 CPU 消耗

我們來看一個示例

gunicorn --workers 1 --worker-class gthread --threads 2 mysite.wsgi
           

這個

--threads

參數隻會影響到

gthread

worker class, 其他的 worker 是不受這個參數影響的

gunicorn workers 差別(sync/eventlet/gevent/thread/tornado)gunicorn workers 差別目錄

每一個 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