【OpenStack源碼分析之四】WSGI與Nova API服務啓動

前言

前文已經介紹了RPC Server的啓動,而Nova API啓動的是WSGI服務,所以先介紹一下WSGI。

WSGI

Web服務器網關接口(Python Web Server Gateway Interface,縮寫爲WSGI)是爲Python語言定義的Web服務器和Web應用程序或框架之間的一種簡單而通用的接口。自從WSGI被開發出來以後,許多其它語言中也出現了類似接口。WSGI是作爲Web服務器與Web應用程序或應用框架之間的一種低級別的接口,以提升可移植Web應用開發的共同點。WSGI是基於現存的CGI標準而設計的。

WSGI區分爲兩個部份:一爲“服務器”或“網關”,另一爲“應用程序”或“應用框架”。在處理一個WSGI請求時,服務器會爲應用程序提供環境資訊及一個回呼函數(Callback Function)。當應用程序完成處理請求後,透過前述的回呼函數,將結果回傳給服務器。所謂的 WSGI 中間件同時實現了API的兩方,因此可以在WSGI服務和WSGI應用之間起調解作用:從WSGI服務器的角度來說,中間件扮演應用程序,而從應用程序的角度來說,中間件扮演服務器。“中間件”組件可以執行以下功能:

  • 重寫環境變量,根據目標URL,將請求消息路由到不同的應用對象。
  • 允許在一個進程中同時運行多個應用程序或應用框架。
  • 負載均衡和遠程處理,通過在網絡上轉發請求和響應消息。
  • 進行內容後處理,例如應用XSLT樣式表。

以前,如何選擇合適的Web應用程序框架成爲困擾Python初學者的一個問題,這是因爲,一般而言,Web應用框架的選擇將限制可用的Web服務器的選擇,反之亦然。那時的Python應用程序通常是爲CGI,FastCGI,mod_python中的一個而設計,甚至是爲特定Web服務器的自定義的API接口而設計的。WSGI沒有官方的實現, 因爲WSGI更像一個協議。只要遵照這些協議,WSGI應用(Application)都可以在任何服務器(Server)上運行, 反之亦然。WSGI就是Python的CGI包裝,相對於Fastcgi是PHP的CGI包裝。

WSGI將 web 組件分爲三類: web服務器,web中間件,web應用程序, wsgi基本處理模式爲 : WSGI Server -> (WSGI Middleware)* -> WSGI Application 。

WSGI Server/gateway

wsgi server可以理解爲一個符合wsgi規範的web server,接收request請求,封裝一系列環境變量,按照wsgi規範調用註冊的wsgi app,最後將response返回給客戶端。文字很難解釋清楚wsgi server到底是什麼東西,以及做些什麼事情,最直觀的方式還是看wsgi server的實現代碼。以python自帶的wsgiref爲例,wsgiref是按照wsgi規範實現的一個簡單wsgi server。它的代碼也不復雜。

這裏寫圖片描述

  • 服務器創建socket,監聽端口,等待客戶端連接。
  • 當有請求來時,服務器解析客戶端信息放到環境變量environ中,並調用綁定的handler來處理請求。
  • handler解析這個http請求,將請求信息例如method,path等放到environ中。
  • wsgi handler再將一些服務器端信息也放到environ中,最後服務器信息,客戶端信息,本次請求信息全部都保存到了環境變量environ中。
  • wsgi handler 調用註冊的wsgi app,並將environ和回調函數傳給wsgi app
  • wsgi app 將reponse header/status/body 回傳給wsgi handler
  • 最終handler還是通過socket將response信息塞回給客戶端。

WSGI Application

wsgi application就是一個普通的callable對象,當有請求到來時,wsgi server會調用這個wsgi app。這個對象接收兩個參數,通常爲environ,start_response。environ就像前面介紹的,可以理解爲環境變量,跟一次請求相關的所有信息都保存在了這個環境變量中,包括服務器信息,客戶端信息,請求信息。start_response是一個callback函數,wsgi application通過調用start_response,將response headers/status 返回給wsgi server。此外這個wsgi app會return 一個iterator對象 ,這個iterator就是response body。這麼空講感覺很虛,對着下面這個簡單的例子看就明白很多了。

WSGI MiddleWare

