openstack nova-api 服務流程介紹

以下內容轉自 http://www.netfoucs.com/article/xuriwuyun/87993.html


nova-api發佈api服務沒有用到一個些框架,基本都是從頭寫的。在不瞭解它時,以爲它非常複雜,難以掌握。花了兩三天的時間把它分析一遍後,發現它本身的結構比較簡單,主要難點在於對它所使用的一些類庫不瞭解,如paste.deploy/webob/routes。對於paste.deploy,結合它的官網文檔把它的源碼看了兩遍。webob看的是源碼。routes看的是文檔。對於這些類庫提供的函數,如果從文檔中去理解他們想要做什麼,真不是件容易的事。查看其實現源碼,就明瞭了。不過在分析源碼過程中,碰到每一個類庫都去翻一遍它的源碼,這也是非常累人的,後期甚至都不想再看下去了,因爲腦子比較厭煩了。所以在學習routes時主要是看它的文檔,基本理解了。


paste.deploy

用來解析/etc/nova/api-paste.ini文件,加載用於服務的wsgi app。它的功能有:

1 api-paste.ini中配置多個wsgi app,deploy可根據傳入的app name加載指定的wsgi app;

  1. deploy.loadapp("config:/etc/nova/api-paste.ini", name="osapi-compute")  
      加載api-paste.ini中,名爲osapi-compute的WSGI APP,並作爲結果返回。

2 通過寫入api-paste.ini的配置,可方便地實現特定字符開始的url到特定wsgi app的映射。如:

[composite:osapi_compute]                                                       use = call:nova.api.openstack.urlmap:urlmap_factory                             /: oscomputeversions                                                 /v2: openstack_compute_api_v2 
       通過該配置,以“/v2”開始的url將交給名爲openstack_compute_api_v2的WSGI APP處理,其它以“/”開的url就交給oscomputerversions處理。其實這並非deploy的功能,而是上面配置的urlmap實現的。不過通過配置文件,再由deploy調用urlmap,使用就更簡單了。

3 middle ware的簡單加載和去除。

[composite:openstack_compute_api_v2]                                            use = call:nova.api.auth:pipeline_factory             keystone = faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2
     上面的faultwrap sizelimit authtoken keystonecontext ratelimit都爲middle ware(在nova代碼中稱爲MiddleWare,deploy中稱爲filter),osapi_compute_app_v2纔是wsgi app。請求先交給middle ware處理,再由middle ware決定要不要或者怎麼交給wsgi app處理。這些middle ware是可以添加和去除的,如果不想對osapi_compute_app_v2進行限速,那麼去掉ratelimit就可以了。其實這是pipeline_factory實現的功能,不過通過deploy來配置加載更方便。

      nova-api中提供了很多服務API:ec2(與EC2兼容的API),osapi_compute(openstack compute自己風格的API),osapi_volume(openstack volume服務API),metadata(metadata 服務API)等。通過配置文件api-paste.ini,可以方便管理這些API。

      deploy提供了server、filter、app的概念,其中filter(即middle ware)、app在nova-api被重度使用,的確太好用了。發佈每個API時,它們有時需要一些相同的功能,如:keystone驗證、限速、錯誤處理等功能。nova將這些功能實現爲middle ware,如果需要,通過api-paste.ini配置,給它加上就可以。比如,我需要記錄每個訪問nova-api的ip,及它們的訪問次數和訪問的時間。那麼我實現一個middle ware--nova.api.middleware:MonitorMiddleware來記錄這些數據。通過下面的api-paste.ini配置,就可對nova-api的訪問進行記錄了。

[composite:openstack_compute_api_v2]                                            use = call:nova.api.auth:pipeline_factory             keystone = faultwrap sizelimit authtoken keystonecontext ratelimit <strong>monitor</strong> osapi_compute_app_v2[filter:<strong>monitor</strong>]                                                                paste.filter_factory = nova.api.middleware:MonitorMiddleware.factory

webob

用於對wsgi app進行封裝,簡化wsgi app的定義與編寫。webob主要提供三種功能。

