☰
在 App Engine Python 3 中用 Django 访问 Bundled Deferred 服务:完整实现与部署指南
2026/10/2 15:45:38 网站建设 项目流程
  • 示例工程

【免费下载链接】python-docs-samples

Code samples used on cloud.google.com

项目地址:https://gitcode.com/GitHub_Trending/py/python-docs-samples
点击查看免费下载

导读

本文围绕 python-docs-samples 仓库中的 Django 示例应用,深入讲解如何在 App Engine 标准环境 Python 3 运行时下,通过 Bundled Deferred API 将耗时任务异步化——在 HTTP 请求返回后由 Task Queue 在后台延迟执行。读完本文,你将掌握wrap_wsgi_app(use_deferred=True)的接入方式、deferred.defer()的三种调度形态(立即执行、_countdown延时、自定义_url回调)、无settings.py文件的最小 Django 配置技巧,以及一套基于独立版本隔离的可并行端到端测试方案。

一、示例背景:Bundled Services 下的 Deferred 服务

App Engine 标准环境早年基于 Python 2.7 的运行时提供了一批"打包服务"(Bundled Services),其中 Deferred 服务允许开发者把函数调用序列化后投递到任务队列,由系统在请求上下文之外异步执行,非常适合后台数据聚合、延迟通知、批量更新等场景。

在迁移到 Python 3 标准运行时后,这类能力并未消失,而是通过appengine-python-standard提供的兼容层继续可用。仓库在 bundled-services 目录 下按服务类型(deferred、mail、blobstore)组织了示例,而 deferred 子目录 内放置了三份功能完全一致、仅 Web 框架不同的应用,用于演示同一套 Deferred API 的三套写法:

框架说明示例位置
Flask通过app.wsgi_app = wrap_wsgi_app(...)包装deferred/flask
Django通过wrap_wsgi_app(get_wsgi_application(), ...)包装deferred/django
App Engine 原生 WSGI直接包装 WSGI 应用deferred/wsgi

本文聚焦其中的Django 版本,其完整源码位于 main.py。

二、应用行为与 URL 路由设计

示例应用实现了一个简单的计数器,功能闭环清晰:

  • GET /counter/get:返回当前计数值;
  • GET /counter/increment:通过 Deferred 服务触发三次递增,随后立即返回响应;
  • POST /custom/path:自定义的 Deferred 任务回调端点。

上层 deferred/README.md 描述了通用的行为约定:计数器通过 Deferred 服务递增,任务分为"立即执行一次、延时执行一次、再延时执行一次"三档。以本 Django 示例的实际源码为准,main.py 中三次调度的间隔分别为0 秒、60 秒(_countdown=60)与 120 秒(_countdown=120),这一点由 main_test.py 中的断言链(40s→10、100s→20、160s→30)逐段验证。

URL 路由在 main.py 中以 Django 的urlpatterns声明:

urlpatterns = ( path("counter/get", view_counter, name="view_counter"), path("counter/increment", increment_counter, name="increment_counter"), path("custom/path", custom_deferred, name="custom_deferred"), )

三、核心源码逐段剖析

3.1 无 settings.py 的最小 Django 应用

示例刻意省略了独立的settings.py、manage.py与项目脚手架,直接在入口模块内通过settings.configure(...)完成最小配置(main.py):

