All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m35s
- 受控系统配置要求 Key 至少三段,原两段式 Key(如 polling.rate_limit_mode)注册被拒,后台不可见、限流器只能使用默认值 - Key 改为 polling.gateway.* 与 polling.stop_resume_record.*;新增迁移 000247 将已落库的旧 Key 改名并保留取值 - 后续提案中规划的开关同步改为三段式(polling.scheduler.mode、polling.stop_resume.queue_enabled)
5.6 KiB
5.6 KiB
Context
完整设计见 docs/engineering/轮询按运营商账户限流技术方案.md 第三部分第 15.1、7.27.6、8、9、14 节。本文记录实现决策。
现状:internal/polling/scheduler.go 每秒对 16 个分片 × 5 类型 Lua 出队后逐条 Enqueue 到 Asynq(MaxRetry(0)、无 TaskID);按 ZCARD 背压(50 万);polling_base.go 全局 + 类型并发令牌;优先提示 List polling:priority:{taskType} 与手动触发 List;OnCardStatusChanged 重新入队全部类型;业务事件经 Outbox AppendSeriesRequested 创建"立即、3 分钟、5 分钟"观测序列,读入口经 BestEffortSeriesDispatcher(C 端资产详情、后台资产实时状态、C 端与后台实名链接;OpenAPI 不触发)。Redis 单节点;仓库无 NSQ 依赖;自动化测试为 N/A。生产 98,430 张卡、89,962 张开启轮询、37 个运营商账户。
Goals / Non-Goals
Goals: 按账户隔离、不丢卡、同卡串行、去重;业务事件检查合并且受限流;整体切换可回退。
Non-Goals: 停复机指令队列与设备维度判断(第 6 个 Change,本 Change 中套餐步骤与 Outbox 消费者仍按现有方式同步停复机);批量查询;手动刷新行为。
Decisions
- Redis 结构(前缀新,不与旧 key 冲突):
- 账户:
polling:carrier:{cid}:priority|queue(ZSET,member=card_id)、:running(ZSET,member=token,score=到期,仅计数)、:dispatch_lock。stop_resume、sr_running在第 6 个 Change 使用,本 Change 预留领取顺序。 - 卡:
polling:card:{id}:due(Hash:各类型到期、优先标记、连续失败、首次失败、最近错误、最近成功时间)、:claim(值=token)、:exec(值=token+实例)。
- 账户:
- Lua 脚本:领取(清理过期 running → 计算上限 → 优先/普通按占比 → 预计调用数对比剩余 QPS →
SET claim NX PX→ 推后 score → 写 running)、续期(校验 token → PEXPIRE claim/exec → 推后 score 与 running)、收尾(校验 token → 删 claim/exec → 移出 running → 写 due → 按优先标记选队列设 score)、请求检查(设 due[type]=now、冷却判断、放入 priority 或打标记)。全部单节点 Lua,不跨 slot 问题。 - NSQ 而非 Asynq(已定保留 NSQ):每账户独立 topic
polling.card.{cid},channelworker;消费者max_in_flight按账户参数;msg_timeout大于续期间隔,执行中Touch;租约 >msg_timeout× 允许重投次数。动态订阅随运营商账户与配置刷新。 - 执行权 key:NSQ 在消费者假死时会重投同一 token,仅核对 claim 无法区分;
SET exec token+实例 NX PX 续期间隔×2抢不到即 FIN。 - 步骤函数:原 4 个 handler 拆为
step(ctx, StepInput{Card, Result}) (nextDue, err);Result预留批量查询与推送结果。套餐步骤本轮流量已触发评估时可跳过(配置)。沿用polling_priority_claim.go的优先项认领与收尾。卡流量同步锁保留与手动刷新互斥。 - 限流等待模式:NSQ 消费者使用第 1 个 Change 限流器的"等待至超时"模式(不占 Asynq 协程),等待超过单步超时返回限流超时,按短延迟重排不计失败。
- 请求检查取代观测序列:5 处
AppendSeriesRequested与 3 处读入口分发改为调用"请求检查(卡、类型、场景)";场景表tb_polling_check_scene预置 scene 行(订单支付、套餐激活、停复机成功、设备操作、自动续费购包、C 端资产详情、后台资产实时状态、实名链接),读入口默认配置冷却。观测序列中与卡检查无关的部分(设备信息同步query_device_info)保持不变。 - 失败告警:
tb_polling_failure_alert部分唯一索引(卡、检查类型、未关闭),告警写入走异步、失败只记日志;通知走现有alert_service.go。停复机方向的告警在第 6 个 Change 接入同一张表。 - 整体切换:
polling.scheduler.mode放受控系统配置;切换由 Worker 监听配置变更执行:停旧调度 → 初始化新结构(按批读库,进度复用/polling-stats/init-progress)→ 开启分发。回退对称。旧 handler 执行前检查模式。 - 监控:队列与执行数读 Redis;QPS、耗时读第 1 个 Change 的分钟计数;告警读告警表。
Risks / Trade-offs
- [不灰度,新调度出问题影响全部 9 万张卡] → 测试环境按生产规模压测(造卡或回放生产卡 ID),覆盖 tasks 第 11 节全部场景;上线首日重点监控,异常用
scheduler_mode回退。 - [初始化 10 万张卡耗时] → 测试环境实测并写入上线步骤;初始化期间不领取,切换窗口内轮询暂停。
- [新增 NSQ 组件] → nsqd 与 nsqlookupd 部署、监控与告警纳入运维;NSQ 消息丢失由租约到期兜底。
- [Redis 内存增加] → 每卡一个
dueHash(约 10 字段),上线前按生产卡量估算并确认maxmemory与淘汰策略不淘汰轮询 key。 - [读入口冷却期内页面数据不新] → 已接受;页面照常展示数据库数据。
Migration Plan
- 测试环境部署 nsqd + nsqlookupd;执行迁移(三张新表、系统配置项、并发配置表注释)。
- 部署 API 与 Worker,
scheduler_mode = legacy;运营配置账户调度参数与场景。 - 切
carrier,按 tasks 第 11 节核对;演练回退与再次切换。 - 生产:维护者部署 NSQ、执行迁移与发布,选择低峰期切
carrier,首日监控。 - 稳定运行后另行删除旧分片 key 与旧调度代码。