1 Request。該類用於對傳給wsgi app的env參數進行封裝,簡化對HTTP請求參數的訪問和設置。這種簡化體現在(假設用env對Request實例化了一個對象req):

1) 使用間接明瞭的屬性名對HTTP參數進行訪問,req.method可獲取HTTP請求方法(替代REQUEST_METHOD);req.scheme可獲取HTTP請求協議http or https(替代wsgi.url_scheme);req.body獲取請求body(替代wsgi.input)。

2)大量使用property,省去繁瑣細節,簡化HTTP參數的訪問和設置。req.body直接訪問HTTP請求的body,而不用考慮(body的長度和字符編碼);req.POST以字典形式返回POST請求的參數;req.GET以字典形式返回GET請求的參數。

nova.api.openstack.wsgi.Request繼承該類,並加了一個緩存已訪問db的記錄的功能,和對content_type判斷和檢測的功能。

2 Response。該類用於對HTTP的返回值進行封裝。與Request對象類似,同樣使用了property簡化對http參數的訪問和設置。支持wsgi app一次返回status和body,這樣更直觀。其實Response實例本身也是一個wsgi app。

3 decorator--wsgify,裝飾wsgi app,使其可以以如下方式定義:

  1. @webob.dec.wsgify                                     def wsgi_app(req):    #do something with req    return req.Response(...)  

其中參數req是一個Request(默認)或其子類(通過wsgify(RequestClass=XXX)指定)的實例,是用env初始化的。req.Response默認爲webob.Response。以該種方式定義的wsgi app,其結果可以以三種形式返回:

1)返回一個字符串。wsgify將其作爲body,並加上一些默認的參數,如status=“200 OK", content_type, content_length等,構造成一個HTTP響應結果並返回;

2)返回一個Response實例,直接返回該resp代表的HTTP請求結果;

3)返回一個wsgi app,wsgify會繼續調用該app,並返回app的響應結果。

nova.wsgi.Router就是用第三種返回形式,兩次返回wsgi app,最終將HTTP請求根據url映射到對應的controller處理。 


routes

       用來給服務內部定義url到具體函數的映射。deploy也有url到服務映射功能,但這個映射層次偏高一點。根據上面配置,deploy將以“/v2”開始的url將交給名爲openstack_compute_api_v2處理。但openstack_compute_api_v2怎麼將/v2/project_id/servers/的GET請求交給nova.api.openstack.compute.servers.Controller.index()處理,並且將POST請求交給create()處理呢;怎麼將/v2/project_id/servers/id的GET請求交給show()處理呢?這個就是routes.mappers所提供的功能,它根據path和請求方法,將請求映射到具體的函數上。如在nova中,添加/v2/project_id/servers/{list_vm_state, os_vmsum}兩個GET請求來分別獲取指定VM的狀態和VM的總數。可在nova.api.openstack.compute.APIRouter中添加如下兩行,將請求分別交給list_vm_state和os_vmsum兩個函數處理並返回結果:

  1. self.resources['servers'] = servers.create_resource(ext_mgr)        mapper.resource("server""servers",                        controller=self.resources['servers'],                        <strong>collection={'list_vm_state''GET',                                    'os_vmsum''GET'}</strong>)  
      這裏利用了routes.mapper支持restful api的特性,僅用兩條指令,就定義了十多個url到函數的映射。當然你可以如下方式添加接口,不過代碼稍多,風格不那麼統一:

  1. mapper.connect("server",                       "/{project_id}/servers/list_vm_state",                       controller=self.resources['servers'],                       action='list_vm_state',                       conditions={'list_vm_state''GET'})        mapper.connect("server",                       "/{project_id}/servers/os_vmsum",                       controller=self.resources['servers'],                       action='os_vmsum',                       conditions={'os_vmsum''GET'})  


