优化 local 模式兼容性与部署可靠性

- SQL 方言层:agent updated_at、dashboard 小时分桶、清理任务跨 SQLite/MySQL
- Cache Capabilities:redka 跳过 ZSet/PubSub,Agent 聚合改 DB 直读
- local 模式 redka 初始化失败 Fatal;/version 返回 features
- SQLite WAL + 连接池限制;K8s data 改 PVC 持久化
- run.sh:旧参数迁移、升级后 /version 校验
This commit is contained in:
Logan.Li committed 2026-06-26 13:28:31 +08:00
1 parent 1f4f63cf6e
commit b7166acda4
13 files changed
+338 -116

No files matched your search

+145 -75
View File
@@ -1,6 +1,7 @@
package controller package controller
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math" "math"
@@ -122,7 +123,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
return `` return ``
}() + `, }() + `,
hostname = CASE WHEN hostname != ? AND ip = ? THEN ? ELSE hostname END, hostname = CASE WHEN hostname != ? AND ip = ? THEN ? ELSE hostname END,
updated_at = NOW()` updated_at = ?`
args := []interface{}{ args := []interface{}{
req.CPU.UsagePercent, req.CPU.IOWait, req.Memory.UsagePercent, diskAvg, req.CPU.UsagePercent, req.CPU.IOWait, req.Memory.UsagePercent, diskAvg,
req.CPU.CoreCount, memMB, diskGB, req.CPU.CoreCount, memMB, diskGB,
@@ -149,6 +150,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
args = append(args, req.System.Uptime) args = append(args, req.System.Uptime)
} }
} }
args = append(args, time.Now())
updateSQL += " WHERE hostname = ? OR ip = ?" updateSQL += " WHERE hostname = ? OR ip = ?"
args = append(args, req.Hostname, matchIP) args = append(args, req.Hostname, matchIP)
@@ -219,12 +221,10 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
}) })
} }
// 写入 Redis 有序集合(热数据,保留7天) // 写入 Redis 有序集合热数据(redka/local 无 ZSet,数据已在 monitor_agent_reports)// Logan.Li
if h.cache != nil { if h.cache != nil && h.cache.Capabilities().SortedSet {
timestamp := float64(reportTime.Unix()) timestamp := float64(reportTime.Unix())
ttl := 7 * 24 * time.Hour // 7天 ttl := 7 * 24 * time.Hour
// 使用 reportTime.UnixNano() 确保 member 唯一(避免相同值碰撞)
h.writeHotMetric(c, "metrics:cpu:"+req.Hostname, req.CPU.UsagePercent, timestamp, ttl, reportTime) h.writeHotMetric(c, "metrics:cpu:"+req.Hostname, req.CPU.UsagePercent, timestamp, ttl, reportTime)
h.writeHotMetric(c, "metrics:mem:"+req.Hostname, req.Memory.UsagePercent, timestamp, ttl, reportTime) h.writeHotMetric(c, "metrics:mem:"+req.Hostname, req.Memory.UsagePercent, timestamp, ttl, reportTime)
h.writeHotMetric(c, "metrics:load1:"+req.Hostname, req.CPU.Load1, timestamp, ttl, reportTime) h.writeHotMetric(c, "metrics:load1:"+req.Hostname, req.CPU.Load1, timestamp, ttl, reportTime)
@@ -343,7 +343,7 @@ func (h *AgentHandler) GetHostHistory(c *gin.Context) {
}) })
} }
// AggregateMetrics 聚合 Redis 热数据到 MySQL klines // AggregateMetrics 聚合指标到 monitor_metrics_aggregate(Redis ZSet 或 DB 直读)// Logan.Li
// POST /api/v1/agent/aggregate // POST /api/v1/agent/aggregate
func (h *AgentHandler) AggregateMetrics(c *gin.Context) { func (h *AgentHandler) AggregateMetrics(c *gin.Context) {
var req model.AggregateReq var req model.AggregateReq
@@ -352,13 +352,101 @@ func (h *AgentHandler) AggregateMetrics(c *gin.Context) {
return return
} }
if h.cache == nil { var totalUpserted int64
c.JSON(http.StatusServiceUnavailable, model.Response{Code: 503, Message: "Redis 缓存不可用"}) var err error
if h.cache != nil && h.cache.Capabilities().SortedSet {
totalUpserted, err = h.aggregateFromRedis(c.Request.Context(), req)
} else {
totalUpserted, err = h.aggregateFromReports(req)
}
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
return return
} }
// 支持的聚合间隔及对应的回溯时长 c.JSON(http.StatusOK, model.Response{
intervals := []struct { Code: 200,
Message: fmt.Sprintf("聚合完成,写入/更新 %d 条记录", totalUpserted),
Data: gin.H{"upserted": totalUpserted, "source": aggregateSource(h.cache)},
})
}
func aggregateSource(cache ezredis.Cache) string {
if cache != nil && cache.Capabilities().SortedSet {
return "redis"
}
return "database"
}
func (h *AgentHandler) aggregateFromRedis(ctx context.Context, req model.AggregateReq) (int64, error) {
if h.cache == nil {
return 0, fmt.Errorf("Redis 缓存不可用")
}
intervals := aggregateIntervals()
redisKey := fmt.Sprintf("metrics:%s:%s", req.Metric, req.Hostname)
var total int64
for _, iv := range intervals {
lookback := iv.duration * 200
now := time.Now()
since := now.Add(-lookback)
members, err := h.cache.ZRangeByScore(ctx, redisKey,
fmt.Sprintf("%d", since.Unix()), fmt.Sprintf("%d", now.Unix()+1))
if err != nil || len(members) == 0 {
continue
}
buckets := make(map[int64][]float64)
for _, m := range members {
ts := int64(m.Score)
bucketStart := (ts / int64(iv.duration.Seconds())) * int64(iv.duration.Seconds())
buckets[bucketStart] = append(buckets[bucketStart], parseMemberValue(m.Member))
}
n, err := h.upsertMetricBuckets(req, iv.name, buckets)
if err != nil {
return total, err
}
total += n
}
return total, nil
}
// aggregateFromReports local 模式:从 monitor_agent_reports 聚合(无需 Redis ZSet)// Logan.Li
func (h *AgentHandler) aggregateFromReports(req model.AggregateReq) (int64, error) {
intervals := aggregateIntervals()
var total int64
for _, iv := range intervals {
lookback := iv.duration * 200
since := time.Now().Add(-lookback)
var reports []model.AgentReport
if err := h.db.Where("hostname = ? AND report_time >= ?", req.Hostname, since).
Order("report_time ASC").Find(&reports).Error; err != nil {
return total, err
}
if len(reports) == 0 {
continue
}
buckets := make(map[int64][]float64)
sec := int64(iv.duration.Seconds())
for _, r := range reports {
ts := r.ReportTime.Unix()
bucketStart := (ts / sec) * sec
buckets[bucketStart] = append(buckets[bucketStart], metricValueFromReport(r, req.Metric))
}
n, err := h.upsertMetricBuckets(req, iv.name, buckets)
if err != nil {
return total, err
}
total += n
}
return total, nil
}
func aggregateIntervals() []struct {
name string
duration time.Duration
} {
return []struct {
name string name string
duration time.Duration duration time.Duration
}{ }{
@@ -369,70 +457,32 @@ func (h *AgentHandler) AggregateMetrics(c *gin.Context) {
{"6h", 6 * time.Hour}, {"6h", 6 * time.Hour},
{"1d", 24 * time.Hour}, {"1d", 24 * time.Hour},
} }
}
redisKey := fmt.Sprintf("metrics:%s:%s", req.Metric, req.Hostname) func metricValueFromReport(r model.AgentReport, metric string) float64 {
ctx := c.Request.Context() switch metric {
var totalUpserted int64 case "cpu":
return r.CPUUsage
for _, iv := range intervals { case "mem":
// 回溯窗口:取 interval × 200 倍,确保覆盖足够多的数据点 return r.MemUsage
lookback := iv.duration * 200 case "load1":
now := time.Now() return r.Load1
since := now.Add(-lookback) default:
minScore := fmt.Sprintf("%d", since.Unix()) return 0
maxScore := fmt.Sprintf("%d", now.Unix()+1)
members, err := h.cache.ZRangeByScore(ctx, redisKey, minScore, maxScore)
if err != nil || len(members) == 0 {
continue
} }
}
// 按间隔分桶 func (h *AgentHandler) upsertMetricBuckets(req model.AggregateReq, interval string, buckets map[int64][]float64) (int64, error) {
buckets := make(map[int64][]float64) // bucketTime (unix) → values var total int64
for _, m := range members {
ts := int64(m.Score)
bucketStart := (ts / int64(iv.duration.Seconds())) * int64(iv.duration.Seconds())
val := parseMemberValue(m.Member)
buckets[bucketStart] = append(buckets[bucketStart], val)
}
// 计算 max/min/avg/p95
for bucketTs, values := range buckets { for bucketTs, values := range buckets {
if len(values) == 0 { if len(values) == 0 {
continue continue
} }
maxV := values[0] maxV, minV, avg, p95 := calcAggregateStats(values)
minV := values[0]
sum := 0.0
for _, v := range values {
sum += v
if v > maxV {
maxV = v
}
if v < minV {
minV = v
}
}
avg := sum / float64(len(values))
// 计算 P95(排序后取 95% 位置的值)
p95 := maxV
if len(values) >= 20 {
sorted := make([]float64, len(values))
copy(sorted, values)
sort.Float64s(sorted)
idx := int(float64(len(sorted)) * 0.95)
if idx >= len(sorted) {
idx = len(sorted) - 1
}
p95 = sorted[idx]
}
agg := model.MetricsAggregate{ agg := model.MetricsAggregate{
Hostname: req.Hostname, Hostname: req.Hostname,
Metric: req.Metric, Metric: req.Metric,
Interval: iv.name, Interval: interval,
Avg: math.Round(avg*100) / 100, Avg: math.Round(avg*100) / 100,
MaxV: math.Round(maxV*100) / 100, MaxV: math.Round(maxV*100) / 100,
MinV: math.Round(minV*100) / 100, MinV: math.Round(minV*100) / 100,
@@ -440,8 +490,6 @@ func (h *AgentHandler) AggregateMetrics(c *gin.Context) {
Count: len(values), Count: len(values),
BucketTime: time.Unix(bucketTs, 0), BucketTime: time.Unix(bucketTs, 0),
} }
// Upsert: hostname + metric + interval + bucket_time 唯一
result := h.db.Clauses(clause.OnConflict{ result := h.db.Clauses(clause.OnConflict{
Columns: []clause.Column{ Columns: []clause.Column{
{Name: "hostname"}, {Name: "hostname"},
@@ -453,18 +501,40 @@ func (h *AgentHandler) AggregateMetrics(c *gin.Context) {
"avg", "max_v", "min_v", "p95", "count", "avg", "max_v", "min_v", "p95", "count",
}), }),
}).Create(&agg) }).Create(&agg)
if result.Error != nil {
return total, result.Error
}
total++
}
return total, nil
}
if result.Error == nil { func calcAggregateStats(values []float64) (maxV, minV, avg, p95 float64) {
totalUpserted++ maxV = values[0]
minV = values[0]
sum := 0.0
for _, v := range values {
sum += v
if v > maxV {
maxV = v
}
if v < minV {
minV = v
} }
} }
avg = sum / float64(len(values))
p95 = maxV
if len(values) >= 20 {
sorted := make([]float64, len(values))
copy(sorted, values)
sort.Float64s(sorted)
idx := int(float64(len(sorted)) * 0.95)
if idx >= len(sorted) {
idx = len(sorted) - 1
} }
p95 = sorted[idx]
c.JSON(http.StatusOK, model.Response{ }
Code: 200, return
Message: fmt.Sprintf("聚合完成,写入/更新 %d 条记录", totalUpserted),
Data: gin.H{"upserted": totalUpserted},
})
} }
// GetAggregate 查询聚合数据(原 GetKline) // GetAggregate 查询聚合数据(原 GetKline)
+9 -3
View File
@@ -10,6 +10,7 @@ import (
"time" "time"
"ezops/model" "ezops/model"
"ezops/pkg/database"
ezredis "ezops/pkg/redis" ezredis "ezops/pkg/redis"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -39,7 +40,8 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
const cacheKey = "dashboard:stats" const cacheKey = "dashboard:stats"
const cacheTTL = 30 * time.Second const cacheTTL = 30 * time.Second
// 尝试从 Redis 缓存读取 // 尝试从缓存读取
if h.cache != nil {
if cached, err := h.cache.Get(c.Request.Context(), cacheKey); err == nil && cached != "" { if cached, err := h.cache.Get(c.Request.Context(), cacheKey); err == nil && cached != "" {
var stats model.DashboardStats var stats model.DashboardStats
if json.Unmarshal([]byte(cached), &stats) == nil { if json.Unmarshal([]byte(cached), &stats) == nil {
@@ -51,6 +53,7 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
return return
} }
} }
}
var stats model.DashboardStats var stats model.DashboardStats
@@ -231,10 +234,12 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
} }
stats.ExpiringCerts = expiringCerts stats.ExpiringCerts = expiringCerts
// 写入 Redis 缓存(异步,缓存失败不影响响应) // 写入缓存(异步,缓存失败不影响响应)// Logan.Li
if h.cache != nil {
if data, err := json.Marshal(stats); err == nil { if data, err := json.Marshal(stats); err == nil {
_ = h.cache.Set(context.Background(), cacheKey, string(data), cacheTTL) _ = h.cache.Set(context.Background(), cacheKey, string(data), cacheTTL)
} }
}
c.JSON(http.StatusOK, model.Response{ c.JSON(http.StatusOK, model.Response{
Code: 200, Code: 200,
@@ -327,8 +332,9 @@ func (h *DashboardHandler) GetMetrics(c *gin.Context) {
AvgMem float64 AvgMem float64
} }
var rows []avgPoint var rows []avgPoint
bucketExpr := database.HourBucketExpr(database.DriverName(h.db))
h.db.Model(&model.AgentReport{}). h.db.Model(&model.AgentReport{}).
Select("UNIX_TIMESTAMP(report_time) DIV 3600 as bucket_ts, ROUND(AVG(cpu_usage),2) as avg_cpu, ROUND(AVG(mem_usage),2) as avg_mem"). Select(fmt.Sprintf("%s as bucket_ts, ROUND(AVG(cpu_usage),2) as avg_cpu, ROUND(AVG(mem_usage),2) as avg_mem", bucketExpr)).
Where("report_time >= ?", since). Where("report_time >= ?", since).
Group("bucket_ts"). Group("bucket_ts").
Order("bucket_ts ASC"). Order("bucket_ts ASC").
+4 -2
View File
@@ -60,10 +60,12 @@ func (h *PipelineHandler) executePipeline(pipeline model.Pipeline, run model.Pip
entries = append(entries, entry) entries = append(entries, entry)
buf, _ := json.Marshal(entries) buf, _ := json.Marshal(entries)
h.cache.Set(ctx, logKey, string(buf), 30*time.Minute) h.cache.Set(ctx, logKey, string(buf), 30*time.Minute)
// Pub/Sub 实时推送 // Pub/Sub 实时推送(redka/local 无 Pub/Sub,WebSocket 走轮询)// Logan.Li
if h.cache.Capabilities().PubSub {
h.cache.Publish(ctx, pubCh, string(data)) h.cache.Publish(ctx, pubCh, string(data))
} }
} }
}
updateStatus := func(status string, duration int) { updateStatus := func(status string, duration int) {
now := time.Now() now := time.Now()
@@ -76,7 +78,7 @@ func (h *PipelineHandler) executePipeline(pipeline model.Pipeline, run model.Pip
} }
h.db.Model(&model.PipelineRun{}).Where("id = ?", runID).Updates(updates) h.db.Model(&model.PipelineRun{}).Where("id = ?", runID).Updates(updates)
// 推送状态变更 // 推送状态变更
if h.cache != nil { if h.cache != nil && h.cache.Capabilities().PubSub {
statusData, _ := json.Marshal(map[string]interface{}{"type": "status", "status": status, "duration": duration}) statusData, _ := json.Marshal(map[string]interface{}{"type": "status", "status": status, "duration": duration})
h.cache.Publish(ctx, pubCh, string(statusData)) h.cache.Publish(ctx, pubCh, string(statusData))
} }
+20 -9
View File
@@ -139,10 +139,21 @@ func main() {
DataDir: "data/redka", DataDir: "data/redka",
}) })
if err != nil { if err != nil {
if cfg.Mode == "local" {
log.Fatal("local 模式缓存(redka)初始化失败", zap.Error(err))
}
log.Warn("缓存初始化失败,继续启动", zap.Error(err)) log.Warn("缓存初始化失败,继续启动", zap.Error(err))
cache = nil cache = nil
} else { } else {
log.Info("缓存已就绪", zap.String("driver", cfg.Redis.Driver)) caps := cache.Capabilities()
log.Info("缓存已就绪",
zap.String("driver", cfg.Redis.Driver),
zap.Bool("sorted_set", caps.SortedSet),
zap.Bool("pubsub", caps.PubSub),
)
if cfg.Mode == "local" && (!caps.SortedSet || !caps.PubSub) {
log.Info("local 模式:Agent 热指标走 DB 直读,流水线日志走轮询")
}
defer cache.Close() defer cache.Close()
} }
@@ -156,14 +167,18 @@ func main() {
// 版本信息 API // 版本信息 API
r.GET("/version", func(c *gin.Context) { r.GET("/version", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ resp := gin.H{
"version": Version, "version": Version,
"build_time": BuildTime, "build_time": BuildTime,
"git_commit": GitCommit, "git_commit": GitCommit,
"mode": cfg.Mode, "mode": cfg.Mode,
"db_driver": cfg.Database.Driver, "db_driver": cfg.Database.Driver,
"redis_driver": cfg.Redis.Driver, "redis_driver": cfg.Redis.Driver,
}) }
if cache != nil {
resp["features"] = cache.Capabilities()
}
c.JSON(http.StatusOK, resp)
}) })
// 注册路由 // 注册路由
@@ -173,13 +188,9 @@ func main() {
go func() { go func() {
ticker := time.NewTicker(24 * time.Hour) ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop() defer ticker.Stop()
cleanupSQL := database.CleanupAgentReportsSQL(cfg.Database.Driver)
for range ticker.C { for range ticker.C {
var rows int64 rows := db.Exec(cleanupSQL).RowsAffected
if cfg.Database.Driver == "sqlite" {
rows = db.Exec("DELETE FROM monitor_agent_reports WHERE report_time < datetime('now', '-7 days')").RowsAffected
} else {
rows = db.Exec("DELETE FROM monitor_agent_reports WHERE report_time < DATE_SUB(NOW(), INTERVAL 7 DAY)").RowsAffected
}
if rows > 0 { if rows > 0 {
log.Info("清理过期的 agent 上报数据", zap.Int64("rows", rows)) log.Info("清理过期的 agent 上报数据", zap.Int64("rows", rows))
} }
+13 -1
View File
@@ -2,6 +2,7 @@ package database
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"ezops/config" "ezops/config"
@@ -28,7 +29,18 @@ func New(cfg config.DatabaseConfig) (*gorm.DB, error) {
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName) cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName)
dialector = postgres.Open(dsn) dialector = postgres.Open(dsn)
case "sqlite": case "sqlite":
dialector = sqlite.Open(cfg.DBName) dsn := cfg.DBName
if !strings.Contains(dsn, "?") {
// WAL 提升并发读,busy_timeout 降低锁冲突 // Logan.Li
dsn += "?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)"
}
dialector = sqlite.Open(dsn)
if cfg.MaxConn > 4 {
cfg.MaxConn = 4
}
if cfg.MaxIdle > 2 {
cfg.MaxIdle = 2
}
default: default:
return nil, fmt.Errorf("unsupported database driver: %s", cfg.Driver) return nil, fmt.Errorf("unsupported database driver: %s", cfg.Driver)
} }
+43
View File
@@ -0,0 +1,43 @@
package database
import (
"strings"
"gorm.io/gorm"
)
// DriverName 从 GORM 连接获取驱动名(sqlite / mysql / postgres)// Logan.Li
func DriverName(db *gorm.DB) string {
if db == nil || db.Dialector == nil {
return "mysql"
}
name := strings.ToLower(db.Dialector.Name())
switch {
case strings.Contains(name, "sqlite"):
return "sqlite"
case strings.Contains(name, "postgres"):
return "postgres"
default:
return "mysql"
}
}
// HourBucketExpr 按小时分桶表达式(跨 SQLite / MySQL)// Logan.Li
func HourBucketExpr(driver string) string {
switch driver {
case "sqlite":
return "cast(strftime('%s', report_time) as integer) / 3600"
default:
return "UNIX_TIMESTAMP(report_time) DIV 3600"
}
}
// CleanupAgentReportsSQL 清理 7 天前 Agent 上报 // Logan.Li
func CleanupAgentReportsSQL(driver string) string {
switch driver {
case "sqlite":
return "DELETE FROM monitor_agent_reports WHERE report_time < datetime('now', '-7 days')"
default:
return "DELETE FROM monitor_agent_reports WHERE report_time < DATE_SUB(NOW(), INTERVAL 7 DAY)"
}
}
+4
View File
@@ -33,6 +33,10 @@ func newGoRedis(cfg Config) (Cache, error) {
return &goRedisCache{client: client}, nil return &goRedisCache{client: client}, nil
} }
func (r *goRedisCache) Capabilities() Capabilities {
return Capabilities{SortedSet: true, PubSub: true}
}
func (r *goRedisCache) Get(ctx context.Context, key string) (string, error) { func (r *goRedisCache) Get(ctx context.Context, key string) (string, error) {
val, err := r.client.Get(ctx, key).Result() val, err := r.client.Get(ctx, key).Result()
if err == goredis.Nil { if err == goredis.Nil {
+7
View File
@@ -40,8 +40,15 @@ type SortedSetMember struct {
Member string Member string
} }
// Capabilities 缓存后端能力(local/redka 与外部 Redis 差异)// Logan.Li
type Capabilities struct {
SortedSet bool `json:"sorted_set"`
PubSub bool `json:"pubsub"`
}
// Cache Redis 缓存接口 // Logan.Li // Cache Redis 缓存接口 // Logan.Li
type Cache interface { type Cache interface {
Capabilities() Capabilities
Get(ctx context.Context, key string) (string, error) Get(ctx context.Context, key string) (string, error)
Set(ctx context.Context, key string, value string, expiration time.Duration) error Set(ctx context.Context, key string, value string, expiration time.Duration) error
Del(ctx context.Context, keys ...string) error Del(ctx context.Context, keys ...string) error
+6 -1
View File
@@ -31,6 +31,10 @@ func newRedka(cfg Config) (Cache, error) {
return &redkaCache{db: db}, nil return &redkaCache{db: db}, nil
} }
func (r *redkaCache) Capabilities() Capabilities {
return Capabilities{SortedSet: false, PubSub: false}
}
func (r *redkaCache) Get(ctx context.Context, key string) (string, error) { func (r *redkaCache) Get(ctx context.Context, key string) (string, error) {
// Redka v0.2.x: Str().Get() 返回 core.Value // Logan.Li // Redka v0.2.x: Str().Get() 返回 core.Value // Logan.Li
val, err := r.db.Str().Get(key) val, err := r.db.Str().Get(key)
@@ -115,5 +119,6 @@ func (r *redkaCache) ZRemRangeByScore(ctx context.Context, key string, min, max
var errPubSubNotSupported = fmt.Errorf("Pub/Sub is not supported by the redka backend; use go-redis (driver=redis)") var errPubSubNotSupported = fmt.Errorf("Pub/Sub is not supported by the redka backend; use go-redis (driver=redis)")
func (r *redkaCache) Publish(ctx context.Context, channel string, message string) error { func (r *redkaCache) Publish(ctx context.Context, channel string, message string) error {
return errPubSubNotSupported // local/redka 无 Pub/Sub,流水线日志走轮询 Get,此处 no-op // Logan.Li
return nil
} }
+2 -1
View File
@@ -107,7 +107,8 @@ spec:
- name: uploads - name: uploads
emptyDir: {} emptyDir: {}
- name: data - name: data
emptyDir: {} persistentVolumeClaim:
claimName: aiops-data
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
+16
View File
@@ -0,0 +1,16 @@
# pvc-aiops-data.yaml — local 模式 SQLite + redka 数据持久化
# 部署: kubectl apply -f pvc-aiops-data.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: aiops-data
namespace: devops
labels:
app.kubernetes.io/name: aiops
app.kubernetes.io/component: storage
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
+19 -23
View File
@@ -13,10 +13,17 @@ metadata:
namespace: devops namespace: devops
data: data:
config.yaml: | config.yaml: |
mode: release mode: local
port: "8080" port: "8080"
log_level: info log_level: info
database:
driver: sqlite
dbname: data/sqlite3.db
redis:
driver: redka
jwt: jwt:
secret: "aiops-jwt-secret-key-2026" secret: "aiops-jwt-secret-key-2026"
expire_hour: 24 expire_hour: 24
@@ -60,29 +67,17 @@ spec:
value: "local" value: "local"
- name: TZ - name: TZ
value: Asia/Shanghai value: Asia/Shanghai
# 远程数据库 (MODE=release 时生效)
- name: DATABASE_DRIVER
value: "mysql"
- name: DATABASE_HOST
value: "10.10.10.75"
- name: DATABASE_PORT
value: "3306"
- name: DATABASE_USER
value: "root"
- name: DATABASE_PASSWORD
value: "aiops_root_2026"
- name: DATABASE_DBNAME
value: "aiops"
# 远程 Redis (MODE=release 时生效)
- name: REDIS_DRIVER
value: "redis"
- name: REDIS_HOST
value: "10.10.10.75"
- name: REDIS_PORT
value: "6379"
# JWT
- name: JWT_SECRET - name: JWT_SECRET
value: "aiops-jwt-secret-key-2026" value: "aiops-jwt-secret-key-2026"
# 集群模式 (MODE=release) 时取消注释:
# - name: DATABASE_DRIVER
# value: mysql
# - name: DATABASE_HOST
# value: "10.10.10.75"
# - name: REDIS_DRIVER
# value: redis
# - name: REDIS_HOST
# value: "10.10.10.75"
volumeMounts: volumeMounts:
- name: config - name: config
mountPath: /app/config.yaml mountPath: /app/config.yaml
@@ -102,7 +97,8 @@ spec:
configMap: configMap:
name: aiops-config name: aiops-config
- name: data - name: data
emptyDir: {} persistentVolumeClaim:
claimName: aiops-data
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
+50 -1
View File
@@ -451,7 +451,38 @@ save_binary_args() {
printf '%s\n' "$@" > "$BINARY_ARGS_FILE" printf '%s\n' "$@" > "$BINARY_ARGS_FILE"
} }
# 将旧版 --db-driver sqlite 参数迁移为 --mode local // Logan.Li
migrate_binary_args_file() {
[ -f "$BINARY_ARGS_FILE" ] || return 0
if grep -qE '^--mode ' "$BINARY_ARGS_FILE" 2>/dev/null; then
return 0
fi
if ! grep -qE '^--db-driver' "$BINARY_ARGS_FILE" 2>/dev/null; then
return 0
fi
local port="8080"
local lines=()
while IFS= read -r line || [ -n "$line" ]; do
[ -n "$line" ] && lines+=("$line")
done < "$BINARY_ARGS_FILE"
local i=0
while [ $i -lt ${#lines[@]} ]; do
if [ "${lines[$i]}" = "--port" ] && [ $((i + 1)) -lt ${#lines[@]} ]; then
port="${lines[$((i + 1))]}"
break
fi
if [[ "${lines[$i]}" == --port=* ]]; then
port="${lines[$i]#--port=}"
break
fi
i=$((i + 1))
done
info "迁移启动参数: --db-driver → --mode local --port $port"
save_binary_args --mode local --port "$port"
}
load_binary_args() { load_binary_args() {
migrate_binary_args_file
local -n _out=$1 local -n _out=$1
_out=() _out=()
if [ -f "$BINARY_ARGS_FILE" ]; then if [ -f "$BINARY_ARGS_FILE" ]; then
@@ -464,6 +495,21 @@ load_binary_args() {
fi fi
} }
# 升级后校验 /version 驱动信息 // Logan.Li
verify_binary_version() {
local port=${1:-8080}
local body
body=$(curl -sf "http://localhost:${port}/version" 2>/dev/null) || {
warning "/version 未响应"
return 1
}
info "版本探测: $body"
echo "$body" | grep -q '"mode":"local"' && success "mode=local 已生效" || warning "mode 非 local"
echo "$body" | grep -q '"db_driver":"sqlite"' && success "db_driver=sqlite 已生效" || true
echo "$body" | grep -q '"redis_driver":"redka"' && success "redis_driver=redka 已生效" || true
return 0
}
binary_listen_port() { binary_listen_port() {
local args=("$@") local args=("$@")
local i=0 local i=0
@@ -492,7 +538,7 @@ start_binary_daemon() {
if wait_binary_ready "$port"; then if wait_binary_ready "$port"; then
success "二进制已启动 (PID: $(cat "$BINARY_PID_FILE"), 端口: $port)" success "二进制已启动 (PID: $(cat "$BINARY_PID_FILE"), 端口: $port)"
info "日志: $BACKEND_DIR/logs/aiops.log" info "日志: $BACKEND_DIR/logs/aiops.log"
info "健康检查: http://localhost:${port}/version" verify_binary_version "$port" || true
return 0 return 0
fi fi
return 1 return 1
@@ -896,6 +942,9 @@ cmd_kubernetes() {
kubectl apply -f "$KUBERNETES_MANIFEST_DIR/secret.yaml" kubectl apply -f "$KUBERNETES_MANIFEST_DIR/secret.yaml"
kubectl apply -f "$KUBERNETES_MANIFEST_DIR/configmap.yaml" kubectl apply -f "$KUBERNETES_MANIFEST_DIR/configmap.yaml"
info "部署 local 模式数据卷 (PVC)..."
kubectl apply -f "$KUBERNETES_MANIFEST_DIR/pvc-aiops-data.yaml"
info "部署 MySQL + Redis..." info "部署 MySQL + Redis..."
kubectl apply -f "$KUBERNETES_MANIFEST_DIR/mysql.yaml" kubectl apply -f "$KUBERNETES_MANIFEST_DIR/mysql.yaml"
kubectl apply -f "$KUBERNETES_MANIFEST_DIR/redis.yaml" kubectl apply -f "$KUBERNETES_MANIFEST_DIR/redis.yaml"