- Implemented KnowledgeHandler for managing knowledge groups and items, including listing, creating, updating, and deleting operations. - Added database migration scripts to create necessary tables for knowledge management, including knowledge_groups, knowledge_items, knowledge_parse_tasks, and knowledge_chunks_meta. - Introduced prompt_rule_knowledge_groups table to associate prompt rules with knowledge groups.
39 KiB
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 与采集可行性方案之间的口径断层,尤其是:
run_mode未入模,导致标准回答和搜索增强回答不可比。- 问题版本未完整进入汇总表唯一键,历史口径会被覆盖。
- 采集时间窗与聚合时序冲突,无法同时满足 T+1 发布与一致性。
- 在线缓存与离线任务队列共用同一 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 |
核心规则:
- 跨平台曝光、竞品、问题排行默认只看
api_standard。 - 引用相关页面默认只看
api_search_grounded。 - 不支持搜索增强或引文的平台,在引用页面中标记为“未接入 / 不支持”,不按 0 值并入分母。
ui_sample只用于验证,不参与monitoring_*主汇总。
2.2 Schema Delta(覆盖 V2 Schema 的必要修订)
V2 的基础表结构仍可复用,但以下修订是必需项。
2.2.1 ai_platforms
增加平台能力字段,避免查询层硬编码:
supports_standard BOOLEAN NOT NULL DEFAULT truesupports_search_grounded BOOLEAN NOT NULL DEFAULT falsesupports_citation BOOLEAN NOT NULL DEFAULT falsesupports_batch BOOLEAN NOT NULL DEFAULT falsedefault_run_mode VARCHAR(30) NOT NULL DEFAULT 'api_standard'
2.2.2 question_monitor_runs
新增以下字段:
business_date DATE NOT NULL:统计归属日,允许run_at发生在次日凌晨。run_mode VARCHAR(30) NOT NULL:api_standard/api_search_grounded/ui_sampleprovider_model VARCHAR(100) NOT NULLprompt_template_version VARCHAR(30) NOT NULLsearch_enabled BOOLEAN NOT NULL DEFAULT falserequest_id VARCHAR(100)retry_count INT NOT NULL DEFAULT 0
建议增加幂等索引:
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
新增以下字段:
run_mode VARCHAR(30) NOT NULLsearch_hit BOOLEAN NOT NULL DEFAULT falsesearch_info_json_key TEXTextractor_version VARCHAR(30) NOT NULL DEFAULT 'v1'
2.2.4 新增任务与质量表
新增下列表:
monitoring_collect_tasks:调度任务、重试、死信、优先级、租约状态monitoring_citation_facts:引用规范化事实表,保存normalized_url/domain/citation_hashmonitoring_parse_badcases:低置信度样本与人工回标结果
2.2.5 汇总表统一增加 run_mode
以下汇总表必须增加 run_mode,并进入唯一键:
monitoring_brand_daily_overviewmonitoring_platform_dailymonitoring_platform_question_dailymonitoring_competitor_dailymonitoring_competitor_platform_dailymonitoring_topic_dailymonitoring_question_dailymonitoring_citation_source_dailymonitoring_citation_page_dailymonitoring_brand_impression
2.2.6 问题版本进入汇总主键
以下表必须增加 question_version_id 并进入唯一键:
monitoring_platform_question_dailymonitoring_question_daily
推荐唯一键修订:
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 聚合:
- 页面级汇总按
normalized_url - 站点级汇总按
domain/site_name - 原始
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,其余等待结果。
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
缓存过期后,先返回旧数据,后台异步刷新。用户不感知延迟。
// 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 纳入缓存维度。
约定:
scope=standard对应run_mode=api_standardscope=search对应run_mode=api_search_groundedui_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 数据上线,用户访问“昨日完整快照”
说明:
run_at是实际执行时间,可能发生在次日凌晨。business_date是统计归属日,Dashboard 晨间看到的是 T+1 快照。- 晚到任务走 backfill,单独触发品牌级重聚合,不污染主发布窗口。
预留 6 小时采集窗口(vs V2 的 2 小时),即使 P99 延迟也有足够 buffer。
4.5 采集重试与限流
// 每个 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 和平台能力做任务路由:
- 不支持
api_search_grounded的平台不生成搜索增强任务。 ui_sample任务只进入 PoC / badcase 流水线,不进入主聚合队列。api_standard与api_search_grounded使用不同的 prompt 模板版本。
4.6 采集容错
// 单条任务失败不阻塞批次
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 是每个进程独立初始化的。
修正后的连接池规划:
# 每个 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 配置模型,必须扩展为以下结构:
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 通过后再开启
这意味着:
server/internal/shared/config/config.go必须扩展结构体,不是“小改动”。bootstrap.go必须初始化双 DB pool、双 Redis client、provider registry。- 监测模块必须摆脱全局单 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 故障域:
redis.cache:面向 API 读流量,承接缓存与会话redis.queue:面向离线采集,承接 Stream 队列
推荐配置:
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:
cacheRDB := newRedisClient(cfg.Redis.Cache)
queueRDB := newRedisClient(cfg.Redis.Queue)
5.4 PostgreSQL 读写分离
API 服务的监测查询走 PG Replica。通过在 MonitoringService 中注入两个 pool:
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):
// 每个 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 压测前置条件
- 数据准备:100 品牌 × 100 问题 × 5 平台 × 30 天汇总数据已填充
- 用户池:50,000 个预创建的测试用户 token
- 环境:与生产同规格的独立压测环境
- 监控: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、平台能力字段 migrationquestion_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 项核心告警指标 + 阈值 |