mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-11 22:00:59 +00:00
perf: prevent cron job overlap, auto-set GOMEMLIMIT, fix tgbot userStates race
cron: SkipIfStillRunning stops a slow 5s/10s job from overlapping itself and racing the shared xrayAPI (grpc conn leak) and the StatsLastValues map (fatal concurrent map write). memlimit: auto-detect a Go soft memory limit from XUI_MEMORY_LIMIT, the cgroup limit, or system RAM (about 90 percent); opt-in pprof via XUI_PPROF. tgbot: userStates now goes through a mutex-guarded store with TTL pruning (was raced by worker-pool and delayed-delete goroutines). check_client_ip: prefilter inbounds by settings LIKE limitIp instead of loading and JSON-parsing all of them every scan. minor: prune StatsLastValues, RateLimiter.lastSent, reportedRemoteTagConflict. docker-compose: document the memory knobs.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
)
|
||||
|
||||
// memLimitHeadroomPercent is the share of detected memory used for the soft
|
||||
// limit, leaving room for non-heap (stacks, mmap, the xray child) before the OS
|
||||
// OOM-kills the process.
|
||||
const memLimitHeadroomPercent = 90
|
||||
|
||||
// ApplyMemoryLimit sets a Go soft memory limit (the runtime's GOMEMLIMIT) when
|
||||
// one is not already configured, so a long-running panel in a memory-capped
|
||||
// container or VPS triggers GC as it approaches the cap instead of growing RSS
|
||||
// until the OS OOM-kills it. Precedence: an explicit GOMEMLIMIT env is left to
|
||||
// the runtime; otherwise XUI_MEMORY_LIMIT (in MiB) wins; otherwise the limit is
|
||||
// derived from the cgroup memory limit, falling back to total system RAM.
|
||||
// Returns the limit applied in bytes (0 when none) and a short source label.
|
||||
func ApplyMemoryLimit() (int64, string) {
|
||||
if strings.TrimSpace(os.Getenv("GOMEMLIMIT")) != "" {
|
||||
return 0, "GOMEMLIMIT env (handled by the Go runtime)"
|
||||
}
|
||||
|
||||
if v := strings.TrimSpace(os.Getenv("XUI_MEMORY_LIMIT")); v != "" {
|
||||
if mb, err := strconv.ParseInt(v, 10, 64); err == nil && mb > 0 {
|
||||
limit := mb << 20
|
||||
debug.SetMemoryLimit(limit)
|
||||
return limit, "XUI_MEMORY_LIMIT=" + v + "MiB"
|
||||
}
|
||||
}
|
||||
|
||||
total, source := detectAvailableMemory()
|
||||
if total <= 0 {
|
||||
return 0, "undetectable; left at Go default"
|
||||
}
|
||||
limit := total / 100 * memLimitHeadroomPercent
|
||||
debug.SetMemoryLimit(limit)
|
||||
return limit, source
|
||||
}
|
||||
|
||||
func detectAvailableMemory() (int64, string) {
|
||||
if v, ok := cgroupMemoryLimit(); ok {
|
||||
return v, "cgroup limit"
|
||||
}
|
||||
if vm, err := mem.VirtualMemory(); err == nil && vm.Total > 0 {
|
||||
return int64(vm.Total), "system RAM"
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
// cgroupMemoryLimit reads the container memory limit from cgroup v2 then v1.
|
||||
// A "max" value or the v1 unlimited sentinel (~8 EiB) means no limit at this
|
||||
// level, so it reports not-found and the caller falls back to system RAM. The
|
||||
// files are absent off Linux, which also yields not-found.
|
||||
func cgroupMemoryLimit() (int64, bool) {
|
||||
const unlimited = int64(1) << 62
|
||||
|
||||
if b, err := os.ReadFile("/sys/fs/cgroup/memory.max"); err == nil {
|
||||
if s := strings.TrimSpace(string(b)); s != "" && s != "max" {
|
||||
if v, err := strconv.ParseInt(s, 10, 64); err == nil && v > 0 && v < unlimited {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if b, err := os.ReadFile("/sys/fs/cgroup/memory/memory.limit_in_bytes"); err == nil {
|
||||
if v, err := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64); err == nil && v > 0 && v < unlimited {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
Reference in New Issue
Block a user