django專案在uwsgi+nginx上部署遇到的坑

網易雲社群發表於2018-09-30

本文來自網易雲社群

作者:王超


問題背景

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

在大多數情況, WSGIServer下的能正常工作的程式碼, 在uwsgi中也能正常執行。 但是也有很多坑點, 導致uwsgi下的結果與WSGIServer的結果完全不同。 這裡就來聊聊這些坑點。

坑點集錦

程式碼載入順序

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

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()複製程式碼

應用的啟動方式不同, 導致應用中各個模組的載入順序也完全不同。

為了研究具體的載入順序, 我們在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啟動應用, 發現列印出來的資訊如下:

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啟動時, 我們寫的所有相關程式碼(除了那些完全獨立的程式碼), 都會在應用啟動時全部載入。

然後, 我們使用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+款雲產品!

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


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


相關文章