Files

161 lines
5.3 KiB
Go

package common
import (
"fmt"
"sync/atomic"
"time"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/mem"
)
// DiskSpaceInfo 磁盘空间信息
type DiskSpaceInfo struct {
// 总空间(字节)
Total uint64 `json:"total"`
// 可用空间(字节)
Free uint64 `json:"free"`
// 已用空间(字节)
Used uint64 `json:"used"`
// 使用百分比
UsedPercent float64 `json:"used_percent"`
}
// SystemStatus 系统状态信息
type SystemStatus struct {
CPUUsage float64
MemoryUsage float64
DiskUsage float64
SampledAt time.Time
// 过载判定带连续性要求:单一采样尖峰不再直接熔断,需连续多次超阈值。
CPUOverloaded bool
MemoryOverloaded bool
DiskOverloaded bool
}
// SystemOverload 是哨兵据以拒绝请求的过载项,Error 文本带当前值与生效阈值。
type SystemOverload struct {
Code string
Resource string
Usage float64
Threshold int
}
func (o *SystemOverload) Error() string {
return fmt.Sprintf("system %s overloaded (current: %.1f%%, threshold: %d%%)", o.Resource, o.Usage, o.Threshold)
}
var latestSystemStatus atomic.Value
const (
systemMonitorInterval = 5 * time.Second
// 连续 3 次采样(约 15 秒)仍高于阈值才判定过载,
// 任一采样回落到阈值以下即恢复,避免瞬时尖峰打满重试。
overloadConsecutiveSamples = 3
// 采样超过该时长未刷新(监控未运行或被阻塞)时,哨兵不据此熔断。
systemStatusMaxAge = 30 * time.Second
)
func init() {
latestSystemStatus.Store(SystemStatus{})
}
// StartSystemMonitor 启动系统监控
func StartSystemMonitor() {
go func() {
usage := newContainerUsage()
var detector overloadDetector
for {
config := GetPerformanceMonitorConfig()
if !config.Enabled {
detector = overloadDetector{}
latestSystemStatus.Store(SystemStatus{SampledAt: time.Now()})
time.Sleep(systemMonitorInterval)
continue
}
status := collectSystemStatus(usage)
detector.mark(&status, config)
latestSystemStatus.Store(status)
time.Sleep(systemMonitorInterval)
}
}()
}
// overloadDetector 为每项资源累计连续超阈值的采样次数:连续 overloadConsecutiveSamples 次
// 才标记过载,任一次回落立即清零并重新累计。
type overloadDetector struct {
cpuSamples int
memorySamples int
diskSamples int
}
func (d *overloadDetector) mark(status *SystemStatus, config PerformanceMonitorConfig) {
status.CPUOverloaded = countOverloadSample(&d.cpuSamples, exceedsThreshold(status.CPUUsage, config.CPUThreshold))
status.MemoryOverloaded = countOverloadSample(&d.memorySamples, exceedsThreshold(status.MemoryUsage, config.MemoryThreshold))
status.DiskOverloaded = countOverloadSample(&d.diskSamples, exceedsThreshold(status.DiskUsage, config.DiskThreshold))
}
func countOverloadSample(samples *int, over bool) bool {
if !over {
*samples = 0
return false
}
*samples = min(*samples+1, overloadConsecutiveSamples)
return *samples == overloadConsecutiveSamples
}
// exceedsThreshold 判断采样值是否超过阈值;阈值 <= 0 表示不检查该项。
func exceedsThreshold(usage float64, threshold int) bool {
return threshold > 0 && int(usage) > threshold
}
func collectSystemStatus(usage *containerUsage) SystemStatus {
now := time.Now()
status := SystemStatus{SampledAt: now}
// CPU / 内存优先按容器 cgroup 配额计算;容器口径不可用时回退宿主机口径,
// 保持对未限制资源或被挂载为宿主 cgroup 的环境的兼容。
if saturation, ok := usage.cpuSaturation(now); ok {
status.CPUUsage = saturation
} else if percents, err := cpu.Percent(0, false); err == nil && len(percents) > 0 {
status.CPUUsage = percents[0]
}
if containerMemory, ok := usage.memoryUsage(); ok {
status.MemoryUsage = containerMemory
} else if memInfo, err := mem.VirtualMemory(); err == nil {
status.MemoryUsage = memInfo.UsedPercent
}
diskInfo := GetDiskSpaceInfo()
if diskInfo.Total > 0 {
status.DiskUsage = diskInfo.UsedPercent
}
return status
}
// GetSystemStatus 获取当前系统状态
func GetSystemStatus() SystemStatus {
return latestSystemStatus.Load().(SystemStatus)
}
// CheckSystemOverload 返回哨兵应据以熔断的过载项。监控关闭、采样缺失或已超过
// systemStatusMaxAge 时返回 nil。过载标记来自连续采样,阈值按当前配置复核,
// 运行时关闭或调高阈值后立即放行,不必等待下一次采样。
func CheckSystemOverload(status SystemStatus, config PerformanceMonitorConfig, now time.Time) *SystemOverload {
if !config.Enabled || status.SampledAt.IsZero() || now.Sub(status.SampledAt) > systemStatusMaxAge {
return nil
}
if status.CPUOverloaded && exceedsThreshold(status.CPUUsage, config.CPUThreshold) {
return &SystemOverload{Code: "system_cpu_overloaded", Resource: "cpu", Usage: status.CPUUsage, Threshold: config.CPUThreshold}
}
if status.MemoryOverloaded && exceedsThreshold(status.MemoryUsage, config.MemoryThreshold) {
return &SystemOverload{Code: "system_memory_overloaded", Resource: "memory", Usage: status.MemoryUsage, Threshold: config.MemoryThreshold}
}
if status.DiskOverloaded && exceedsThreshold(status.DiskUsage, config.DiskThreshold) {
return &SystemOverload{Code: "system_disk_overloaded", Resource: "disk", Usage: status.DiskUsage, Threshold: config.DiskThreshold}
}
return nil
}