Python Web - WSGI 与 ASGI

Python Web - WSGI 与 ASGI

[TOC]

从 Web 服务器开始

Web 服务器的核心使命:URL 与资源的映射

从本质上讲,Web 服务器的核心工作,就是实现 URL 和服务器资源之间的映射。当我们谈论“资源”时,主要指两类:

  1. 静态资源 (Static Resources):这些是预先存在于服务器硬盘上的文件,无需额外处理即可直接发送给浏览器。例如:CSS 样式表 (style.css)、图片 (logo.png)、HTML 文件 (index.html) 以及 JavaScript 脚本 (main.js)。
  2. 动态资源 (Dynamic Resources):这些资源并非现成的文件,而是程序代码实时运行后生成的结果。例如,当请求 /api/users/123 时,服务器需要运行一段 Python 代码,去数据库查询 ID 为 123 的用户信息,然后将这些信息格式化为 JSON 字符串返回。这个动态生成的 JSON 字符串就是动态资源。

为了最高效地处理这两类截然不同的资源,Web 服务器的生态系统逐渐演化出了明确的分工。

术业有专攻:Nginx 与 Gunicorn

在实际部署中,我们通常会组合使用不同特长的服务器。Nginx 和 Gunicorn 就是一对经典的黄金搭档。

Nginx:面向网络 I/O 的“交通警察”

我们要建立一个博客网站,最主要的功能就是用户访问某个网址的时候,网站返回给他们一个 HTML 页面。这就需要一个可以处理静态资源的 Web 服务器,Nginx 就是最经常选取的方案。它是一个用 C 语言编写的高性能 Web 服务器,其设计哲学是以最高的效率处理网络 I/O 和高并发连接。

  • 处理静态资源 (核心强项): 当一个 GET /images/logo.png 请求到达时,Nginx 会迅速定位到硬盘上的对应文件,并利用操作系统底层的高效机制将文件内容直接发送到网络。这个过程快如闪电。
  • 处理动态资源 (角色:转发): Nginx 自身无法执行 Python 代码。当一个 GET /api/users 请求到达时,Nginx 会扮演“交通警察”的角色,将这个请求原封不动地转发 (Proxy) 给在后端等待的 Python 应用服务器(比如 Gunicorn)。

Gunicorn:专注运行 Python 应用的“执行官”

我们要启动一个 Flask 的 Web 应用,用户访问某个 API,后台就执行相应 Python 代码并返回结果。这里就需要一个可以处理动态资源的 Web 应用服务器。Gunicorn 是一个用 Python 编写的典型应用服务器,它的核心使命是为 Python Web 应用提供一个标准的、健壮的运行时环境。

  • 处理动态资源 (核心使命): 当 Gunicorn 收到 /api/users 这个请求时,它的工作是加载并执行我们的 Python Web 应用代码,生成动态内容,然后将结果返回。
  • 处理静态资源 (能力有限,效率低下): Gunicorn 也能处理静态文件,但这无异于让一位米其林大厨去送外卖,效率极低。在生产环境中,这项工作应该完全交给 Nginx。

黄金搭档的工作模式

特性 Nginx Gunicorn / Uvicorn
主要语言 C Python
设计哲学 高性能网络 I/O,事件驱动 运行和管理 Python 应用
URL 映射 URL -> 静态文件 或 代理地址 (被代理的)请求 -> Python 可执行对象
静态资源处理 极其高效 (专长) 非常低效 (不推荐)
动态资源处理 无法执行,只能转发 (代理) 执行 Python 代码生成动态内容 (专长)
核心角色 反向代理、负载均衡器、静态文件服务器 应用服务器 (Application Server)

服务器与应用的对话:协议的诞生

Web 应用服务器,最关键的一步就是,Gunicorn 该如何将请求交给我们的 Flask 应用,并让它执行代码呢?

思考一下我们写的 Flask 代码,这里的 app 就是我们的 Flask Web 应用:

1
2
3
4
5
6
7
from flask import Flask
app = Flask(__name__)

@app.route('/api/users')
def get_users():
# ...查询数据库等逻辑...
return {"users": [...]}
当我们运行 Gunicorn 时,我们会告诉它去加载并运行这个 app 对象。对于 Gunicorn 来说,我们整个复杂的 Web 应用,其实就是这一个 app 应用对象 (Application Object)。

