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

叁叁肆2018-09-30 09:49

本文来自网易云社区

作者:王超


问题背景

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+款云产品! 


更多网易研发、产品、运营经验分享请访问网易云社区


相关文章:
【推荐】 认识用户访谈
【推荐】 知物由学 | 一文读懂互联网内容审核机制