django項目在uwsgi+nginx上部署遇到的坑

本文來自網易雲社區python

做者:王超nginx


問題背景

django框架提供了一個開發調試使用的WSGIServer, 使用這個服務器能夠很方便的開發web應用。可是 正式環境下卻不建議使用這個服務器, 其性能、安全性都堪憂。一個推薦的作法是使用uwsgi+Nginx來部署django應用。如何使用uwsgi部署不在本文的討論範圍裏。web

在大多數狀況, WSGIServer下的能正常工做的代碼, 在uwsgi中也能正常運行。 可是也有不少坑點, 致使uwsgi下的結果與WSGIServer的結果徹底不一樣。 這裏就來聊聊這些坑點。數據庫

坑點集錦

代碼加載順序

在使用WSGIServer開發時, django應用是經過python manage.py 0.0.0.0:80的命令來啓動的, 這個命令對應的python代碼就是django

from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)複製代碼

而經過uwsgi部署django時, django應用是經過uwsgi -http 8080 --wsgi-file wsgi來啓動的, 這個命令其實就是去加載wsgi.py中的代碼安全

from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()複製代碼

應用的啓動方式不一樣, 致使應用中各個模塊的加載順序也徹底不一樣。bash

爲了研究具體的加載順序, 咱們在ViewBase中加入瞭如下元類, 這個元類會在全部ViewBase的子類被建立時, 打印出此時的調用堆棧與進程ID(爲何要打印進程id, 後文後解釋)服務器

import tracebackclass MetaCls(type):
    def __new__(cls, name, bases, attrs):
        pid = os.getpid()
        print( '%d proc load module: %s' % (pid, attrs["__module__"]) )
        print( "".join(traceback.format_stack()) )        return super(MetaCls, cls).__new__(cls, name, bases, attrs)class ViewBase(object):
    __metaclass__ = MetaCls
    .......複製代碼

首先使用python manage.py runserver啓動應用, 發現打印出來的信息以下:session

2017-04-24 16:22:23,095 __new__[line:231] thread:MainThread: 9428 proc load module: app.BsLogic.Admin.views
  File "manage.py", line 26, in <module>
    execute_from_command_line(sys.argv)  File "D:\project\qaweb\django\core\management\__init__.py", line 367, in execute_from_command_line
    utility.execute()
  ......    #這裏省略django內部調用
  File "D:\project\qaweb\django\core\checks\urls.py", line 14, in check_url_config  # 從這裏開始要加載urls了
    return check_resolver(resolver)
  ......    #這裏省略django內部調用
  File "D:\project\qaweb\qaweb\urls.py", line 30, in <module>
    urlpart = import_string(str_module)
  ......    #省略
  File "D:\project\qaweb\app\BsLogic\Admin\__init__.py", line 3, in <module>    # 這裏開始就是咱們寫的代碼了
    import urls  File "D:\project\qaweb\app\BsLogic\Admin\urls.py", line 4, in <module>
    import views  File "D:\project\qaweb\app\BsLogic\Admin\views.py", line 16, in <module>
    class HotFix(ViewBase):
  File "D:\project\qaweb\app\BsLogic\Common.py", line 232, in __new__
    print( "".join(traceback.format_stack()) )複製代碼

爲了便於分析, 這裏省略了django內部的調用。 能夠發現, 程序的入口就是execute_from_command_line, 而後通過一系列的內部調用, 再開始加載urls, 由於urls會映射到咱們寫的views, 因此咱們寫的代碼也會跟着加載, 簡言之, 使用manage.py啓動時, 咱們寫的全部相關代碼(除了那些徹底獨立的代碼), 都會在應用啓動時所有加載。app

而後, 咱們使用uwsgi的方式啓動應用, 發現居然沒有打印信息, 難道咱們寫的代碼根本沒有被加載。 爲了弄清楚緣由, 只能看django源碼。 果真, 發現經過get_wsgi_application()啓動應用時, 僅僅加載了中間件的代碼

# wsgi.py application = get_wsgi_application()# django/core/wsgi.py line 14return WSGIHandler()# django/core/handlers/wsgi.py line 153self.load_middleware()# django/core/handlers/base.pyload_middleware(self)複製代碼

