request processing in Django

最近有个需求,提供一个http代理,提供后端分布式文件存储系统的下载、上传两个功能,文件大小极限值在100GB+。考虑到其他一些限制,最终选择使用 Nginx + uWSGI + Django的方案。

这里仅仅是记录了开发过程中对Django默认配置下,WSGIHandler处理http请求过程的分析。重点关注了请求数据、返回数据的解析、处理过程。因为要避免大量数据载入内存导致异常,以及大量数据落盘导致的开销。
文章中的代码片段并没有遵守python语法,为了描述的简洁混杂了c++的语法。

django code version: 1.11.3

Request

默认构造一个WSGIRequest对象。ref
解析请求参数过程使用了lazy loading方式,GET & POST字段都是property。
GET是每次都解析 self.environ[‘QUERY_STRING’] 参数。
POST或FILES是其中一个首次使用时(即_post, _files字段为空),调用父类的 _load_post_and_files() 函数,该函数解析post方式的数据,并设置 _post, _files 两个字段,此时POST & FILES都处于直接可用的状态。

_load_post_and_files() 函数分析

忽略异常判断逻辑。
代码只支持 'multipart/form-data' & 'application/x-www-form-urlencoded' 两种POST数据编码方式。使用其他方式不会报错,仅仅是 POST & FILES 为空。
根据WSGIRequest::__init__(),post entity-body数据流保存在_stream字段,并未读取。

'application/x-www-form-urlencoded' 数据解析

一次性读取出所有的entity-body数据,并赋值给_body字段,然后解码。
这种方式是只能传递form参数,不能上传文件。
同时这种方式可以通过 settings.DATA_UPLOAD_MAX_MEMORY_SIZE 设置数据大小上限。

'multipart/form-data' 数据解析

首先判断数据源,如果有_body字段则使用该字段的数据,并封装出来readable object,即有read()函数。
如果没有则使用self,self提供了read()函数读取self._stream数据。
然后使用 parse_file_upload() 函数解析数据,该函数同时解析出来 POST & FILES参数,不要被函数名误导。该函数使用django.http.multipartparser.MultiPartParser类解析。

此处上传数据有两种:请求字段,文件。
对于上传的普通字段,可以通过 settings.DATA_UPLOAD_MAX_NUMBER_FIELDS 限制上传的字段个数,不包含文件个数,以及通过 settings.DATA_UPLOAD_MAX_MEMORY_SIZE 限制字段值长度和的上限,不包括文件大小。

对于上传的文件,MultiPartParser class本身没有具体的逻辑,而是提供了一套处理机制,由提供的实现 class FileUploadHandler 的类完成处理。可以提供多个handler,通过参数 settings.FILE_UPLOAD_HANDLERS 配置,有默认值,参考文档:link。这些handler会对每一个Request初始化一次,然后使用同一个handler instance处理request内所有的文件上传信息,根据代码Django遵循了POST数据内每一个 form field or file chunk 都是顺序连续出现的,不会有交叉。
MultiPartParser循环读取上传文件的每个chunk,然后循环调用每个handlers处理当前chunk。每个handlers最后返回一个Filelike object,因此理论上上传一个文件,server端会产生len(handlers)个文件对象,但是我们通过FILES[FILENAME]只会返回一个object,这是因为FILES是 django.utils.datastructures.MultiValueDict 类型,接口类似dict,但是逻辑不同,参考代码:link
GET & POST 类型为 class QueryDict(MultiValueDict),集成增加了 mutable 开关。

看到这里我们知道file upload handler的设置是全局的,如果想针对某个接口做定制化的配置并不能直接达到目的。那么如果在业务代码收到request object时设置 http.HttpRequest::upload_handlers 字段也是可以的,只要代码中的middlewares,或其他部分没有使用过POST或FILES参数。
在某一个时间点上是可以做到的,但是随着项目迭代这种约定很容易被破坏。

参考链接:

  1. Django 1.11 Doc: Modifying upload handlers on the fly
  2. Django 1.11 Doc: Writing custom upload handlers

Process

接下来通过

1
response = WSGIHandler::BaseHandler.get_response(request)

进入处理流程。

get_response() 主要逻辑就是调用 middleware_chain。settings.MIDDLEWARE 是按顺序执行的,不要被初始化时的 reversed(settings.MIDDLEWARE) 操作混淆。
初始化middleware_chain时首先会插入 BaseHandler::_get_response(), 该函数在调用链中最后执行,也就是在该函数里走入url dispatch逻辑,并获取业务代码产生的response object。

settings.ROOT_URLCONF 配置了 url dispatch patterns。

Response

python manage.py runserver 方式启动

server实现为 django.core.servers.basehttp.WSGIServer 类,该类继承自python标准库的类:

1
wsgiref.simple_server.WSGIServer(server_address, RequestHandlerClass)

manual: link

处理逻辑入口为RequestHandlerClass::handle()函数,Django的继承实现类为 django.core.servers.basehttp.WSGIRequestHandler

处理框架在 class wsgiref.handlers.BaesHandler 里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class BaseHandler:
"""Manage the invocation of a WSGI application"""

...

def run(self, application):
"""Invoke the application"""
# Note to self: don't move the close()! Asynchronous servers shouldn't
# call close() from finish_response(), so if you close() anywhere but
# the double-error branch here, you'll break asynchronous servers by
# prematurely closing. Async servers must return from 'run()' without
# closing if there might still be output to iterate over.
try:
self.setup_environ()
self.result = application(self.environ, self.start_response)
self.finish_response()
except:
try:
self.handle_error()
except:
# If we get an error handling an error, just give up already!
self.close()
raise # ...and let the actual server figure it out.

def finish_response(self):
"""Send any iterable data, then close self and the iterable

Subclasses intended for use in asynchronous servers will
want to redefine this method, such that it sets up callbacks
in the event loop to iterate over the data, and to call
'self.close()' once the response is finished.
"""
try:
if not self.result_is_file() or not self.sendfile():
for data in self.result:
self.write(data)
self.finish_content()
finally:
self.close()

self.result 即Django框架返回的 response object,必须是一个generator,可以迭代。
Django response有 stream HTTP response & normal HTTP response 两种返回值,在内部已经统一封装成Iterator,对外无区别。

uWSGI 方式启动

todo