主題--nova-api服務流程分析

       上面介紹了nova-api發佈所用到的一些lib庫,有了上面的基礎知識,再來分析nova-api的發佈流量,就比較輕鬆了。
       nova-api可以提供多種api服務:ec2, osapi_compute, osapi_volume, metadata。可以通過配置項enabled_apis來設置啓動哪些服務,默認情況下,四種服務都是啓動的。

       從nova-api的可執行腳本中,可以看出每個nova-api服務都是通過nova.service.WSGIService來管理的:

  1. class WSGIService(object):    def __init__(self, name, loader=None):        self.name = name        self.manager = self._get_manager()        self.loader = loader or wsgi.Loader()        self.app = self.loader.load_app(name)        self.host = getattr(FLAGS, '%s_listen' % name, "0.0.0.0")        self.port = getattr(FLAGS, '%s_listen_port' % name, 0)        self.workers = getattr(FLAGS, '%s_workers' % name, None)        self.server = wsgi.Server(name,                               #這裏通過eventlet來啓動服務                                  self.app,                                  host=self.host,                                  port=self.port)    def start(self):        if self.manager:            self.manager.init_host()        self.server.start()    ......  

       從上可知,WSGIService使用self.app = self.loader.load_app(name)來加載wsgi app,app加載完成後,使用nova.wsgi.Server來發布服務。Server首先用指定ip和port實例化一個監聽socket,並使用wsgi.server以協程的方式來發布socket,並將監聽到的http請求交給app處理。對於Server的啓動過程,代碼上理解還是比較簡單的,沒多少分析的。下面我們主要來分析處理HTTP請求的wsgi app是如何構建的,對於每一個請求,它是如何根據url和請求方法將請求分發到具體的具體函數處理的。

       上個語句self.loader.load_app(name)中的loader是nova.wsgi.Loader的實例。Loader.load_app(name)執行下面指令,使用deploy來加載wsgi app:

  1. deploy.loadapp("config:%s" % self.config_path, name=name)    

       self.config_path爲api-paste.ini文件路徑,一般爲/etc/nova/api-paste.ini。name爲ec2, osapi_compute, osapi_volume, metadata之一,根據指定的name不同來加載不同的wsgi app。下面以name=“osapi_compute”時,加載提供openstack compute API服務的wsgi app作爲具體分析。osapi_compute的配置如下

[composite:osapi_compute]                                                       use = call:nova.api.openstack.urlmap:urlmap_factory                             /: oscomputeversions                                                /v2: openstack_compute_api_v2

        osapi_compute是調用urlmap_factory函數返回的一個nova.api.openstack.urlmap.URLMap實例,nova.api.openstack.urlmap.URLMap繼承paste.urlmap.URLMap,它提供了wsgi調用接口,所以該實例爲wsgi app。但是函數nova.api.openstack.urlmap:urlmap_factory與paste.urlmap.urlmap_factory定義完全一樣,不過由於它們所在的module不同,使得它們所用的URLMap分別爲與它處於同一module的URLMap。paste.urlmap.urlmap_factory咋不支持一個傳參,來指定URLMap呢?這樣nova就不用重寫一樣的urlmap_factory了。paste.urlmap.URLMap實現的功能很簡單:根據配置將url映射到特定wsgi app,並根據url的長短作一個優先級排序,url較長的將優先進行匹配。所以/v2將先於/進行匹配。URLMap在調用下層的wsgi app前,會更新SCRIPT_NAME和PATH_INFO。nova.api.openstack.urlmap.URLMap繼承了paste.urlmap.URLMap,並寫了一堆代碼,其實只是爲了實現對請求類型的判斷,並設置environ['nova.best_content_type']:如果url的後綴名爲json(如/xxxx.json),那麼environ['nova.best_content_type']=“application/json”。如果url沒有後綴名,那麼將通過HTTP headers的content_type字段中mimetype判斷。否則默認environ['nova.best_content_type']=“application/json”。

       經上面配置加載的osapi_compute爲一個URLMap實例,wsgi server的接受的HTTP請求將直接交給該實例處理。它將url爲'/v2/.*'的請求將交給openstack_compute_api_v2,url爲'/'的請求交給oscomputerversions處理(它直接返回系統版本號)。其它的url請求,則返回NotFound。下面繼續分析openstack_compute_api_v2,其配置如下:

[composite:openstack_compute_api_v2]                                            use = call:nova.api.auth:pipeline_factory                                       noauth = faultwrap sizelimit noauth ratelimit osapi_compute_app_v2              keystone = faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2keystone_nolimit = faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v2

openstack_compute_api_v2是調用nova.api.auth.pipeline_factory()返回的wsgi app。pipeline_factory()根據配置項auth_strategy來加載不同的filter和最終的osapi_compute_app_v2。filter的大概配置如下:

[filter:faultwrap]                                                              paste.filter_factory = nova.api.openstack:FaultWrapper.factory
filter在nova中對應的是nova.wsgi.Middleware,它的定義如下:

  1. class Middleware(Application):                                                                                                                                                           @classmethod                                                                    def factory(cls, global_config, **local_config):                                                                           def _factory(app):                                                                  return cls(app, **local_config)                                             return _factory                                                                                                                                             def __init__(self, application):                                                    self.application = application                                                                                                                              def process_request(self, req):                                                     return None                                                                                                                                                 def process_response(self, response):                                   return response                                                                                                                                             @webob.dec.wsgify(RequestClass=Request)                                         def __call__(self, req):                                                            response = self.process_request(req)                                            if response:                                                                        return response                                                             response = req.get_response(self.application)                                   return self.process_response(response)  
       Middleware初始化接收一個wsgi app,在調用wsgi app之前,執行process_request()對請求進行預處理,判斷請求是否交給傳入的wsgi app,還是直接返回,或者修改些req再給傳入的wsgi app處理。wsgi app返回的response再交給process_response()處理。例如,對於進行驗證的邏輯,可以放在process_request中,如果驗證通過則繼續交給app處理,否則返回“Authentication required”。不過查看nova所有Mddlerware的編寫,似乎都不用這種定義好的結構,而是把處理邏輯都放到__call__中,這樣導致__call__變得複雜,代碼不夠整潔。對於FaultWrapper尚可理解,畢竟需要捕獲wsgi app處理異常嘛,但其它的Middleware就不應該了。這可能是不同程序員寫,規範就忽略了。
      當auth_strategy=“keystone”時,openstack_compute_api_v2=FaultWrapper(RequestBodySizeLimiter(auth_token(NovaKeystoneContext(RateLimitingMiddleware(osapi_compute_app_v2)))))。所以HTTP請求需要經過五個Middleware的處理,才能到達osapi_compute_app_v2。這五個Middleware分別完成:

1)異常捕獲,防止服務內部處理異常導致wsgi server掛掉;

