todos 表,并通过一个小型 JSON 视图处理增删改查请求。所有连接均使用 TLS,并启用完整的证书验证。
前置条件
- Python 3.12 或更高版本 (Django 6 的最低要求) 。本指南已在 Python 3.13、Django 6.1.1 和 psycopg 3.3.6 环境下测试通过。
- 一个 ClickHouse Cloud 账户
psql,用于创建数据库。也可以在 SQL 控制台中执行CREATE DATABASE语句。
创建 ClickHouse Managed Postgres 服务
在 ClickHouse Cloud 控制台中,点击 New service,然后选择 Postgres。实例会在几分钟内就绪。详细步骤请参阅快速入门。获取连接详细信息
在服务的左侧边栏中点击 Connect。弹出的对话框会显示您的用户名、密码、服务器和端口,并提供 Directly / via PgBouncer 切换开关。 本指南通过内置的 PgBouncer (端口6432) 连接 Django。生产环境中的 Django 应用通常会在多台机器上运行多个工作进程,每个进程都会建立各自的连接。PgBouncer 以事务池化模式运行,让所有这些进程共享少量 Postgres 连接。要使用事务池化,Django 需要进行一些相应设置,本指南已包含这些设置,并在适用于 Django 的 PgBouncer 设置中加以说明。
选择 via PgBouncer 即可查看池化连接的详细信息。此时端口会变为 6432,连接字符串中也会包含 sslmode=verify-full。
在对话框中点击 Download CA certificate。您也可以在 Settings → CA Certificate 中下载。该证书为您的实例所独有,因此驱动程序可借助它验证所连接的确实是您的服务器。
搭建项目
创建一个项目目录并在其中配置虚拟环境,安装 Django 和 psycopg 3,然后创建一个包含todos 应用的 Django 项目:
psycopg[binary] 自带较新版本的 libpq,因此无需在本地安装 PostgreSQL。
配置连接
将下载的 CA 证书移动到项目目录中manage.py 所在的位置,并将其重命名为 ca-certificate.pem。
为应用创建一个数据库。将 <PASSWORD> 和主机名替换为 Connect 弹窗中显示的值:
mysite/settings.py,在 from pathlib import Path 下方添加 import os,然后将 DATABASES 配置项替换为:
mysite/settings.py
OPTIONS 中的键传给 psycopg,再由 psycopg 交给 libpq。将 sslmode 设为 verify-full 后,libpq 会校验服务器证书是否由您服务的 CA 签发,以及证书是否与主机名匹配。BASE_DIR 会将证书路径转换为绝对路径,因此无论从哪个目录启动 Django,该设置都能正常生效。
如果
sslrootcert 指向了错误的 CA,连接会失败并报错 SSL error: certificate verify failed。如果未设置 sslrootcert,则会报错 root certificate file ".../.postgresql/root.crt" does not exist,因为 libpq 会回退到其默认的 CA 位置。如果使用 IP 地址而非主机名进行连接,则会报错 server certificate for "..." does not match host name。创建模型并运行迁移
在mysite/settings.py 中,将 todos 应用添加到 INSTALLED_APPS:
mysite/settings.py
todos/models.py 中定义一个 Todo 模型:
todos/models.py
todos_todo 外,migrate 还会为 Django 的内置应用 (例如 auth_user 和 django_session) 创建表,并将已应用的迁移记录在 django_migrations 中。如果是现有应用,请跳过创建 todos 模型这一步,直接运行 python manage.py migrate,该命令会在新数据库中创建应用的所有表。
migrate 不会获取会话级咨询锁,因此可以通过 PgBouncer 正常运行。但它也无法阻止并发执行,因此请只在一个地方运行迁移,例如放在某个单独的部署步骤中。
添加 JSON 端点
将todos/views.py 替换为以下两个视图,它们通过 Django ORM 创建、列出、更新和删除待办事项:
todos/views.py
mysite/urls.py 的内容,将 /todos 和 /todos/<id> 路由到这些视图:
mysite/urls.py
运行与验证
启动开发服务器:PATCH 请求会返回更新后的行:
http://localhost:3111/todos,即可列出剩余的待办事项。响应如下所示:
guide_django 和 public,然后点击 todos_todo 表。表中包含剩余的那一行:
适用于 Django 的 PgBouncer 设置
PgBouncer 以事务池化模式运行:每个事务 (或事务之外的每条语句) 都可能在不同的 Postgres 连接上执行。下面说明这对上述设置以及 Django 其他功能的影响:DISABLE_SERVER_SIDE_CURSORS:QuerySet.iterator通过服务器端游标分块读取行。在事务之外,Django 会将游标声明为WITH HOLD,并逐块分别拉取,因此经由 PgBouncer 时,某次拉取可能落到并不持有该游标的 Postgres 连接上,并报错cursor "_django_curs_..." does not exist。只有当其他客户端同时使用连接池时才会出现这种情况,所以在开发环境中往往难以发现。启用该设置后,.iterator()会改用普通游标,由 psycopg 将整个结果加载到内存中。如需流式读取超大表,请关闭该设置,并通过端口5432直连运行相应代码。CONN_MAX_AGE:默认值为0,此时 Django 会为每个请求新建一个连接。无论经由 PgBouncer 还是直连,建立 TLS 连接都需要多次网络往返,因此复用连接能显著提升请求速度。在一次从笔记本电脑发起、使用四个 Gunicorn 工作进程的测试中,将CONN_MAX_AGE设为600后,请求耗时中位数从约 670 ms 降至 95 ms。经由 PgBouncer 时,已打开的 Django 连接在事务之间不会占用 Postgres 连接,因此持久连接不会耗尽max_connections。启用CONN_HEALTH_CHECKS后,Django 会在每个请求开始时检查复用的连接,如果连接已关闭则重新连接。- 连接池:Django 内置的连接池 (在
OPTIONS中设置"pool": True,需先执行pip install "psycopg[binary,pool]") 能带来类似的提速效果,同样可以经由 PgBouncer 使用。它要求将CONN_MAX_AGE设为0,并且每个进程至少保持四个打开的连接。使用 PgBouncer 时,对大多数应用而言CONN_MAX_AGE已经足够;如果使用多线程或 ASGI 服务器,可以考虑连接池。 - 预处理语句:无需任何设置。Django 默认将 psycopg 的
prepare_threshold设为None,并在客户端绑定查询参数,因此不会创建服务器端预处理语句。如果在OPTIONS中启用"server_side_binding": True并设置"prepare_threshold",预处理语句同样可以经由 PgBouncer 使用,因为psycopg[binary]内置了libpq18,会通过协议层的Close消息释放语句。如果 psycopg 基于libpq16 或更早版本构建,在事务内释放语句时会报错prepared statement "_pg3_0" does not exist。可运行python -c "import psycopg; print(psycopg.pq.version())"检查版本,结果需为170000或更高。 - 会话状态:
SET语句 (例如SET search_path) 只对执行它的 Postgres 连接生效,因此后续查询可能看不到其效果,其他客户端反而可能受到影响。PgBouncer 会拒绝 Django 的"options": "-c search_path=..."设置,并报错unsupported startup parameter in options: search_path。请改为在数据库级别设置默认值,例如执行ALTER DATABASE guide_django SET search_path = myschema, public;。不要使用assume_role选项,因为它会执行SET ROLE。Django 自身的TIME_ZONE处理以及isolation_level选项均可经由 PgBouncer 正常工作。
PORT 设为 5432 并移除 DISABLE_SERVER_SIDE_CURSORS。此时每个 Django 工作进程都会各自持有一个 Postgres 连接,因此请确保工作进程总数低于 max_connections。