feat: 按运营商账户调度的迁移、配置与账户队列 Lua

- 迁移 000251~000254:运营商账户调度参数、请求检查场景(预置 8 个)、卡轮询失败告警表,以及调度模式、步骤、退避、告警等受控系统配置(默认 legacy,行为不变);并发配置表注释标记废弃
- 受控系统配置注册与 JSON 结构校验:步骤只能是 4 种检查类型且不重复、退避序列递增
- yaml 增加 NSQ 地址、msg_timeout、租约、续期间隔、分发间隔、默认执行上限与 max_in_flight;引入 go-nsq
- 测试环境 docker-compose 增加 nsqlookupd 与 nsqd,Worker 注入 NSQ 地址
- 账户队列 Redis 结构与 Lua:领取(执行上限、优先占比、按限流组剩余 QPS 估算)、抢执行权、续期、收尾、请求检查(合并、冷却、执行中打优先标记)、移出、发布失败释放
This commit is contained in:
邹进
2026-10-25 18:28:42 +09:00
parent f49baff7c2
commit dae44bed30
20 changed files with 1454 additions and 0 deletions

View File

@@ -213,6 +213,37 @@ type PollingConfig struct {
// VerboseLog 开启后,所有轮询 handler 在成功获取上游数据时以 Info 级别输出详细日志
// 环境变量:JUNHONG_POLLING_VERBOSE_LOG
VerboseLog bool `mapstructure:"verbose_log"`
// Carrier 按运营商账户调度(add-carrier-polling-scheduler)的部署参数;调度模式本身放受控系统配置 polling.scheduler.mode。
Carrier CarrierSchedulerConfig `mapstructure:"carrier"`
}
// CarrierSchedulerConfig 按运营商账户调度的 yaml 参数。
// 约束:msg_timeout > renew_interval;lease > msg_timeout × max_attempts;执行权 TTL = renew_interval × 2。
type CarrierSchedulerConfig struct {
// NSQDAddress 分发器发布消息的 nsqd TCP 地址,如 nsqd:4150;为空时新调度不可用(切到 carrier 会告警并保持不领取)。
// 环境变量:JUNHONG_POLLING_CARRIER_NSQD_ADDRESS
NSQDAddress string `mapstructure:"nsqd_address"`
// NSQLookupdAddresses 消费者发现 nsqd 的 nsqlookupd HTTP 地址,逗号分隔,如 nsqlookupd:4161;为空时消费者直连 NSQDAddress。
// 环境变量:JUNHONG_POLLING_CARRIER_NSQLOOKUPD_ADDRESSES
NSQLookupdAddresses string `mapstructure:"nsqlookupd_addresses"`
// MsgTimeout NSQ 消息超时,须大于续期间隔,执行中靠 Touch 延长。
MsgTimeout time.Duration `mapstructure:"msg_timeout"`
// MaxAttempts NSQ 允许的投递次数(含首次)。
MaxAttempts uint16 `mapstructure:"max_attempts"`
// Lease 处理中标记 TTL(租约),须大于 msg_timeout × max_attempts。
Lease time.Duration `mapstructure:"lease"`
// RenewInterval 租约续期间隔,默认租约 / 3 以内。
RenewInterval time.Duration `mapstructure:"renew_interval"`
// DispatchInterval 分发间隔。
DispatchInterval time.Duration `mapstructure:"dispatch_interval"`
// ShutdownGrace 优雅退出宽限期:停止分发与订阅后等待执行中任务完成的时长。
ShutdownGrace time.Duration `mapstructure:"shutdown_grace"`
// DefaultInflightLimit 未配置调度参数的账户同时执行卡任务的上限(跨实例)。
DefaultInflightLimit int `mapstructure:"default_inflight_limit"`
// DefaultCardMaxInFlight 未配置调度参数的账户每进程消费卡任务的 max_in_flight。
DefaultCardMaxInFlight int `mapstructure:"default_card_max_in_flight"`
// SubscriberProcesses 订阅卡任务 topic 的 Worker 进程数,用于校验执行上限不超过各进程 max_in_flight 之和。
SubscriberProcesses int `mapstructure:"subscriber_processes"`
}
// PollingAutoTriggerConfig 轮询自动触发配置

View File

@@ -133,6 +133,19 @@ gateway:
# 轮询配置
polling:
verbose_log: false # 是否开启详细日志(环境变量:JUNHONG_POLLING_VERBOSE_LOG)
# 按运营商账户调度(调度模式在受控系统配置 polling.scheduler.mode 切换)
carrier:
nsqd_address: "" # nsqd TCP 地址,如 nsqd:4150(环境变量:JUNHONG_POLLING_CARRIER_NSQD_ADDRESS)
nsqlookupd_addresses: "" # nsqlookupd HTTP 地址,逗号分隔(环境变量:JUNHONG_POLLING_CARRIER_NSQLOOKUPD_ADDRESSES)
msg_timeout: 90s # NSQ 消息超时,须大于续期间隔
max_attempts: 3 # NSQ 允许投递次数(含首次)
lease: 300s # 处理中标记 TTL,须大于 msg_timeout × max_attempts
renew_interval: 30s # 租约续期间隔
dispatch_interval: 1s # 分发间隔
shutdown_grace: 30s # 优雅退出宽限期
default_inflight_limit: 20 # 未配置账户的执行上限(跨实例)
default_card_max_in_flight: 20 # 未配置账户的每进程 max_in_flight
subscriber_processes: 1 # 订阅卡任务的 Worker 进程数(校验执行上限用)
# 轮询自动触发配置
polling_auto_trigger:

