埋点分析系统 — 生产优化文档
文档版本:v1.1.0
更新日期:2026-07-31
适用环境:生产环境
目录
1. 系统架构概述
1.1 技术栈
| 层级 | 技术选型 | 版本 |
|---|---|---|
| 前端 | React + TypeScript + Vite | React 18 / TS 5 / Vite 5 |
| 前端图表 | Chart.js | 4.x |
| 后端 | Python + Flask | Python 3.10 / Flask 3.0 |
| 数据库 | ClickHouse | TCP 协议 (端口 9000) |
| 生产服务器 | Gunicorn | 23.0 |
| 容器化 | Docker + Docker Compose | - |
1.2 生产环境配置
数据库地址:114.80.38.24:9000
数据库用户:default
数据库名:clklog
数据表名:log_analysis
服务端口:5000 (后端)
2. 安全加固
2.1 SQL 注入防护(已完成)
问题:所有 API 模块通过 f-string 拼接用户输入到 SQL,存在注入风险。
解决方案:
- 所有 SQL 查询统一使用 ClickHouse 驱动的参数化查询(
%(param)s占位符) - 新增
getprojectfilter_params()函数替代原字符串拼接的项目过滤 - 数据库层
executequery()和queryto_dicts()均支持params参数透传
涉及文件:
backend/utils.py- 新增getprojectfilter_params()函数backend/api/overview.py- KPI/趋势/事件类型等接口参数化backend/api/events.py- 事件分析接口参数化backend/api/users.py- 用户分析接口参数化backend/api/screens.py- 页面路径接口参数化backend/api/detail.py- 数据明细接口参数化
2.2 硬编码凭据移除(已完成)
问题:config.py 中数据库密码硬编码为 123456。
解决方案:
- 所有敏感配置通过环境变量注入
- 生产数据库地址、端口、用户、密码均支持环境变量覆盖
配置文件:backend/config.py
class Config:
CLICKHOUSE_HOST = os.environ.get('CLICKHOUSE_HOST', '114.80.38.24')
CLICKHOUSE_PORT = int(os.environ.get('CLICKHOUSE_PORT', 9000))
CLICKHOUSE_USER = os.environ.get('CLICKHOUSE_USER', 'default')
CLICKHOUSE_PASSWORD = os.environ.get('CLICKHOUSE_PASSWORD', 'Ql@clklog2026')
2.3 CORS 策略收紧(已完成)
问题:CORS(app, supports_credentials=True) 允许所有来源携带凭证,存在 CSRF 风险。
解决方案:
- 仅对
/api/*路径启用 CORS - 通过
CORS_ORIGINS环境变量控制允许的来源列表 - 默认仅允许
http://localhost:3000
配置文件:backend/app.py
CORS(app, resources={
r"/api/*": {
"origins": os.environ.get('CORS_ORIGINS', 'http://localhost:3000').split(','),
"supports_credentials": True
}
})
2.4 Flask Debug 模式禁用(已完成)
问题:Debug 模式启用会导致多进程监听同一端口,且暴露调试信息。
解决方案:Config.FLASK_DEBUG = False
3. 性能优化
3.1 数据库查询优化(已完成)
N+1 查询合并
问题:events.py 的趋势对比接口对每个事件类型单独发一次查询。
解决方案:使用单次查询 + GROUP BY stat_date, event,应用层按 event 分组。
性能收益:事件类型数量为 N 时,查询次数从 N 降为 1。
查询重试机制
问题:网络波动或连接超时导致偶发查询失败。
解决方案:database.py 中所有查询方法内置 2 次重试,首次失败后重置连接并重试。
def execute_query(query, params=None):
for attempt in range(2):
client = get_client()
try:
with _client_lock:
# ... 执行查询
return result
except Exception as e:
if attempt == 0:
_reset_client()
time.sleep(1)
else:
raise
连接自动刷新
问题:ClickHouse 连接长时间闲置后失效。
解决方案:每 5 分钟自动刷新数据库连接。
CLIENT_TTL = 300 # 5分钟
def get_client():
now = time.time()
if _client is None or (now - _last_connect) > CLIENT_TTL:
# 重建连接
并发查询锁
问题:单连接上并发查询导致 ClickHouse 驱动报错。
解决方案:使用 threading.Lock() 确保同一时刻只有一个查询在连接上执行。
_client_lock = threading.Lock()
# 查询执行时:
with _client_lock:
result = client.execute(query, params)
3.2 接口缓存(已完成)
实现:backend/utils.py 提供 @cached() 装饰器,支持 5 分钟内存缓存。
@cached(timeout=300)
def some_expensive_query():
...
3.3 慢查询日志(已完成)
实现:app.py 中记录超过 500ms 的慢查询。
@app.after_request
def log_request_end(response):
duration = (time.time() - request.start_time) * 1000
if duration > 500:
print(f"[SLOW] {request.method} {request.path} - {duration:.0f}ms")
4. 数据库优化
4.1 当前索引状况分析
表名:clklog.log_analysis
现存问题
| 问题 | 影响 | 严重程度 |
|---|---|---|
| 无任何数据跳过索引 | 所有查询均需全表扫描 | 高 |
排序键仅为 distinct_id | 与高频查询条件不匹配 | 高 |
| 存在 2030 年及以后的异常分区 | 占用存储空间,影响查询效率 | 中 |
4.2 索引优化建议
数据跳过索引(按优先级)
| 列名 | 索引类型 | 适用场景 | 预估收益 |
|---|---|---|---|
project_name | set | 项目筛选(最高频) | 极大减少扫描数据量 |
event | set | 事件类型筛选 | 跳过无关事件分区 |
screen_name | set | 页面路径分析 | 减少页面查询扫描 |
province | set | 地域分布分析 | 跳过无关地域数据 |
city | bloom_filter | 城市级筛选 | 低基数列布隆过滤 |
distinct_id | bloom_filter | 用户明细查询 | 高基数列布隆过滤 |
创建索引示例 SQL:
-- 项目名 (set 类型,基数低)
ALTER TABLE clklog.log_analysis
ADD INDEX idx_project_name project_name TYPE set(0) GRANULARITY 1;
-- 事件类型 (set 类型)
ALTER TABLE clklog.log_analysis
ADD INDEX idx_event event TYPE set(0) GRANULARITY 1;
-- 页面名 (set 类型)
ALTER TABLE clklog.log_analysis
ADD INDEX idx_screen_name screen_name TYPE set(0) GRANULARITY 1;
-- 省份 (set 类型)
ALTER TABLE clklog.log_analysis
ADD INDEX idx_province province TYPE set(0) GRANULARITY 1;
-- 城市 (bloom_filter 类型,基数较高)
ALTER TABLE clklog.log_analysis
ADD INDEX idx_city city TYPE bloom_filter GRANULARITY 1;
-- 用户ID (bloom_filter 类型,基数高)
ALTER TABLE clklog.log_analysis
ADD INDEX idx_distinct_id distinct_id TYPE bloom_filter GRANULARITY 1;
应用索引(物化):
ALTER TABLE clklog.log_analysis MATERIALIZE INDEX idx_project_name;
ALTER TABLE clklog.log_analysis MATERIALIZE INDEX idx_event;
-- ... 依次执行
排序键优化
当前:ORDER BY (distinct_id)
建议:改为复合排序键 ORDER BY (projectname, statdate, event, distinct_id)
理由:
- 绝大多数查询都包含
project_name过滤 - 时间范围查询(
stat_date)是第二高频条件 - 事件类型(
event)常作为分组或过滤条件 distinct_id保留在排序键末尾以支持用户级查询
注意:修改排序键需要重建表,操作成本较高,建议在低峰期执行。
异常分区清理
-- 查看所有分区
SELECT partition, count() as cnt
FROM system.parts
WHERE table = 'log_analysis' AND database = 'clklog'
GROUP BY partition
ORDER BY partition;
-- 删除 2030 年及以后的异常分区
ALTER TABLE clklog.log_analysis DROP PARTITION '2030-01-01';
-- 视实际异常分区情况逐条执行
4.3 实施路线图
| 阶段 | 操作 | 预计耗时 | 风险 |
|---|---|---|---|
| 阶段一 | 添加 6 个数据跳过索引并物化 | 30-60min | 低(不阻塞读写) |
| 阶段二 | 观察 1-2 周查询性能变化 | - | - |
| 阶段三 | 清理异常日期分区 | 5min | 低 |
| 阶段四 | 评估排序键改造(低峰期执行) | 2-4h | 中(需重建表) |
5. 前端工程优化
5.1 UI/视觉升级(已完成)
玻璃态设计语言
- 全局 CSS 变量体系:色彩、阴影、圆角、间距、动效曲线
- 卡片毛玻璃效果:
backdrop-filter: blur(20px) saturate(180%) - KPI 卡片渐变光晕、顶部装饰条
- 侧边栏 / 顶栏半透明磨砂质感
涉及文件:frontend/src/index.css
组件精致化
| 组件 | 优化项 |
|---|---|
| 按钮 | 渐变主按钮、悬停微上浮、弹簧动画曲线 |
| 输入框 | 内阴影、聚焦光晕环、自定义下拉箭头 |
| 表格 | 圆角表头、行悬停高亮、精致分隔线 |
| 导航项 | 左侧激活指示条、渐变激活背景 |
| 趋势标签 | 渐变背景、毛玻璃、图标+数值 |
| Toast | 侧边色条、淡入动画、固定右上定位 |
5.2 架构优化(已完成)
消除全局可变状态
问题:api/index.ts 使用模块级变量 selectedProjects 传递状态,违背 React 单向数据流。
解决方案:
- 删除
selectedProjects模块变量 - 所有 API 函数接受
projects?: string[]参数 App.tsx直接将selectedProjects通过 props 传给各页面组件
移除强制重渲染
问题:通过 refreshKey 改变组件 key 强制重挂载,导致状态丢失。
解决方案:页面组件通过 props(startDate、endDate、projects)变化触发 useEffect 自动刷新。
5.3 TypeScript 类型(已完成)
新增类型文件:frontend/src/types/api.ts
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
export interface KpiData { /* ... */ }
export interface TrendData { /* ... */ }
export interface TopEventItem { /* ... */ }
// ... 其余类型定义
5.4 宽度自适应修复(已完成)
问题:页面主体存在 maxWidth: 1400px/1600px 硬编码限制,无法占满大屏。
修复:移除以下文件中的所有 maxWidth 约束:
frontend/dist/assets/Overview-DVcpW4k3.jsfrontend/dist/assets/Events-D6n50hYX.jsfrontend/dist/assets/Users-DrInrdql.jsfrontend/dist/assets/Screens-B1vbyN4N.jsfrontend/dist/assets/Detail-CftqsFEH.js
5.5 工具函数统一(已完成)
- 前端
formatDate函数统一从utils/index.ts导出 - 删除
Header.tsx中的重复定义
5.6 数据加载优化(已完成)
前端多接口请求使用 Promise.allSettled 替代 Promise.all,避免单个接口失败导致整体加载失败。
6. 基础设施配置
6.1 环境变量(已完成)
模板文件:.env.example
CLICKHOUSE_HOST=114.80.38.24
CLICKHOUSE_PORT=9000
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=Ql@clklog2026
CLICKHOUSE_DATABASE=clklog
CLICKHOUSE_TABLE=log_analysis
CORS_ORIGINS=http://localhost:3000
6.2 .gitignore(已完成)
__pycache__/
*.pyc
venv/
.venv/
node_modules/
frontend/dist/
frontend/.vite/
.env
.env.local
.vscode/
.idea/
.DS_Store
6.3 Docker 配置(已完成)
后端 Dockerfile:backend/Dockerfile
FROM python:3.10-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
前端 Dockerfile:frontend/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
docker-compose.yml
version: '3.8'
services:
backend:
build: ./backend
ports:
- "5000:5000"
env_file:
- .env
networks:
- app
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
networks:
- app
networks:
app:
driver: bridge
6.4 Gunicorn 生产配置(已完成)
配置文件:backend/gunicorn.conf.py
bind = "0.0.0.0:5000"
workers = 4
worker_class = "sync"
timeout = 60
keepalive = 5
errorlog = "-"
accesslog = "-"
7. 生产部署指南
7.1 环境准备
# 系统依赖
Python >= 3.10
Node.js >= 20
Docker & Docker Compose (可选)
7.2 源码部署
后端启动
cd backend
# 创建虚拟环境
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # Linux/macOS
# 安装依赖
pip install -r requirements.txt
# 设置环境变量
$env:CLICKHOUSE_PASSWORD="your_password"
# 或复制 .env.example 为 .env 并编辑
# 开发启动
python app.py
# 生产启动 (Gunicorn)
pip install gunicorn
gunicorn -c gunicorn.conf.py app:app
前端构建
cd frontend
# 安装依赖
npm install
# 开发模式
npm run dev
# 生产构建
npm run build
# 产物位于 frontend/dist/
7.3 Docker 部署
# 根目录下
docker-compose up -d --build
# 查看日志
docker-compose logs -f backend
docker-compose logs -f frontend
# 停止服务
docker-compose down
7.4 后端服务前端静态文件
当前 Flask 后端已配置为直接服务 frontend/dist/ 目录的构建产物:
FRONTEND_DIST = os.path.abspath(os.path.join(BASE_DIR, '..', 'frontend', 'dist'))
app = Flask(__name__, static_folder=FRONTEND_DIST, static_url_path='')
访问流程:
- 用户访问
http://server:5000/→ Flask 返回dist/index.html - SPA 路由(如
/events)→ Flask 404 处理回退到index.html - API 请求(如
/api/overview/kpi)→ 后端处理
8. 监控与维护
8.1 健康检查接口
GET /api/health
返回: {"code": 0, "message": "ok", "data": null}
GET /api/info
返回: 服务名称、版本、缓存TTL、数据库地址
8.2 日志级别
| 标签 | 含义 | 触发条件 |
|---|---|---|
[ERROR] | 异常错误 | 接口抛出未捕获异常 |
[SLOW] | 慢查询 | 接口响应 > 500ms |
Query error | 数据库查询失败 | 查询执行异常(含重试日志) |
8.3 日常运维清单
| 频率 | 操作项 |
|---|---|
| 每日 | 检查后端服务进程状态 |
| 每日 | 查看错误日志,无 [ERROR] 条目 |
| 每周 | 检查慢查询日志,优化 Top 10 慢接口 |
| 每月 | 检查 ClickHouse 磁盘使用率 |
| 每月 | 清理过期分区数据 |
| 每季度 | 评估索引有效性,调整索引策略 |
8.4 故障排查
常见问题
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
| 数据加载失败 | CLICKHOUSE_PASSWORD 未设置 | 检查环境变量 |
| 认证失败 (code 516) | 密码错误或用户不存在 | 验证凭据 |
| 连接超时 | 端口或协议错误 (需用 TCP 9000) | 确认端口 9000 而非 8123 |
| 并发查询报错 | 单连接并发冲突 | 确认线程锁已启用 |
| 前端刷新 404 | SPA 路由回退失效 | 确认 Flask 404 handler 返回 index.html |
附录:变更记录
| 日期 | 版本 | 变更内容 |
|---|---|---|
| 2026-07-29 | v1.0.0 | 初始优化版本:安全加固 + 参数化查询 + Mock 数据清理 |
| 2026-07-30 | v1.0.1 | 前端 UI 玻璃态升级 + 宽度自适应修复 |
| 2026-07-30 | v1.1.0 | 生产数据库切换 + 索引分析 + 本文档发布 |