2)限制HTTP請求body大小,對於太大的body,將直接返回BadRequest;

3)對請求keystone對header中token id進行驗證;

4)利用headers初始化一個nova.context.RequestContext實例,並賦給req.environ['nova.context'];

5)限制用戶的訪問速度。

      當HTTP請經過上面五個Middlerware處理後,最終交給osapi_compute_app_v2,它是怎麼繼續處理呢?它的配置如下:

[app:osapi_compute_app_v2]                                                      paste.app_factory = nova.api.openstack.compute:APIRouter.factory
       osapi_compute_app_v2是調用nova.api.openstack.compute.APIRouter.factory()返回的一個APIRouter實例。nova.api.openstack.compute.APIRouter繼承nova.api.openstack.APIRouter,nova.api.openstack.APIRouter又繼承nova.wsgi.APIRouter。APIRouter通過A它的成員變量mapper來建立和維護url與controller之間的映射,該mapper是nova.api.openstack.ProjectMapper的實例,它繼承nova.api.openstack.APIMapper(routes.Mapper)。APIMapper將每個url的format限制爲json或xml,對於其它擴展名的url將返回NotFound。ProjectMapper在每個請求url前面加上一個project_id,這樣每個請求的url都需要帶上用戶所屬的project id,所以一般請求的url爲/v2/project_id/resources。nova.api.openstack.compute.APIRouter.setup_routes代碼如下:

  1. class APIRouter(nova.api.openstack.APIRouter):                                                                        ExtensionManager = extensions.ExtensionManager                                                                                                                  def _setup_routes(self, mapper, ext_mgr):        self.resources['servers'] = servers.create_resource(ext_mgr)                    mapper.resource("server""servers",                                                            controller=self.resources['servers'])                                                                                                           self.resources['ips'] = ips.create_resource()                                   mapper.resource("ip""ips", controller=self.resources['ips'],                                  parent_resource=dict(member_name='server',                                                           collection_name='servers'))        ......                           
       APIRouter通過調用routes.Mapper.resource()函數建立RESTFUL API,也可以通過routes.Mapper.connect()來建立url與controller的映射。如上所示,servers相關請求的controller設爲servers.create_resource(ext_mgr),該函數返回的是一個用nova.api.openstack.compute.servers.Controller()作爲初始化參數的nova.api.openstack.wsgi.Resource實例,ips相關請求的controller設爲由nova.api.openstack.ips.Controller()初始化的nova.api.openstack.wsgi.Resource實例。因爲調用mapper.resource建立ips的url映射時,添加了一個parent_resource參數,使得請求ips相關api的url形式爲/v2/project_id/servers/server_id/ips。對於limits、flavors、metadata等請求情況類似。當osapi_compute_app_v2接收到HTTP請求時,將調用nova.wsgi.Router.__call__,它的定義如下:

  1. class Router(object):                                                                                                 def __init__(self, mapper):        self.map = mapper                                                               self._router = routes.middleware.RoutesMiddleware(self._dispatch,                                                                 self.map)                                                                                                 @webob.dec.wsgify(RequestClass=Request)                                         def __call__(self, req):        return self._router                                                         @staticmethod                                                                   @webob.dec.wsgify(RequestClass=Request)                                         def _dispatch(req):        match = req.environ['wsgiorg.routing_args'][1]                                  if not match:                                                                       return webob.exc.HTTPNotFound()                                             app = match['controller']                                                       return app     
        這裏開始讓我迷惑了一下,__call__()怎麼能返回一個wsgi app呢,直接返回wsgi app那它又怎麼被調用呢?查看一下wsgify源碼,發現如果函數返回的是wsgi app時,它還會被繼續調用,並返回它的處理結果。所以它會繼續調用self._router,_router是routes.middleware.RoutesMiddleware的實例,使用self._dispatch和self.map來初始化的,self.map是在Router的子類nova.api.openstack.APIMapper.__init__中,被初始化爲ProjectMapper實例,並調用_setup_routes建立好url與cotroller之間的映射。routes.middleware.RoutesMiddleware.__call__調用mapper.routematch來獲取該url映射的controller等參數,以{"controller":Resource(Controller()), "action": funcname, "project_id": uuid, ...}的格式放在match中。並設置如下的environ變量,方便後面調用的self._dispatch訪問。最後調用self._dispatch。

  1. environ['wsgiorg.routing_args'] = ((url), match)                                environ['routes.route'] = route                                                 environ['routes.url'] = url   
        _dispatch具體負責url到controller的映射,它通過前面設置environ['wsgiorg.routing_args']來找到url對應的controller。這裏的controller就是通過_setup_resource函數設置的controller,及使用響應Controller初始化的Resource實例。Resource通過environ['wsgiorg.routing_args']獲取上面設置的match,該match有一個action屬性,它指定了所有調用Crotroller成員函數的名子,以及其它相關的調用參數。在我們定義Controller的成員函數時,一般需要通過nova.api.openstack.wsgi.{serializers, deserializers}來指定解釋body內容的模板,可以是xml或者json格式的。前面說過重定義nova.api.openstack.urlmap.URLMap的目的是爲了判斷content_type。Resource在解析body時會參考content_type,然後調用響應的解析器進行解析(如XMLDeserializer、JSONDeserializer),然後將body update進action_args,使用action_args來調用Controller成員函數,即最終的http請求處理函數。最後將執行結果使用指定的序列化器序列化,並返回結果。


參考文獻:

[1] https://docs.python.org/2/library/re.html

[2] http://routes.readthedocs.org/en/latest/restful.html

[3] http://pythonpaste.org/deploy/

[4] https://wiki.python.org/moin/MiniDom


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