feat: 实现 IoT 卡轮询系统(支持千万级卡规模)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m35s
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>
This commit is contained in:
@@ -6,10 +6,17 @@ import (
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||||
"github.com/break/junhong_cmp_fiber/internal/polling"
|
||||
pollingSvc "github.com/break/junhong_cmp_fiber/internal/service/polling"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/bootstrap"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/database"
|
||||
@@ -97,17 +104,47 @@ func main() {
|
||||
// 初始化对象存储服务(可选)
|
||||
storageSvc := initStorage(cfg, appLogger)
|
||||
|
||||
// 初始化 Gateway 客户端(可选,用于轮询任务)
|
||||
gatewayClient := initGateway(cfg, appLogger)
|
||||
|
||||
// 创建 Asynq 客户端(用于调度器提交任务)
|
||||
asynqClient := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: redisAddr,
|
||||
Password: cfg.Redis.Password,
|
||||
DB: cfg.Redis.DB,
|
||||
})
|
||||
defer func() {
|
||||
if err := asynqClient.Close(); err != nil {
|
||||
appLogger.Error("关闭 Asynq 客户端失败", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
// 创建 Asynq Worker 服务器
|
||||
workerServer := queue.NewServer(redisClient, &cfg.Queue, appLogger)
|
||||
|
||||
// 创建任务处理器管理器并注册所有处理器
|
||||
taskHandler := queue.NewHandler(db, redisClient, storageSvc, appLogger)
|
||||
// 初始化轮询调度器(在创建 Handler 之前,因为 Handler 需要使用调度器作为回调)
|
||||
scheduler := polling.NewScheduler(db, redisClient, asynqClient, appLogger)
|
||||
if err := scheduler.Start(ctx); err != nil {
|
||||
appLogger.Error("启动轮询调度器失败", zap.Error(err))
|
||||
// 调度器启动失败不阻止 Worker 启动,但不传递给 Handler
|
||||
} else {
|
||||
appLogger.Info("轮询调度器已启动")
|
||||
}
|
||||
|
||||
// 创建任务处理器管理器并注册所有处理器(传递 scheduler 作为轮询回调)
|
||||
taskHandler := queue.NewHandler(db, redisClient, storageSvc, gatewayClient, scheduler, appLogger)
|
||||
taskHandler.RegisterHandlers()
|
||||
|
||||
appLogger.Info("Worker 服务器配置完成",
|
||||
zap.Int("concurrency", cfg.Queue.Concurrency),
|
||||
zap.Any("queues", cfg.Queue.Queues))
|
||||
|
||||
// 初始化告警服务并启动告警检查器
|
||||
alertChecker := startAlertChecker(ctx, db, redisClient, appLogger)
|
||||
|
||||
// 初始化数据清理服务并启动定时清理任务
|
||||
cleanupChecker := startCleanupScheduler(ctx, db, appLogger)
|
||||
|
||||
// 优雅关闭
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||
@@ -125,6 +162,15 @@ func main() {
|
||||
<-quit
|
||||
appLogger.Info("正在关闭 Worker 服务器...")
|
||||
|
||||
// 停止告警检查器
|
||||
close(alertChecker)
|
||||
|
||||
// 停止数据清理定时任务
|
||||
close(cleanupChecker)
|
||||
|
||||
// 停止轮询调度器
|
||||
scheduler.Stop()
|
||||
|
||||
// 优雅关闭 Worker 服务器(等待正在执行的任务完成)
|
||||
workerServer.Shutdown()
|
||||
|
||||
@@ -150,3 +196,107 @@ func initStorage(cfg *config.Config, appLogger *zap.Logger) *storage.Service {
|
||||
|
||||
return storage.NewService(provider, &cfg.Storage)
|
||||
}
|
||||
|
||||
// initGateway 初始化 Gateway 客户端
|
||||
func initGateway(cfg *config.Config, appLogger *zap.Logger) *gateway.Client {
|
||||
if cfg.Gateway.BaseURL == "" {
|
||||
appLogger.Info("Gateway 未配置,跳过初始化(轮询任务将无法查询真实数据)")
|
||||
return nil
|
||||
}
|
||||
|
||||
client := gateway.NewClient(
|
||||
cfg.Gateway.BaseURL,
|
||||
cfg.Gateway.AppID,
|
||||
cfg.Gateway.AppSecret,
|
||||
).WithTimeout(time.Duration(cfg.Gateway.Timeout) * time.Second)
|
||||
|
||||
appLogger.Info("Gateway 客户端初始化成功",
|
||||
zap.String("base_url", cfg.Gateway.BaseURL),
|
||||
zap.String("app_id", cfg.Gateway.AppID))
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
// startAlertChecker 启动告警检查器
|
||||
// 返回一个 stop channel,关闭它可以停止检查器
|
||||
func startAlertChecker(ctx context.Context, db *gorm.DB, redisClient *redis.Client, appLogger *zap.Logger) chan struct{} {
|
||||
stopChan := make(chan struct{})
|
||||
|
||||
// 创建告警服务所需的 stores
|
||||
ruleStore := postgres.NewPollingAlertRuleStore(db)
|
||||
historyStore := postgres.NewPollingAlertHistoryStore(db)
|
||||
alertService := pollingSvc.NewAlertService(ruleStore, historyStore, redisClient, appLogger)
|
||||
|
||||
// 启动检查器 goroutine
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Minute) // 每分钟检查一次
|
||||
defer ticker.Stop()
|
||||
|
||||
appLogger.Info("告警检查器已启动,检查间隔: 1分钟")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := alertService.CheckAlerts(ctx); err != nil {
|
||||
appLogger.Error("告警检查失败", zap.Error(err))
|
||||
}
|
||||
case <-stopChan:
|
||||
appLogger.Info("告警检查器已停止")
|
||||
return
|
||||
case <-ctx.Done():
|
||||
appLogger.Info("告警检查器因 context 取消而停止")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return stopChan
|
||||
}
|
||||
|
||||
// startCleanupScheduler 启动数据清理定时任务
|
||||
// 每天凌晨2点运行清理任务
|
||||
func startCleanupScheduler(ctx context.Context, db *gorm.DB, appLogger *zap.Logger) chan struct{} {
|
||||
stopChan := make(chan struct{})
|
||||
|
||||
// 创建清理服务
|
||||
configStore := postgres.NewDataCleanupConfigStore(db)
|
||||
logStore := postgres.NewDataCleanupLogStore(db)
|
||||
cleanupService := pollingSvc.NewCleanupService(configStore, logStore, appLogger)
|
||||
|
||||
go func() {
|
||||
// 计算到下一个凌晨2点的时间间隔
|
||||
calcNextRun := func() time.Duration {
|
||||
now := time.Now()
|
||||
next := time.Date(now.Year(), now.Month(), now.Day(), 2, 0, 0, 0, now.Location())
|
||||
if now.After(next) {
|
||||
next = next.Add(24 * time.Hour)
|
||||
}
|
||||
return time.Until(next)
|
||||
}
|
||||
|
||||
timer := time.NewTimer(calcNextRun())
|
||||
defer timer.Stop()
|
||||
|
||||
appLogger.Info("数据清理定时任务已启动,每天凌晨2点执行")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
appLogger.Info("开始执行定时数据清理")
|
||||
if err := cleanupService.RunScheduledCleanup(ctx); err != nil {
|
||||
appLogger.Error("定时数据清理失败", zap.Error(err))
|
||||
}
|
||||
// 重置定时器到下一个凌晨2点
|
||||
timer.Reset(calcNextRun())
|
||||
case <-stopChan:
|
||||
appLogger.Info("数据清理定时任务已停止")
|
||||
return
|
||||
case <-ctx.Done():
|
||||
appLogger.Info("数据清理定时任务因 context 取消而停止")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return stopChan
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user