CPU 根因:每个 Agent 请求都做一次 bcrypt 校验(实测 72ms), 30 台主机 18 req/s × 72ms ≈ 1.35 核,实测容器 CPU 135%。 - agentauth: 校验结果按 Token 哈希缓存(TTL 5min,轮换即失效), last_used_at 写入按 60s 节流。实测 CPU 135% → 1.6%,请求延迟 103ms → 1.6ms - metricagg: 新增后台逐级降采样聚合 raw→1m/5m→1h→6h/1d,取代前端手动触发 - metrics_aggregate: 补 uniq_metric_agg 唯一索引(原 idx_agg 非唯一导致 upsert 插重复行) - agent 合并配置接口 GET /agent/config:一次返回任务+探针+端口策略+频率, 配置轮询 5s→30s;服务端可下发 report_interval,Agent 热更新 ticker 无需重装 - retention: agent_reports 保留期 7d→1d(趋势由聚合表承担) - redis: 增加 maxmemory 512mb + volatile-lru - fix(upgrade): 升级脚本改为 fail-fast,修复下载失败仍报 UPGRADE_OK 的静默谎报 // Logan.Li
199 lines
6.6 KiB
Go
199 lines
6.6 KiB
Go
package controller
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"ezops/middleware"
|
|
"ezops/model"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// Agent 采集策略的兜底默认值(表内无记录时使用)。 // Logan.Li
|
|
const (
|
|
defaultReportInterval = 10
|
|
defaultConfigPollInterval = 30
|
|
// intervalFloor 与 Agent 侧校验保持一致:过密会打爆平台。 // Logan.Li
|
|
intervalFloor = 5
|
|
)
|
|
|
|
// GetAgentConfig 合并下发 Agent 配置。
|
|
//
|
|
// 原先 Agent 每 5 秒要分别请求 tasks/poll、health-probes/config、port-policy 三个接口,
|
|
// 配置类请求量是上报本身的 6 倍;合并为单次响应后请求量降到原来的 1/3。 // Logan.Li
|
|
//
|
|
// GET /api/v1/agent/config
|
|
func (h *AgentHandler) GetAgentConfig(c *gin.Context) {
|
|
hostname := c.Query("hostname")
|
|
if hostname == "" {
|
|
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "hostname 不能为空"})
|
|
return
|
|
}
|
|
if !middleware.RequireAgentHostname(c, hostname) {
|
|
return
|
|
}
|
|
|
|
setting := h.resolveAgentSetting(hostname)
|
|
|
|
// 任务:拉取 pending 并标记 running(与 /tasks/poll 行为一致)。 // Logan.Li
|
|
tasks := h.pickPendingTasks(hostname)
|
|
|
|
// 探针配置。 // Logan.Li
|
|
probes := []model.HealthProbe{}
|
|
h.db.Where("hostname = ? AND enabled = ?", hostname, true).Order("id ASC").Find(&probes)
|
|
|
|
// 端口策略。 // Logan.Li
|
|
policy := h.resolvePortPolicy(hostname)
|
|
|
|
dto := model.AgentConfigDTO{
|
|
ReportInterval: setting.ReportInterval,
|
|
ConfigPollInterval: setting.ConfigPollInterval,
|
|
Tasks: tasks,
|
|
HealthProbes: probes,
|
|
PortPolicy: policy,
|
|
}
|
|
dto.ConfigVersion = agentConfigVersion(dto)
|
|
|
|
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: dto})
|
|
}
|
|
|
|
// SaveAgentSetting 平台侧设置采集策略:hostname 为空串表示全局默认。 // Logan.Li
|
|
// PUT /api/v1/agent/settings
|
|
func (h *AgentHandler) SaveAgentSetting(c *gin.Context) {
|
|
var req struct {
|
|
Hostname string `json:"hostname"`
|
|
ReportInterval int `json:"report_interval"`
|
|
ConfigPollInterval int `json:"config_poll_interval"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求格式错误: " + err.Error()})
|
|
return
|
|
}
|
|
if req.ReportInterval < intervalFloor {
|
|
c.JSON(http.StatusBadRequest, model.Response{
|
|
Code: 400, Message: fmt.Sprintf("上报间隔不能小于 %d 秒", intervalFloor)})
|
|
return
|
|
}
|
|
if req.ConfigPollInterval < intervalFloor {
|
|
c.JSON(http.StatusBadRequest, model.Response{
|
|
Code: 400, Message: fmt.Sprintf("配置轮询间隔不能小于 %d 秒", intervalFloor)})
|
|
return
|
|
}
|
|
|
|
setting := model.AgentSetting{Hostname: req.Hostname}
|
|
h.db.Where("hostname = ?", req.Hostname).Assign(map[string]interface{}{
|
|
"report_interval": req.ReportInterval,
|
|
"config_poll_interval": req.ConfigPollInterval,
|
|
"updated_by": middleware.GetUsername(c),
|
|
}).FirstOrCreate(&setting)
|
|
|
|
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已保存", Data: setting})
|
|
}
|
|
|
|
// ListAgentSettings 列出全局默认与所有按主机覆盖项。 // Logan.Li
|
|
// GET /api/v1/agent/settings
|
|
func (h *AgentHandler) ListAgentSettings(c *gin.Context) {
|
|
var items []model.AgentSetting
|
|
h.db.Order("hostname ASC").Find(&items)
|
|
if items == nil {
|
|
items = []model.AgentSetting{}
|
|
}
|
|
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: gin.H{
|
|
"defaults": gin.H{
|
|
"report_interval": defaultReportInterval,
|
|
"config_poll_interval": defaultConfigPollInterval,
|
|
},
|
|
"items": items,
|
|
}})
|
|
}
|
|
|
|
// resolveAgentSetting 解析生效的采集策略:按主机覆盖 > 全局默认 > 内置默认。 // Logan.Li
|
|
func (h *AgentHandler) resolveAgentSetting(hostname string) model.AgentSetting {
|
|
effective := model.AgentSetting{
|
|
Hostname: hostname,
|
|
ReportInterval: defaultReportInterval,
|
|
ConfigPollInterval: defaultConfigPollInterval,
|
|
}
|
|
|
|
// 全局默认。 // Logan.Li
|
|
var global model.AgentSetting
|
|
if err := h.db.Where("hostname = ?", "").Take(&global).Error; err == nil {
|
|
applyAgentSetting(&effective, global)
|
|
}
|
|
// 按主机覆盖。 // Logan.Li
|
|
var perHost model.AgentSetting
|
|
if err := h.db.Where("hostname = ?", hostname).Take(&perHost).Error; err == nil {
|
|
applyAgentSetting(&effective, perHost)
|
|
}
|
|
return effective
|
|
}
|
|
|
|
// applyAgentSetting 仅在来源值合法时覆盖,避免脏数据把间隔写成 0。 // Logan.Li
|
|
func applyAgentSetting(dst *model.AgentSetting, src model.AgentSetting) {
|
|
if src.ReportInterval >= intervalFloor {
|
|
dst.ReportInterval = src.ReportInterval
|
|
}
|
|
if src.ConfigPollInterval >= intervalFloor {
|
|
dst.ConfigPollInterval = src.ConfigPollInterval
|
|
}
|
|
}
|
|
|
|
// pickPendingTasks 取出该主机待执行任务并标记 running,最多 5 个。 // Logan.Li
|
|
func (h *AgentHandler) pickPendingTasks(hostname string) []model.AgentTask {
|
|
tasks := []model.AgentTask{}
|
|
h.db.Where("hostname = ? AND status = ?", hostname, "pending").
|
|
Order("created_at ASC").Limit(5).Find(&tasks)
|
|
if len(tasks) == 0 {
|
|
return tasks
|
|
}
|
|
now := time.Now()
|
|
for _, t := range tasks {
|
|
// 条件更新防止多 Agent 重复认领同一任务。 // Logan.Li
|
|
h.db.Model(&model.AgentTask{}).
|
|
Where("id = ? AND status = ?", t.ID, "pending").
|
|
Updates(map[string]interface{}{"status": "running", "picked_at": now})
|
|
}
|
|
return tasks
|
|
}
|
|
|
|
// resolvePortPolicy 读取主机端口策略,无记录时返回空白名单。 // Logan.Li
|
|
func (h *AgentHandler) resolvePortPolicy(hostname string) *model.AgentPortPolicyDTO {
|
|
empty := &model.AgentPortPolicyDTO{Hostname: hostname, PortFilterMode: "whitelist", PortWhitelist: []int{}}
|
|
var policy model.AgentPortPolicy
|
|
if err := h.db.Where("hostname = ?", hostname).First(&policy).Error; err != nil {
|
|
return empty
|
|
}
|
|
whitelist := []int{}
|
|
_ = json.Unmarshal([]byte(policy.PortWhitelist), &whitelist)
|
|
return &model.AgentPortPolicyDTO{
|
|
Hostname: policy.Hostname,
|
|
PortFilterMode: policy.PortFilterMode,
|
|
PortWhitelist: whitelist,
|
|
}
|
|
}
|
|
|
|
// agentConfigVersion 对配置内容取指纹,Agent 可据此跳过未变更配置的处理。 // Logan.Li
|
|
func agentConfigVersion(dto model.AgentConfigDTO) string {
|
|
payload := struct {
|
|
ReportInterval int
|
|
ConfigPollInterval int
|
|
Tasks []model.AgentTask
|
|
HealthProbes []model.HealthProbe
|
|
PortPolicy *model.AgentPortPolicyDTO
|
|
}{
|
|
dto.ReportInterval, dto.ConfigPollInterval, dto.Tasks, dto.HealthProbes, dto.PortPolicy,
|
|
}
|
|
raw, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return strconv.FormatInt(time.Now().Unix(), 10)
|
|
}
|
|
sum := sha256.Sum256(raw)
|
|
return hex.EncodeToString(sum[:8])
|
|
}
|