有些功能可能介於服務器程序和應用程序之間,例如,服務器拿到了客戶端請求的URL, 不同的URL需要交由不同的函數處理,這個功能叫做 URL Routing,這個功能就可以放在二者中間實現,這個中間層就是 middleware。middleware對服務器程序和應用是透明的,也就是說,服務器程序以爲它就是應用程序,而應用程序以爲它就是服務器。這就告訴我們,middleware需要把自己僞裝成一個服務器,接受應用程序,調用它,同時middleware還需要把自己僞裝成一個應用程序,傳給服務器程序。
其實無論是服務器程序,middleware 還是應用程序,都在服務端,爲客戶端提供服務,之所以把他們抽象成不同層,就是爲了控制複雜度,使得每一次都不太複雜,各司其職

CGI
關於CGI的解釋,知乎上有位hellocode兄弟在
https://www.zhihu.com/question/19998865/answer/29395327 講的比較清楚,我摘抄如下:

CGI是比較原始的開發動態網站的方式。你可以想象一下,一個網站的動態內容肯定是程序生成的,光是靜態的html頁面無法達到這個效果。那麼,這個程序就需要接受客戶端的請求,然後進行相應處理,再返回給客戶端,客戶端和服務端的通信當然是通過HTTP協議。

然後我們會發現,這個程序在處理客戶端請求的時候,大部分時候會進行很多重複的工作,比如說HTTP請求的解析。也就是說,你的程序需要解析HTTP請求,我的程序也需要解析。

於是爲了DRY原則,Web服務器誕生了。(以下所說的都是CGI的工作模式)於是Web服務器可以解析這個HTTP請求,然後把這個請求的各種參數寫進進程的環境變量,比如REQUEST_METHOD,PATH_INFO之類的。之後呢,服務器會調用相應的程序來處理這個請求,這個程序也就是我們所要寫的CGI程序了。它會負責生成動態內容,然後返回給服務器,再由服務器轉交給客戶端。服務器和CGI程序之間通信,一般是通過進程的環境變量和管道。

這樣做雖然很清晰,但缺點就是每次有請求,服務器都會fork and exec,每次都會有一個新的進程產生,開銷還是比較大的。原因在與CGI程序是一個獨立的程序,它是可以獨立運行的(在提供HTTP請求的情況下),它可以用幾乎所有語言來寫,包括perl,c,lua,python等等。所以對於一個程序,服務器只能以fork and exec的方式來調用它了。

Nova API 服務啓動流程

OpenStack api-paste.ini 詳解
這裏先摘抄 Nova模塊的api-paste.ini文件如下:

############
# Metadata #
############
[composite:metadata]
use = egg:Paste#urlmap
/: meta

[pipeline:meta]
pipeline = cors metaapp

[app:metaapp]
paste.app_factory = nova.api.metadata.handler:MetadataRequestHandler.factory

#############
# OpenStack #
#############

[composite:osapi_compute]
use = call:nova.api.openstack.urlmap:urlmap_factory
/: oscomputeversions
# v21 is an exactly feature match for v2, except it has more stringent
# input validation on the wsgi surface (prevents fuzzing early on the
# API). It also provides new features via API microversions which are
# opt into for clients. Unaware clients will receive the same frozen
# v2 API feature set, but with some relaxed validation
/v2: openstack_compute_api_v21_legacy_v2_compatible
/v2.1: openstack_compute_api_v21

[composite:openstack_compute_api_v21]
use = call:nova.api.auth:pipeline_factory_v21
noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler noauth2 osapi_compute_app_v21
keystone = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler authtoken keystonecontext osapi_compute_app_v21

[composite:openstack_compute_api_v21_legacy_v2_compatible]
use = call:nova.api.auth:pipeline_factory_v21
noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler noauth2 legacy_v2_compatible osapi_compute_app_v21
keystone = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler authtoken keystonecontext legacy_v2_compatible osapi_compute_app_v21

[filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory

[filter:compute_req_id]
paste.filter_factory = nova.api.compute_req_id:ComputeReqIdMiddleware.factory

[filter:faultwrap]
paste.filter_factory = nova.api.openstack:FaultWrapper.factory

[filter:noauth2]
paste.filter_factory = nova.api.openstack.auth:NoAuthMiddleware.factory

[filter:osprofiler]
paste.filter_factory = nova.profiler:WsgiMiddleware.factory

[filter:sizelimit]
paste.filter_factory = oslo_middleware:RequestBodySizeLimiter.factory

[filter:http_proxy_to_wsgi]
paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory

[filter:legacy_v2_compatible]
paste.filter_factory = nova.api.openstack:LegacyV2CompatibleWrapper.factory

[app:osapi_compute_app_v21]
paste.app_factory = nova.api.openstack.compute:APIRouterV21.factory

[pipeline:oscomputeversions]
pipeline = cors faultwrap http_proxy_to_wsgi oscomputeversionapp

[app:oscomputeversionapp]
paste.app_factory = nova.api.openstack.compute.versions:Versions.factory

##########
# Shared #
##########

[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = nova

[filter:keystonecontext]
paste.filter_factory = nova.api.auth:NovaKeystoneContext.factory

[filter:authtoken]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory

這裏面涉及到幾個術語,分別解釋一下:

  • composite section:Request 進來後第一個通過的 Section,表示需要將一個 HTTP URL Request 調度到一個或者多種 Application 上。
  • use:是一個關鍵字,指定處理請求的代碼,這裏表示我們使用 Paste egg包中 urlmap 來實現 composite 請求分發方式。 ==> 決定請求的分發方式爲 urlmap(urlmap 算是一個通用的請求分發程序)。
  • pipeline section : 指定的 section 有如下要求:
    • 1.最後一個名字對應的 section 一定要是一個 app 類型。
    • 2.非最後一個名字對應的 section 一定要是一個 filter 類型。
  • filter section: 是一個實現了過濾器功能的中間件(將 Application 進行進一步的封裝),用於過濾 Request 和 Response。
  • App: 一個 app 就是一個實現主要功能的具體的 application 。所以 app 必須是 Callable Object 類型,接受的參數(environ,start_response),這是WSGI Server交給Application的符合WSGI規範的參數。

EXAMPLE: Keystone Request URL 爲 http://homename:35357/v3/auth/tokens

Step1. (hostname:35357): 這一部分由 Web Server 來獲取並處理的(EG.虛擬機功能)。

Step2. (/v3/auth/tokens): 根據 paste.ini 中的配置來對剩下的 URL(/v3/auth/tokens)部分進行處理。首先請求的 Port =35357 決定了會經過 [composite:admin] section 。

Step3. (/v3): composite section 會根據 /v3 這個 URL 前綴來決定將 Request 路由到哪一個 pipeline secion,這裏就把請求轉發給 [pipeline:api_v3] 處理,轉發之前,會把 /v3 這個部分的 URL 去掉。

Step4. (/auth/tokens) : [pipeline:api_v3] 收到請求,URL_Path是 (/auth/tokens),然後開始調用各個 filter(中間件) 來處理請求。最後會把請求交給 [app:service_v3] 進行處理。

Step5. (/auth/tokens): [app:service_v3] 收到請求,URL_Path是 (/auth/tokens),最後交由的 WSGI Application:keystone.service:v3_app_factory 去處理。

服務如何部署?

目前Python有兩種方式來開發和部署一個Web應用:用WSGI和不用WSGI。OpenStack的API服務都是使用WSGI的方式來部署的。在生產環境中部署WSGI,一般會考慮使用Web服務器 + 應用服務器 + 應用(框架)的方案。OpenStack官方推薦的是使用Apache + mod_wsgi的方案,不過這個要換成其他方案也很容易,你也可以選nginx + uWSGI。對於開發調試的目的,有些項目也會提供使用eventlet的單進程部署方案,比如Keystone項目的keystone-all命令。採用eventlet這種異步架構來進行應用開發也是一個比較大的話題,本文不覆蓋這方面的內容。

當然,也可以不用WSGI。在Python中,如果不使用WSGI的化,一般開發者會選擇一些專門的服務器和框架,比如Tornado,或者最新最潮的aiohttp。不過在OpenStack的項目中我還沒見過不使用WSGI的。

參考文獻:
https://www.biaodianfu.com/cgi-fastcgi-wsgi.html
http://blog.csdn.net/Jmilk/article/details/52081748
https://segmentfault.com/a/1190000003718598

發佈了31 篇原創文章 · 獲贊 1 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章