爲了驗證想法, 咱們在中間件代碼中加入打印堆棧的語句, 而後重啓服務, 這樣打印出來的堆棧是:

File "/home/wc/wangchao/mqaDjango/qaweb/wsgi_django.py", line 13, in <module>
    application = get_wsgi_application()
  File "./django/core/wsgi.py", line 14, in get_wsgi_application    return WSGIHandler()
  File "./django/core/handlers/wsgi.py", line 153, in __init__
    self.load_middleware()
  File "./django/core/handlers/base.py", line 80, in load_middleware
    middleware = import_string(middleware_path)
  File "./django/utils/module_loading.py", line 20, in import_string    module = import_module(module_path)
  File "/usr/lib/python2.7/importlib/__init__.py", line 37, in import_module
    __import__(name)
  File "./app/BsLogic/MiddleWare/__init__.py", line 3, in <module>    import AuthMiddleWare
  File "./app/BsLogic/MiddleWare/AuthMiddleWare.py", line 15, in <module>
    from ..Common import createLogger, getIp
  File "./app/BsLogic/Common.py", line 234, in <module>    print traceback.format_stack()複製代碼

結果與咱們的猜測一致。 那麼, 咱們寫的views代碼, 究竟去哪了呢? 先按捺住這個疑問, 咱們經過web訪問咱們的站點, 同時留意咱們打印的堆棧信息。 咱們會發現, 出現了咱們想要的加載views代碼的堆棧:

File "./django/core/handlers/wsgi.py", line 170, in __call__      # 入口
    response = self.get_response(request)
  ...... #省略django的內部調用
  File "./django/urls/resolvers.py", line 313, in url_patterns      # 這裏開始要加載urls了
    patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
  ...... # 省略加載urls時, django的內部調用 
  File "./app/BsLogic/scm/urls.py", line 5, in <module>             # 這裏就是咱們寫的代碼了
    import views  File "./app/BsLogic/scm/views.py", line 30, in <module>
    class BinPackage(ViewBase):
  File "./app/BsLogic/Common.py", line 232, in __new__
    print traceback.format_stack()複製代碼

也就是說, 咱們寫的代碼, 並不會在應用啓動時就會加載, 而是在接收到第一個request以後, 纔開始加載urls, 而後再加載咱們的views代碼。 若是在views的代碼中定義了全局變量, 而後在其餘地方使用了這變量, 就頗有可能出現NameError: name 'xxx' is not defined的bug, 這是由於, 定義全局變量的語句尚未執行(坑啊~)

結論:

  1. 使用execute_from_command_line方式啓動django應用時, 會先加載urls, 從而會加載咱們寫的業務代碼(views中的代碼); 而後再加載中間件代碼. 在應用啓動完成時, 全部相關代碼都已經被加載入內存。

  2. 使用get_wsgi_application方式啓動django應用時, 會先加載中間件代碼, 這與1中的是徹底相反的。 此時, 咱們的業務代碼仍然沒有被加載, 直到第一個請求過來。 若是咱們在代碼中, 使用了未加載的代碼中的全局變量, 就會出現莫名其妙的bug

多進程

uwsgi是一個優秀的web server, 可是出於性能和安全性的考慮, 每每會在uwsgi上面再包一層Nginx。而Nginx是一個異步多進程的服務器, 因此在使用中每每會fork多個nginx的worker進程, 來提升處理request的效率。worker進程數通常是cpu核心數。

經過uwsgi來啓動django服務時, 在monitor.log中能夠看到worker進程的信息

Python main interpreter initialized at 0xb52bc0your server socket listen backlog is limited to 100 connections
your mercy for graceful operations on workers is 60 seconds
your request buffer size is 4096 bytes
mapped 364080 bytes (355 KB) for 4 cores

