1
0

memlimit.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. package sys
  2. import (
  3. "fmt"
  4. "os"
  5. "runtime/debug"
  6. "strconv"
  7. "strings"
  8. )
  9. const (
  10. memLimitHeadroomPercent = 90
  11. defaultGCPercent = 75
  12. defaultReleaseMinutes = 10
  13. )
  14. // ApplyMemoryTuning configures the Go runtime for a lower, steadier footprint and
  15. // returns one log line per decision. It does NOT derive a soft limit from total
  16. // system RAM: on a shared or uncontrolled host that gives no benefit (GOGC, not
  17. // the limit, paces GC while the heap is far below it) and risks GC thrashing, so
  18. // memory is kept low via GOGC plus the periodic release job instead.
  19. func ApplyMemoryTuning() []string {
  20. lines := []string{applyGCPercent()}
  21. if limit, source := applyMemoryLimit(); limit > 0 {
  22. lines = append(lines, fmt.Sprintf("Go memory soft limit set to %d MiB (%s)", limit>>20, source))
  23. } else {
  24. lines = append(lines, "Go memory soft limit not enforced: "+source)
  25. }
  26. return lines
  27. }
  28. // applyGCPercent lowers GOGC so the heap high-water mark, and thus RSS, stays
  29. // smaller. An explicit GOGC env (including GOGC=off) is left to the runtime.
  30. func applyGCPercent() string {
  31. if _, ok := os.LookupEnv("GOGC"); ok {
  32. return "GC percent: GOGC env (handled by the Go runtime)"
  33. }
  34. pct := defaultGCPercent
  35. if v := strings.TrimSpace(os.Getenv("XUI_GOGC")); v != "" {
  36. if n, err := strconv.Atoi(v); err == nil {
  37. pct = n
  38. }
  39. }
  40. if pct <= 0 {
  41. return "GC percent left at Go default"
  42. }
  43. debug.SetGCPercent(pct)
  44. return fmt.Sprintf("GC percent set to %d", pct)
  45. }
  46. // applyMemoryLimit sets the soft limit only from an explicit budget: GOMEMLIMIT
  47. // env (left to the runtime), XUI_MEMORY_LIMIT in MiB, or a real cgroup limit at
  48. // 90% to leave headroom for non-heap and the xray child. No budget -> Go default.
  49. func applyMemoryLimit() (int64, string) {
  50. if strings.TrimSpace(os.Getenv("GOMEMLIMIT")) != "" {
  51. return 0, "GOMEMLIMIT env (handled by the Go runtime)"
  52. }
  53. if v := strings.TrimSpace(os.Getenv("XUI_MEMORY_LIMIT")); v != "" {
  54. if mb, err := strconv.ParseInt(v, 10, 64); err == nil && mb > 0 {
  55. limit := mb << 20
  56. debug.SetMemoryLimit(limit)
  57. return limit, "XUI_MEMORY_LIMIT=" + v + "MiB"
  58. }
  59. }
  60. if v, ok := cgroupMemoryLimit(); ok {
  61. limit := v / 100 * memLimitHeadroomPercent
  62. debug.SetMemoryLimit(limit)
  63. return limit, "cgroup limit"
  64. }
  65. return 0, "no explicit budget; soft limit left at Go default"
  66. }
  67. // MemoryReleaseIntervalMinutes reports how often freed heap memory is returned to
  68. // the OS via debug.FreeOSMemory. XUI_MEMORY_RELEASE_INTERVAL overrides the
  69. // default; an explicit 0 disables the periodic release.
  70. func MemoryReleaseIntervalMinutes() int {
  71. v := strings.TrimSpace(os.Getenv("XUI_MEMORY_RELEASE_INTERVAL"))
  72. if v == "" {
  73. return defaultReleaseMinutes
  74. }
  75. if n, err := strconv.Atoi(v); err == nil && n >= 0 {
  76. return n
  77. }
  78. return defaultReleaseMinutes
  79. }
  80. // cgroupMemoryLimit reads the container memory limit from cgroup v2 then v1.
  81. // A "max" value or the v1 unlimited sentinel (~8 EiB) means no limit at this
  82. // level, so it reports not-found and the caller falls back to the Go default. The
  83. // files are absent off Linux, which also yields not-found.
  84. func cgroupMemoryLimit() (int64, bool) {
  85. const unlimited = int64(1) << 62
  86. if b, err := os.ReadFile("/sys/fs/cgroup/memory.max"); err == nil {
  87. if s := strings.TrimSpace(string(b)); s != "" && s != "max" {
  88. if v, err := strconv.ParseInt(s, 10, 64); err == nil && v > 0 && v < unlimited {
  89. return v, true
  90. }
  91. }
  92. }
  93. if b, err := os.ReadFile("/sys/fs/cgroup/memory/memory.limit_in_bytes"); err == nil {
  94. if v, err := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64); err == nil && v > 0 && v < unlimited {
  95. return v, true
  96. }
  97. }
  98. return 0, false
  99. }