Refactor(desktop-client): code structure for improved readability and maintainability
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,934 +0,0 @@
|
||||
# AI 品牌曝光监测系统技术方案 V3
|
||||
|
||||
## 1. 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 文档名称 | AI 品牌曝光监测系统技术方案 V3(容量与口径修订稿) |
|
||||
| 文档版本 | V3.0 |
|
||||
| 文档状态 | 待评审 |
|
||||
| 创建日期 | 2026-04-05 |
|
||||
| 基线文档 | `docs/ai-brand-monitoring-tech-design-v2.md` |
|
||||
| 适用范围 | 数据追踪模块全部页面 |
|
||||
| 关联文档 | `docs/geo-platform-prd-v1.md` (PRD 8.4 数据追踪) |
|
||||
| 关联文档 | `docs/question-driven-monitoring-design-v1.md` (V1 数据模型) |
|
||||
| 容量目标 | 5 万并发 **活跃** 用户(同时打开页面、切换筛选、触发突刺流量) |
|
||||
|
||||
## 2. V3 修订范围
|
||||
|
||||
本文档不再只是 V2 的“容量补丁”。本版同时修正了 V2 与采集可行性方案之间的口径断层,尤其是:
|
||||
|
||||
1. `run_mode` 未入模,导致标准回答和搜索增强回答不可比。
|
||||
2. 问题版本未完整进入汇总表唯一键,历史口径会被覆盖。
|
||||
3. 采集时间窗与聚合时序冲突,无法同时满足 T+1 发布与一致性。
|
||||
4. 在线缓存与离线任务队列共用同一 Redis 故障域,风险过高。
|
||||
|
||||
读者应先读完 V2 全文,再用本文档替换或补充以下章节:
|
||||
|
||||
| V2 章节 | V3 替换 / 补充章节 | 变更原因 |
|
||||
| --- | --- | --- |
|
||||
| 5. 数据库 Schema | 2.2 数据口径与 Schema Delta | V2 未建模 `run_mode`、版本键、引用事实 |
|
||||
| 8. 缓存策略 | 3. 读流量模型与缓存架构 | V2 低估页面 fan-out,缺少防雪崩手段 |
|
||||
| 10. 采集流水线设计 | 4. 采集流水线扩容 | V2 采集时间窗 / worker 容量不足 |
|
||||
| 11. 部署拓扑 | 5. 部署拓扑与资源 | V2 连接池计算有误、单点问题 |
|
||||
| 7. 后端架构 / 7.6 配置扩展 | 4.5、5.3、5.4 | V2 仍按单 provider / 单 Redis / 单 DB 结构假设 |
|
||||
| (无) | 6. 降级与容灾 | V2 缺失 |
|
||||
| 13. 实施计划 (Phase 21) | 7. 压测验收标准 | V2 缺失 |
|
||||
|
||||
V2 其他章节(页面需求、前端组件、API 响应结构、指标定义)原则上保持不变;但只要与本文中的口径分层、Schema Delta、采集时序冲突,应以 V3 为准。
|
||||
|
||||
### 2.1 数据口径分层与 UI 取数规则
|
||||
|
||||
为避免把不可比的数据混到同一张榜单中,V3 将监测数据拆成 3 种采集模式:
|
||||
|
||||
| `run_mode` | 含义 | 是否进入主统计 | 默认消费页面 |
|
||||
| --- | --- | --- | --- |
|
||||
| `api_standard` | 固定 prompt、低温、无搜索增强的标准 API 回答 | 是 | Dashboard、平台占比、竞争对手、业务主题、AI 对话问题、品牌印象 |
|
||||
| `api_search_grounded` | 开启平台官方搜索 / 引文能力后的回答 | 是,但单独统计 | AI 引用排名;未来可选做“搜索增强视角”专题页 |
|
||||
| `ui_sample` | 面向 PoC / 验证的前台 UI 采样结果 | 否 | 仅验证报表和 badcase,不进主 Dashboard |
|
||||
|
||||
核心规则:
|
||||
|
||||
1. **跨平台曝光、竞品、问题排行默认只看 `api_standard`。**
|
||||
2. **引用相关页面默认只看 `api_search_grounded`。**
|
||||
3. **不支持搜索增强或引文的平台,在引用页面中标记为“未接入 / 不支持”,不按 0 值并入分母。**
|
||||
4. **`ui_sample` 只用于验证,不参与 `monitoring_*` 主汇总。**
|
||||
|
||||
### 2.2 Schema Delta(覆盖 V2 Schema 的必要修订)
|
||||
|
||||
V2 的基础表结构仍可复用,但以下修订是必需项。
|
||||
|
||||
#### 2.2.1 `ai_platforms`
|
||||
|
||||
增加平台能力字段,避免查询层硬编码:
|
||||
|
||||
1. `supports_standard BOOLEAN NOT NULL DEFAULT true`
|
||||
2. `supports_search_grounded BOOLEAN NOT NULL DEFAULT false`
|
||||
3. `supports_citation BOOLEAN NOT NULL DEFAULT false`
|
||||
4. `supports_batch BOOLEAN NOT NULL DEFAULT false`
|
||||
5. `default_run_mode VARCHAR(30) NOT NULL DEFAULT 'api_standard'`
|
||||
|
||||
#### 2.2.2 `question_monitor_runs`
|
||||
|
||||
新增以下字段:
|
||||
|
||||
1. `business_date DATE NOT NULL`:统计归属日,允许 `run_at` 发生在次日凌晨。
|
||||
2. `run_mode VARCHAR(30) NOT NULL`:`api_standard` / `api_search_grounded` / `ui_sample`
|
||||
3. `provider_model VARCHAR(100) NOT NULL`
|
||||
4. `prompt_template_version VARCHAR(30) NOT NULL`
|
||||
5. `search_enabled BOOLEAN NOT NULL DEFAULT false`
|
||||
6. `request_id VARCHAR(100)`
|
||||
7. `retry_count INT NOT NULL DEFAULT 0`
|
||||
|
||||
建议增加幂等索引:
|
||||
|
||||
```sql
|
||||
CREATE UNIQUE INDEX uk_monitor_run_idempotent
|
||||
ON question_monitor_runs(tenant_id, brand_id, question_version_id, ai_platform_id, run_mode, business_date);
|
||||
```
|
||||
|
||||
#### 2.2.3 `question_monitor_parse_results`
|
||||
|
||||
新增以下字段:
|
||||
|
||||
1. `run_mode VARCHAR(30) NOT NULL`
|
||||
2. `search_hit BOOLEAN NOT NULL DEFAULT false`
|
||||
3. `search_info_json_key TEXT`
|
||||
4. `extractor_version VARCHAR(30) NOT NULL DEFAULT 'v1'`
|
||||
|
||||
#### 2.2.4 新增任务与质量表
|
||||
|
||||
新增下列表:
|
||||
|
||||
1. `monitoring_collect_tasks`:调度任务、重试、死信、优先级、租约状态
|
||||
2. `monitoring_citation_facts`:引用规范化事实表,保存 `normalized_url/domain/citation_hash`
|
||||
3. `monitoring_parse_badcases`:低置信度样本与人工回标结果
|
||||
|
||||
#### 2.2.5 汇总表统一增加 `run_mode`
|
||||
|
||||
以下汇总表必须增加 `run_mode`,并进入唯一键:
|
||||
|
||||
1. `monitoring_brand_daily_overview`
|
||||
2. `monitoring_platform_daily`
|
||||
3. `monitoring_platform_question_daily`
|
||||
4. `monitoring_competitor_daily`
|
||||
5. `monitoring_competitor_platform_daily`
|
||||
6. `monitoring_topic_daily`
|
||||
7. `monitoring_question_daily`
|
||||
8. `monitoring_citation_source_daily`
|
||||
9. `monitoring_citation_page_daily`
|
||||
10. `monitoring_brand_impression`
|
||||
|
||||
#### 2.2.6 问题版本进入汇总主键
|
||||
|
||||
以下表必须增加 `question_version_id` 并进入唯一键:
|
||||
|
||||
1. `monitoring_platform_question_daily`
|
||||
2. `monitoring_question_daily`
|
||||
|
||||
推荐唯一键修订:
|
||||
|
||||
```sql
|
||||
UNIQUE (tenant_id, brand_id, ai_platform_id, question_id, question_version_id, run_mode, metric_date)
|
||||
UNIQUE (tenant_id, brand_id, question_id, question_version_id, run_mode, metric_date)
|
||||
```
|
||||
|
||||
#### 2.2.7 引用汇总改为从规范化事实表聚合
|
||||
|
||||
`monitoring_citation_source_daily` 与 `monitoring_citation_page_daily` 不再直接从 `citation_urls JSONB` 聚合,而是从 `monitoring_citation_facts` 聚合:
|
||||
|
||||
1. 页面级汇总按 `normalized_url`
|
||||
2. 站点级汇总按 `domain` / `site_name`
|
||||
3. 原始 `page_url` 仅用于回放与调试
|
||||
|
||||
---
|
||||
|
||||
## 3. 读流量模型与缓存架构
|
||||
|
||||
### 3.1 页面 Fan-out 矩阵
|
||||
|
||||
V2 错误地按"每人 30 秒 1 个请求"建模。实际上,每个页面打开时会 **并行** 发出多个 API 请求。
|
||||
|
||||
| 页面 | 并行请求数 | 对应 API |
|
||||
| --- | --- | --- |
|
||||
| 主页 Dashboard | 6 | overview, exposure-trend, competitors, citation-sources, recent-questions, brand-impression |
|
||||
| 平台占比分析 | 2 | platform-stats, platform-questions |
|
||||
| 竞争对手分析 | 2 | competitor-exposure, competitor-detail |
|
||||
| 业务主题分析 | 1 | topics |
|
||||
| AI 对话问题 | 1 | questions (分页) |
|
||||
| AI 引用排名 | 2 | citation-trend, citation-pages |
|
||||
|
||||
加权平均(按访问占比估算:主页 50%、其他页各 10%):
|
||||
|
||||
```
|
||||
weighted_fan_out = 0.50 × 6 + 0.10 × 2 + 0.10 × 2 + 0.10 × 1 + 0.10 × 1 + 0.10 × 2
|
||||
= 3.0 + 0.2 + 0.2 + 0.1 + 0.1 + 0.2
|
||||
= 3.8 次/页面打开
|
||||
```
|
||||
|
||||
### 3.2 QPS 模型
|
||||
|
||||
| 场景 | 计算 | QPS |
|
||||
| --- | --- | --- |
|
||||
| 稳态(50K 用户,平均 30 秒交互 1 次) | 50000 × 3.8 / 30 | **6,333** |
|
||||
| 突刺(早会/开盘集中打开,10 秒内 30% 用户首屏加载) | 15000 × 6 / 10 | **9,000** |
|
||||
| 筛选切换(20% 用户同时切换品牌/时间范围) | 10000 × 3.8 / 5 | **7,600** |
|
||||
| 设计容量(取 P99 峰值 × 1.5 安全余量) | max(6333, 9000, 7600) × 1.5 | **~13,500** |
|
||||
|
||||
**结论**:系统必须在 Redis 层稳定承接 13,500 QPS。穿透到 PG 的流量目标 < 1,350 QPS(10% miss rate)。
|
||||
|
||||
### 3.3 BFF 聚合层(减少 fan-out 的关键手段)
|
||||
|
||||
为降低首页 6 次并行请求的压力,引入 **BFF 聚合接口**:
|
||||
|
||||
```
|
||||
GET /api/tenant/monitoring/dashboard/composite
|
||||
→ 服务端并行查询 6 个数据源,一次返回全部首页数据
|
||||
→ 默认固定 `scope=standard`
|
||||
→ 单次 Redis GET 命中完整 composite 缓存即可返回
|
||||
→ 缓存 miss 时服务端内部 goroutine 并行查 6 个汇总表
|
||||
```
|
||||
|
||||
前端首屏只发 **1 个请求**,极端情况下用户手动刷新某个区块再发单接口。
|
||||
|
||||
| 方案 | 首页 QPS (50K 用户) | Redis 压力 |
|
||||
| --- | --- | --- |
|
||||
| 6 个独立接口 | ~10,000 | 60K ops/s |
|
||||
| composite 聚合 | ~1,667 | 1,667 ops/s |
|
||||
|
||||
其他页面 fan-out ≤ 2,不需要聚合。
|
||||
|
||||
### 3.4 缓存防雪崩机制
|
||||
|
||||
V2 仅使用 TTL + 聚合后全量删除,存在雪崩风险。V3 引入 4 层防护:
|
||||
|
||||
#### 3.4.1 Singleflight
|
||||
|
||||
同一 cache key 的并发 miss 请求,只放行 1 个打到 PG,其余等待结果。
|
||||
|
||||
```go
|
||||
import "golang.org/x/sync/singleflight"
|
||||
|
||||
var sf singleflight.Group
|
||||
|
||||
func (s *MonitoringService) getWithCache(ctx context.Context, key string, ttl time.Duration, loader func() (any, error)) (any, error) {
|
||||
// 1. 尝试 Redis GET
|
||||
if cached, err := s.cache.Get(ctx, key); err == nil {
|
||||
return cached, nil
|
||||
}
|
||||
// 2. Singleflight: 同 key 只执行一次 loader
|
||||
result, err, _ := sf.Do(key, func() (any, error) {
|
||||
data, err := loader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 写回 Redis,TTL 加随机抖动
|
||||
jitteredTTL := ttl + time.Duration(rand.Int63n(int64(ttl/5)))
|
||||
_ = s.cache.Set(ctx, key, data, jitteredTTL)
|
||||
return data, nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.4.2 TTL 随机抖动
|
||||
|
||||
防止大量 key 同时过期导致集中穿透。
|
||||
|
||||
```
|
||||
实际 TTL = 基础 TTL + random(0, 基础 TTL × 20%)
|
||||
```
|
||||
|
||||
例如 5 分钟基础 TTL → 实际 300~360 秒。
|
||||
|
||||
#### 3.4.3 Stale-While-Revalidate
|
||||
|
||||
缓存过期后,先返回旧数据,后台异步刷新。用户不感知延迟。
|
||||
|
||||
```go
|
||||
// Redis 存储两个 key:
|
||||
// mon:{key} → 数据 (TTL = base_ttl)
|
||||
// mon:{key}:stale → 数据 (TTL = base_ttl × 3,作为兜底)
|
||||
//
|
||||
// 读取流程:
|
||||
// 1. GET mon:{key} → 命中 → 返回
|
||||
// 2. miss → GET mon:{key}:stale → 命中 → 返回 stale 数据 + 异步触发刷新
|
||||
// 3. 都 miss → 同步查 PG + singleflight
|
||||
```
|
||||
|
||||
#### 3.4.4 热点保护(Hot Key)
|
||||
|
||||
对高频访问的品牌(如首页默认品牌),使用 **进程内 L1 缓存**(Go sync.Map 或 groupcache):
|
||||
|
||||
```
|
||||
请求 → L1 进程缓存 (TTL 10s) → Redis (TTL 5min) → PG
|
||||
```
|
||||
|
||||
L1 缓存的极短 TTL 保证数据不会太旧,但能挡住 Redis 的瞬时压力。
|
||||
|
||||
### 3.5 缓存 Key 修订
|
||||
|
||||
修复 V2 的分页缓存 key 缺少 `page_size` 的问题,同时将统计口径 `scope` 纳入缓存维度。
|
||||
|
||||
约定:
|
||||
|
||||
1. `scope=standard` 对应 `run_mode=api_standard`
|
||||
2. `scope=search` 对应 `run_mode=api_search_grounded`
|
||||
3. `ui_sample` 不进入线上缓存
|
||||
|
||||
```
|
||||
# 分页接口必须包含 scope + page + page_size
|
||||
mon:{tenant}:{brand}:platform:{pid}:questions:{days}:{scope}:{page}:{pageSize}
|
||||
mon:{tenant}:{brand}:questions:{pid}:{days}:{scope}:{page}:{pageSize}
|
||||
mon:{tenant}:{brand}:citation_pages:{days}:{scope}:{page}:{pageSize}
|
||||
|
||||
# composite 聚合接口
|
||||
mon:{tenant}:{brand}:dashboard_composite:{days}:{scope}
|
||||
|
||||
# 非分页接口也加 scope
|
||||
mon:{tenant}:{brand}:overview:{days}:{scope}
|
||||
mon:{tenant}:{brand}:trend:{days}:{scope}
|
||||
mon:{tenant}:{brand}:competitors:{days}:{scope}
|
||||
...
|
||||
```
|
||||
|
||||
### 3.6 缓存失效策略修订
|
||||
|
||||
V2 的"聚合后删除品牌全部缓存"策略改为 **渐进式失效**:
|
||||
|
||||
| 时机 | 操作 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 聚合 Job 完成 | **不删除** 现有缓存 | 避免 5 万用户同时穿透 |
|
||||
| 聚合 Job 完成 | 异步预热 Top 20 热门品牌的 composite 缓存 | 用新数据替换旧缓存 |
|
||||
| 聚合 Job 完成 | 设置全局标记 `mon:agg_version:{tenant}` +1 | 通知各实例 L1 缓存过期 |
|
||||
| 自然过期 | TTL 到期后正常 singleflight 回源 | 冷数据自然刷新 |
|
||||
|
||||
### 3.7 修订后的 TTL 矩阵
|
||||
|
||||
| 数据类型 | Redis TTL | Stale TTL | L1 TTL | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Dashboard composite | 5 min | 15 min | 10 s | 首页入口,高频访问 |
|
||||
| Overview 指标卡 | 5 min | 15 min | 10 s | 同上 |
|
||||
| 趋势图 | 15 min | 45 min | 30 s | 日级数据,变化慢 |
|
||||
| 竞品排名 | 15 min | 45 min | 30 s | 同上 |
|
||||
| 引用来源 | 15 min | 45 min | 30 s | 同上 |
|
||||
| 业务主题 | 30 min | 90 min | 60 s | 低频变更 |
|
||||
| 问题列表(分页) | 10 min | 30 min | 不缓存 | 粒度太细,L1 命中率低 |
|
||||
| 品牌印象 | 30 min | 90 min | 60 s | 日级聚合 |
|
||||
| AI 平台列表 | 1 hour | 3 hour | 5 min | 几乎不变 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 采集流水线扩容
|
||||
|
||||
### 4.1 V2 瓶颈分析
|
||||
|
||||
V2 的问题:
|
||||
- 5 worker、50,000 任务 → 每任务仅 0.72 秒,不现实
|
||||
- 凌晨 1 点采集、3 点聚合 → 2 小时时间窗太紧
|
||||
- Collector ×1、Aggregator ×1 → 单点故障
|
||||
|
||||
### 4.2 任务量重新估算
|
||||
|
||||
```
|
||||
标准模式:
|
||||
100 品牌 × 100 问题/品牌 × 5 AI 平台 = 50,000 任务/天
|
||||
|
||||
搜索增强模式(首批仅 3 平台: qwen / doubao / hunyuan):
|
||||
100 品牌 × 100 问题/品牌 × 3 AI 平台 = 30,000 任务/天
|
||||
|
||||
合计 = 80,000 任务/天
|
||||
|
||||
# 但问题 hash 去重后(V1 设计)
|
||||
假设去重率 30% → 实际 56,000 唯一任务
|
||||
|
||||
# 每次 AI API 调用 + 解析 + 入库:
|
||||
平均耗时: 8 秒 (含 AI 生成 5s + 解析 2s + 入库 1s)
|
||||
P99 耗时: 20 秒 (含重试)
|
||||
```
|
||||
|
||||
### 4.3 Worker 池设计
|
||||
|
||||
```
|
||||
所需总处理时间 = 56,000 × 8s = 448,000 秒
|
||||
可用采集时间窗 = 6 小时 = 21,600 秒 (0:00 ~ 6:00)
|
||||
|
||||
所需并发 worker = 448,000 / 21,600 ≈ 21
|
||||
安全系数 1.4 → 30 worker
|
||||
```
|
||||
|
||||
**架构**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 采集调度器 (Scheduler) │
|
||||
│ • 凌晨 0:00 启动 │
|
||||
│ • 生成 business_date = yesterday 的全部任务 │
|
||||
│ • 任务维度: 品牌 × 去重问题 × 平台 × run_mode │
|
||||
│ • 写入 Queue Redis Stream: mon:collect:tasks │
|
||||
└───────────────────────┬─────────────────────────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┬─────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
|
||||
│ Collector │ │ Collector │ │ Collector │ │ Collector │
|
||||
│ Pod #1 │ │ Pod #2 │ │ Pod #3 │ │ standby HPA│
|
||||
│ worker ×10 │ │ worker ×10 │ │ worker ×10 │ │ 按需扩容 │
|
||||
└──────┬─────┘ └──────┬─────┘ └──────┬─────┘ └──────┬─────┘
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Queue Redis Stream │
|
||||
│ mon:collect:tasks (pending → ack) │
|
||||
│ Consumer Group: collectors │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**为什么用 Redis Stream 而不是内存队列**:
|
||||
- 多 Pod 竞争消费,自动负载均衡
|
||||
- 消息持久化,Pod 重启不丢任务
|
||||
- Consumer Group 的 XACK 机制提供 at-least-once 保证
|
||||
- 但必须使用 **独立 Queue Redis 故障域**,不能和在线缓存共用实例
|
||||
|
||||
### 4.4 时间窗修订
|
||||
|
||||
```
|
||||
0:00 调度器生成 business_date = yesterday 的全部采集任务 → Queue Redis Stream
|
||||
0:00 ~ 6:00 Collector workers 消费任务,调用 AI API + 解析 + 写 PG/MinIO
|
||||
6:00 检查未完成任务数量,告警如果 > 5%
|
||||
6:10 将未完成任务转入 backfill 队列,不阻塞晨间主聚合
|
||||
6:30 Aggregator 聚合 business_date = yesterday 且 status = succeeded 的数据
|
||||
7:00 聚合完成 → 预热热门品牌缓存
|
||||
7:30 T+1 数据上线,用户访问“昨日完整快照”
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
1. `run_at` 是实际执行时间,可能发生在次日凌晨。
|
||||
2. `business_date` 是统计归属日,Dashboard 晨间看到的是 T+1 快照。
|
||||
3. 晚到任务走 backfill,单独触发品牌级重聚合,不污染主发布窗口。
|
||||
|
||||
预留 6 小时采集窗口(vs V2 的 2 小时),即使 P99 延迟也有足够 buffer。
|
||||
|
||||
### 4.5 采集重试与限流
|
||||
|
||||
```go
|
||||
// 每个 AI 平台独立的限流器
|
||||
type PlatformRateLimiter struct {
|
||||
limiters map[string]*rate.Limiter // provider_key → limiter
|
||||
}
|
||||
|
||||
// 默认限流配置
|
||||
var defaultLimits = map[string]rate.Limit{
|
||||
"deepseek": 10, // 10 QPS
|
||||
"qwen": 10,
|
||||
"doubao": 10,
|
||||
"hunyuan": 5,
|
||||
"ernie": 5,
|
||||
}
|
||||
|
||||
// 重试策略
|
||||
const (
|
||||
maxRetries = 3
|
||||
retryBaseWait = 2 * time.Second // 指数退避: 2s, 4s, 8s
|
||||
)
|
||||
```
|
||||
|
||||
此外,Collector 必须基于 `run_mode` 和平台能力做任务路由:
|
||||
|
||||
1. 不支持 `api_search_grounded` 的平台不生成搜索增强任务。
|
||||
2. `ui_sample` 任务只进入 PoC / badcase 流水线,不进入主聚合队列。
|
||||
3. `api_standard` 与 `api_search_grounded` 使用不同的 prompt 模板版本。
|
||||
|
||||
### 4.6 采集容错
|
||||
|
||||
```go
|
||||
// 单条任务失败不阻塞批次
|
||||
func (w *CollectorWorker) processTask(ctx context.Context, task CollectTask) {
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
err := w.collect(ctx, task)
|
||||
if err == nil {
|
||||
w.stream.XAck(ctx, "mon:collect:tasks", "collectors", task.ID)
|
||||
return
|
||||
}
|
||||
if isRateLimited(err) {
|
||||
time.Sleep(retryBaseWait * time.Duration(1<<(attempt-1)))
|
||||
continue
|
||||
}
|
||||
// 非限流错误直接标记失败
|
||||
w.markFailed(ctx, task, err)
|
||||
w.stream.XAck(ctx, "mon:collect:tasks", "collectors", task.ID)
|
||||
return
|
||||
}
|
||||
w.markFailed(ctx, task, fmt.Errorf("max retries exceeded"))
|
||||
w.stream.XAck(ctx, "mon:collect:tasks", "collectors", task.ID)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.7 聚合 Job 扩容
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────┐
|
||||
│ Aggregator Job │
|
||||
│ • 6:30 启动(聚合 business_date = yesterday) │
|
||||
│ • 按租户 × 品牌分片,每片独立事务 │
|
||||
│ • 并发聚合 goroutine: 10 │
|
||||
│ • 聚合完一个品牌 → 异步预热该品牌缓存 │
|
||||
│ • 全部完成后 → 全局 agg_version +1 │
|
||||
└───────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
聚合 Job 部署 **2 个实例**(主备),使用 **PG advisory lock** 选主,只有一个实例实际运行,另一个待命。
|
||||
|
||||
---
|
||||
|
||||
## 5. 部署拓扑与资源
|
||||
|
||||
### 5.1 修正:连接池是 per-process 的
|
||||
|
||||
V2 错误地把 `max_open_conns: 50` 当作"3 个实例共享"。实际上 `bootstrap.go` 的 `pgxpool.Pool` 是每个进程独立初始化的。
|
||||
|
||||
**修正后的连接池规划**:
|
||||
|
||||
```yaml
|
||||
# 每个 API 实例
|
||||
database:
|
||||
max_open_conns: 20 # 每实例 20
|
||||
max_idle_conns: 5
|
||||
|
||||
# 3 个 API 实例 → PG 总连接: 60
|
||||
# 3 个 Collector 实例 → PG 总连接: 30 (各 10)
|
||||
# 1 个 Aggregator 实例 → PG 总连接: 20
|
||||
# 合计: 110 连接
|
||||
|
||||
# PostgreSQL 需配置:
|
||||
# max_connections = 160 (留 50 余量给运维和监控)
|
||||
```
|
||||
|
||||
### 5.1.1 配置模型 Delta
|
||||
|
||||
V3 不能继续沿用当前项目的单 `database`、单 `redis`、单 `llm` 配置模型,必须扩展为以下结构:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
primary:
|
||||
host: pg-primary
|
||||
...
|
||||
replica:
|
||||
host: pg-replica
|
||||
...
|
||||
|
||||
redis:
|
||||
cache:
|
||||
sentinel:
|
||||
master_name: geo-cache-master
|
||||
addrs: ["cache-sentinel-1:26379", "cache-sentinel-2:26379", "cache-sentinel-3:26379"]
|
||||
db: 0
|
||||
queue:
|
||||
sentinel:
|
||||
master_name: geo-queue-master
|
||||
addrs: ["queue-sentinel-1:26379", "queue-sentinel-2:26379", "queue-sentinel-3:26379"]
|
||||
db: 0
|
||||
|
||||
monitoring:
|
||||
providers:
|
||||
deepseek:
|
||||
enabled: true
|
||||
supports_standard: true
|
||||
supports_search_grounded: false
|
||||
qwen:
|
||||
enabled: true
|
||||
supports_standard: true
|
||||
supports_search_grounded: true
|
||||
doubao:
|
||||
enabled: true
|
||||
supports_standard: true
|
||||
supports_search_grounded: true
|
||||
hunyuan:
|
||||
enabled: true
|
||||
supports_standard: true
|
||||
supports_search_grounded: true
|
||||
ernie:
|
||||
enabled: false # PoC 通过后再开启
|
||||
```
|
||||
|
||||
这意味着:
|
||||
|
||||
1. `server/internal/shared/config/config.go` 必须扩展结构体,不是“小改动”。
|
||||
2. `bootstrap.go` 必须初始化双 DB pool、双 Redis client、provider registry。
|
||||
3. 监测模块必须摆脱全局单 provider `llm.Client` 的限制。
|
||||
|
||||
### 5.2 部署架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ CDN (静态资源) │
|
||||
│ admin-web 打包后的 JS/CSS │
|
||||
└──────────────┬──────────────┘
|
||||
│
|
||||
┌──────────────▼──────────────┐
|
||||
│ Nginx / 云 LB (SLB/ALB) │
|
||||
│ API 路由 + 健康检查 │
|
||||
│ 限流: 每 IP 200 req/s │
|
||||
└──────────┬───────────────────┘
|
||||
│
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌────────────┐ ┌────────────┐ ┌────────────┐
|
||||
│ tenant-api │ │ tenant-api │ │ tenant-api │
|
||||
│ Pod #1 │ │ Pod #2 │ │ Pod #3 │
|
||||
│ API 20conn │ │ API 20conn │ │ API 20conn │
|
||||
│ L1 cache │ │ L1 cache │ │ L1 cache │
|
||||
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
||||
│ │ │
|
||||
┌──────┴─────────────────────┴─────────────────────┴──────┐
|
||||
│ Cache Redis 6+ (主从 Sentinel) │
|
||||
│ 用途: API 缓存 + JWT 会话 │
|
||||
└────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────────┼────────────────────────┬────────┐
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ PG Primary │────────▶│ PG Replica │ │ MinIO │ │ Queue Redis │
|
||||
│ (读写) │ 流复制 │ (只读) │ │ (原始回答) │ │ Streams │
|
||||
│ 160 conns │ │ 50 conns │ │ S3 兼容 │ │ 独立故障域 │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
|
||||
▲ ▲ │
|
||||
│ │ │
|
||||
│ ┌────────────────────┘ Aggregator 的重查询走 Replica │
|
||||
│ │ │
|
||||
┌──────┴───┴──────────────────────────────────────────────┐
|
||||
│ 独立进程(不接入 LB) │
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ │ Collector Pod #1 │ │ Collector Pod #2 │ │ Collector Pod #3 │ ×3 Pod
|
||||
│ │ worker ×10 │ │ worker ×10 │ │ worker ×10 │ (竞争消费)
|
||||
│ │ PG conn: 10 │ │ PG conn: 10 │ │ PG conn: 10 │
|
||||
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
||||
│ │ │ │
|
||||
│ └────────────────────┴────────────────────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ Queue Redis Stream
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ Aggregator #1 │ │ Aggregator #2 │ 主备(锁选主)│
|
||||
│ │ (active) │ │ (standby) │ │
|
||||
│ │ PG conn: 20 │ │ PG conn: 0 │ │
|
||||
│ │ 读走 Replica │ │ │ │
|
||||
│ └─────────────────┘ └─────────────────┘ │
|
||||
╚══════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
### 5.3 Redis 高可用与故障域隔离
|
||||
|
||||
V2 使用 Redis 单点,且在线缓存与离线任务共用同一实例。V3 修正为 **双 Redis 故障域**:
|
||||
|
||||
1. `redis.cache`:面向 API 读流量,承接缓存与会话
|
||||
2. `redis.queue`:面向离线采集,承接 Stream 队列
|
||||
|
||||
推荐配置:
|
||||
|
||||
```yaml
|
||||
redis:
|
||||
cache:
|
||||
sentinel:
|
||||
master_name: "geo-cache-master"
|
||||
addrs:
|
||||
- "cache-sentinel-1:26379"
|
||||
- "cache-sentinel-2:26379"
|
||||
- "cache-sentinel-3:26379"
|
||||
db: 0
|
||||
queue:
|
||||
sentinel:
|
||||
master_name: "geo-queue-master"
|
||||
addrs:
|
||||
- "queue-sentinel-1:26379"
|
||||
- "queue-sentinel-2:26379"
|
||||
- "queue-sentinel-3:26379"
|
||||
db: 0
|
||||
```
|
||||
|
||||
Go 客户端(go-redis v9)原生支持 Sentinel,但这里需要初始化 **两个** client:
|
||||
|
||||
```go
|
||||
cacheRDB := newRedisClient(cfg.Redis.Cache)
|
||||
queueRDB := newRedisClient(cfg.Redis.Queue)
|
||||
```
|
||||
|
||||
### 5.4 PostgreSQL 读写分离
|
||||
|
||||
API 服务的监测查询走 **PG Replica**。通过在 `MonitoringService` 中注入两个 pool:
|
||||
|
||||
```go
|
||||
type MonitoringService struct {
|
||||
writePool *pgxpool.Pool // Primary,仅供 aggregator 写入
|
||||
readPool *pgxpool.Pool // Replica,Dashboard 查询
|
||||
cache cache.Cache
|
||||
sf singleflight.Group
|
||||
}
|
||||
```
|
||||
|
||||
在 `bootstrap.go` 中初始化时,如果配置了 `database.replica`,则创建第二个连接池。
|
||||
|
||||
### 5.5 资源规格修订
|
||||
|
||||
| 组件 | 实例数 | 规格 | PG 连接 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **tenant-api** | 3 | 4C8G | 20/实例 | API 含 L1 缓存、singleflight |
|
||||
| **Cache Redis Sentinel** | 1主1从3哨兵 | 8G 内存 | - | API 缓存 + JWT 会话 |
|
||||
| **Queue Redis Sentinel** | 1主1从3哨兵 | 4G 内存 | - | Stream 队列,独立故障域 |
|
||||
| **PG Primary** | 1 | 8C32G | max 160 | SSD,汇总表 + 原始采集表 |
|
||||
| **PG Replica** | 1 | 4C16G | max 50 | SSD,Dashboard 只读查询 |
|
||||
| **MinIO** | 1 | 4C8G, 100G SSD | - | 原始回答对象存储 |
|
||||
| **Collector** | 3 Pod | 2C4G | 10/Pod | Queue Redis Stream 竞争消费 |
|
||||
| **Aggregator** | 2 Pod (主备) | 4C8G | 20 (active only) | 分布式锁选主 |
|
||||
| **CDN** | - | - | - | 静态资源 + Nginx 限流 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 降级与容灾
|
||||
|
||||
### 6.1 降级策略
|
||||
|
||||
| 故障场景 | 降级措施 | 用户感知 |
|
||||
| --- | --- | --- |
|
||||
| **Cache Redis 主节点故障** | Sentinel 自动切换从节点为主(< 30s) | 短暂超时后恢复 |
|
||||
| **Cache Redis 完全不可用** | API 直接查 PG,跳过缓存;Nginx 层限流降至 50 req/s/IP | 响应变慢但可用 |
|
||||
| **Queue Redis 故障** | 采集调度与 Collector 暂停;线上查询不受影响 | 当天数据延迟,但 Dashboard 继续服务旧数据 |
|
||||
| **PG Primary 故障** | Dashboard 查询走 Replica 不受影响;采集/聚合暂停 | Dashboard 可用,数据冻结在最后聚合时间 |
|
||||
| **PG Replica 故障** | Dashboard 查询回退到 Primary | 无感知,Primary 负载上升 |
|
||||
| **单个 AI 平台 API 超时** | 该平台任务标记 failed,其他平台正常采集 | 该平台在对应模式下标记“数据缺失”,不强制展示为 0 |
|
||||
| **全部 AI 平台不可用** | 当天采集任务全部失败,Dashboard 展示前一天数据 | 显示"数据更新时间:昨日" |
|
||||
| **Collector Pod 全挂** | Queue Redis 消息堆积,等待 Pod 恢复后继续消费 | 当天数据延迟 |
|
||||
| **Aggregator 异常** | 另一个 Aggregator Pod 抢锁接管 | 无感知 |
|
||||
|
||||
### 6.2 熔断器
|
||||
|
||||
对外部 AI 平台 API 使用熔断器(circuit breaker):
|
||||
|
||||
```go
|
||||
// 每个 AI 平台独立的熔断器
|
||||
type PlatformBreaker struct {
|
||||
breakers map[string]*circuitbreaker.CircuitBreaker
|
||||
}
|
||||
|
||||
// 配置
|
||||
// 连续 5 次失败 → 打开熔断 → 30 秒后半开 → 1 次成功 → 关闭
|
||||
```
|
||||
|
||||
### 6.3 监控告警
|
||||
|
||||
| 指标 | 告警阈值 | 通知方式 |
|
||||
| --- | --- | --- |
|
||||
| API P99 延迟 | > 2s 持续 5 分钟 | 钉钉/飞书 |
|
||||
| Cache Redis 命中率 | < 80% 持续 10 分钟 | 钉钉/飞书 |
|
||||
| Queue Redis backlog | > 10,000 持续 10 分钟 | 钉钉/飞书 |
|
||||
| PG 连接池使用率 | > 80% | 钉钉/飞书 |
|
||||
| 采集任务失败率 | > 10% 单平台 | 钉钉/飞书 |
|
||||
| 聚合 Job 未在 8:00 前完成 | 超时 | 电话 + 钉钉 |
|
||||
| Cache/Queue Redis Sentinel 主从切换 | 任何切换事件 | 电话 + 钉钉 |
|
||||
|
||||
### 6.4 数据一致性保障
|
||||
|
||||
| 场景 | 措施 |
|
||||
| --- | --- |
|
||||
| 聚合 Job 被中断 | 每个品牌的聚合是独立事务,失败后可重跑该品牌 |
|
||||
| 采集与聚合时间窗重叠 | 聚合只处理 `business_date = yesterday AND status = succeeded` 的数据 |
|
||||
| 缓存与 PG 不一致 | stale-while-revalidate 保证最终一致,不保证强一致 |
|
||||
| 原始回答写 MinIO 失败 | 标记该 run 为 failed,解析结果不写入 |
|
||||
| 晚到补采任务 | 进入 backfill 队列,只重聚合受影响品牌,不改写已发布快照的全局口径 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 压测验收标准
|
||||
|
||||
### 7.1 压测场景模型
|
||||
|
||||
| 场景编号 | 场景名称 | 虚拟用户数 | 行为 | 持续时间 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| S1 | 稳态浏览 | 50,000 | 每 30 秒打开 1 个页面(加权 fan-out 3.8) | 30 min |
|
||||
| S2 | 早高峰突刺 | 50,000 | 0~60 秒内全部打开首页(composite 接口) | 1 min ramp-up |
|
||||
| S3 | 筛选风暴 | 20,000 | 每 5 秒切换品牌/时间范围 | 10 min |
|
||||
| S4 | 缓存失效 | 50,000 | 手动清空 Cache Redis 后观察恢复 | 5 min |
|
||||
| S5 | Cache Redis 故障 | 50,000 | kill Cache Redis master,观察 Sentinel 切换 | 2 min |
|
||||
| S6 | Queue Redis 故障 | 0 | kill Queue Redis master,观察采集暂停与恢复 | 30 min |
|
||||
|
||||
### 7.2 性能目标(SLA)
|
||||
|
||||
| 指标 | 目标值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| API P50 延迟 | < 50 ms | 稳态,Redis 命中 |
|
||||
| API P95 延迟 | < 200 ms | 稳态 |
|
||||
| API P99 延迟 | < 500 ms | 含缓存 miss + PG 查询 |
|
||||
| API P99 延迟(突刺) | < 2 s | S2 场景 |
|
||||
| 错误率 | < 0.1% | 5xx 错误 |
|
||||
| Cache Redis 命中率 | > 90% | 稳态 |
|
||||
| PG QPS(穿透) | < 1,500 | 10% miss rate |
|
||||
| 采集完成率 | > 95% | 日采集任务成功率 |
|
||||
| 聚合 SLA | 8:00 前完成 | 用户上班前数据可用 |
|
||||
| Cache Redis 故障恢复 | < 30 s | Sentinel 自动切换 |
|
||||
| Queue Redis 恢复后 backlog 清空 | < 60 min | 采集可恢复性 |
|
||||
|
||||
### 7.3 压测工具与脚本
|
||||
|
||||
```
|
||||
# 推荐工具: k6 (grafana/k6)
|
||||
# 脚本位置: tests/load/
|
||||
|
||||
tests/load/
|
||||
├── k6.config.js # 共享配置
|
||||
├── scenarios/
|
||||
│ ├── s1_steady_browse.js # 稳态浏览
|
||||
│ ├── s2_peak_burst.js # 早高峰突刺
|
||||
│ ├── s3_filter_storm.js # 筛选风暴
|
||||
│ ├── s4_cache_flush.js # 缓存失效
|
||||
│ └── s5_cache_redis_failover.js # Cache Redis 故障
|
||||
├── helpers/
|
||||
│ ├── auth.js # 登录获取 token
|
||||
│ └── brands.js # 随机品牌选择
|
||||
└── README.md # 运行说明(S6 Queue Redis 故障为运维演练,无 k6 脚本)
|
||||
```
|
||||
|
||||
### 7.4 压测前置条件
|
||||
|
||||
1. **数据准备**:100 品牌 × 100 问题 × 5 平台 × 30 天汇总数据已填充
|
||||
2. **用户池**:50,000 个预创建的测试用户 token
|
||||
3. **环境**:与生产同规格的独立压测环境
|
||||
4. **监控**:Prometheus + Grafana 大盘实时观察
|
||||
|
||||
### 7.5 验收流程
|
||||
|
||||
```
|
||||
Phase 1: 基线测试
|
||||
→ 5,000 用户稳态,确认 P99 < 100ms
|
||||
→ 记录基线指标
|
||||
|
||||
Phase 2: 容量爬坡
|
||||
→ 逐步提升: 10K → 20K → 30K → 40K → 50K
|
||||
→ 每级停留 5 分钟,观察指标是否劣化
|
||||
→ 记录每级的 P50/P95/P99、错误率、Redis/PG 指标
|
||||
|
||||
Phase 3: 极限测试
|
||||
→ 50K 稳态 30 分钟
|
||||
→ 中间插入 S2 突刺、S3 筛选风暴
|
||||
→ 确认所有 SLA 指标达标
|
||||
|
||||
Phase 4: 容灾演练
|
||||
→ S4 缓存失效:确认 singleflight 生效,PG 不被打爆
|
||||
→ S5 Cache Redis 故障:确认 Sentinel 切换 < 30s,API 降级可用
|
||||
→ S6 Queue Redis 故障:确认在线 API 不受影响,采集恢复后 backlog 可消化
|
||||
|
||||
Phase 5: 报告
|
||||
→ 输出压测报告,包含:
|
||||
- 每个场景的延迟分布图
|
||||
- 资源使用率(CPU/内存/连接池/磁盘IO)
|
||||
- 瓶颈分析与调优建议
|
||||
- 通过/未通过结论
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 实施计划修订
|
||||
|
||||
在 V2 原有 Phase 15~21 的基础上,调整以下 Phase:
|
||||
|
||||
### Phase 15 修订:DB 迁移 + 数据口径
|
||||
|
||||
新增:
|
||||
- `run_mode`、`business_date`、平台能力字段 migration
|
||||
- `question_version_id` 进入问题级汇总唯一键
|
||||
- 新增 `monitoring_collect_tasks` / `monitoring_citation_facts` / `monitoring_parse_badcases`
|
||||
- 引用聚合改为从规范化事实表出发
|
||||
|
||||
### Phase 16 修订:后端查询服务
|
||||
|
||||
新增:
|
||||
- `getWithCache()` 通用缓存方法 + singleflight + stale-while-revalidate
|
||||
- Dashboard composite 聚合接口
|
||||
- `MonitoringService` 注入 readPool/writePool
|
||||
- L1 进程内缓存层
|
||||
- 所有查询默认显式带 `run_mode` / `scope`
|
||||
|
||||
### Phase 17 修订:采集流水线
|
||||
|
||||
新增:
|
||||
- Queue Redis Stream 任务队列替代内存队列
|
||||
- `MonitoringProvider` 平台适配器注册表
|
||||
- `PlatformRateLimiter` 每平台独立限流
|
||||
- `PlatformBreaker` 熔断器
|
||||
- 多 Pod 竞争消费架构
|
||||
- 重试 + 容错逻辑
|
||||
- `business_date` + backfill 流程
|
||||
- `api_standard` / `api_search_grounded` 分流调度
|
||||
|
||||
### Phase 18 修订:聚合 Job
|
||||
|
||||
新增:
|
||||
- 分布式锁选主
|
||||
- 聚合后预热热门品牌缓存(替代全量删除)
|
||||
- 并发 goroutine 聚合(10 并发)
|
||||
- 聚合读走 PG Replica
|
||||
- 晚到任务品牌级重聚合
|
||||
- 引用事实表 → 日汇总表的聚合逻辑
|
||||
|
||||
### 新增 Phase 22: 基础设施
|
||||
|
||||
- Cache Redis Sentinel 部署配置
|
||||
- Queue Redis Sentinel 部署配置
|
||||
- PG 流复制 + Replica 配置
|
||||
- Nginx 限流配置
|
||||
- 监控告警配置(Prometheus + Grafana)
|
||||
- 估时:3 天
|
||||
|
||||
### 新增 Phase 23: 压测与验收
|
||||
|
||||
- 编写 k6 压测脚本(5 个压测场景)+ 1 个运维演练场景
|
||||
- 数据准备脚本(100 品牌 × 30 天种子数据)
|
||||
- 执行 Phase 1~4 验收流程
|
||||
- 输出压测报告
|
||||
- 估时:3 天
|
||||
|
||||
### 新增 Phase 24: 平台 PoC 与口径验收
|
||||
|
||||
- 3 品牌 × 30 问题 × 3 天的平台 PoC
|
||||
- 验证搜索增强引用质量与波动
|
||||
- 决定百度千帆是否进入首批上线白名单
|
||||
- 估时:4 天
|
||||
|
||||
### 修订后总估时
|
||||
|
||||
| Phase | 内容 | V2 估时 | V3 估时 | 增量 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 15 | DB 迁移 + 数据口径 | 1 天 | 3 天 | +2(run_mode、business_date、任务/引用事实表) |
|
||||
| 16 | 查询服务 + 缓存层 | 3 天 | 6 天 | +3(singleflight、composite、L1、读写分离、scope 过滤) |
|
||||
| 17 | 采集流水线 | 3 天 | 7 天 | +4(Provider Registry、双 run_mode、Queue Redis、多 Pod、限流熔断) |
|
||||
| 18 | 聚合 Job | 2 天 | 4 天 | +2(分布式锁、预热、并发聚合、backfill) |
|
||||
| 19 | 种子数据 | 1 天 | 1 天 | 0 |
|
||||
| 20 | 前端 6 页面 + ECharts | 4 天 | 4 天 | 0 |
|
||||
| 21 | 前端验证 + 集成测试 | 1 天 | 1 天 | 0 |
|
||||
| 22 | 基础设施(新增) | - | 3 天 | +3 |
|
||||
| 23 | 压测与验收(新增) | - | 3 天 | +3 |
|
||||
| 24 | 平台 PoC 与口径验收(新增) | - | 4 天 | +4 |
|
||||
| **合计** | | **15 天** | **32 天** | **+17 天** |
|
||||
|
||||
---
|
||||
|
||||
## 9. V2 → V3 变更汇总
|
||||
|
||||
| 问题编号 | V2 问题 | V3 解决方案 |
|
||||
| --- | --- | --- |
|
||||
| **H0** | `run_mode` 未入模,标准回答与搜索增强回答混算 | 主统计按 `api_standard` / `api_search_grounded` 分层,默认页面显式取数规则 |
|
||||
| **H1** | 读流量按 1 req/30s 估算,实际首页 6 个并行请求 | 重新建模 fan-out,引入 BFF composite 接口将首页降为 1 请求 |
|
||||
| **H2** | TTL + 聚合后全删缓存,无防雪崩 | 4 层防护:singleflight + TTL 抖动 + stale-while-revalidate + L1 热点缓存 |
|
||||
| **H3** | 5 worker、2 小时时间窗,采集跑不完 | 30 worker、6 小时窗口、Queue Redis 多 Pod 竞争消费 |
|
||||
| **H4** | 问题版本未进入全部汇总唯一键 | `question_version_id` 进入问题级汇总唯一键,避免历史口径覆盖 |
|
||||
| **H5** | 日采集与日聚合时序矛盾 | 使用 `business_date = yesterday` 的 T+1 发布模型 + backfill |
|
||||
| **M1** | max_open_conns 50 被当作共享,实际 per-process | 显式规划每进程连接数,PG max_connections=160 |
|
||||
| **M2** | 缺少压测验收 | 6 个场景模型 + SLA 指标 + k6 脚本 + 4 阶段验收流程 |
|
||||
| **M3** | 分页缓存 key 缺 page_size | 所有分页 key 加入 `scope + page_size` 维度 |
|
||||
| **M4** | 引用按原始 URL 聚合,易重复计数 | 引入 `monitoring_citation_facts`,按 `normalized_url/domain` 聚合 |
|
||||
| **M5** | 在线缓存与离线队列共用 Redis 故障域 | Cache Redis / Queue Redis 拆分,分别高可用 |
|
||||
| **新增** | 无降级容灾策略 | 双 Redis Sentinel + PG 读写分离 + 熔断器 + 降级措施矩阵 |
|
||||
| **新增** | 无监控告警 | 7 项核心告警指标 + 阈值 |
|
||||
Reference in New Issue
Block a user