Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions common/performance_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package common

import "sync/atomic"

// PerformanceMonitorConfig 性能监控配置
type PerformanceMonitorConfig struct {
Enabled bool
CPUThreshold int
MemoryThreshold int
DiskThreshold int
}

var performanceMonitorConfig atomic.Value

func init() {
// 初始化默认配置
performanceMonitorConfig.Store(PerformanceMonitorConfig{
Enabled: true,
CPUThreshold: 90,
MemoryThreshold: 90,
DiskThreshold: 90,
})
}

// GetPerformanceMonitorConfig 获取性能监控配置
func GetPerformanceMonitorConfig() PerformanceMonitorConfig {
return performanceMonitorConfig.Load().(PerformanceMonitorConfig)
}

// SetPerformanceMonitorConfig 设置性能监控配置
func SetPerformanceMonitorConfig(config PerformanceMonitorConfig) {
performanceMonitorConfig.Store(config)
}
81 changes: 81 additions & 0 deletions common/system_monitor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package common

import (
"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
}

var latestSystemStatus atomic.Value

func init() {
latestSystemStatus.Store(SystemStatus{})
}

// StartSystemMonitor 启动系统监控
func StartSystemMonitor() {
go func() {
for {
config := GetPerformanceMonitorConfig()
if !config.Enabled {
time.Sleep(30 * time.Second)
continue
}

updateSystemStatus()
time.Sleep(5 * time.Second)
}
}()
}

func updateSystemStatus() {
var status SystemStatus

// CPU
// 注意:cpu.Percent(0, false) 返回自上次调用以来的 CPU 使用率
// 如果是第一次调用,可能会返回错误或不准确的值,但在循环中会逐渐正常
percents, err := cpu.Percent(0, false)
if err == nil && len(percents) > 0 {
status.CPUUsage = percents[0]
}

// Memory
memInfo, err := mem.VirtualMemory()
if err == nil {
status.MemoryUsage = memInfo.UsedPercent
}

// Disk
diskInfo := GetDiskSpaceInfo()
if diskInfo.Total > 0 {
status.DiskUsage = diskInfo.UsedPercent
}

latestSystemStatus.Store(status)
}