这里就产生了一个核心问题:

Gunicorn 是一个通用的服务器,它需要能运行任何遵循标准的 Python Web 框架(Flask, Django, Falcon 等)。而 Flask 是一个通用的框架,它也希望能被任何遵循标准的服务器(Gunicorn, uWSGI, Waitress 等)运行。

Gunicorn 的作者并不知道 Flask 的 app 对象内部有什么方法;同样,Flask 的作者也不知道 Gunicorn 会如何调用它。它们之间是如何实现精确对话的呢?

答案是:制定一个标准化的协议 (Protocol)

这个协议就像两者之间的一份“合同”,清晰地规定了服务器和应用之间如何沟通。它定义了:

  1. 服务器(Gunicorn)的责任:必须将收到的 HTTP 请求,转换成一种 Python 应用能够理解的、标准化的格式(例如,一个包含所有请求信息的字典)。
  2. 应用(Flask app)的责任:必须是一个“可调用”(callable)的对象,并且能够接收服务器传递过来的标准化格式的请求信息,然后返回一个标准格式的响应。

因此,完整的动态请求流程是这样的:

  1. Gunicorn 接收到原始的 HTTP 请求报文。
  2. Gunicorn 按照“协议”规定,将这个报文翻译成一个 Python 对象(比如一个字典),里面包含了所有请求的细节(URL、请求头、方法等)。
  3. Gunicorn 调用我们代码中的 app 对象,并将翻译好的 Python 对象作为参数传给它。
  4. Flask 框架(app 对象内部的逻辑)接收这个标准化的对象,解析它,执行我们编写的视图函数(get_users)。
  5. 我们的代码返回一个响应,Flask 将其打包成一个标准化的 Python 响应格式。
  6. Gunicorn 接收到这个 Python 响应,再按照协议翻译回一个真正的 HTTP 响应报文,返回给浏览器。
sequenceDiagram
    autonumber
    participant B as 浏览器
    participant G as 服务器 / Gunicorn(WSGI)
    participant A as 应用 / Flask app
    participant V as 视图函数 get_users

    B->>G: 发送原始 HTTP 请求
    Note over G: 服务器责任:将 HTTP 请求翻译为
WSGI environ(标准化的字典) G->>A: 调用 app(environ, start_response) Note over A: 应用责任:作为可调用对象,
接收标准化请求并生成标准响应 A->>V: 解析请求并执行 get_users() V-->>A: 返回响应数据(body) A-->>G: 调用 start_response(status, headers)
并返回可迭代响应体 Note over G: 将 Python 响应翻译为真正的
HTTP 响应报文 G-->>B: 返回 HTTP 响应

这个在服务器和应用之间充当“翻译官”和“合同”角色的协议,就是我们接下来要深入探讨的主角。在 Python 的世界里,最主流的两个同步和异步 Web 应用协议,就是 WSGIASGI

WSGI (同步) 阵营

WSGI (Web Server Gateway Interface) 是一个为 Python Web 应用定义的同步标准接口。它就像一个桥梁,连接了 Web 服务器(如 Gunicorn、uWSGI)和 Web 框架(如 Flask、Django)。WSGI 的工作模式是同步的,即一个请求在一个工作进程中处理,处理完成之前会阻塞该进程。同步是传统的工作模式,成熟稳定,生态系统庞大,适合常规的、以 CRUD(增删改查)为主的 Web 应用。

原理

WSGI 的核心思想是简单。它规定了一个服务器和应用之间唯一的、标准的接口。这个接口就像一个插头和插座,任何符合 WSGI 规范的服务器都能运行任何符合 WSGI 规范的应用。

1. WSGI 的原理与接口

WSGI 的约定非常简单:应用方必须提供一个可调用对象 (callable),而服务器方则负责调用它。

这个可调用对象通常命名为 application,它必须接受两个参数:

  1. environ:一个包含所有 HTTP 请求信息的 Python 字典。它就像一个巨大的信息包,里面有请求路径 (PATH_INFO)、请求方法 (REQUEST_METHOD)、CGI 变量、HTTP 头信息 (HTTP_...) 等。
  2. start_response:一个由服务器提供的函数(回调函数)。应用在发送响应体之前,必须先调用这个函数,告诉服务器即将发送的 HTTP 状态码和响应头。

