perf: SQLite WAL + 复合索引 + GZip + scheduler jitter + 构建缓存

This commit is contained in:
SmartUp Developer
2026-05-25 00:08:10 +08:00
parent 3a31d185a4
commit 41a439d830
10 changed files with 77 additions and 8 deletions
+4 -2
View File
@@ -1,4 +1,4 @@
# syntax=docker/dockerfile:1
# syntax=docker/dockerfile:1.4
# ---- Stage 1: Build frontend ----
FROM node:20-alpine AS frontend-build
WORKDIR /frontend
@@ -42,7 +42,9 @@ RUN --mount=type=cache,target=/root/.cache/pip \
-r requirements.txt
# Playwright Chromium:安装在镜像层中,业务代码变更不会触发重下
RUN playwright install chromium
# --mount=type=cache 利用 BuildKit 缓存避免每次构建重下 ~170 MB 浏览器
RUN --mount=type=cache,target=/root/.cache/ms-playwright,sharing=locked \
playwright install chromium
# 源码层:业务代码变更不影响上面所有依赖层
COPY backend/ .
+40 -1
View File
@@ -1,4 +1,4 @@
from sqlalchemy import create_engine, inspect, text
from sqlalchemy import create_engine, event, inspect, text
from sqlalchemy.orm import sessionmaker, DeclarativeBase
from app.config import get_settings
@@ -8,6 +8,21 @@ engine = create_engine(
settings.database_url,
connect_args={"check_same_thread": False},
)
# ── SQLite 性能 PRAGMAWAL + 缓存 + 超时) ──
@event.listens_for(engine, "connect", insert=True)
def _set_sqlite_pragma(dbapi_connection, connection_record):
if engine.dialect.name != "sqlite":
return
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA journal_mode=WAL;")
cursor.execute("PRAGMA synchronous=NORMAL;")
cursor.execute("PRAGMA busy_timeout=5000;")
cursor.execute("PRAGMA foreign_keys=ON;")
cursor.execute("PRAGMA cache_size=-20000;") # 20 MB page cache
cursor.execute("PRAGMA mmap_size=67108864;") # 64 MB(小内存容器友好)
cursor.close()
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@@ -28,11 +43,35 @@ def init_db():
# import models so SQLAlchemy registers them
from app.models import admin_user, upstream, snapshot, webhook_config, notification_log, custom_page, website, revoked_token, upstream_key # noqa: F401
Base.metadata.create_all(bind=engine)
_ensure_indexes()
_migrate_custom_pages()
_migrate_upstreams()
_migrate_upstream_generated_keys()
# ── 已有数据库幂等索引迁移 ─────────────────────────────────
_NEW_INDEXES = [
"CREATE INDEX IF NOT EXISTS ix_snapshot_upstream_captured ON upstream_rate_snapshots(upstream_id, captured_at DESC)",
"CREATE INDEX IF NOT EXISTS ix_notif_event_created ON notification_logs(event_type, created_at DESC)",
"CREATE INDEX IF NOT EXISTS ix_notif_status_created ON notification_logs(status, created_at DESC)",
"CREATE INDEX IF NOT EXISTS ix_sync_website_created ON website_sync_logs(website_id, created_at DESC)",
"CREATE INDEX IF NOT EXISTS ix_sync_binding_created ON website_sync_logs(binding_id, created_at DESC)",
"CREATE INDEX IF NOT EXISTS ix_upstream_enabled ON upstreams(enabled)",
"CREATE INDEX IF NOT EXISTS ix_key_upstream_name ON upstream_generated_keys(upstream_id, key_name)",
]
def _ensure_indexes() -> None:
"""创建 InitDB 时可能未包含的复合索引,幂等执行。"""
with engine.begin() as conn:
for stmt in _NEW_INDEXES:
try:
conn.execute(text(stmt))
except Exception:
logger = __import__("logging").getLogger(__name__)
logger.warning("index creation failed (non-fatal): %s", stmt[:60])
def _migrate_custom_pages():
"""Apply small SQLite-safe migrations for deployments without Alembic."""
inspector = inspect(engine)
+2
View File
@@ -5,6 +5,7 @@ from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
@@ -88,6 +89,7 @@ app.add_middleware(
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(GZipMiddleware, minimum_size=1024)
# API routers
app.include_router(auth.router)
+6 -1
View File
@@ -1,6 +1,6 @@
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import Integer, String, DateTime, Text, ForeignKey
from sqlalchemy import Index, Integer, String, DateTime, Text, ForeignKey, text
from sqlalchemy.orm import mapped_column, Mapped
from app.database import Base
@@ -19,3 +19,8 @@ class NotificationLog(Base):
status: Mapped[str] = mapped_column(String(16), nullable=False)
response_text: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc), index=True)
__table_args__ = (
Index("ix_notif_event_created", "event_type", text("created_at DESC")),
Index("ix_notif_status_created", "status", text("created_at DESC")),
)
+5 -1
View File
@@ -1,5 +1,5 @@
from datetime import datetime, timezone
from sqlalchemy import Integer, Text, DateTime, ForeignKey
from sqlalchemy import Index, Integer, Text, DateTime, ForeignKey, text
from sqlalchemy.orm import mapped_column, Mapped
from app.database import Base
@@ -11,3 +11,7 @@ class UpstreamRateSnapshot(Base):
upstream_id: Mapped[int] = mapped_column(Integer, ForeignKey("upstreams.id", ondelete="CASCADE"), index=True)
snapshot_json: Mapped[str] = mapped_column(Text, nullable=False)
captured_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc), index=True)
__table_args__ = (
Index("ix_snapshot_upstream_captured", "upstream_id", text("captured_at DESC")),
)
+5 -1
View File
@@ -1,6 +1,6 @@
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import Integer, String, Boolean, DateTime, Text, Float
from sqlalchemy import Index, Integer, String, Boolean, DateTime, Text, Float
from sqlalchemy.orm import mapped_column, Mapped
from app.database import Base
@@ -36,3 +36,7 @@ class Upstream(Base):
updated_at: Mapped[datetime] = mapped_column(
DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)
)
__table_args__ = (
Index("ix_upstream_enabled", "enabled"),
)
+2 -1
View File
@@ -1,7 +1,7 @@
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
@@ -30,4 +30,5 @@ class UpstreamGeneratedKey(Base):
__table_args__ = (
UniqueConstraint("upstream_id", "group_id", "key_name", name="uq_upstream_group_key"),
Index("ix_key_upstream_name", "upstream_id", "key_name"),
)
+6 -1
View File
@@ -1,7 +1,7 @@
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text, text
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
@@ -66,3 +66,8 @@ class WebsiteSyncLog(Base):
status: Mapped[str] = mapped_column(String(16), nullable=False)
message: Mapped[str] = mapped_column(Text, default="")
created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc), index=True)
__table_args__ = (
Index("ix_sync_website_created", "website_id", text("created_at DESC")),
Index("ix_sync_binding_created", "binding_id", text("created_at DESC")),
)
+2
View File
@@ -284,6 +284,8 @@ def refresh_upstream(upstream_id: int, interval_seconds: int = 0, enabled: bool
replace_existing=True,
coalesce=True,
max_instances=1,
misfire_grace_time=60,
jitter=30,
)
logger.info("scheduler job %s set to %ds interval", job_id, interval_seconds)
+5
View File
@@ -19,6 +19,11 @@ services:
- DATABASE_URL=sqlite:////app/data/app.db
- TZ=${TZ:-Asia/Shanghai}
- UNHEALTHY_THRESHOLD=${UNHEALTHY_THRESHOLD:-3}
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/healthz"]
interval: 30s