1 WSGI
WSGI是 Web Server Gateway Interface 的縮寫,它是 Python應用程式(application)或架構(如 Django)和 Web伺服器之間的一種接口,已經被廣泛接受。
它是一種協定,一種規範,其是在 PEP 333提出的,并在 PEP 3333 進行補充(主要是為了支援 Python3.x)。這個協定旨在解決衆多 web 架構和web server軟體的相容問題。有了WSGI,你不用再因為你使用的web 架構而去選擇特定的 web server軟體。WSGI規定了伺服器怎麼把請求資訊告訴給應用,應用怎麼把執行情況回傳給伺服器,這樣的話,伺服器與應用都按一個标準辦事,隻要實作了這個标準,伺服器與應用随意搭配就可以,靈活度大大提高。
相對正式的WSGI定義:
WSGI 接口有服務端和應用端兩部分,服務端也可以叫網關端,應用端也叫架構端。服務端調用一個由應用端提供的可調用對象。如何提供這個對象,由服務端決定。例如某些伺服器或者網關需要應用的部署者寫一段腳本,以建立伺服器或者網關的執行個體,并且為這個執行個體提供一個應用執行個體。另一些伺服器或者網關則可能使用配置檔案或其他方法以指定應用執行個體應該從哪裡導入或擷取。
WSGI 規範了些什麼,下圖能很直覺的說明:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIwczX0xiRGZkRGZ0Xy9GbvNGL2EzXlpXazxSNCh0YohmMYBDayoFc1ITYr5kMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL3ETOwMjN0ETMyITMwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
上圖也可以看出一個HTTP請求的過程可以分為兩個階段,第一階段是從用戶端到WSGI Server,第二階段是從WSGI Server 到WSGI Application。
常見的web應用架構有:Django,Flask,sanic等;
常用的web伺服器軟體有:uWSGI,Gunicorn,Gevent等。
web server要履行的任務:
- 接收HTTP請求;
- 解析HTTP請求;
- 準備 environ請求參數;
- 定義 start_response 函數;
- 組裝響應頭和相應體傳回給用戶端。
web application要履行的責任:
- 解析伺服器發來的請求資訊;
- 根據請求資訊,進行業務處理;
- 傳回所需要的資料。
WSGI 對于 application 對象有如下三點要求
- 必須是一個可調用的對象;
- 接收兩個必選參數environ、start_response;
- 傳回值必須是可疊代對象,用來表示http body。
2 python web服務一般部署方式
2.1 兩級結構
兩級結構即
wsgi server + wsgi application
,在這種結構裡,
uWSGI/gevent/gunicorn
作為伺服器,它用到了HTTP協定以及wsgi協定,flask應用作為application,實作了wsgi協定。當有用戶端發來請求,uWSGI接受請求,調用flask app得到相應,之後相應給用戶端。
這裡說一點,通常來說,Flask等web架構會自己附帶一個wsgi伺服器(這就是flask應用可以直接啟動的原因),但是這隻是在開發階段用到的,在生産環境是不夠用的,是以用到了uwsgi/gevent/gunicorn這些性能高的wsgi伺服器。
2.2 三級結構
三級結構即
nginx+wsgi server+wsgi application
,這種結構裡,
wsgi server
作為中間件,它用到了uwsgi協定(與nginx通信),wsgi協定(調用Flask app)。
當有用戶端發來請求,nginx先做處理,判斷是否通路的是靜态資源,如果是靜态資源,讀取靜态檔案,傳回靜态檔案給client;如果是通路的是動态資源,交給uwsgi伺服器,
wsgi server
根據自身uwsgi和WSGI協定,找到對應的
web application
中的應用程式做邏輯處理,将傳回值發送到
wsgi server
,然後
wsgi server
再傳回給nginx,最後nginx将傳回值傳回給client進行渲染展示給使用者。
多了一層反向代理有什麼好處?
提高web server性能(
wsgi server
處理靜态資源能力不如nginx;nginx會在收到一個完整的http請求後再轉發給
wsgi server
);
nginx可以做負載均衡(前提是有多個伺服器),保護了實際的web伺服器(用戶端是和nginx互動而不是
wsgi server
)。
2 WSGI Server
2.1 Gunicorn
官方位址
官方文檔
Gunicorn“Green Unicorn”是一個用于UNIX的Python WSGI HTTP伺服器。Gunicorn伺服器與各種web架構廣泛相容,實作簡單,伺服器資源少,速度相當快。
2.1.1 安裝
要求python版本号 ≥ \geq ≥ 3.5;
$ pip install gunicorn
如果需要以異步
worker
使用
Gunicorn
,還需要安裝
greenlet
、
eventlet
和
gevent
。
$ pip install greenlet # Required for both
$ pip install eventlet # For eventlet workers
$ pip install gunicorn[eventlet] # Or, using extra
$ pip install gevent # For gevent workers
$ pip install gunicorn[gevent] # Or, using extra
gunicorn[eventlet]
- Eventlet-based greenlets workers
gunicorn[gevent]
- Gevent-based greenlets workers
gunicorn[gthread]
- Threaded workers
gunicorn[tornado]
- Tornado-based workers, not recommended
gunicorn[setproctitle]
- Enables setting the process name,如果運作了多個Gunicorn執行個體,可以通過setproctitle給每個執行個體命名。
也可以通過
pip install gunicorn[gevent,setproctitle]
方式,同時安裝多個包。
2.1.2 運作Gunicorn
Gunicorn可以通過指令行單獨使用,也可以和web application架構聯合使用。
安裝Gunicorn之後,就可以在指令行中使用
gunicorn
指令了,基本用法是:
[WSGI_APP]
:
格式為
$(MODULE_NAME):$(VARIABLE_NAME)
,
MODULE_NAME
是子產品名,
VARIABLE_NAME
是子產品中WSGI可調用對象。例如,定義
test.py
中的代碼:
def app(environ, start_response):
"""Simplest possible application object"""
data = b'Hello, World!\n'
status = '200 OK'
response_headers = [
('Content-type', 'text/plain'),
('Content-Length', str(len(data)))
]
start_response(status, response_headers)
return iter([data])
可以在指令行中使用
gunicorn --workers=2 test:app
運作該服務。
VARIABLE_NAME
也可以是一個普通函數,但函數的傳回值一定要是一個application,那麼就需要按照下述方式進行調用:
def create_app():
app = FrameworkApp()
...
return app
調用:
#注意必須加括号
$ gunicorn --workers=2 'test:create_app()'
可以給函數傳遞位置和關鍵字參數,但是建議通過環境變量指定配置檔案來傳遞參數。
[OPTIONS]
:
-
,指定配置檔案,格式可以是-c CONFIG,--config=CONFIG
、$(PATH)
或file:$(PATH)
,預設是從Gunicorn啟動位置讀取python:$(MODULE_NAME)
;gunicorn.conf.py
-
,指定綁定的-b BIND, --bind=BIND
,可以是socket
、$(HOST)
、$(HOST):$(PORT)
或fd://$(FD)
,IP符合unix:$(PATH)
格式;$(HOST)
-
,指定worker程序的數量,一般每個cpu核對應-w WORKERS, --workers=WORKERS
個worker程序;2 - 4
-
,指定worker程序的類型,可以是-k WORKERCLASS, --worker-class=WORKERCLASS
、sync
、eventlet
、gevent
和tornado
,預設是gthread
;sync
-
,如果安裝了-n APP_NAME, --name=APP_NAME
,可以設定在程序表中顯示的Gunicorn 程序的名稱。setproctitle
這裡記錄一個bug:RuntimeError: Working outside of request context. with gunicorn
解決辦法:
格式為
$(MODULE_NAME):$(VARIABLE_NAME)
,
MODULE_NAME
是子產品名,
VARIABLE_NAME
是子產品中WSGI可調用對象,
app(environ, start_response)
是可調用對象,
app
是wsgi可調用對象,但
index
函數不是。
2.2.3 設計原理
2.2.3.1 server module
Gunicorn基于pre-fork worker module進行實作,這表示通過一個master程序管理一組worker程序。master程序無需了解用戶端的任何資訊,所有的請求和響應都是由worker程序處理的。
2.2.3.2 master
master程序接收不同的程序信号并予以響應來管理各worker程序,接收的信号包括TTIN、TTOU和CHLD。TTIN表示需要增加worker程序的數量,TTOU表示需要減少worker程序的數量,CHLD表示有一個child程序異常終止了,master程序需要自動重新開機該child程序。
2.2.3.3 sync worker
預設的worker類型是同步worker,一次隻處理一個請求。這個模型是最容易解釋的,因為任何錯誤最多隻會影響一個請求。盡管正如我們在下面所描述的,一次隻處理一個請求需要一些關于應用程式程式設計方式的假設。
sync
worker不支援持續連接配接,即便在應用的頭資訊中添加了
Keep-Alive
,傳回了響應資訊一個連接配接就被關閉了。
2.2.3.4 Async worker
可用的異步worker基于greenlet(通過Eventlet和Gevent)。greenlet是Python協程庫。一般來說,應用程式可以在不做任何更改的情況下使用這些worker類。
常用的Async worker是
gevent
;
2.2.3.5 tornado
tornado worker可以用來編寫使用Tornado架構的應用程式。盡管Tornado workers能夠為WSGI應用程式提供服務,但并不推薦。
2.2.3.6 AsyncIO worker
gthread worker是線程輔助worker。它接受主循環中的連接配接,接受的連接配接作為連接配接作業添加到線程池中。以keep alive模式工作時,連接配接被放回循環中等待event。如果直到keep alive逾時都沒有等到任何event觸發,則關閉該連接配接。
還可以使用
aiohttp
的
web.Application
API和使用
aiohttp.worker.GunicornWebWorker
worker。
2.2.3.7 選擇合适的worker類型
預設的同步worker假設應用程式在CPU和網絡帶寬方面是資源受限的。一般來說,這意味着您的應用程式不應該執行任何需要未定義時間的操作。對網際網路的請求就是一個需要花費大量時間的例子。在某個時刻,外部網絡可能會出現故障,請求将堆積在您的伺服器上。是以,從這個意義上說,任何向api送出請求的web應用程式都将受益于異步worker。
這種資源受限的假設就是為什麼我們需要在預設配置Gunicorn前面使用緩沖代理的原因。如果将同步worker暴露在internet上,那麼DOS攻擊隻會建立一個向伺服器傳輸資料的負載。
需要異步worker的例子:
- 需要長期阻塞的應用;
- 直接請求網際網路;
- 流式請求和響應;
- 長輪詢;
- web sockets;
- Comet。
2.2.3.8 worker數量
不要根據請求的數量設定worker的數量,對于每秒數百/千的請求量,Gunicorn隻需要4 - 12個worker程序。Gunicorn處理請求時,依賴作業系統進行負載均衡,一般建議啟動時建議使用
(2 x $num_cores) + 1
個worker,雖然不太科學,但這個公式是基于這樣的假設:對于給定的核心,一個worker程序将從套接字讀寫,而另一個worker程序正在處理請求。顯然,不同的硬體和應用程式将會對應不同的最佳worker數量。我們的建議是在應用程式加載時使用TTIN和TTOU信号從上述猜測開始進行自适應調整。
永遠記住,worker太多是一個麻煩。某個點之後,worker程序将開始猛擊系統資源,進而降低整個系統的吞吐量。
2.2.3.9 線程數量
自Gunicorn 19開始,threads選項可用于處理多個線程中的請求。使用線程假定使用gthread worker。線程的一個好處是,在通知主程序某個請求未當機且不應終止時,請求的處理時間可能長于worker的逾時時間。根據系統的不同,使用多個線程、多個worker程序或混合使用可能會産生最佳結果。例如,當使用線程時,CPython的性能可能不如Jython,因為每個線程的實作方式不同。使用線程而不是程序是減少Gunicorn記憶體占用的一個好方法,同時仍然允許使用重新加載信号進行應用程式更新,因為應用程式代碼将在工作程序之間共享,但隻在工作程序中加載(與使用預加載設定時不同,預加載設定在主程序中加載代碼)。
2.2.4 可配置項
完整配置官方文檔
配置檔案:
-
config:
指令行:
or-c CONFIG
--config CONFIG
;
預設:
'./gunicorn.conf.py'
;
格式:
,PATH
, orfile:PATH
python:MODULE_NAME
;
含義:設定配置檔案;
注意點:從Gunicorn 19.4開始,從python module加載配置項需要
字首。python:
-
wsgi_app
WSGI應用路徑,格式為
;$(MODULE_NAME):$(VARIABLE_NAME)
調試:
-
reload
設定為
則在代碼改變後重新開機worker,預設為True
;False
-
reload_engine
和reload搭配使用;
-
spew
按照一個調試函數,輸出伺服器執行的每一行,預設
;False
日志:
-
accesslog
通路的日志檔案;
-
access_log_format
日志格式,預設
,具體含義參考官方文檔;'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
-
errorlog
記錄錯誤資訊的日志檔案;
-
loglevel
日志級别,可用的有
、debug
、info
、warning
和error
;critical
程序名:
-
proc_name
程序名,預設是
;gunicorn
SSL:
Security:
-
limit_request_line
HTTP請求的最大尺寸,機關是
bytes
,預設4094。
此參數用于限制用戶端HTTP請求行的允許大小。由于請求行由HTTP方法、URI和協定版本組成,是以該指令對伺服器上請求所允許的請求URI的長度進行了限制。伺服器需要此值足夠大,以容納其任何資源名稱,包括GET請求的查詢部分中可能傳遞的任何資訊。值是從0(無限制)到8190的數字。 這個參數可用來抵禦DDOS攻擊。
-
limit_request_fields
限制請求中HTTP中header的數量,預設100。
此參數用于限制請求中的header的數量,以防止DDOS攻擊。與limit_request_line一起使用,可以提供更大的安全性。預設情況下,此值為100,不能大于32768。
-
limit_request_field_size
限制HTTP請求頭字段的允許大小。
值是正數或0。将其設定為0将允許不受限制的頭字段大小,預設8190。
Server Socket:
-
bind
設定綁定的socket,預設是
,如果設定了PORT變量,預設是127.0.0.1:8000
0.0.0.0:8000
;
可以綁定多個位址,如:
$ gunicorn -b 127.0.0.1:8000 -b [::1]:8000 test:app
-
backlog
挂起的最大連接配接數,預設是2048;
這是指可以等待服務響應的請求的數量。超過此數字将導緻用戶端在嘗試連接配接時出錯。隻影響負載較大的伺服器。必須是正整數。一般設定在64-2048範圍内。
Worker Processes:
-
workers
處理請求的worker程序的數量,預設是1;
通常設定為
的範圍,需要根據自己的實際情況進行調整;(2 - 4) *$(NUM_CORES)
-
worker_class
worker類别,使用指令行時的參數為
,預設-k
sync
;
參考2.2.3.7節選擇合适的worker類型;
-
threads
使用
gthread
模式時,設定各worker的線程數,預設是1;
通常設定為
(2 - 4) *$(NUM_CORES)
的範圍,需要根據自己的實際情況進行調整;
注意,如果使用的是
模式且設定sync
值大于1,那麼會自動切換到threads
模式。gthread
-
worker_connections
同時存在的用戶端的最大數目,隻影響
和eventlet
worker類型;gevent
-
max_requests
一個worker重新開機前能夠處理的最多請求數量;預設為0,表示關閉自動重新開機,該方法用來限制記憶體溢出。
-
max_requests_jitter
在max_requests基礎上添加的最大抖動量,即添加了一個随機抖動值,取值範圍在
,目的是避免所有的worker同時重新開機。randint(0, max_requests_jitter)
-
timeout
超過該設定時間沒有響應的worker會被重新開機,預設時間是30,機關是秒;
-
graceful_timeout
重新開機逾時時間,預設是30,機關是秒;
在接收到重新開機信号後,worker有這麼多時間來完成服務請求。逾時後仍活着的worker(從收到重新開機信号開始)被強制殺死。
-
keepalive
在一個Keep-Alive連接配接中,燈帶響應的秒數。預設是2;
對于直接連接配接到用戶端的伺服器(例如,當您沒有單獨的負載平衡器時),通常設定在1-5秒的範圍内。當Gunicorn部署在負載平衡器後面時,将其設定為更高的值通常是有意義的。
worker不支援持久連接配接,會忽略掉該參數。sync
2.2.5 部署Gunicorn
本節原始位址
強烈建議在代理伺服器之後使用Gunicorn;
Nginx配置:
雖然有多個HTTP代理伺服器,但這裡還是強烈建議使用Nginx。如果您選擇其他的代理伺服器,則需要確定在使用預設Gunicorn workers時,它會緩沖慢速用戶端。沒有這個緩沖區,Gunicorn很容易受到拒絕服務攻擊。您可以使用Hey檢查代理是否正常工作。
Nginx的示例配置如下面所示:
worker_processes 1;
user nobody nogroup;
# 'user nobody nobody;' for systems with 'nobody' as a group instead
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024; # increase if you have lots of clients
accept_mutex off; # set to 'on' if nginx worker_processes > 1
# 'use epoll;' to enable for Linux 2.6+
# 'use kqueue;' to enable for FreeBSD, OSX
}
http {
include mime.types;
# fallback in case we can't determine a type
default_type application/octet-stream;
access_log /var/log/nginx/access.log combined;
sendfile on;
upstream app_server {
# fail_timeout=0 means we always retry an upstream even if it failed
# to return a good HTTP response
# for UNIX domain socket setups
server unix:/tmp/gunicorn.sock fail_timeout=0;
# for a TCP configuration
# server 192.168.0.7:8000 fail_timeout=0;
}
server {
# if no Host match, close the connection to prevent host spoofing
listen 80 default_server;
return 444;
}
server {
# use 'listen 80 deferred;' for Linux
# use 'listen 80 accept_filter=httpready;' for FreeBSD
listen 80;
client_max_body_size 4G;
# set the correct host(s) for your site
server_name example.com www.example.com;
keepalive_timeout 5;
# path for static files
root /path/to/app/current/public;
location / {
# checks for static file, if not found proxy to app
try_files $uri @proxy_to_app;
}
location @proxy_to_app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://app_server;
}
error_page 500 502 503 504 /500.html;
location = /500.html {
root /path/to/app/current/public;
}
}
}
如果您希望能夠處理流式請求/響應或其他奇特的功能(如Comet、長輪詢或websockets),則需要關閉代理緩沖。執行此操作時,必須使用一個異步worker類運作。
要關閉緩沖區,隻需要在
location
中添加
proxy_buffering off;
即可,如下所示:
...
location @proxy_to_app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_buffering off;
proxy_pass http://app_server;
}
...
建議将協定資訊傳遞給Gunicorn。許多web架構使用這些資訊來生成url。如果沒有這些資訊,應用程式可能會在“https”響應中錯誤地生成“http”url,進而導緻混合内容警告或應用程式損壞。要将Nginx配置為傳遞适當的header,需要在
location
中添加
proxy_set_header
設定項:
...
proxy_set_header X-Forwarded-Proto $scheme;
...
如果Nginx和Gunicorn運作在不同的機器上,那麼就需要告訴Gunicorn信任由Nginx發送來的
X-Forwarded-*
頭資訊。預設情況下,為了防止惡意欺騙,Gunicorn隻信任來自于本機的連接配接。如下面所示,設定多個信任IP:
$ gunicorn -w 3 --forwarded-allow-ips="10.170.3.217,10.170.3.220" test:app
當Gunicorn主機與外部網絡完全防火牆連接配接,使得所有連接配接都來自受信任的代理(例如Heroku)時,此值可以設定為’*’。如果到Gunicorn的連接配接可能來自不受信任的代理或直接來自用戶端,則使用此值可能是危險的,因為應用程式可能會被欺騙,通過不安全的連接配接提供僅限SSL的内容。
Gunicorn 19引入了一個關于如何處理遠端位址的突破性變化。在Gunicorn 19之前,它被設定為X-Forwarded-For的值(如果從受信任的代理接收)。但是,這不符合RFC3875,這就是為什麼遠端位址現在是代理的IP位址而不是實際使用者。
要使通路日志訓示代理時的實際使用者IP,請将通路日志格式設定為包含X-Forwarded-For的格式。例如,此格式使用X-Forwarded-For代替遠端位址:
如果将Gunicorn綁定到一個UNIX Socket而不是一個TCP的
host:port
元組,那麼
REMOTE_ADDR
将為空。
服務螢幕:
請注意:如果使用了下面所列的服務螢幕,請不要将Gunicorn設定為守護程序模式。這些服務螢幕希望自己所監視的程序需要被監視(如果監視的是守護程序,程序從系統啟動就開啟,直到系統停止才停止,監視這樣的程序沒有意義)。
如果需要将不同的程序的代碼對應到不同的顯示卡,需要使用server hook配合本地日志檔案進行實作,在配置檔案中可以采用:
import pickle
base_path = './datasets/logs/Gunicorn.logs'
def on_starting(server):
DeviceFile = base_path+os.sep+'DeviceAllocate.log'
DeviceAllocate = {}
ProcessAllocate = {}
if not os.path.exists(DeviceFile):
nDevice = 8#pynvml.nvmlDeviceGetCount()
for index in range(nDevice):
DeviceAllocate[str(index)] = 0
file = open(DeviceFile,'wb')
pickle.dump([DeviceAllocate,ProcessAllocate],file)
file.close()
print("啟動master程序")
else:
os.remove(DeviceFile)
def post_fork(server,worker):
import pynvml,fcntl
from fcntl import LOCK_EX,LOCK_UN
pynvml.nvmlInit()
DeviceFile = base_path+os.sep+'DeviceAllocate.log'
file = open(DeviceFile,'rb')
# fcntl.flock(file.fileno( ),LOCK_EX)
info = pickle.load(file)
DeviceAllocate = info[0]
ProcessAllocate = info[1]
key = min(DeviceAllocate,key=lambda x: DeviceAllocate[x])
os.environ["CUDA_VISIBLE_DEVICES"]=key
DeviceAllocate[key] += 1
ProcessAllocate[worker.pid] = key
# print('建立程序:%s ,對應顯示卡 %s' %(worker.pid,key))
# print([DeviceAllocate,ProcessAllocate])
file = open(DeviceFile,'wb')
pickle.dump([DeviceAllocate,ProcessAllocate],file)
# fcntl.flock(file.fileno( ),LOCK_UN)
file.close()
def child_exit(server,worker):
import fcntl
from fcntl import LOCK_EX,LOCK_UN
DeviceFile = base_path+os.sep+'DeviceAllocate.log'
file = open(DeviceFile,'rb')
# print('acquire lock')
# fcntl.flock(file.fileno( ),LOCK_EX)
info = pickle.load(file)
DeviceAllocate = info[0]
ProcessAllocate = info[1]
file.close()
# print([DeviceAllocate,ProcessAllocate])
device = ProcessAllocate[worker.pid]
DeviceAllocate[device] -= 1
del ProcessAllocate[worker.pid]
# print('退出對應于顯示卡%s的程序:%s' %(device,worker.pid))
file = open(DeviceFile,'wb')
pickle.dump([DeviceAllocate,ProcessAllocate],file)
# fcntl.flock(file.fileno( ),LOCK_UN)
# print('release lock')
file.close()
def on_exit(server):
if os.path.exists(base_path+os.sep+'DeviceAllocate.log'):
os.remove(base_path+os.sep+'DeviceAllocate.log')
print('關閉所有程序')
Gaffer:
官方位址
示例:
[process:gunicorn]
cmd = gunicorn -w 3 test:app
cwd = /path/to/project
Runit:
官方位址
示例:
#!/bin/sh
GUNICORN=/usr/local/bin/gunicorn
ROOT=/path/to/project
PID=/var/run/gunicorn.pid
APP=main:application
if [ -f $PID ]; then rm $PID; fi
cd $ROOT
exec $GUNICORN -c $ROOT/gunicorn.conf.py --pid=$PID $APP
Supervisor:
官方位址
示例:
[program:gunicorn]
command=/path/to/gunicorn main:application -c /path/to/gunicorn.conf.py
directory=/path/to/project
user=nobody
autostart=true
autorestart=true
redirect_stderr=true
Upstart:
參考部落格
配置“myapp”應用示例:
/etc/init/myapp.conf:
description "myapp"
start on (filesystem)
stop on runlevel [016]
respawn
setuid nobody
setgid nogroup
chdir /path/to/app/directory
exec /path/to/virtualenv/bin/gunicorn myapp:app
Systemd:
官方位址
在linux系統上開始流行的一個工具是Systemd。它是一個系統服務管理器,允許嚴格的程序管理、資源和權限控制。
下面是使用systemd為傳入的Gunicorn請求建立unix套接字的配置檔案和說明。Systemd将監聽這個套接字并自動啟動gunicorn以響應通信量。本節後面将介紹如何配置Nginx以将web流量轉發到新建立的unix套接字:
/etc/systemd/system/gunicorn.service:
[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target
[Service]
Type=notify
# the specific user that our service will run as
User=someuser
Group=someuser
# another option for an even more restricted service is
# DynamicUser=yes
# see http://0pointer.net/blog/dynamic-users-with-systemd.html
RuntimeDirectory=gunicorn
WorkingDirectory=/home/someuser/applicationroot
ExecStart=/usr/bin/gunicorn applicationname.wsgi
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
[Install]
WantedBy=multi-user.target
/etc/systemd/system/gunicorn.socket:
[Unit]
Description=gunicorn socket
[Socket]
ListenStream=/run/gunicorn.sock
# Our service won't need permissions for the socket, since it
# inherits the file descriptor by socket activation
# only the nginx daemon will need access to the socket
SocketUser=www-data
# Optionally restrict the socket permissions even more.
# SocketMode=600
[Install]
WantedBy=sockets.target
2.2 Gevent
官方位址
Gevent是一個基于協程的python網絡庫,在libev或libuv事件循環的基礎上使用greenlet提供高層同步API。支援協程的并發式操作,也可以對外當成wsgi server使用。
下面的代碼展示了如何以并行的方式運作多個任務:
>>> import gevent
>>> from gevent import socket
>>> urls = ['www.google.com', 'www.example.com', 'www.python.org']
>>> jobs = [gevent.spawn(socket.gethostbyname, url) for url in urls]
>>> _ = gevent.joinall(jobs, timeout=2)
>>> [job.value for job in jobs]
['74.125.79.106', '208.77.188.166', '82.94.164.162']
在設定了所有的任務之後,gevent在等待所有的任務完成,最長等待時間為2秒。通過
jobs
的
value
屬性來收集傳回值。
gevent.socket.gethostbyname()
和标準的
socket.gethostbyname()
接口含義一緻,但能夠以不阻塞解釋器的方式并行運作多個任務。
Monky Patch:
上面的代碼中使用
gevent.socket
進行套接字操作,如果使用的是标準的socket包,因為需要序列化處理DNS請求,總的處理時間會是上面并行方式的三倍。在greenlet中使用标準socket子產品使gevent變得毫無意義,那麼建構在socket之上的現有子產品和包(包括urllib之類的标準庫子產品)自然也就無法并行處理了。
這就是mokey patch的作用了,
gevent.monky
會将标準socket中的函數和類替換為協程版本,這樣,即使不知道gevent的子產品也可以從多greenlet環境中獲益。
例如:
>>> from gevent import monkey; monkey.patch_socket()
>>> import requests # it's usable from multiple greenlets now
當然,标準庫還有其他幾個部分可以阻止整個解釋器并導緻序列化處理行為。gevent也提供了其中許多的合作版本。它們可以通過單個函數獨立地進行修補,但是大多數使用monkey修補的程式都希望使用gevent.monkey.patch()函數一次性修複全部的功能,如下代碼所示:
>>> from gevent import monkey; monkey.patch_all()
>>> import subprocess # it's usable from multiple greenlets now
提示:打猴子更新檔時,建議在生命周期内盡早打。如果可能的話,猴子更新檔應該在第一行執行。如果猴子更新檔打晚了,特别是如果本機線程已經建立,atexit或信号處理程式已經安裝,或套接字已經建立,可能會導緻不可預知的結果,包括意外的LoopExit錯誤。
事件循環:
gevent不會阻塞和等待socket操作完成(一種稱為輪詢的技術),而是安排作業系統傳遞一個事件,例如讓它知道何時從socket中讀取的資料已經到達。這樣,gevent就可以繼續運作另一個greenlet了,也許它本身已經為它準備好了一個事件。注冊事件并在事件到達時對其作出反應的重複過程就是事件循環。
與其他網絡庫不同,盡管gevent的方式與eventlet類似,但它在一個專用的greenlet中隐式地啟動事件循環,沒有必要顯式調用run()或dispatch()函數。當gevent的API中的函數想要阻塞時,它會獲得gevent.hub.Hub執行個體(運作事件循環的特殊greenlet)并切換到它(據說greenlet将控制權讓給了中心)。如果還沒有Hub執行個體,則會自動建立一個。
每一個線程都有自己的Hub,這就允許多個線程使用gevent阻塞式API。
協作式多任務:
所有的greenlet都在同一個OS線程中運作,并且是協同排程的。這意味着,除非某個特定的greenlet放棄控制(通過調用将切換到Hub的阻塞函數),否則其他greenlet将沒有機會運作。對于綁定I/O的應用程式來說,這通常不是問題,但是在執行CPU密集型操作時,或者在調用繞過事件循環的阻塞I/O函數時,應該注意到這一點。甚至一些表面上協作的函數,比如gevent.sleep(),在某些情況下都可以暫時優先于等待的I/O操作。
在大多數情況下,greenlet中共享的對象的通路不必要進行同步的(因為通路控制通常是顯式的),是以像gevent.lock.BoundedSemaphore, gevent.lock.RLock檔案以及gevent.lock.Semaphore檔案類雖然存在,但并不經常使用。線程和多程序的其他抽象在協作世界中仍然有用:
- Event允許喚醒一組通過調用wait函數處于等待狀态的greenlet;
- AsyncResult和Event功能想死,但是允許給等待的greenlet傳遞值或異常資訊;
- 也提供了Queue和JoinableQueue對象。
輕量級僞線程:
通過建立一個新的greenlet對象并且調用其start函數建立一個協程。一旦已建立的greenlet對象不占用cpu之後,就可以通過新建立的greenlet的start函數啟動其執行。
Greenlet對象中有用的關鍵函數:
- join – 等待greenlet執行完成;
- kill – 中斷greenlet的執行;
- get – 擷取greenlet的傳回值或重新抛出關閉協程的異常。
>>> from gevent import Greenlet
>>> g = Greenlet(gevent.sleep, 4)
>>> g.start()
>>> g.kill()
>>> g.dead
True
WSGI Server:
gevent提供了兩種WSGI server,分别是:
- gevent.wsgi.WSGIServer
- gevent.pywsgi.WSGIServer
使用舉例:
gevent + flask
from gevent import monkey
monkey.patch_all()
from gevent import pywsgi
app = flask(__name__)
if __name__ == "__main__":
server = pywsgi.WSGIServer(('0.0.0.0',5000),app)
server.serve_forever()
gevent啟動多程序WSGI:
多程序模式:
from gevent import monkey
monkey.patch_all()
from gevent.pywsgi import WSGIServer
from multiprocessing import cpu_count, Process
from bottle import Bottle
app = Bottle()
@app.get("/")
def index():
return {"hello": "world"}
server = WSGIServer(('', 8000), app, log=None)
server.start()
def serve_forever():
server.start_accepting()
server._stop_event.wait()
if __name__ == "__main__":
# server.serve_forever()
# 啟動的程序數為cpu個數
for i in range(cpu_count()):
p = Process(target=serve_forever)
p.start()
單程序模式:
- 屏蔽 server.start();
- 打開 server.serve_forever();
- 屏蔽 最後三行
2.3 uWSGI
官方位址
參考文檔
參考:
花了兩個星期,我終于把 WSGI 整明白了
WSGI協定的作用和實作原理詳解
說說我對 WSGI 的了解
什麼是 web 架構?
gevent啟動多程序WSGI