问题描述
已登录用户打开 Web 端(例如 /app/overview)时,应用外壳会立即显示,但首屏约 30 秒内处于近似空状态:
- 右上角没有用户头像
- 账本选择器和账本数据未加载
- 约 30 秒后,头像、账本及页面数据才集中出现
- 部分首屏 API 会在等待 30 秒后返回 500
这不是静态资源或反向代理加载慢:HTML/JS 和应用外壳均能立即显示,阻塞发生在 origin 后端 API。
环境
- Docker 部署,使用当前
latest 镜像
- 单个 Uvicorn worker
- SQLite,WAL 模式
- 数据库主文件约 824 KiB
- 约 14 条 transaction projection
- 约 239 条 sync change
数据量很小,因此不像是大数据量导致的慢查询。
后端日志(已匿名化)
同一轮首屏加载中可观察到:
GET /api/v1/profile/me → 200 30107.3ms
GET /api/v1/read/ledgers → 500 30086.8ms
GET /api/v1/sync/pull → 200 30371.0ms
sqlalchemy.exc.TimeoutError:
QueuePool limit of size 5 overflow 10 reached,
connection timed out, timeout 30.00
同一时刻,accounts、tags、categories、budgets、analytics 等多个请求也会等待约 30 秒;一部分返回 500,另一部分在连接释放后返回 200。拥塞解除后,同类请求通常只需数毫秒到几十毫秒。
初步分析
服务端创建 engine 时没有显式配置连接池:
engine_kwargs: dict = {"pool_pre_ping": True}
engine = create_engine(settings.database_url, **engine_kwargs)
因此使用 SQLAlchemy QueuePool 默认参数:
pool_size=5
max_overflow=10
pool_timeout=30
Web 首屏会并行请求 profile、ledgers、admin probe、categories、accounts、tags、budgets,以及多组 analytics;同时还可能建立 WebSocket 并执行 sync pull。并发加载时连接池被占满,后续请求恰好等待默认的 30 秒后超时。
还需要进一步确认连接被长期占用的具体原因,例如:
- FastAPI 同步 generator dependency 的 session 释放是否在高并发下发生线程池/连接池互相等待
- 某个请求或流式响应是否延迟释放 session
- 首屏请求是否存在重复、无上限并发或可以合并的查询
前端目前以 ledgers=[]、profileMe=null 直接渲染,数据返回前会隐藏账本选择器和头像,也没有 shell loading 状态,因此后端等待表现为“空 UI”。
复现步骤
- 使用 SQLite 启动 Docker 服务
- 已登录状态下打开或刷新 Web Overview
- 观察 Network 和后端日志
- 在问题出现时,多个数据库 API 等待约 30 秒,并伴随上述 QueuePool TimeoutError
- 连接释放后再次调用相同 API,响应恢复到毫秒级
期望行为
- profile 和 ledgers 应在首屏快速返回,不能因首页其他查询而等待连接池超时
- 首屏请求不应触发 QueuePool exhaustion 或 500
- profile/ledgers 未完成时应显示明确的加载状态,而不是空账户/空账本 UI
建议方向
- 先定位并修正数据库 session/connection 的持有与释放时机
- 为 SQLite 选择合适的 pool 策略,而不是仅增大
pool_size
- 对 Web 首屏请求做去重、限流或分阶段加载,优先 profile 和 ledgers
- 增加并发回归测试:并行发起超过 15 个首屏读请求,断言不会等待 30 秒或返回 500
- 为 AppShell 增加显式 loading/error 状态
问题描述
已登录用户打开 Web 端(例如
/app/overview)时,应用外壳会立即显示,但首屏约 30 秒内处于近似空状态:这不是静态资源或反向代理加载慢:HTML/JS 和应用外壳均能立即显示,阻塞发生在 origin 后端 API。
环境
latest镜像数据量很小,因此不像是大数据量导致的慢查询。
后端日志(已匿名化)
同一轮首屏加载中可观察到:
同一时刻,accounts、tags、categories、budgets、analytics 等多个请求也会等待约 30 秒;一部分返回 500,另一部分在连接释放后返回 200。拥塞解除后,同类请求通常只需数毫秒到几十毫秒。
初步分析
服务端创建 engine 时没有显式配置连接池:
因此使用 SQLAlchemy
QueuePool默认参数:pool_size=5max_overflow=10pool_timeout=30Web 首屏会并行请求 profile、ledgers、admin probe、categories、accounts、tags、budgets,以及多组 analytics;同时还可能建立 WebSocket 并执行 sync pull。并发加载时连接池被占满,后续请求恰好等待默认的 30 秒后超时。
还需要进一步确认连接被长期占用的具体原因,例如:
前端目前以
ledgers=[]、profileMe=null直接渲染,数据返回前会隐藏账本选择器和头像,也没有 shell loading 状态,因此后端等待表现为“空 UI”。复现步骤
期望行为
建议方向
pool_size