178 lines
12 KiB
Markdown
178 lines
12 KiB
Markdown
# 可乐平台 异步化架构(ADRF + Granian + PostgreSQL + Nginx)
|
||
|
||
## 架构总览
|
||
|
||
```
|
||
┌──────────┐ :8080 ┌─────────────────────┐
|
||
│ 浏览器 │──────────▶│ Nginx (frontend) │
|
||
└──────────┘ │ ├─ / → React SPA 静态产物 (build/, 动静分离)
|
||
│ ├─ /api 等前缀 ─┐
|
||
│ └─ /ws/ ─┤ 反向代理
|
||
└──────────────────┼───┘
|
||
▼
|
||
┌───────────────────────────────┐
|
||
│ Granian (Rust) :8000 │
|
||
│ Django 5.2 ASGI + ADRF │
|
||
│ ├─ 异步视图 (async def) │
|
||
│ ├─ 异步 ORM (aget/afilter) │
|
||
│ └─ Channels (WebSocket) │
|
||
└───────┬──────────────┬────────┘
|
||
▼ ▼
|
||
┌──────────────┐ ┌──────────────┐
|
||
│ PostgreSQL 16 │ │ Redis 7 │
|
||
│ psycopg3 异步 │ │ cache/session│
|
||
│ ORM 驱动 │ │ celery/ws层 │
|
||
└──────────────┘ └──────────────┘
|
||
```
|
||
|
||
## 技术选型
|
||
|
||
| 层 | 技术 | 说明 |
|
||
|---|---|---|
|
||
| Web 框架 | Django 5.2 + DRF 3.17 | 原生异步能力 |
|
||
| 异步视图 | **ADRF 0.1.14** | 所有 API 视图跑在异步事件循环上 |
|
||
| 应用服务器 | **Granian 2.8.2 (Rust)** | 替代 Daphne,ASGI + WebSocket,吞吐量显著提升 |
|
||
| 数据库 | **PostgreSQL 16** | Django 5 异步 ORM 必备(MySQL 后端不支持异步) |
|
||
| DB 驱动 | **psycopg3 (binary) 3.2.13** | 支持异步连接 |
|
||
| 缓存/队列 | Redis 7 | 缓存、Session、Celery broker、Channels layer |
|
||
| 前端 | React 19 + Vite 8 | 独立 SPA,与 Django 模板层完全解耦 |
|
||
| 反代/静态 | Nginx | SPA 静态资源直出 + API 反向代理 |
|
||
|
||
## 关键设计决策
|
||
|
||
1. **全链路异步**:视图 `async def` → ORM `aget/afirst/aexists/acount/acreate/asave/aupdate/adelete` → PostgreSQL(psycopg3)。`CONN_MAX_AGE=0`(异步 ORM 不支持持久连接)。
|
||
2. **ADRF 兼容性**:`view_is_async` 自动检测处理器;纯 CPU 视图(图形验证码、图像压缩、文本 diff)保持同步 `def`,ADRF 自动 `sync_to_async` 线程池兜底。
|
||
3. **外部 HTTP 调用 aiohttp 化**:weather / air_quality / currency / getIpData 全部改为 `aiohttp.ClientSession`,无阻塞 I/O。
|
||
4. **Redis 缓存 / Celery / SMTP / 密码哈希**:同步客户端用 `sync_to_async` 包裹(不阻塞事件循环)。
|
||
5. **事务兜底**:`transaction.atomic` + `select_for_update` 段(钱包、任务领取)抽成同步闭包整体 `sync_to_async` 执行,保住行锁语义。
|
||
6. **数据库可切换**:`DB_ENGINE` 环境变量支持 `django.db.backends.postgresql`(默认)与 `django.db.backends.mysql`(本地过渡)。
|
||
|
||
## 前后端分离
|
||
|
||
- 开发:`pnpm dev`(Vite HMR :5173),`BACKEND_URL` 环境变量指定后端(默认 `http://127.0.0.1:8002`,Docker 后端用 `http://127.0.0.1:8000`)。API 全部走 Vite proxy,无 CORS 负担。
|
||
- 生产:`pnpm build` 产物由 Nginx 容器直出(`try_files → index.html` SPA 回退),带指纹资源 1 年 immutable 缓存,index.html no-cache。
|
||
- 前后端仅通过 RESTful API + WebSocket 交互。
|
||
|
||
## 快速开始
|
||
|
||
### 一键部署(生产形态)
|
||
```bash
|
||
docker compose up -d --build
|
||
# 前端 http://localhost:8080
|
||
# 后端 API http://localhost:8000
|
||
# Swagger http://localhost:8080/swagger/
|
||
```
|
||
|
||
### 本地开发
|
||
```bash
|
||
# 1. 启动数据层
|
||
docker compose up -d postgres redis
|
||
|
||
# 2. 后端(异步栈)
|
||
cd chunyu_project
|
||
export DB_ENGINE=django.db.backends.postgresql DB_HOST=localhost DB_PORT=5432 \
|
||
DB_NAME=chunyu DB_USER=chunyu DB_PASSWORD=chunyu_pg_pass \
|
||
REDIS_HOST=localhost
|
||
python manage.py migrate
|
||
granian --interface asgi --host 0.0.0.0 --port 8000 chunyu_project.asgi:application
|
||
|
||
# 3. 前端(Vite HMR)
|
||
cd chunyu_project_react
|
||
BACKEND_URL=http://127.0.0.1:8000 pnpm dev # http://localhost:5173
|
||
```
|
||
|
||
## 异步化转换统计
|
||
|
||
### Phase 1:视图层全异步(ADRF 事件循环)
|
||
- 覆盖 16 个应用、45 个视图文件:`api` `user` `article` `learn` `chat` `message` `history` `search` `bug` `tool` `weather` `air_quality` `currency` `shorturl` `logs` `apidirectory` `app`
|
||
- 处理器全部 `async def`(约 90+ 个);ORM 调用全部异步化
|
||
- 转换规范见 `chunyu_project/docs/ADRF_CONVERSION_GUIDE.md`
|
||
|
||
### Phase 2:serializer 层原生异步(100% 异步化收口)
|
||
- **10 个 serializer 文件**全部迁移至 `adrf.serializers`(原生异步 ModelSerializer/Serializer):
|
||
`article` `learn` `bug` `chat` `message` `history` `tool` `apidirectory` `user/serializers/user_serializers` `user/serializers/region_serializers`
|
||
- 全部 `SerializerMethodField` 的 getter 改为 `async def` + 异步 ORM(含嵌套外键改 id 访问/`select_related` 消懒加载)
|
||
- 写路径 `create/update` → `acreate/aupdate`,视图统一 `await serializer.asave(**kwargs)`(adrf 原生,kwargs 合入 validated_data)
|
||
- 序列化输出统一 `await serializer.adata`(替代同步 `.data` 与 `sync_to_async` 兜底)
|
||
- **async_cache 基建**(`utils/async_cache.py`):`redis.asyncio` 直连 + msgpack 序列化 + 同步缓存降级,验证码/缓存场景(注册/登录/改密/邮箱/手机号/IP 定位)全部切换
|
||
- 外键懒加载治理:黑名单列表 `select_related('blocked_user')`、聊天消息 `select_related('sender')`、评论父作者异步加载等
|
||
|
||
### 终态核验(服务器实跑 grep)
|
||
- serializer 层裸同步 ORM getter:**0**
|
||
- 视图层 `serializer.data` 同步残留:**0**
|
||
- `sync_to_async(serializer…)` 残留:仅 8 处 `is_valid`(**设计边界**:Django validate 钩子同步调用,且 user 系列校验器含 DB 查询/argon2 哈希)+ 已清零的 save
|
||
- `async def` 总量:**310+**
|
||
- 剩余 `sync_to_async`(约 160 处)全部为**无异步 API 的库**:SMTP/Celery `.delay`/密码哈希/文件存储/DRF 分页器求值/`transaction.atomic` 事务段(Django 5.2 限制,行锁语义必须同步闭包)
|
||
|
||
## 验证记录
|
||
|
||
- ✅ `python manage.py check` 0 错误
|
||
- ✅ URLCONF 全量导入成功(所有视图模块可加载)
|
||
- ✅ 205 个 py 文件 py_compile 0 失败
|
||
- ✅ Granian 启动 + 冒烟:`/`、`/article/articles/`、`/learn/courses/`、`/api/apidirectory/categories/`、`/api/weather/?city=Beijing`、`/api/air-quality/?city=Shanghai`、`/api/currency/rates/?base=USD`、`/api/image-captcha/`、`/api/captcha/generate/`、`/swagger.json`(331KB) 全部 200
|
||
- ✅ `docker compose config` 校验通过
|
||
- ✅ Nginx 配置语法 `nginx -t` 通过
|
||
- ✅ Phase-2 服务器回归:7/9 端点组全绿、写路径(短链/评论)异步落库、并发 240/240
|
||
- ✅ Phase-3 全链路深度回归:公开 11/11 + 认证 6/6(JWT 登录/钱包/黑名单/短链/登录记录/任务)+ 并发 240/240,评论"创建→异步序列化→PG 持久化"全链路打通
|
||
|
||
### Phase 3:全链路深度测试暴露并修复的原有 Bug(非异步化回归,均已确认在改造前就存在)
|
||
1. `ArticleCommentSerializer.created_at` 缺字段级 `read_only=True` —— DRF/adrf 的 `Meta.read_only_fields` 对显式声明字段无效,导致**评论接口一直无法用**(前端只发 content/parent)
|
||
2. `@method_decorator(never_cache, name='dispatch')` 同步包装器在 adrf 协程上崩溃(wallet/login_record/tasks)—— 新增 `utils/async_decorators.py::async_never_cache_dispatch`(兼容同步/异步 dispatch)
|
||
3. `FUser` 没有 `nickname` 字段 —— article 通知/message sender_name/bug user_name 共 6 处改用 `first_name`(前两处一直被 Bug1 掩盖,从未执行到)
|
||
|
||
> 教训:这三个 Bug 层层叠加掩盖——Bug1 让评论接口 400(走不到后面),Bug3 在 Bug1 修复后才暴露;never_cache 502 则在无认证流量时不可见。深度回归必须带认证流量。
|
||
|
||
## 服务器部署(192.168.5.7 生产/联调环境)
|
||
|
||
本地 Windows 宿主虚拟化暂时不可用(Hypervisor 未加载,见下),已将整套异步架构部署至本地 Linux 服务器:
|
||
|
||
| 项 | 值 |
|
||
|---|---|
|
||
| 服务器 | Debian,16 核(`lan-19216857` SSH profile) |
|
||
| 部署目录 | `/mnt/sda/chunyu` |
|
||
| 访问地址 | `http://192.168.5.7:18080`(前端 SPA + API 反代) |
|
||
| 后端直连 | `http://192.168.5.7:18000`(Granian) |
|
||
| PostgreSQL | `192.168.5.7:15432`(容器内 5432) |
|
||
| Redis | `192.168.5.7:16379`(容器内 6379) |
|
||
|
||
端口重映射原因:服务器既有服务占用 5432/6379/8080,覆盖文件 `docker-compose.server.yml` 将宿主端口改为 15432/16379/18000/18080,容器间通信仍走内部网络不受影响。
|
||
|
||
部署命令:
|
||
```bash
|
||
cd /mnt/sda/chunyu
|
||
docker compose -f docker-compose.yml -f docker-compose.server.yml up -d --build
|
||
```
|
||
|
||
服务器端到端验证记录(全部通过):
|
||
- ✅ 四容器运行:chunyu-backend(healthy) / chunyu-frontend / chunyu-postgres(healthy) / chunyu-redis(healthy)
|
||
- ✅ SPA 静态资源 Nginx 直出 200;`/api/apidirectory/categories/`、`/article/articles/`、`/swagger.json` 经反代 200
|
||
- ✅ aiohttp 外部 API(天气)经反代 200
|
||
- ✅ 异步 ORM 写路径:短链创建成功落库(PostgreSQL 66 张迁移表)
|
||
- ✅ Redis PONG(缓存/Session 层)
|
||
- ✅ WebSocket:Granian + Channels,JWT 认证后 accept 成功(匿名拒绝为原有业务设计,ChatConsumer 强制认证)
|
||
- ✅ 并发冒烟:80 并发 × 5 轮 = 400 请求,**400/400 成功,187.9 RPS**(穿透 Nginx → Granian → PostgreSQL 全链路)
|
||
- ✅ Windows 局域网访问 `http://192.168.5.7:18080/*` 全部 200
|
||
|
||
> 说明:`DJANGO_DEBUG` 插值默认 True(局域网无 TLS;False 会触发 `SECURE_SSL_REDIRECT` 301)。正式上 TLS 后设 `DJANGO_DEBUG=False`。
|
||
> 本地 Windows 宿主:2026-09-05 22:22 重启后 HypervisorPresent=False(BCD hypervisorlaunchtype 被关),Docker Desktop 不可用;管理员执行 `bcdedit /set "{current}" hypervisorlaunchtype auto` 重启后可恢复本地 Docker。
|
||
|
||
## 环境变量
|
||
|
||
见 `docker.env.example`。核心项:
|
||
|
||
| 变量 | 默认 | 说明 |
|
||
|---|---|---|
|
||
| `DB_ENGINE` | postgresql | django.db.backends.postgresql / mysql |
|
||
| `DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PASSWORD` | postgres/5432/chunyu/chunyu | 数据库连接 |
|
||
| `REDIS_HOST/REDIS_PORT/REDIS_PASSWORD` | redis/6379 | Redis 连接 |
|
||
| `POSTGRES_PASSWORD` | chunyu_pg_pass | PostgreSQL root 密码 |
|
||
| `DJANGO_DEBUG` | False | 生产必须 False |
|
||
| `CORS_ALLOWED_ORIGINS` | localhost:8080/5173 | 前端来源 |
|
||
|
||
## 已知限制
|
||
|
||
1. **serializer 层仍有同步 ORM**(经 sync_to_async 兜底,功能正确但该段走线程池):`chat/serializers.py` ConversationSerializer 的 MethodField、`bug` 列表 get_images_count、若干 `serializer.create()`。后续可对 serializer 原生异步化。
|
||
2. **Celery worker 不在默认编排内**(views 已适配:同步调用失败自动降级 submit_task 异步队列)。如需启用:`docker compose up -d` 后手动 `docker compose run backend celery -A chunyu_project worker -l info`。
|
||
3. Channels ASGI Lifespan 在 Granian 下有 warning(`asginl` 接口可消除),不影响 WebSocket 功能。
|
||
4. 纯 CPU 视图(验证码/压缩/diff)保持同步,ADRF 自动线程池兜底。
|