settings.configure( DEBUG=True, SECRET_KEY="thisisthesecretkey", ROOT_URLCONF=__name__, MIDDLEWARE_CLASSES=( "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ), ALLOWED_HOSTS=["*"], )

要点说明:

  • ROOT_URLCONF=__name__把当前模块当作 URL 配置入口,配合上方urlpatterns即完成路由装载;
  • MIDDLEWARE_CLASSES是 Django 2.x 之前引入的配置键名,示例保留它以最小化配置面;在新版本 Django 中通常使用MIDDLEWARE,此处沿用源码写法以保证示例行为与仓库一致;
  • ALLOWED_HOSTS=["*"]保证 App Engine 生成的*.appspot.com域名请求不会被主机头校验拦截;
  • SECRET_KEY仅为演示值,生产环境务必通过环境变量注入真实密钥。

3.2 WSGI 接入:wrap_wsgi_app 与 use_deferred

Deferred 服务能够拦截并处理任务回调,关键在于把标准 WSGI 应用用 App Engine 的wrap_wsgi_app包装,并显式打开use_deferred=True(main.py):

app = wrap_wsgi_app(get_wsgi_application(), use_deferred=True)

这一行是整套方案的"开关":get_wsgi_application()产出标准 Django WSGI 应用,wrap_wsgi_app注入 App Engine 打包 API 的兼容处理,use_deferred=True则告知中间件:当请求命中 Deferred 任务端点时,转交给 Deferred 处理器而非业务视图。若遗漏该参数,后台任务回调将无法被正确识别执行。对比同目录下的 Flask 版本,写法完全同构:app.wsgi_app = wrap_wsgi_app(app.wsgi_app, use_deferred=True)。

3.3 数据模型:基于 NDB 的计数器

计数值存放于 App Engine 的 NDB 数据存储(main.py):

class Counter(ndb.Model): count = ndb.IntegerProperty(indexed=False) def do_something_later(key, amount): entity = Counter.get_or_insert(key, count=0) entity.count += amount entity.put()
  • indexed=False关闭该属性索引——计数器只需要读写、不需要按值查询,可降低写入成本;
  • get_or_insert(key, count=0)是原子化的"取或建"操作,避免并发首写时产生重复实体;
  • do_something_later(key, amount)即被投递到后台的任务函数,它接收任务参数并对计数累加。

3.4 三种 defer 调度形态

视图函数 increment_counter 集中展示了deferred.defer()的三种典型用法:

# 形态一:使用默认 URL 与队列、无任务名、立即执行 deferred.defer(do_something_later, my_key, 10) # 形态二:默认参数,但 60 秒后执行 deferred.defer(do_something_later, my_key, 10, _countdown=60) # 形态三:自定义任务端点路径,并延迟 120 秒执行 deferred.defer(do_something_later, my_key, 10, _url="/custom/path", _countdown=120)

参数语义整理如下:

参数含义示例取值
位置参数传递给任务函数的参数my_key(键)、10(递增量)
_countdown延迟执行的秒数60、120;缺省即尽快执行
_url任务回调的 HTTP 路径默认使用系统内置端点;此处显式指向/custom/path
_queue/_task_name(未演示)指定任务队列与任务名缺省使用默认队列、自动命名

注意:所有以_开头的关键字参数属于 Deferred API 的调度元数据,不会被序列化传递给任务函数本身。任务函数的普通参数会被 pickle 序列化后随任务一起投递,这也解释了 app.yaml 中需要设置NDB_USE_CROSS_COMPATIBLE_PICKLE_PROTOCOL的原因(见下文)。

3.5 自定义任务回调端点

当_url指定了非默认路径时,应用需要自己提供对应的处理视图。custom_deferred 演示了如何手工把 WSGI 环境交给 Deferred 处理器:

def custom_deferred(request): print("Executing deferred task.") # request.environ 即 WSGI 的 environ 字典(见 PEP 3333) response, status, headers = deferred.Handler().post(request.environ) return HttpResponse(response, status=status.value)
  • request.environ携带完整的 WSGI 环境变量(PEP 3333),Deferred 处理器依赖其中解析出任务载荷;
  • deferred.Handler().post(environ)负责反序列化任务、调用目标函数并生成响应;
  • status是 HTTP 状态对象,取其.value得到数值状态码交给 Django 的HttpResponse。

需要留意的是,常规的use_deferred=True中间件会自动处理系统默认端点;只有像示例这样显式自定义_url时,才需要自行实现回调视图。

四、app.yaml 与运行环境配置

应用运行于 App Engine 标准环境 Python 3 运行时,app.yaml 的配置如下:

runtime: python313 app_engine_apis: true env_variables: NDB_USE_CROSS_COMPATIBLE_PICKLE_PROTOCOL: "True"

三个配置项逐一说明:

  • runtime: python313:声明运行时版本。Bundled Services 兼容层要求 Python 3 运行时,仓库当前采用python313;
  • app_engine_apis: true:关键开关。它启用 App Engine 打包 API(Bundled APIs)的访问权限,没有它google.appengine.ext.deferred、ndb等模块将无法在 Python 3 运行时工作;
  • NDB_USE_CROSS_COMPATIBLE_PICKLE_PROTOCOL: "True":Deferred 任务需要把函数参数 pickle 序列化。该环境变量使 NDB 实体采用跨版本兼容的 pickle 协议,保证任务载荷在 Python 2 时代与 Python 3 时代之间可以互通(示例的测试与历史数据迁移场景依赖于此)。

依赖清单见 requirements.txt:

Django==6.1.1; python_version >= "3.12" django-environ==0.13.0 google-cloud-logging==3.5.0 appengine-python-standard>=0.3.1

其中appengine-python-standard是 Python 3 运行时提供 Bundled API 兼容层的核心依赖,版本下限>=0.3.1保证wrap_wsgi_app与deferred模块可用;django-environ用于从环境变量读取配置,google-cloud-logging提供云端日志对接。

五、部署方式

按 deferred/README.md 的说明,部署只需一条命令:

gcloud app deploy

在项目根目录执行后,gcloud会读取 app.yaml,上传应用并分配https://<版本>-dot-<项目>.appspot.com形式的访问地址。部署完成后即可通过GET /counter/get查看计数、GET /counter/increment触发后台递增任务。

六、端到端测试:基于版本隔离的并行测试方案

Bundled Services 只能在正确配置的 App Engine 应用内运行,因此本地单元测试无法覆盖 Deferred 服务的真实行为,必须部署到 App Engine 后测试。仓库给出的 main_test.py 提供了一套精巧的测试策略,其流程为:

  1. 以独立版本部署:调用gcloud app deploy --no-promote --version=<uuid>启动一个新版本但不向它路由任何网络流量(--no-promote的作用);
  2. 直接访问版本专属 URL:形如https://<version_id>-dot-<project_id>.appspot.com,绕开默认流量分配,只与本次测试的版本交互;
  3. 验证后删除版本:测试结束调用gcloud app versions delete <version_id>清理资源——正因为该版本从未被路由流量,删除不会影响线上服务;
  4. 并行安全:由于每个测试都使用独立版本、独立 URL,多个测试可以同时运行而互不干扰。

测试的计时逻辑精确对应三次 defer 调度(main_test.py):

触发后经过时间期望计数值依据
40 秒10立即执行的任务(增量 10)已完成
100 秒2060 秒延时的任务(增量 10)已完成
160 秒30120 秒延时的任务(增量 10)已完成
再等 30 秒30无新任务,计数值保持不变

测试基础设施方面:gcloud_cli()封装了带--quiet --format=json的 gcloud 调用并用backoff做指数退避重试;wait_for_app()会轮询等待新版本初始化完成后再进入断言;versionfixture 承担部署与清理的完整生命周期。测试依赖见 requirements-test.txt(backoff、pytest、requests),nox 测试版本矩阵在 noxfile_config.py 中配置(忽略 3.8–3.11 与 3.13,默认在 3.12 下运行)。

七、迁移要点与常见坑位

结合源码与配置,将这套 Django 版 Deferred 示例迁移到自己的应用时,建议重点核对以下几点:

  1. 务必开启app_engine_apis: true,否则google.appengine.ext.deferred与ndb导入即失败;
  2. 务必以wrap_wsgi_app(get_wsgi_application(), use_deferred=True)作为最终 WSGI 入口,且该包装必须包住 Django 应用最外层,否则任务回调无法被中间件捕获;
  3. 自定义_url时必须自行实现回调视图,并正确调用deferred.Handler().post(environ),同时确认该路由能接收 POST 请求(示例中测试直接以 GET 驱动任务投递,但回调端点按 WSGI 规范应接受任务推送);
  4. 任务函数与参数需可 pickle 序列化;涉及 NDB 实体跨环境传递时,保持NDB_USE_CROSS_COMPATIBLE_PICKLE_PROTOCOL开启;
  5. 测试必须真部署:Bundled Services 依赖 App Engine 环境,本地pytest无法替代;可复用仓库的"独立版本 + 专属 URL + 用完即删"模式实现并行 CI。

延伸阅读

  • 三个框架版本的横向对比:见 deferred 目录 下的 README.md;
  • Flask 版等价实现:deferred/flask/main.py;
  • 原生 WSGI 版:deferred/wsgi;
  • 同一迁移体系下的其他打包服务示例(Mail、Blobstore):见 bundled-services 目录;
  • 仓库整体的 Python 3 标准环境示例:见 appengine/standard_python3。
  • 示例工程

【免费下载链接】python-docs-samples

Code samples used on cloud.google.com

项目地址:https://gitcode.com/GitHub_Trending/py/python-docs-samples
点击查看免费下载
上一篇:从激活困境到自动化解决方案:KMS_VL_ALL_AIO的技术实现与应用指南
下一篇:ik_llama.cpp FlashMLA 崩溃排障实录:GGML_ASSERT(fms.S[j] > 0) 失败的定位、修复与复现验证

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询