memlimit.go 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. package sys
  2. import (
  3. "os"
  4. "runtime/debug"
  5. "strconv"
  6. "strings"
  7. "github.com/shirou/gopsutil/v4/mem"
  8. )
  9. // memLimitHeadroomPercent is the share of detected memory used for the soft
  10. // limit, leaving room for non-heap (stacks, mmap, the xray child) before the OS
  11. // OOM-kills the process.
  12. const memLimitHeadroomPercent = 90
  13. // ApplyMemoryLimit sets a Go soft memory limit (the runtime's GOMEMLIMIT) when
  14. // one is not already configured, so a long-running panel in a memory-capped
  15. // container or VPS triggers GC as it approaches the cap instead of growing RSS
  16. // until the OS OOM-kills it. Precedence: an explicit GOMEMLIMIT env is left to
  17. // the runtime; otherwise XUI_MEMORY_LIMIT (in MiB) wins; otherwise the limit is
  18. // derived from the cgroup memory limit, falling back to total system RAM.
  19. // Returns the limit applied in bytes (0 when none) and a short source label.
  20. func ApplyMemoryLimit() (int64, string) {
  21. if strings.TrimSpace(os.Getenv("GOMEMLIMIT")) != "" {
  22. return 0, "GOMEMLIMIT env (handled by the Go runtime)"
  23. }
  24. if v := strings.TrimSpace(os.Getenv("XUI_MEMORY_LIMIT")); v != "" {
  25. if mb, err := strconv.ParseInt(v, 10, 64); err == nil && mb > 0 {
  26. limit := mb << 20
  27. debug.SetMemoryLimit(limit)
  28. return limit, "XUI_MEMORY_LIMIT=" + v + "MiB"
  29. }
  30. }
  31. total, source := detectAvailableMemory()
  32. if total <= 0 {
  33. return 0, "undetectable; left at Go default"
  34. }
  35. limit := total / 100 * memLimitHeadroomPercent
  36. debug.SetMemoryLimit(limit)
  37. return limit, source
  38. }
  39. func detectAvailableMemory() (int64, string) {
  40. if v, ok := cgroupMemoryLimit(); ok {
  41. return v, "cgroup limit"
  42. }
  43. if vm, err := mem.VirtualMemory(); err == nil && vm.Total > 0 {
  44. return int64(vm.Total), "system RAM"
  45. }
  46. return 0, ""
  47. }
  48. // cgroupMemoryLimit reads the container memory limit from cgroup v2 then v1.
  49. // A "max" value or the v1 unlimited sentinel (~8 EiB) means no limit at this
  50. // level, so it reports not-found and the caller falls back to system RAM. The
  51. // files are absent off Linux, which also yields not-found.
  52. func cgroupMemoryLimit() (int64, bool) {
  53. const unlimited = int64(1) << 62
  54. if b, err := os.ReadFile("/sys/fs/cgroup/memory.max"); err == nil {
  55. if s := strings.TrimSpace(string(b)); s != "" && s != "max" {
  56. if v, err := strconv.ParseInt(s, 10, 64); err == nil && v > 0 && v < unlimited {
  57. return v, true
  58. }
  59. }
  60. }
  61. if b, err := os.ReadFile("/sys/fs/cgroup/memory/memory.limit_in_bytes"); err == nil {
  62. if v, err := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64); err == nil && v > 0 && v < unlimited {
  63. return v, true
  64. }
  65. }
  66. return 0, false
  67. }