View File

@@ -131,6 +131,9 @@ func bindEnvVariables(v *viper.Viper) {
"gateway.app_secret",
"gateway.timeout",
"polling.verbose_log",
"polling.carrier.nsqd_address",
"polling.carrier.nsqlookupd_addresses",
"polling.carrier.subscriber_processes",
"polling_auto_trigger.enable_auto_trigger",
"polling_auto_trigger.auto_trigger_system_user_id",
"worker.role",

View File

@@ -0,0 +1,205 @@
package constants
import "fmt"
// 卡轮询调度模式(受控系统配置 polling.scheduler.mode),全局整体切换,不按账户灰度。
const (
PollingSchedulerModeLegacy = "legacy" // 旧分片调度(Asynq)
PollingSchedulerModeCarrier = "carrier" // 按运营商账户调度(Redis 延时队列 + NSQ)
)
// 卡任务检查类型(新调度内部与配置使用的短名)。
const (
CheckTypeCardStatus = "card_status"
CheckTypeCarddata = "carddata"
CheckTypeRealname = "realname"
CheckTypePackage = "package"
)
// AllCheckTypes 返回全部检查类型(默认步骤顺序)。返回新切片。
func AllCheckTypes() []string {
return []string{CheckTypeCardStatus, CheckTypeCarddata, CheckTypeRealname, CheckTypePackage}
}
// CheckTypeFromTaskType 把旧任务类型(polling:xxx)转为检查类型短名;不认识返回空串。
func CheckTypeFromTaskType(taskType string) string {
switch taskType {
case TaskTypePollingCardStatus:
return CheckTypeCardStatus
case TaskTypePollingCarddata:
return CheckTypeCarddata
case TaskTypePollingRealname:
return CheckTypeRealname
case TaskTypePollingPackage:
return CheckTypePackage
default:
return ""
}
}
// TaskTypeFromCheckType 把检查类型短名转为旧任务类型(polling:xxx);不认识返回空串。
func TaskTypeFromCheckType(checkType string) string {
switch checkType {
case CheckTypeCardStatus:
return TaskTypePollingCardStatus
case CheckTypeCarddata:
return TaskTypePollingCarddata
case CheckTypeRealname:
return TaskTypePollingRealname
case CheckTypePackage:
return TaskTypePollingPackage
default:
return ""
}
}
// CheckTypeName 返回检查类型中文名称。
func CheckTypeName(checkType string) string {
switch checkType {
case CheckTypeCardStatus:
return "卡状态检查"
case CheckTypeCarddata:
return "流量检查"
case CheckTypeRealname:
return "实名检查"
case CheckTypePackage:
return "套餐检查"
case PollingAlertCheckTypeAll:
return "全部"
default:
return checkType
}
}
// CheckTypeCallsGateway 判断检查类型是否调用运营商网关(套餐检查只按规则判断停复机,不调网关)。
func CheckTypeCallsGateway(checkType string) bool {
return checkType == CheckTypeCardStatus || checkType == CheckTypeCarddata || checkType == CheckTypeRealname
}
// 请求检查场景(tb_polling_check_scene.scene,代码预置)。
const (
PollingCheckSceneOrderPaid = "order_paid"
PollingCheckScenePackageActivated = "package_activated"
PollingCheckSceneStopResumeSucceeded = "stop_resume_succeeded"
PollingCheckSceneDeviceOperation = "device_operation"
PollingCheckSceneAutoRenewalPurchase = "auto_renewal_purchase"
PollingCheckSceneClientAssetDetail = "client_asset_detail"
PollingCheckSceneAdminAssetRealtime = "admin_asset_realtime"
PollingCheckSceneRealnameLink = "realname_link"
)
// 失败告警级别与状态(tb_polling_failure_alert)。
const (
PollingAlertLevelCard = "card"
PollingAlertLevelCarrier = "carrier"
// PollingAlertCheckTypeAll 是账户级告警的检查类型。
PollingAlertCheckTypeAll = "all"
PollingAlertStatusOpen = 1 // 未处理
PollingAlertStatusRecovered = 2 // 已恢复(首次成功自动转入)
PollingAlertStatusPollingDisabled = 3 // 已关闭轮询
PollingAlertStatusHandled = 4 // 已处理(人工标记)
)
// PollingAlertStatusName 返回失败告警状态名称。
func PollingAlertStatusName(status int) string {
switch status {
case PollingAlertStatusOpen:
return "未处理"
case PollingAlertStatusRecovered:
return "已恢复"
case PollingAlertStatusPollingDisabled:
return "已关闭轮询"
case PollingAlertStatusHandled:
return "已处理"
default:
return ""
}
}
// 失败类别中不计入连续失败的取值(限流等待超时与上游频率限制)。
const (
// PollingFailureRateLimited 本账户限流等待超时(请求未发出)。
PollingFailureRateLimited = "rate_limited"
)
// 按运营商账户调度的受控系统配置键(module = polling)。
const (
SystemConfigPollingSchedulerMode = "polling.scheduler.mode"
SystemConfigPollingStepOrder = "polling.scheduler.step_order"
SystemConfigPollingStepTimeout = "polling.scheduler.step_timeout_seconds"
SystemConfigPollingTaskTimeoutMargin = "polling.scheduler.task_timeout_margin_seconds"
SystemConfigPollingSkipPackageAfterFlow = "polling.scheduler.skip_package_after_flow"
SystemConfigPollingFailureBackoff = "polling.failure.backoff_seconds"
SystemConfigPollingAlertFailCount = "polling.failure.alert_fail_count"
SystemConfigPollingAlertFailDuration = "polling.failure.alert_fail_duration_seconds"
SystemConfigPollingImmediateAlert = "polling.failure.immediate_alert_categories"
SystemConfigPollingAccountAlertWindow = "polling.failure.account_alert_window_seconds"
SystemConfigPollingAccountAlertThreshold = "polling.failure.account_alert_threshold"
)
// ========================================
// 按运营商账户调度的 Redis Key(前缀与旧分片调度不冲突)
// ========================================
// RedisCarrierPriorityQueueKey 账户优先队列(ZSET,member=card_id,score=可执行时间毫秒)。
func RedisCarrierPriorityQueueKey(carrierID uint) string {
return fmt.Sprintf("polling:carrier:%d:priority", carrierID)
}
// RedisCarrierQueueKey 账户普通队列(ZSET,member=card_id,score=该卡最早到期时间毫秒)。
func RedisCarrierQueueKey(carrierID uint) string {
return fmt.Sprintf("polling:carrier:%d:queue", carrierID)
}
// RedisCarrierRunningKey 账户执行中计数(ZSET,member=派发 token,score=租约到期毫秒),只用于执行上限与监控。
func RedisCarrierRunningKey(carrierID uint) string {
return fmt.Sprintf("polling:carrier:%d:running", carrierID)
}
// RedisCarrierDispatchLockKey 账户分发锁:同一账户每轮只由一个分发器实例领取。
func RedisCarrierDispatchLockKey(carrierID uint) string {
return fmt.Sprintf("polling:carrier:%d:dispatch_lock", carrierID)
}
// RedisCarrierExpiredClaimsKey 账户处理中标记到期次数(Hash,field=分钟,value=次数),供监控。
func RedisCarrierExpiredClaimsKey(carrierID uint) string {
return fmt.Sprintf("polling:carrier:%d:expired_claims", carrierID)
}
// RedisCardDueKey 卡到期信息(Hash):各检查类型 due:{type}(毫秒)、fail:{type}、first_fail:{type}、
// err:{type}、ok:{type}(最近成功毫秒)、confirm:{type}(待确认延迟序列)、carrier(所属账户)、priority(优先标记)。
func RedisCardDueKey(cardID uint) string {
return fmt.Sprintf("polling:card:%d:due", cardID)
}
// RedisCardClaimKey 卡任务处理中标记,值=派发 token,TTL=租约。
func RedisCardClaimKey(cardID uint) string {
return fmt.Sprintf("polling:card:%d:claim", cardID)
}
// RedisCardExecKey 卡任务执行权,值=派发 token + 消费者实例,TTL=续期间隔×2。
func RedisCardExecKey(cardID uint) string {
return fmt.Sprintf("polling:card:%d:exec", cardID)
}
// RedisCarrierSchedulerHeartbeatKey 分发器心跳(Hash,field=实例,value=毫秒)。
const RedisCarrierSchedulerHeartbeatKey = "polling:carrier:dispatcher:heartbeat"
// RedisCarrierSchedulerInitStateKey 新调度初始化状态(String:running / done),初始化完成前分发器不领取。
const RedisCarrierSchedulerInitStateKey = "polling:carrier:init_state"
// RedisCarrierAccountAlertKey 账户级告警窗口计数(ZSET,member=告警ID,score=毫秒)。
func RedisCarrierAccountAlertKey(carrierID uint) string {
return fmt.Sprintf("polling:carrier:%d:alert_window", carrierID)
}
// NSQ topic 与 channel。
const (
NSQChannelWorker = "worker"
)
// NSQCardTopic 返回账户卡任务 topic。
func NSQCardTopic(carrierID uint) string {
return fmt.Sprintf("polling.card.%d", carrierID)
}