All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m35s
实现功能: - 实名状态检查轮询(可配置间隔) - 卡流量检查轮询(支持跨月流量追踪) - 套餐检查与超额自动停机 - 分布式并发控制(Redis 信号量) - 手动触发轮询(单卡/批量/条件筛选) - 数据清理配置与执行 - 告警规则与历史记录 - 实时监控统计(队列/性能/并发) 性能优化: - Redis 缓存卡信息,减少 DB 查询 - Pipeline 批量写入 Redis - 异步流量记录写入 - 渐进式初始化(10万卡/批) 压测工具(scripts/benchmark/): - Mock Gateway 模拟上游服务 - 测试卡生成器 - 配置初始化脚本 - 实时监控脚本 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
709 lines
22 KiB
Markdown
709 lines
22 KiB
Markdown
## Context
|
||
|
||
### 背景
|
||
|
||
系统当前管理 1000 万+的 IoT 卡资产,需要定期检查:
|
||
1. **实名状态**:未实名卡需要高频检查(30-60秒),已实名卡低频检查(1小时)
|
||
2. **流量使用**:已激活卡需要监控流量消耗,防止超额使用
|
||
3. **套餐流量**:检查套餐是否用完或过期,及时停机
|
||
|
||
### 当前状态
|
||
|
||
- 已有 Gateway Client 封装(`internal/gateway`),提供实名查询、流量查询、停复机等 HTTP 接口
|
||
- 已有 Asynq 任务队列基础设施(`pkg/queue`)
|
||
- 已有 IoT 卡、套餐、设备等数据模型
|
||
- **缺失**:轮询调度机制,无法自动定期检查,依赖人工或外部触发
|
||
|
||
### 约束
|
||
|
||
- **规模约束**:1000万+ 卡量,未来持续增长
|
||
- **性能约束**:
|
||
- 数据库查询延迟 < 50ms
|
||
- Redis 内存配置:16 GB
|
||
- Gateway API 无明确限流,但需控制并发避免打挂
|
||
- **业务约束**:
|
||
- 不同卡状态需要不同轮询策略(梯度配置)
|
||
- Gateway 返回的流量是自然月总量(每月1号重置)
|
||
- 行业卡无需实名检查
|
||
- 并发数需要动态调整,无需重启
|
||
- **架构约束**:严格遵守 Handler → Service → Store → Model 分层
|
||
|
||
### 利益相关方
|
||
|
||
- **运营团队**:需要轮询配置管理接口,调整检查策略
|
||
- **开发团队**:需要监控面板,查看轮询任务执行情况
|
||
- **运维团队**:需要告警机制,及时发现问题
|
||
|
||
---
|
||
|
||
## Goals / Non-Goals
|
||
|
||
### Goals(目标)
|
||
|
||
1. **高性能轮询调度**:支持百万级卡的高效调度,Worker 启动时间 < 10秒
|
||
2. **灵活配置管理**:支持按卡状态、卡类型、运营商配置不同的轮询策略
|
||
3. **动态并发控制**:支持实时调整并发数,无需重启 Worker
|
||
4. **准确的流量计算**:正确处理 Gateway 返回的月总量,计算跨月流量
|
||
5. **完善的监控告警**:实时监控队列状态、任务执行情况,支持告警通知
|
||
6. **数据生命周期管理**:定期清理历史数据,避免数据膨胀
|
||
|
||
### Non-Goals(非目标)
|
||
|
||
1. ❌ 不支持分布式调度(单 Worker 进程调度,多 Worker 并发执行任务)
|
||
2. ❌ 不支持实时流量监控(轮询间隔最短 30 秒,非实时)
|
||
3. ❌ 不实现 Gateway API 限流(在并发控制层面控制调用频率)
|
||
4. ❌ 不支持跨运营商批量查询(Gateway API 当前不支持)
|
||
5. ❌ 不支持历史流量数据分析报表(只记录原始数据,报表需单独开发)
|
||
|
||
---
|
||
|
||
## Decisions
|
||
|
||
### 决策 1:使用 Redis Sorted Set 实现轮询队列
|
||
|
||
**问题**:百万级卡量如何高效调度?
|
||
|
||
**选择**:使用 Redis Sorted Set 存储 `{card_id: next_check_timestamp}`,Score 为下次检查的 Unix 时间戳。
|
||
|
||
**理由**:
|
||
- **性能**:Redis Sorted Set 的 `ZRANGEBYSCORE` 操作时间复杂度 O(log(N)+M),可以高效查询到期的卡
|
||
- **内存可控**:1000万卡 × 20字节(Score + Member)≈ 200 MB,三个队列共 600 MB,可接受
|
||
- **自然语义**:Score 即下次检查时间,直观且易于调试
|
||
|
||
**替代方案**:
|
||
- ❌ **数据库轮询**:`SELECT * FROM iot_cards WHERE last_check_at <= NOW() - interval`
|
||
- 问题:百万行扫描,即使有索引也慢;高频查询打爆数据库
|
||
- ❌ **Redis List**:只能 FIFO,无法按时间排序
|
||
- ❌ **延迟队列(DelayQueue)**:需要额外组件,增加复杂度
|
||
|
||
**权衡**:
|
||
- ✅ 高性能,低延迟
|
||
- ✅ 易于实现优先级(Score 越小越优先)
|
||
- ⚠️ Redis 内存占用增加(但在可接受范围内)
|
||
- ⚠️ 需要保持 Redis 和数据库数据一致性(通过定期同步和懒加载机制)
|
||
|
||
---
|
||
|
||
### 决策 2:渐进式初始化 + 懒加载
|
||
|
||
**问题**:1000万卡全量初始化到 Redis 需要 10-20 分钟,Worker 启动时间太长。
|
||
|
||
**选择**:三阶段初始化策略
|
||
|
||
**阶段 1:快速启动(10秒内)**
|
||
- 只加载轮询配置到 Redis
|
||
- 启动调度器 Goroutine
|
||
- Worker 进程立即可用
|
||
|
||
**阶段 2:后台渐进式初始化(20-30分钟)**
|
||
- 异步任务分批加载卡数据(每批 10万张)
|
||
- 每批处理后 sleep 1秒,避免打爆数据库
|
||
- 使用游标(主键范围)而不是 OFFSET,提升性能
|
||
- 进度存储在 Redis,支持断点续传
|
||
|
||
**阶段 3:懒加载机制(运行时)**
|
||
- 如果卡未初始化但被触发操作(API 调用、手动触发),实时加载
|
||
- 保证热点卡优先初始化
|
||
|
||
**理由**:
|
||
- **快速启动**:Worker 10秒可用,不阻塞服务
|
||
- **平缓负载**:数据库压力平滑,不会突发高峰
|
||
- **支持中断恢复**:Worker 重启不会重新初始化
|
||
- **热点优先**:频繁访问的卡优先加载
|
||
|
||
**替代方案**:
|
||
- ❌ **全量初始化**:启动时间 10-20 分钟,不可接受
|
||
- ❌ **完全懒加载**:第一次访问时加载,会有延迟
|
||
|
||
**权衡**:
|
||
- ✅ 启动快速,用户体验好
|
||
- ✅ 数据库负载平滑
|
||
- ⚠️ 初始化期间,部分卡可能还未入队(通过懒加载补偿)
|
||
- ⚠️ 增加系统复杂度(需要管理初始化进度)
|
||
|
||
---
|
||
|
||
### 决策 3:自定义并发控制而非 Asynq 原生并发
|
||
|
||
**问题**:需要动态调整并发数(通过管理接口),但 Asynq 的并发数在启动时固定。
|
||
|
||
**选择**:基于 Redis 信号量自定义并发控制。
|
||
|
||
**实现**:
|
||
```go
|
||
// 获取信号量
|
||
maxConcurrency := redis.Get("polling:concurrency:config:realname")
|
||
current := redis.Incr("polling:concurrency:current:realname")
|
||
if current > maxConcurrency {
|
||
redis.Decr("polling:concurrency:current:realname")
|
||
return false // 并发已满
|
||
}
|
||
|
||
// 执行任务
|
||
defer redis.Decr("polling:concurrency:current:realname")
|
||
```
|
||
|
||
**理由**:
|
||
- **动态调整**:管理员可以通过接口实时修改并发数,立即生效
|
||
- **分类控制**:不同类型任务(实名、流量、套餐)独立配置并发数
|
||
- **简单实现**:基于 Redis 原子操作,无需复杂分布式锁
|
||
|
||
**替代方案**:
|
||
- ❌ **Asynq 原生并发控制**:启动时固定,需要重启 Worker 才能调整
|
||
- ❌ **信号 + 优雅重启**:修改配置后发送 SIGHUP 重启 Worker
|
||
- 问题:重启有服务中断风险,操作复杂
|
||
|
||
**权衡**:
|
||
- ✅ 实时调整,无需重启
|
||
- ✅ 灵活性高,支持精细化控制
|
||
- ⚠️ 需要在每个 Handler 开头获取信号量(轻微性能开销)
|
||
- ⚠️ 如果 Redis 故障,并发控制失效(通过默认值兜底)
|
||
|
||
---
|
||
|
||
### 决策 4:跨月流量计算方案
|
||
|
||
**问题**:Gateway 返回的是自然月总量(每月1号重置),如何计算增量和累计流量?
|
||
|
||
**选择**:在 `iot_cards` 表增加三个字段:
|
||
- `current_month_usage_mb`:本月已用流量
|
||
- `current_month_start_date`:本月开始日期
|
||
- `last_month_total_mb`:上月结束时的总流量
|
||
|
||
**流程**:
|
||
```
|
||
1. 查询 Gateway 获取本月总量(如 1024 MB)
|
||
2. 判断是否跨月:
|
||
- current_month_start_date != 本月1号 → 跨月了
|
||
3. 如果跨月:
|
||
- 增量 = last_month_total_mb + current_month_total_mb
|
||
- 更新 last_month_total_mb = current_month_usage_mb(上月结束值)
|
||
- 更新 current_month_start_date = 本月1号
|
||
- 更新 current_month_usage_mb = 当前值
|
||
4. 如果同月:
|
||
- 增量 = 当前值 - current_month_usage_mb
|
||
- 更新 current_month_usage_mb = 当前值
|
||
5. 累计流量 += 增量
|
||
```
|
||
|
||
**理由**:
|
||
- **准确计算**:即使跨月时未轮询到,也不会漏掉上月最后的流量
|
||
- **简单实现**:只需要三个字段,逻辑清晰
|
||
- **支持调试**:保留月度数据,便于排查问题
|
||
|
||
**替代方案**:
|
||
- ❌ **记录上次查询值**:如果跨月时未轮询,会漏掉上月最后的流量
|
||
- ❌ **根据激活日期计算账单周期**:Gateway 返回的是自然月,不是账单周期
|
||
|
||
**权衡**:
|
||
- ✅ 计算准确,不漏流量
|
||
- ✅ 支持跨月检测
|
||
- ⚠️ 增加三个数据库字段(开销很小)
|
||
|
||
---
|
||
|
||
### 决策 5:套餐检查混合模式(即时 + 定期)
|
||
|
||
**问题**:套餐流量检查何时触发?
|
||
|
||
**选择**:混合模式
|
||
1. **即时触发**:卡流量检查完成后,立即触发关联套餐的检查
|
||
2. **定期扫描**:Scheduler 定期扫描所有生效中的套餐(兜底)
|
||
|
||
**理由**:
|
||
- **实时性**:流量增加后立即检查套餐,超额立即停机
|
||
- **可靠性**:定期扫描兜底,避免漏检(比如卡流量检查失败)
|
||
|
||
**替代方案**:
|
||
- ❌ **只即时触发**:如果卡流量检查失败,套餐永远不会检查
|
||
- ❌ **只定期扫描**:实时性差,超额后延迟停机
|
||
|
||
**权衡**:
|
||
- ✅ 实时性好,可靠性高
|
||
- ⚠️ 可能有重复检查(但套餐检查逻辑幂等,无影响)
|
||
|
||
---
|
||
|
||
### 决策 6:轮询配置匹配机制
|
||
|
||
**问题**:一张卡可能匹配多个配置(如"未实名卡"和"未实名移动卡"),如何选择?
|
||
|
||
**选择**:优先级机制(数字越小优先级越高)
|
||
|
||
**匹配规则**:
|
||
1. 查询所有启用的配置(`status = 1`),按 `priority ASC` 排序
|
||
2. 逐个检查配置的匹配条件:
|
||
- `card_condition`:卡状态条件(not_real_name/real_name/activated/suspended)
|
||
- `card_category`:卡业务类型(normal/industry)
|
||
- `carrier_id`:运营商 ID
|
||
3. 返回第一个匹配的配置
|
||
|
||
**示例**:
|
||
```
|
||
配置 1:未实名移动卡,priority=10
|
||
配置 2:未实名卡,priority=20
|
||
|
||
卡A:未实名 + 移动 → 匹配配置1(优先级更高)
|
||
卡B:未实名 + 联通 → 匹配配置2
|
||
```
|
||
|
||
**理由**:
|
||
- **灵活性**:可以针对特定运营商设置特殊策略
|
||
- **简单实现**:优先级排序,第一个匹配即返回
|
||
- **易于调试**:配置优先级清晰可见
|
||
|
||
**替代方案**:
|
||
- ❌ **最精确匹配**:条件最多的配置优先
|
||
- 问题:定义"精确度"复杂,难以理解
|
||
- ❌ **多配置合并**:同时应用多个配置
|
||
- 问题:合并逻辑复杂,冲突难以处理
|
||
|
||
**权衡**:
|
||
- ✅ 简单直观,易于理解
|
||
- ✅ 灵活性高,支持特殊策略
|
||
- ⚠️ 配置顺序很重要,需要文档说明
|
||
|
||
---
|
||
|
||
### 决策 7:卡生命周期管理
|
||
|
||
**问题**:新增、删除、状态变更的卡如何同步到轮询系统?
|
||
|
||
**选择**:在 Service 层集成 PollingService,提供生命周期回调:
|
||
- `OnCardCreated(card)`:新卡创建时调用
|
||
- `OnBatchCardsCreated(cards)`:批量卡导入时调用
|
||
- `OnCardStatusChanged(cardID)`:卡状态变化时调用
|
||
- `OnCardDeleted(cardID)`:删除卡时调用
|
||
- `OnCardDisabled(cardID)`:禁用轮询时调用
|
||
- `OnCardEnabled(cardID)`:启用轮询时调用
|
||
|
||
**实现**:
|
||
```go
|
||
// IotCardService.Create()
|
||
func (s *IotCardService) Create(ctx context.Context, req *CreateReq) (*IotCard, error) {
|
||
// 1. 创建卡
|
||
card := &IotCard{...}
|
||
if err := s.store.Create(ctx, card); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 2. 加入轮询系统
|
||
if card.EnablePolling {
|
||
s.pollingService.OnCardCreated(ctx, card)
|
||
}
|
||
|
||
return card, nil
|
||
}
|
||
|
||
// RealNameCheckHandler 检测到状态变化
|
||
func (h *RealNameCheckHandler) HandleRealNameCheck(...) {
|
||
// ...
|
||
if newStatus != oldStatus {
|
||
h.pollingService.OnCardStatusChanged(ctx, cardID)
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**理由**:
|
||
- **自动化**:无需手动干预,卡变化自动同步到轮询系统
|
||
- **解耦**:业务逻辑和轮询系统分离,Service 只需调用回调
|
||
- **可测试**:PollingService 可以独立测试
|
||
|
||
**替代方案**:
|
||
- ❌ **数据库触发器**:Go 生态不推荐使用触发器,调试困难
|
||
- ❌ **定期全量同步**:延迟高,资源浪费
|
||
|
||
**权衡**:
|
||
- ✅ 实时同步,无延迟
|
||
- ✅ 易于维护和测试
|
||
- ⚠️ 需要在多个 Service 方法中调用回调(可以通过拦截器优化)
|
||
|
||
---
|
||
|
||
### 决策 8:监控统计数据存储
|
||
|
||
**问题**:监控指标(成功率、平均耗时、队列长度)如何存储和计算?
|
||
|
||
**选择**:Redis Hash 存储统计数据,每次任务执行后更新。
|
||
|
||
**数据结构**:
|
||
```
|
||
polling:stats:realname → {
|
||
queue_size: 1234567, # 从 Sorted Set 读取
|
||
processing: 50, # 从并发控制读取
|
||
success_count_1h: 12345, # 最近1小时成功次数
|
||
failure_count_1h: 123, # 最近1小时失败次数
|
||
total_duration_1h: 1234567, # 最近1小时总耗时(ms)
|
||
last_reset: "2026-02-04 10:00:00"
|
||
}
|
||
```
|
||
|
||
**计算**:
|
||
- 成功率 = success_count / (success_count + failure_count)
|
||
- 平均耗时 = total_duration / success_count
|
||
|
||
**定期重置**:每小时重置计数器,保持时间窗口滚动。
|
||
|
||
**理由**:
|
||
- **高性能**:Redis Hash 读写快,支持原子操作
|
||
- **简单实现**:无需复杂的时序数据库
|
||
- **实时性**:每次任务执行后立即更新
|
||
|
||
**替代方案**:
|
||
- ❌ **时序数据库(InfluxDB/Prometheus)**:需要额外组件,过度设计
|
||
- ❌ **数据库统计表**:写入性能差,延迟高
|
||
|
||
**权衡**:
|
||
- ✅ 简单高效
|
||
- ✅ 实时性好
|
||
- ⚠️ 只保留最近1小时数据(长期数据需要归档)
|
||
- ⚠️ Redis 重启后数据丢失(可以通过持久化缓解)
|
||
|
||
---
|
||
|
||
### 决策 9:告警检查频率
|
||
|
||
**问题**:告警规则多久检查一次?
|
||
|
||
**选择**:独立的告警检查器(AlertChecker),每 1 分钟运行一次。
|
||
|
||
**流程**:
|
||
1. 读取所有启用的告警规则
|
||
2. 从 Redis 读取对应的监控指标
|
||
3. 判断是否满足告警条件(如 `queue_size > 1000000`)
|
||
4. 如果满足条件且持续时间达到阈值(如 5 分钟),发送告警
|
||
5. 记录告警历史,避免重复发送(冷却期)
|
||
|
||
**理由**:
|
||
- **独立运行**:不阻塞轮询任务
|
||
- **可配置**:告警规则灵活配置
|
||
- **避免误报**:持续时间阈值避免短暂波动触发告警
|
||
|
||
**替代方案**:
|
||
- ❌ **实时告警**:每次任务执行后检查
|
||
- 问题:频率太高,性能开销大
|
||
- ❌ **定时任务(Cron)**:依赖外部调度
|
||
- 问题:增加依赖,不够灵活
|
||
|
||
**权衡**:
|
||
- ✅ 平衡性能和实时性
|
||
- ✅ 易于实现和维护
|
||
- ⚠️ 1 分钟延迟(对告警来说可接受)
|
||
|
||
---
|
||
|
||
### 决策 10:数据清理策略
|
||
|
||
**问题**:流量历史记录(`data_usage_records`)会快速增长,如何清理?
|
||
|
||
**选择**:定时清理任务,每天凌晨 2 点运行。
|
||
|
||
**流程**:
|
||
1. 读取清理配置(`tb_data_cleanup_config`)
|
||
2. 对每个配置的表,删除超过保留天数的数据
|
||
```sql
|
||
DELETE FROM tb_data_usage_record
|
||
WHERE created_at < NOW() - INTERVAL '90 days'
|
||
LIMIT 10000; -- 分批删除,避免锁表
|
||
```
|
||
3. 记录清理日志
|
||
|
||
**理由**:
|
||
- **避免数据膨胀**:定期清理历史数据,控制表大小
|
||
- **可配置**:保留天数可配置
|
||
- **分批删除**:避免长时间锁表
|
||
|
||
**替代方案**:
|
||
- ❌ **分区表(Partition)**:按月自动删除旧分区
|
||
- 问题:需要数据库层面支持,配置复杂
|
||
- ❌ **手动清理**:依赖人工操作,容易遗忘
|
||
|
||
**权衡**:
|
||
- ✅ 简单可靠
|
||
- ✅ 支持灵活配置
|
||
- ⚠️ 删除期间表可能有轻微性能影响(通过 LIMIT 控制)
|
||
|
||
---
|
||
|
||
## Risks / Trade-offs
|
||
|
||
### 风险 1:Redis 内存不足
|
||
|
||
**风险**:1000万卡 × 200字节 = 2 GB 缓存 + 600 MB 队列 = ~3 GB,如果卡量增长到 2000万,需要 6 GB。
|
||
|
||
**缓解措施**:
|
||
- 监控 Redis 内存使用率,设置告警(超过 80%)
|
||
- 卡信息缓存设置 TTL(7天),自动淘汰
|
||
- 如果内存不足,可以只缓存热点卡(LRU 策略)
|
||
|
||
---
|
||
|
||
### 风险 2:Redis 和数据库数据不一致
|
||
|
||
**风险**:Redis 缓存的卡信息可能与数据库不同步(如卡状态变更但未更新 Redis)。
|
||
|
||
**缓解措施**:
|
||
- 定时同步任务(每小时):从数据库读取最近更新的卡,更新 Redis
|
||
- 懒加载机制:如果缓存未命中,从数据库读取并更新缓存
|
||
- 卡状态变更时主动更新 Redis(OnCardStatusChanged)
|
||
|
||
---
|
||
|
||
### 风险 3:Gateway API 调用失败
|
||
|
||
**风险**:Gateway 不可用或超时,导致轮询任务失败。
|
||
|
||
**缓解措施**:
|
||
- 任务失败不重试(`MaxRetry = 0`),避免重复调用打挂 Gateway
|
||
- 失败任务重新入队(按原计划下次检查)
|
||
- 记录失败统计,触发告警(失败率 > 5%)
|
||
- Gateway 调用设置超时(30秒)
|
||
|
||
---
|
||
|
||
### 风险 4:渐进式初始化期间卡未入队
|
||
|
||
**风险**:初始化未完成时,部分卡还未加入轮询队列。
|
||
|
||
**缓解措施**:
|
||
- 懒加载机制:卡被访问时自动加载
|
||
- 监控初始化进度,提供管理接口查看
|
||
- 支持手动触发检查(优先级最高)
|
||
|
||
---
|
||
|
||
### 风险 5:并发控制 Redis 故障
|
||
|
||
**风险**:Redis 故障导致并发控制失效,可能有大量任务同时执行。
|
||
|
||
**缓解措施**:
|
||
- Redis 连接失败时使用默认并发数(50)
|
||
- Asynq 队列本身有并发控制(作为二级保护)
|
||
- 监控 Gateway 负载,设置告警
|
||
|
||
---
|
||
|
||
### Trade-off 1:实时性 vs 资源消耗
|
||
|
||
**权衡**:轮询间隔越短,实时性越好,但资源消耗(数据库、Redis、Gateway API)越高。
|
||
|
||
**选择**:支持灵活配置,根据卡状态动态调整间隔
|
||
- 未实名卡:30-60秒(需要及时发现实名完成)
|
||
- 已实名卡:1小时(状态变化少)
|
||
- 已激活卡:30分钟(流量监控)
|
||
|
||
---
|
||
|
||
### Trade-off 2:缓存一致性 vs 性能
|
||
|
||
**权衡**:强一致性需要每次从数据库读取,性能差;最终一致性性能好,但可能有短暂不一致。
|
||
|
||
**选择**:最终一致性
|
||
- 通过定时同步和懒加载保证最终一致
|
||
- 对业务影响小(轮询任务本身就是定期的,短暂不一致可接受)
|
||
|
||
---
|
||
|
||
### Trade-off 3:自定义并发控制 vs Asynq 原生
|
||
|
||
**权衡**:自定义并发控制灵活性高,但增加复杂度;Asynq 原生简单,但不支持动态调整。
|
||
|
||
**选择**:自定义并发控制
|
||
- 业务需求明确(需要动态调整)
|
||
- 实现简单(基于 Redis 原子操作)
|
||
- 性能开销小(每个任务只需 1 次 INCR/DECR)
|
||
|
||
---
|
||
|
||
## Migration Plan
|
||
|
||
### 部署步骤
|
||
|
||
#### 阶段 1:数据库迁移(无服务中断)
|
||
|
||
```bash
|
||
# 1. 执行数据库迁移(新增表和字段)
|
||
go run cmd/migrate/main.go up
|
||
|
||
# 2. 验证迁移成功
|
||
psql -U user -d database -c "\d tb_polling_config"
|
||
psql -U user -d database -c "\d tb_iot_card"
|
||
```
|
||
|
||
**迁移内容**:
|
||
- 新增表:`tb_polling_config`、`tb_polling_concurrency_config`、`tb_polling_alert_rule`、`tb_data_cleanup_config`
|
||
- 修改表:`tb_iot_card` 增加字段 `current_month_usage_mb`、`current_month_start_date`、`last_month_total_mb`
|
||
|
||
**影响**:无,新增字段有默认值,不影响已有数据
|
||
|
||
#### 阶段 2:初始化配置数据
|
||
|
||
```bash
|
||
# 执行配置初始化脚本
|
||
psql -U user -d database -f scripts/init_polling_config.sql
|
||
```
|
||
|
||
**初始化内容**:
|
||
- 创建默认轮询配置(未实名卡、已实名卡、行业卡等)
|
||
- 创建默认并发控制配置
|
||
- 创建数据清理配置
|
||
|
||
#### 阶段 3:部署新版本 Worker(灰度发布)
|
||
|
||
```bash
|
||
# 1. 先部署一台 Worker 测试
|
||
# 停止旧 Worker
|
||
kill -TERM <worker_pid>
|
||
|
||
# 启动新 Worker
|
||
./bin/worker
|
||
|
||
# 2. 观察日志,确认初始化成功
|
||
tail -f logs/app.log | grep "轮询系统"
|
||
|
||
# 3. 检查 Redis 数据
|
||
redis-cli
|
||
> ZCARD polling:queue:realname
|
||
> HGETALL polling:card:1
|
||
> GET polling:configs
|
||
|
||
# 4. 逐步部署所有 Worker
|
||
```
|
||
|
||
**关键检查点**:
|
||
- Worker 启动时间 < 10秒
|
||
- 渐进式初始化正常运行
|
||
- Redis 队列有数据
|
||
- 轮询任务正常执行
|
||
|
||
#### 阶段 4:部署 API 服务(新增管理接口)
|
||
|
||
```bash
|
||
# 部署新版本 API 服务
|
||
./bin/api
|
||
|
||
# 验证管理接口
|
||
curl http://localhost:8080/api/admin/polling-configs
|
||
curl http://localhost:8080/api/admin/polling-stats
|
||
```
|
||
|
||
#### 阶段 5:启用告警
|
||
|
||
```bash
|
||
# 通过管理接口创建告警规则
|
||
curl -X POST http://localhost:8080/api/admin/polling-alert-rules \
|
||
-d '{
|
||
"rule_name": "实名检查队列积压告警",
|
||
"task_type": "realname",
|
||
"metric_type": "queue_size",
|
||
"operator": "gt",
|
||
"threshold": 1000000,
|
||
"alert_level": "warning"
|
||
}'
|
||
```
|
||
|
||
### 回滚策略
|
||
|
||
#### 回滚 API 服务
|
||
|
||
```bash
|
||
# 部署旧版本 API(不包含轮询管理接口)
|
||
./bin/api-old
|
||
|
||
# 影响:轮询管理接口不可用,但轮询系统仍正常运行
|
||
```
|
||
|
||
#### 回滚 Worker 进程
|
||
|
||
```bash
|
||
# 停止新 Worker
|
||
kill -TERM <worker_pid>
|
||
|
||
# 启动旧 Worker
|
||
./bin/worker-old
|
||
|
||
# 影响:轮询系统停止工作,但不影响其他业务
|
||
```
|
||
|
||
#### 回滚数据库(慎用)
|
||
|
||
```bash
|
||
# 只有在数据异常时才回滚数据库
|
||
go run cmd/migrate/main.go down
|
||
|
||
# 影响:
|
||
# - 删除轮询相关表
|
||
# - 删除 iot_cards 表的新增字段(数据丢失!)
|
||
```
|
||
|
||
**建议**:除非数据严重错误,否则不回滚数据库,新增字段不影响旧版本代码。
|
||
|
||
### 数据迁移(如果需要)
|
||
|
||
**场景**:如果已有卡的流量数据需要迁移到新字段。
|
||
|
||
```sql
|
||
-- 初始化新字段(如果需要)
|
||
UPDATE tb_iot_card
|
||
SET current_month_start_date = DATE_TRUNC('month', CURRENT_DATE),
|
||
current_month_usage_mb = 0,
|
||
last_month_total_mb = 0
|
||
WHERE current_month_start_date IS NULL;
|
||
```
|
||
|
||
### 验证清单
|
||
|
||
- [ ] 数据库迁移成功,表和字段创建完成
|
||
- [ ] 轮询配置初始化成功,有默认配置
|
||
- [ ] Worker 启动时间 < 10秒
|
||
- [ ] 渐进式初始化正常运行,进度可查询
|
||
- [ ] Redis 队列有数据,卡信息缓存正常
|
||
- [ ] 轮询任务正常执行,日志无错误
|
||
- [ ] 管理接口可用,可以查询配置和统计
|
||
- [ ] 手动触发功能正常
|
||
- [ ] 并发控制生效,可以动态调整
|
||
- [ ] 监控面板显示正确数据
|
||
- [ ] 告警规则配置成功,告警通知正常
|
||
|
||
---
|
||
|
||
## Open Questions
|
||
|
||
### 问题 1:告警通知渠道的实现细节
|
||
|
||
**问题**:邮件、短信、Webhook 的发送如何实现?
|
||
|
||
**待决策**:
|
||
- 是否复用现有的邮件发送服务?
|
||
- 短信服务使用哪个供应商(阿里云、腾讯云)?
|
||
- Webhook 是否需要签名验证?
|
||
|
||
**影响**:告警功能的完整性
|
||
|
||
---
|
||
|
||
### 问题 2:分区表优化
|
||
|
||
**问题**:`data_usage_records` 表是否使用 PostgreSQL 分区表(按月分区)?
|
||
|
||
**待决策**:
|
||
- 分区表可以提升查询和删除性能
|
||
- 但增加配置复杂度
|
||
|
||
**影响**:数据清理性能
|
||
|
||
---
|
||
|
||
### 问题 3:分布式部署支持
|
||
|
||
**问题**:是否需要支持多个 Worker 进程部署(分布式调度)?
|
||
|
||
**当前方案**:单 Worker 调度,多 Worker 执行任务(通过 Asynq 队列)
|
||
|
||
**待决策**:
|
||
- 如果卡量增长到亿级,单 Worker 可能成为瓶颈
|
||
- 可以通过分片(Sharding)支持多 Worker 调度
|
||
|
||
**影响**:系统扩展性
|