本文来自网易云社区vue
做者:王超python
django框架提供了一个开发调试使用的WSGIServer, 使用这个服务器能够很方便的开发web应用。可是 正式环境下却不建议使用这个服务器, 其性能、安全性都堪忧。一个推荐的作法是使用uwsgi+Nginx来部署django应用。如何使用uwsgi部署不在本文的讨论范围里。nginx
在大多数状况, WSGIServer下的能正常工做的代码, 在uwsgi中也能正常运行。 可是也有不少坑点, 致使uwsgi下的结果与WSGIServer的结果彻底不一样。 这里就来聊聊这些坑点。web
在使用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中的代码django
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启动应用, 发现打印出来的信息以下: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, 这是由于, 定义全局变量的语句尚未执行(坑啊~)
结论:
使用execute_from_command_line方式启动django应用时, 会先加载urls, 从而会加载咱们写的业务代码(views中的代码); 而后再加载中间件代码. 在应用启动完成时, 全部相关代码都已经被加载入内存。
使用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次......
结论:
除了加载顺序不同以外, 业务代码加载次数也不同, 咱们的代码会在nginx全部子进程中都加载一次
因为进程间不共享内存, 因此在web应用中, 切勿使用全局变量, 在worker A中的修改不会同步到worker B, 必然会出bug
不要试图在master进程中开启线程, 实测无用(奇怪的是, 在master中开的线程, 会被托管到celery中......)
养成好的编码习惯, web应用中不要使用全局变量, 在须要全局变量的状况下, 多考虑是否能用数据库替代。对于"我本身电脑上是好的"这种bug, 要淡定对待, 线上环境确实一堆坑
网易云免费体验馆,0成本体验20+款云产品!
更多网易研发、产品、运营经验分享请访问网易云社区。