// GetSystemStatus 获取当前系统状态
func GetSystemStatus() SystemStatus {
return latestSystemStatus.Load().(SystemStatus)
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
//go:build !windows

package controller
package common

import (
"os"

"github.com/QuantumNous/new-api/common"
"golang.org/x/sys/unix"
)

// getDiskSpaceInfo 获取缓存目录所在磁盘的空间信息 (Unix/Linux/macOS)
func getDiskSpaceInfo() DiskSpaceInfo {
cachePath := common.GetDiskCachePath()
// GetDiskSpaceInfo 获取缓存目录所在磁盘的空间信息 (Unix/Linux/macOS)
func GetDiskSpaceInfo() DiskSpaceInfo {
cachePath := GetDiskCachePath()
if cachePath == "" {
cachePath = os.TempDir()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
//go:build windows

package controller
package common

import (
"os"
"syscall"
"unsafe"

"github.com/QuantumNous/new-api/common"
)

// getDiskSpaceInfo 获取缓存目录所在磁盘的空间信息 (Windows)
func getDiskSpaceInfo() DiskSpaceInfo {
cachePath := common.GetDiskCachePath()
// GetDiskSpaceInfo 获取缓存目录所在磁盘的空间信息 (Windows)
func GetDiskSpaceInfo() DiskSpaceInfo {
cachePath := GetDiskCachePath()
if cachePath == "" {
cachePath = os.TempDir()
}
Expand Down
49 changes: 30 additions & 19 deletions controller/performance.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type PerformanceStats struct {
// 磁盘缓存目录信息
DiskCacheInfo DiskCacheInfo `json:"disk_cache_info"`
// 磁盘空间信息
DiskSpaceInfo DiskSpaceInfo `json:"disk_space_info"`
DiskSpaceInfo common.DiskSpaceInfo `json:"disk_space_info"`
// 配置信息
Config PerformanceConfig `json:"config"`
}
Expand Down Expand Up @@ -50,18 +50,6 @@ type DiskCacheInfo struct {
TotalSize int64 `json:"total_size"`
}

// DiskSpaceInfo 磁盘空间信息
type DiskSpaceInfo struct {
// 总空间(字节)
Total uint64 `json:"total"`
// 可用空间(字节)
Free uint64 `json:"free"`
// 已用空间(字节)
Used uint64 `json:"used"`
// 使用百分比
UsedPercent float64 `json:"used_percent"`
}

// PerformanceConfig 性能配置
type PerformanceConfig struct {
// 是否启用磁盘缓存
Expand All @@ -74,6 +62,15 @@ type PerformanceConfig struct {
DiskCachePath string `json:"disk_cache_path"`
// 是否在容器中运行
IsRunningInContainer bool `json:"is_running_in_container"`

// MonitorEnabled 是否启用性能监控
MonitorEnabled bool `json:"monitor_enabled"`
// MonitorCPUThreshold CPU 使用率阈值(%)
MonitorCPUThreshold int `json:"monitor_cpu_threshold"`
// MonitorMemoryThreshold 内存使用率阈值(%)
MonitorMemoryThreshold int `json:"monitor_memory_threshold"`
// MonitorDiskThreshold 磁盘使用率阈值(%)
MonitorDiskThreshold int `json:"monitor_disk_threshold"`
}

// GetPerformanceStats 获取性能统计信息
Expand All @@ -91,16 +88,30 @@ func GetPerformanceStats(c *gin.Context) {

// 获取配置信息
diskConfig := common.GetDiskCacheConfig()
monitorConfig := common.GetPerformanceMonitorConfig()
config := PerformanceConfig{
DiskCacheEnabled: diskConfig.Enabled,
DiskCacheThresholdMB: diskConfig.ThresholdMB,
DiskCacheMaxSizeMB: diskConfig.MaxSizeMB,
DiskCachePath: diskConfig.Path,
IsRunningInContainer: common.IsRunningInContainer(),
DiskCacheEnabled: diskConfig.Enabled,
DiskCacheThresholdMB: diskConfig.ThresholdMB,
DiskCacheMaxSizeMB: diskConfig.MaxSizeMB,
DiskCachePath: diskConfig.Path,
IsRunningInContainer: common.IsRunningInContainer(),
MonitorEnabled: monitorConfig.Enabled,
MonitorCPUThreshold: monitorConfig.CPUThreshold,
MonitorMemoryThreshold: monitorConfig.MemoryThreshold,
MonitorDiskThreshold: monitorConfig.DiskThreshold,
}

// 获取磁盘空间信息
diskSpaceInfo := getDiskSpaceInfo()
// 使用缓存的系统状态,避免频繁调用系统 API
systemStatus := common.GetSystemStatus()
diskSpaceInfo := common.DiskSpaceInfo{
UsedPercent: systemStatus.DiskUsage,
}
// 如果需要详细信息,可以按需获取,或者扩展 SystemStatus
// 这里为了保持接口兼容性,我们仍然调用 GetDiskSpaceInfo,但注意这可能会有性能开销
// 考虑到 GetPerformanceStats 是管理接口,频率较低,直接调用是可以接受的
// 但为了一致性,我们也可以考虑从 SystemStatus 中获取部分信息
diskSpaceInfo = common.GetDiskSpaceInfo()

stats := PerformanceStats{
CacheStats: cacheStats,
Expand Down
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,5 +274,9 @@ func InitResources() error {
if err != nil {
return err
}

// 启动系统监控
common.StartSystemMonitor()

return nil
}
65 changes: 65 additions & 0 deletions middleware/performance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package middleware

import (
"errors"
"net/http"
"strings"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
)

// SystemPerformanceCheck 检查系统性能中间件
func SystemPerformanceCheck() gin.HandlerFunc {
return func(c *gin.Context) {
// 仅检查 Relay 接口 (/v1, /v1beta 等)
// 这里简单判断路径前缀,可以根据实际路由调整
path := c.Request.URL.Path
if strings.HasPrefix(path, "/v1/messages") {
if err := checkSystemPerformance(); err != nil {
c.JSON(err.StatusCode, gin.H{
"error": err.ToClaudeError(),
})
c.Abort()
return
}
} else {
if err := checkSystemPerformance(); err != nil {
c.JSON(err.StatusCode, gin.H{
"error": err.ToOpenAIError(),
})
c.Abort()
return
}
}
c.Next()
}
}

// checkSystemPerformance 检查系统性能是否超过阈值
func checkSystemPerformance() *types.NewAPIError {
config := common.GetPerformanceMonitorConfig()
if !config.Enabled {
return nil
}

status := common.GetSystemStatus()

// 检查 CPU
if config.CPUThreshold > 0 && int(status.CPUUsage) > config.CPUThreshold {
return types.NewErrorWithStatusCode(errors.New("system cpu overloaded"), "system_cpu_overloaded", http.StatusServiceUnavailable)
}

// 检查内存
if config.MemoryThreshold > 0 && int(status.MemoryUsage) > config.MemoryThreshold {
return types.NewErrorWithStatusCode(errors.New("system memory overloaded"), "system_memory_overloaded", http.StatusServiceUnavailable)
}

// 检查磁盘
if config.DiskThreshold > 0 && int(status.DiskUsage) > config.DiskThreshold {
return types.NewErrorWithStatusCode(errors.New("system disk overloaded"), "system_disk_overloaded", http.StatusServiceUnavailable)
Comment on lines +49 to +61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the middleware/performance.go file
fd middleware -type f -name "performance.go"

Repository: QuantumNous/new-api

Length of output: 233


🏁 Script executed:

# Also check for the types/status struct definition
fd -e go | head -20

Repository: QuantumNous/new-api

Length of output: 469


🏁 Script executed:

# Search for the status struct definition that contains CPUUsage, MemoryUsage, DiskUsage
rg "type.*Status.*struct" -A 10

Repository: QuantumNous/new-api

Length of output: 7205


🏁 Script executed:

# Search for config struct with threshold fields
rg "CPUThreshold|MemoryThreshold|DiskThreshold" -B 5 -A 2

Repository: QuantumNous/new-api

Length of output: 5987


Fix float-to-int truncation in threshold checks.

The status.CPUUsage, status.MemoryUsage, and status.DiskUsage fields are float64, while the threshold config values are int. Casting to int truncates (e.g., 90.9 → 90), allowing values just below the integer threshold to bypass the check. Compare as floats instead:

- if config.CPUThreshold > 0 && int(status.CPUUsage) > config.CPUThreshold {
+ if config.CPUThreshold > 0 && status.CPUUsage > float64(config.CPUThreshold) {
     return types.NewErrorWithStatusCode(errors.New("system cpu overloaded"), "system_cpu_overloaded", http.StatusServiceUnavailable)
   }
- if config.MemoryThreshold > 0 && int(status.MemoryUsage) > config.MemoryThreshold {
+ if config.MemoryThreshold > 0 && status.MemoryUsage > float64(config.MemoryThreshold) {
     return types.NewErrorWithStatusCode(errors.New("system memory overloaded"), "system_memory_overloaded", http.StatusServiceUnavailable)
   }
- if config.DiskThreshold > 0 && int(status.DiskUsage) > config.DiskThreshold {
+ if config.DiskThreshold > 0 && status.DiskUsage > float64(config.DiskThreshold) {
     return types.NewErrorWithStatusCode(errors.New("system disk overloaded"), "system_disk_overloaded", http.StatusServiceUnavailable)
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 检查 CPU
if config.CPUThreshold > 0 && int(status.CPUUsage) > config.CPUThreshold {
return types.NewErrorWithStatusCode(errors.New("system cpu overloaded"), "system_cpu_overloaded", http.StatusServiceUnavailable)
}
// 检查内存
if config.MemoryThreshold > 0 && int(status.MemoryUsage) > config.MemoryThreshold {
return types.NewErrorWithStatusCode(errors.New("system memory overloaded"), "system_memory_overloaded", http.StatusServiceUnavailable)
}
// 检查磁盘
if config.DiskThreshold > 0 && int(status.DiskUsage) > config.DiskThreshold {
return types.NewErrorWithStatusCode(errors.New("system disk overloaded"), "system_disk_overloaded", http.StatusServiceUnavailable)
// 检查 CPU
if config.CPUThreshold > 0 && status.CPUUsage > float64(config.CPUThreshold) {
return types.NewErrorWithStatusCode(errors.New("system cpu overloaded"), "system_cpu_overloaded", http.StatusServiceUnavailable)
}
// 检查内存
if config.MemoryThreshold > 0 && status.MemoryUsage > float64(config.MemoryThreshold) {
return types.NewErrorWithStatusCode(errors.New("system memory overloaded"), "system_memory_overloaded", http.StatusServiceUnavailable)
}
// 检查磁盘
if config.DiskThreshold > 0 && status.DiskUsage > float64(config.DiskThreshold) {
return types.NewErrorWithStatusCode(errors.New("system disk overloaded"), "system_disk_overloaded", http.StatusServiceUnavailable)
}
🤖 Prompt for AI Agents
In `@middleware/performance.go` around lines 49 - 61, The threshold checks cast
float usage values to int causing truncation; update the three comparisons (the
CPU, Memory and Disk checks that reference config.CPUThreshold,
config.MemoryThreshold, config.DiskThreshold and status.CPUUsage,
status.MemoryUsage, status.DiskUsage) to compare floats instead (e.g., convert
the int thresholds to float64 or compare status.*Usage >=
float64(config.*Threshold)) and keep the same error returns via
types.NewErrorWithStatusCode when the condition is met.

}

return nil
}
6 changes: 6 additions & 0 deletions router/relay-router.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ func SetRelayRouter(router *gin.Engine) {
}

playgroundRouter := router.Group("/pg")
playgroundRouter.Use(middleware.SystemPerformanceCheck())
playgroundRouter.Use(middleware.UserAuth(), middleware.Distribute())
{
playgroundRouter.POST("/chat/completions", controller.Playground)
}
relayV1Router := router.Group("/v1")
relayV1Router.Use(middleware.SystemPerformanceCheck())
relayV1Router.Use(middleware.TokenAuth())
relayV1Router.Use(middleware.ModelRequestRateLimit())
{
Expand Down Expand Up @@ -159,13 +161,16 @@ func SetRelayRouter(router *gin.Engine) {
}

relayMjRouter := router.Group("/mj")
relayMjRouter.Use(middleware.SystemPerformanceCheck())
registerMjRouterGroup(relayMjRouter)

relayMjModeRouter := router.Group("/:mode/mj")
relayMjModeRouter.Use(middleware.SystemPerformanceCheck())
registerMjRouterGroup(relayMjModeRouter)
//relayMjRouter.Use()

relaySunoRouter := router.Group("/suno")
relaySunoRouter.Use(middleware.SystemPerformanceCheck())
relaySunoRouter.Use(middleware.TokenAuth(), middleware.Distribute())
{
relaySunoRouter.POST("/submit/:action", controller.RelayTask)
Expand All @@ -174,6 +179,7 @@ func SetRelayRouter(router *gin.Engine) {
}

relayGeminiRouter := router.Group("/v1beta")
relayGeminiRouter.Use(middleware.SystemPerformanceCheck())
relayGeminiRouter.Use(middleware.TokenAuth())
relayGeminiRouter.Use(middleware.ModelRequestRateLimit())
relayGeminiRouter.Use(middleware.Distribute())
Expand Down
21 changes: 21 additions & 0 deletions setting/performance_setting/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ type PerformanceSetting struct {
DiskCacheMaxSizeMB int `json:"disk_cache_max_size_mb"`
// DiskCachePath 磁盘缓存目录
DiskCachePath string `json:"disk_cache_path"`

// MonitorEnabled 是否启用性能监控
MonitorEnabled bool `json:"monitor_enabled"`
// MonitorCPUThreshold CPU 使用率阈值(%)
MonitorCPUThreshold int `json:"monitor_cpu_threshold"`
// MonitorMemoryThreshold 内存使用率阈值(%)
MonitorMemoryThreshold int `json:"monitor_memory_threshold"`
// MonitorDiskThreshold 磁盘使用率阈值(%)
MonitorDiskThreshold int `json:"monitor_disk_threshold"`
}

// 默认配置
Expand All @@ -23,6 +32,11 @@ var performanceSetting = PerformanceSetting{
DiskCacheThresholdMB: 10, // 超过 10MB 使用磁盘缓存
DiskCacheMaxSizeMB: 1024, // 最大 1GB 磁盘缓存
DiskCachePath: "", // 空表示使用系统临时目录

MonitorEnabled: true,
MonitorCPUThreshold: 90,
MonitorMemoryThreshold: 90,
MonitorDiskThreshold: 90,
}

func init() {
Expand All @@ -40,6 +54,13 @@ func syncToCommon() {
MaxSizeMB: performanceSetting.DiskCacheMaxSizeMB,
Path: performanceSetting.DiskCachePath,
})

common.SetPerformanceMonitorConfig(common.PerformanceMonitorConfig{
Enabled: performanceSetting.MonitorEnabled,
CPUThreshold: performanceSetting.MonitorCPUThreshold,
MemoryThreshold: performanceSetting.MonitorMemoryThreshold,
DiskThreshold: performanceSetting.MonitorDiskThreshold,
})
}

// GetPerformanceSetting 获取性能设置
Expand Down
Loading