*** Operational MODE: preforking ***
  File "/home/wc/wangchao/mqaDjango/qaweb/wsgi_django.py", line 13, in <module>
    application = get_wsgi_application()
  File "./django/core/wsgi.py", line 14, in get_wsgi_application    return WSGIHandler()
  File "./django/core/handlers/wsgi.py", line 153, in __init__
    self.load_middleware()
  File "./django/core/handlers/base.py", line 80, in load_middleware
    middleware = import_string(middleware_path)
  File "./django/utils/module_loading.py", line 20, in import_string
    module = import_module(module_path)\n
  File "/usr/lib/python2.7/importlib/__init__.py", line 37, in import_module
    __import__(name)
  File "./app/BsLogic/MiddleWare/__init__.py", line 3, in <module>
    import AuthMiddleWare\n
  File "./app/BsLogic/MiddleWare/AuthMiddleWare.py", line 15, in <module>    from ..Common import createLogger, getIp
  File "./app/BsLogic/Common.py", line 236, in <module>
    print traceback.format_stack()10860 proc load module: app.BsLogic.Common

WSGI app 0 (mountpoint='') ready in 0 seconds on interpreter 0xb52bc0 pid: 10860 (default app)
*** uWSGI is running in multiple interpreter mode ***
gracefully (RE)spawned uWSGI master process (pid: 10860)spawned uWSGI worker 1 (pid: 11398, cores: 1)spawned uWSGI worker 2 (pid: 11399, cores: 1)spawned uWSGI worker 3 (pid: 11400, cores: 1)spawned uWSGI worker 4 (pid: 11401, cores: 1)複製代碼

上面的信息, 咱們能夠看到master進程和worker進程的pid。 還有一點值得注意的是, 上面的調用堆棧, 就是加載中間件代碼的堆棧, 中間件在master進程中加載完成後, 纔開始fork子進程, 因此,切勿在中間件中寫block的代碼, 萬一deadlock, 整個服務就掛了。 其實這也是符合Nginx的設計理念的, Nginx的master進程負責處理request信息, 包括處理處理起始行、提取頭部、負載等, 而後把請求隨機下發到worker進程。一樣的, django的中間件也是處理request的, 包括加載session等等。 因此應該把中間件代碼放在master進程。

根據前文分析, 咱們的業務代碼會在第一個request到來以後加載, 可是究竟是加載到哪一個進程呢(這裏但是有1個master和4個worker), 這也是爲何咱們在打印堆棧的時候要帶上pid的緣由。 爲了弄清楚問題, 咱們屢次訪問咱們的web應用, 看打印出來的日誌:

GET /merge11399 proc load module: app.BsLogic.Merge.viewsGET /merge11400 proc load module: app.BsLogic.Merge.viewsGET /11401 proc load module: app.BsLogic.Package.viewsGET /None11398 proc load module: app.BsLogic.Merge.views複製代碼

分析日誌發現, 全部的worker進程都會加載咱們的業務代碼。 若是某個worker進程, 沒有加載過業務代碼, 那麼當有一個request被下發給它時, 就會去加載。

因爲每一個worker進程都會加載一次咱們的views代碼, 那麼就會產生一個問題。若是咱們在全局的位置, 作了一些特殊的操做, 好比說開了一個線程, 或者定義一把全局鎖, 那麼, 在Nginx多進程下, 就會發生, 每一個進程都開了一個線程, 或者每一個進程都有本身的鎖。 以前就遇到過一個bug, 全局位置開了線程去輪詢某個資源, 而後寫入數據庫, 部署到Nginx後, 發現每一個item都被寫了4次......

     結論:

  1. 除了加載順序不同以外, 業務代碼加載次數也不同, 咱們的代碼會在nginx全部子進程中都加載一次

  2. 因爲進程間不共享內存, 因此在web應用中, 切勿使用全局變量, 在worker A中的修改不會同步到worker B, 必然會出bug

  3. 不要試圖在master進程中開啓線程, 實測無用(奇怪的是, 在master中開的線程, 會被託管到celery中......)

結語

養成好的編碼習慣, web應用中不要使用全局變量, 在須要全局變量的狀況下, 多考慮是否能用數據庫替代。對於"我本身電腦上是好的"這種bug, 要淡定對待, 線上環境確實一堆坑


網易雲免費體驗館,0成本體驗20+款雲產品!

更多網易研發、產品、運營經驗分享請訪問網易雲社區


相關文章:
【推薦】 適配的那些事

相關文章
相關標籤/搜索