application 函数最终必须返回一个可迭代 (iterable) 的、包含响应体字节串的对象。

接口定义 (伪代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def application(environ, start_response):
# 1. (可选) 解析 environ 字典,获取请求信息
# 比如:method = environ.get('REQUEST_METHOD')
# path = environ.get('PATH_INFO')

# 2. 准备 HTTP 状态和响应头
status = '200 OK'
headers = [('Content-type', 'text/plain; charset=utf-8')]

# 3. 调用服务器提供的 start_response 函数
start_response(status, headers)

# 4. 返回一个包含响应体内容的可迭代对象
return [b'Hello, World!']

2. 工作流程(“握手”过程)

让我们想象一下 Gunicorn (服务器) 和一个 Flask (应用) App 交互的瞬间:

  1. 客户端请求抵达:浏览器向 Gunicorn 发送一个 HTTP 请求。
  2. 服务器打包信息:Gunicorn 解析这个 HTTP 请求,把它所有的信息(路径、头、方法等)塞进一个名为 environ 的字典里。同时,Gunicorn 准备好自己的一个内部函数,我们叫它 start_response
  3. 服务器调用应用:Gunicorn 调用我们 Python 应用里那个约定好的 application 对象,并将 environstart_response 作为参数传进去:application(environ, start_response)
  4. 应用处理请求:应用代码开始执行。它从 environ 字典里读取所需信息,执行业务逻辑(比如查数据库),然后准备好要返回给客户端的数据。
  5. 应用设置响应头:在准备好数据后,应用调用 Gunicorn 传给它的 start_response('200 OK', [('Content-type', 'text/html')])。这一步执行后,Gunicorn 就知道接下来要发送的 HTTP 状态是 200,响应头是 Content-type: text/html
  6. 应用返回响应体application 函数返回一个列表 [b'<h1>Hello</h1>']。因为列表是可迭代的,所以符合规范。
  7. 服务器发送数据:Gunicorn 拿到这个可迭代对象,遍历它,并将里面的每一个字节块依次发送给客户端。

这个过程就像一次单向的、同步的电话:服务器打给应用,应用说完所有话(return)后挂断,通话结束。

3. 典型使用场景

  • 传统的请求-响应式网站:如博客、电商网站、企业官网等,用户点击一个链接,服务器返回一个完整的页面。
  • RESTful API:客户端发起一个 API 请求,服务器处理后返回一个 JSON 或 XML 结果。
  • 绝大多数基于 Django 和 Flask 的老项目

WSGI 的局限:它天生是同步阻塞的。在处理一个请求的整个生命周期里,工作进程是被占用的。如果这个请求需要等待 5 秒(比如一个慢查询),那么这个进程就得干等 5 秒,无法处理其他请求,造成资源浪费。

主流实现

WSGI 应用框架

  • Django: 一个“自带电池”的全功能框架,包含了构建复杂、数据库驱动的网站所需的一切,从 ORM 到后台管理一应俱全。
  • Flask: 一个轻量级的“微框架”,核心简单,但扩展性极强,可以根据需要自由组合各种工具。
  • Pyramid: 一个兼具灵活性和规模化的框架,既可以从小项目开始,也能扩展到大型复杂应用。
  • Bottle: 一个极简的单文件微框架,无任何外部依赖,非常适合小型应用和学习。

WSGI 服务器

  • Gunicorn: “绿色独角兽”,一个成熟、稳定、易于配置的纯 Python WSGI 服务器,是生产环境部署的首选之一。
  • uWSGI: 一个功能极其丰富的应用服务器,性能强大,但配置相对复杂。它不仅仅支持 WSGI,还支持多种协议和语言。
  • Waitress: 一个纯 Python 实现的 WSGI 服务器,以简洁和在 Windows 和 Unix 上都能良好运行而著称,对 Pyramid 框架支持尤佳。
  • mod_wsgi: 作为 Apache 的一个模块来运行,能够深度集成 Apache 的功能。

可以像搭积木一样将框架和服务器组合起来,目前最成熟、应用最广泛的部署方式:

框架 (应用) 服务器 (运行环境) 场景说明
Django / Flask Gunicorn 这是生产环境中最经典的组合,Gunicorn 负责进程管理,稳定可靠。
Django / Flask uWSGI 功能强大,性能优异,但配置稍显复杂,适合需要深度定制的场景。
Pyramid Waitress Pyramid 官方文档常推荐的组合,简单易用。

对于 I/O 密集型的 WSGI 应用 (如大量数据库或外部 API 请求),为了弥补 WSGI 同步阻塞的短板,Gunicorn 引入了不同的工作进程类型来提升性能,其中最著名的就是 geventeventlet

geventeventlet 都是基于协程的 Python 网络库。它们通过一种名为 "monkey patching" 的技术,将 Python 标准库中阻塞的 I/O 操作替换为非阻塞的对应项。 这使得 WSGI 应用在处理 I/O 密集型任务(如数据库查询、API 调用)时,能够释放 CPU 去处理其他请求,从而在单个进程内实现高并发,这种并发单元被称为“绿线程”(green threads)。

简单来说,当在 Gunicorn 中使用 geventeventlet worker 时,同步 WSGI 应用(如 Flask)就能在不改变代码的情况下,获得类似异步应用的 I/O 并发能力。

ASGI (异步) 阵营

ASGI (Asynchronous Server Gateway Interface) 是 WSGI 的继任者,专为异步 Python Web 应用设计。随着 async/await 语法的出现,Python 的异步编程能力大大增强。ASGI 顺应了这一趋势,允许在一个进程中通过事件循环并发处理多个请求,非常适合处理长连接(如 WebSocket)和 I/O 密集型任务。异步是现代的编程范式,利用事件循环实现高并发,特别适合 I/O 密集型和需要长连接(如 WebSocket)的应用。

原理

ASGI 的诞生就是为了解决 WSGI 的同步阻塞问题,并原生支持 WebSocket 等长连接协议。

1. ASGI 的原理与接口

ASGI 不再是一个简单的 callable,而是一个异步的可调用对象 (awaitable)。它将应用的整个生命周期抽象成一个事件驱动的对话。

ASGI 应用的接口是一个 async 函数,它接受三个参数:

  1. scope:一个字典,是 environ 的超集。它不仅包含请求信息,还包含一个至关重要的键 type,用来指明连接的类型(如 http, websocket)。
  2. receive:一个由服务器提供的 awaitable 函数。应用通过 await receive() 接收来自服务器的事件,比如 HTTP 请求体、WebSocket 消息。
  3. send:一个由服务器提供的 awaitable 函数。应用通过 await send() 发送事件给服务器,比如 HTTP 响应头、响应体、WebSocket 消息。

接口定义 (伪代码):

1
2
3
4
5
6
7
8
9
async def application(scope, receive, send):
# scope['type'] 会是 'http', 'websocket', 或 'lifespan'

if scope['type'] == 'http':
# 这是一个 HTTP 请求
await http_handler(scope, receive, send)
elif scope['type'] == 'websocket':
# 这是一个 WebSocket 连接
await websocket_handler(scope, receive, send)

2. 工作流程(HTTP 示例)

让我们想象 Uvicorn (服务器) 和 FastAPI (应用) 的一次 HTTP 交互:

  1. 客户端请求抵达:浏览器向 Uvicorn 发送 HTTP 请求。
  2. 服务器建立 Scope:Uvicorn 创建一个 scope 字典,并设置 scope['type'] = 'http'
  3. 服务器调用应用:Uvicorn await 应用的入口:await application(scope, receive, send)
  4. 应用启动并监听:应用代码开始执行。它知道这是一个 HTTP 请求,然后它会 await receive() 来获取请求体等信息。
  5. 服务器发送事件:当 Uvicorn 收到请求体数据时,receive() 调用就会返回一个事件字典,例如:{'type': 'http.request', 'body': b'...', 'more_body': False}
  6. 应用处理并发送响应:应用拿到请求体后,执行业务逻辑。然后,它会分两步发送响应:
    • await send({'type': 'http.response.start', 'status': 200, 'headers': [...]}) 来发送状态和头。
    • await send({'type': 'http.response.body', 'body': b'Hello, ASGI!', 'more_body': False}) 来发送响应体。
  7. 服务器发送数据:Uvicorn 接收到这两个事件后,将它们转换成真正的 HTTP 响应发送给客户端。

这个过程就像一场双向的、异步的短信对话:服务器发个消息给应用(请求来了),应用回个消息(响应头好了),再回个消息(响应体好了),对话结束。在等待 I/O 的时候,事件循环可以去处理其他对话。

如何正确使用 ASGI 并最大化其效果

理解 ASGI 的工作原理后,最重要的问题就变成了:我应该在什么时候使用它?以及如何正确地使用它以获得最佳性能?

ASGI 并非万能灵丹。在错误的场景下使用它,性能可能还不如传统的 WSGI。它的威力只在特定的场景下,通过正确的编码方式才能被完全释放。

1. 编写路由函数:异步优先,兼容同步

  • async def (首选方式):要想完全发挥 ASGI 的威力,路由函数必须声明为 async def。只有这样,才能在函数内部使用 await 关键字,在进行 I/O 操作时将控制权交还给事件循环,从而实现高并发。

  • def (兼容模式):如果在 ASGI 框架(如 FastAPI)中定义了一个普通的 def 同步函数,会发生什么?框架足够智能,它会识别出这是一个同步函数,为了避免它阻塞宝贵的单线程事件循环,它会自动将这个函数在一个独立的外部线程池 (thread pool) 中运行

    • 优点:这提供了一种极好的兼容性,让我们可以平滑地迁移代码,或者在异步项目中调用一些不支持异步的旧版库。
    • 缺点:线程的创建和上下文切换是有开销的。虽然避免了阻塞,但其性能远不如原生的 async def 函数。这是一种“权宜之计”,而非“最佳实践”。

2. 拥抱 I/O 密集型场景

这是 ASGI 最核心、最闪耀的应用场景。I/O 密集型 (I/O-Bound) 指的是程序的大部分时间都在等待外部资源(如网络、数据库、磁盘),而不是在进行 CPU 计算。

  • 具体例子
    • 需要调用多个外部微服务 API 来聚合数据的网关服务。
    • 大量依赖数据库查询的 RESTful API。
    • 需要从网络或磁盘读写大量数据的服务。

✅ 如何最大化效果:

  1. 路由函数必须是 async def:这是开启异步世界大门的第一步。
  2. 使用异步 I/O 库:这是最关键的一点。如果在 async def 函数中使用了同步的 I/O 库(如 requests, psycopg2),它依然会阻塞整个事件循环!必须使用它们对应的异步版本:
    • HTTP 请求:使用 httpxaiohttp
    • 数据库 (PostgreSQL): 使用 asyncpg
    • 数据库 (MySQL): 使用 aiomysql
    • Redis: 使用 redis.asyncio
  3. 在每一个 I/O 操作前使用 awaitawait 关键字就是那个施展魔法的咒语。它告诉事件循环:“我现在要开始等待了,请你先去忙别的,等我好了再回来继续。”

代码示例:正确的数据库访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ❌ 错误的方式:在 async 函数中使用了同步库,会阻塞整个服务!
import psycopg2

@app.get("/users/{user_id}")
async def get_user_wrong(user_id: int):
# 下面这行代码会冻结事件循环,直到数据库响应,是性能杀手!
conn = psycopg2.connect(...)
# ...
return {"message": "This is wrong!"}

# ✅ 正确的方式:使用异步库并 await I/O 操作
import asyncpg

@app.get("/users/{user_id}")
async def get_user_correct(user_id: int):
# 'await' 告诉事件循环可以去处理其他请求
conn = await asyncpg.connect(user=...)
# 再次 'await',再次释放控制权
result = await conn.fetchrow("SELECT name FROM users WHERE id = $1", user_id)
await conn.close()
return {"name": result['name']}

3. 释放长连接的潜力

WSGI 规范无法处理需要长时间保持连接的应用。而 ASGI 对此提供了原生支持,是构建实时应用的理想选择。

  • 具体例子:WebSocket 聊天室、实时通知推送、在线协作工具、流式响应。
  • 实现方式:在 WebSocket 的路由函数中,通过 await websocket.receive_text()await websocket.send_text() 进行双向通信。这些 await 同样会将控制权交还给事件循环,使得单个服务进程可以同时轻松管理数千个活跃的 WebSocket 连接。

4. 警惕 CPU 密集型禁区

这是一个必须警惕的反面教材CPU 密集型 (CPU-Bound) 指的是程序的大部分时间都在进行密集的计算。

  • 具体例子:复杂的科学计算、视频转码、图像处理。
  • 为什么不适用:ASGI 的事件循环是单线程的。一个 CPU 密集型任务会霸占这个唯一的线程,导致整个服务被完全阻塞,停止响应任何其他请求。

对于 CPU 密集型任务,必须将其从 Web 服务进程中剥离,交给一个独立的后台任务队列(如 CeleryDramatiq)来异步处理。ASGI 路由函数只负责快速地提交任务并返回,从而保持 Web 服务的高响应性。

主流实现

ASGI 应用框架

  • FastAPI: 近年来迅速崛起的高性能框架,基于 Starlette 构建,充分利用 Python 类型提示,能自动生成交互式 API 文档,非常适合构建 API 服务。
  • Starlette: 一个轻量级的 ASGI 基础工具库/框架,是 FastAPI 的核心,也适合用来构建高性能的异步服务。
  • Django (Channels): 通过引入 Channels 扩展,Django 也具备了处理 ASGI 的能力,可以同时处理同步的 HTTP 请求和异步的 WebSocket 等长连接。
  • Sanic: 一个追求极致速度的异步 Web 框架,其 API 设计在一定程度上受到了 Flask 的启发。
  • Quart: Flask 的异步版本,其 API 与 Flask 高度兼容,让熟悉 Flask 的开发者可以轻松迁移到异步开发。

ASGI 服务器

  • Uvicorn: 基于 uvloop 和 httptools 构建的闪电般快速的 ASGI 服务器,是 FastAPI 和 Starlette 的官方推荐服务器。
  • Hypercorn: 一个功能全面的 ASGI 服务器,支持 HTTP/1, HTTP/2 和 WebSocket,并兼容 Trio 和 asyncio 两种异步事件循环库。
  • Daphne: 由 Django Channels 项目团队开发的 ASGI 服务器,是 Django 异步部署的参考实现。

同样可以像搭积木一样将框架和服务器组合起来,目前最成熟、应用最广泛的部署方式:

框架 (应用) 服务器 (运行环境) 场景说明
FastAPI / Starlette Uvicorn 官方推荐组合,能最大限度地发挥 FastAPI 的异步高性能优势。
FastAPI / Quart Hypercorn 如果需要 HTTP/2 等更高级的特性,Hypercorn 是一个很好的选择。
Django (Channels) Daphne Django 官方支持的异步部署方案,用于处理 WebSocket 等实时通信。

总结:一个分工明确的生态系统

经过前面的介绍,我们可以看到 Python Web 的世界是一个分工明确、层层协作的生态系统。如果用一个比喻来理解它们的关系,会非常清晰:

  1. 核心协议 (The Languages): WSGIASGI 是这个生态系统的沟通语言规范。它们定义了 Web 服务器与 Python 应用之间如何对话。
    • WSGI: 经典的同步“语言”,稳健而成熟。
    • ASGI: 现代的异步“语言”,为高性能和长连接而生。
  2. Web 框架 (The Native Speakers): Web 框架是这门语言的“母语者”,它们天生就用某种协议来构建应用逻辑。
    • FlaskDjango: 是 WSGI 的“母语者”,它们的底层设计遵循 WSGI 规范。
    • FastAPIStarlette: 则是 ASGI 的“原生使用者”,充分利用了异步的优势。
  3. 应用服务器 (The Listeners): 应用服务器是“倾听者”,它们必须能听懂相应的语言才能运行应用。
    • Gunicorn: 是一位 WSGI 专家。虽然它主要说“同步语言”,但可以通过集成 geventeventlet 等“协程翻译插件”,变得能够高效处理高 I/O 并发的场景。
    • UvicornHypercorn: 则是天生的 ASGI “倾听者”,它们被设计出来就是为了与说 ASGI 语言的应用进行流畅对话。