Use dynamic GOMEMLIMIT: 25% of system RAM, clamped to [2GB, 8GB]

Replace static 2GB default with platform-aware auto-detection:
- Linux: syscall.Sysinfo
- macOS: sysctl hw.memsize
- Windows: GlobalMemoryStatusEx

Falls back to 4GB if detection fails. User config still overrides.
Prevents both OOM kills (upper clamp) and excessive GC pressure (lower clamp).
This commit is contained in:
Martin Vogel committed 2026-03-12 15:29:22 +01:00
1 parent fac0c57359
commit ba626aac50
5 files changed
+103 -7

No files matched your search

+10 -7
View File
@@ -67,14 +67,17 @@ func main() {
log.Fatalf("config err=%v", err)
}
// Apply GOMEMLIMIT from config (e.g. "2G", "512M"), default 2GB.
// Prevents OOM kills on systems without user-configured limits.
memLimitStr := cfg.Get(store.ConfigMemLimit, "2G")
if limit, parseErr := parseByteSize(memLimitStr); parseErr != nil {
log.Printf("config: invalid mem_limit %q: %v", memLimitStr, parseErr)
debug.SetMemoryLimit(2 << 30) // fallback: 2GB
// Apply GOMEMLIMIT from config (e.g. "4G", "512M").
// If not configured, auto-detect: 25% of system memory, clamped to [2GB, 8GB].
// Prevents OOM kills while avoiding unnecessary GC pressure.
if memLimitStr := cfg.Get(store.ConfigMemLimit, ""); memLimitStr != "" {
if limit, parseErr := parseByteSize(memLimitStr); parseErr != nil {
log.Printf("config: invalid mem_limit %q: %v", memLimitStr, parseErr)
} else {
debug.SetMemoryLimit(limit)
}
} else {
debug.SetMemoryLimit(limit)
debug.SetMemoryLimit(autoMemLimit())
}
srv := tools.NewServer(router, tools.WithConfig(cfg))
+30
View File
@@ -0,0 +1,30 @@
package main
import "log"
const (
minMemLimit int64 = 2 << 30 // 2 GB
maxMemLimit int64 = 8 << 30 // 8 GB
memFraction int64 = 4 // use 1/4 of system memory
)
// autoMemLimit returns a GOMEMLIMIT value based on available system memory.
// Uses 25% of total physical RAM, clamped to [2GB, 8GB].
// Falls back to 4GB if detection fails.
func autoMemLimit() int64 {
total := totalSystemMemory()
if total <= 0 {
log.Printf("mem_limit: auto-detect unavailable, default=4GB")
return 4 << 30
}
limit := total / memFraction
if limit < minMemLimit {
limit = minMemLimit
}
if limit > maxMemLimit {
limit = maxMemLimit
}
log.Printf("mem_limit: system=%dGB limit=%dGB", total>>30, limit>>30)
return limit
}
@@ -0,0 +1,14 @@
//go:build darwin
package main
import "golang.org/x/sys/unix"
// totalSystemMemory returns total physical memory in bytes via sysctl hw.memsize.
func totalSystemMemory() int64 {
mem, err := unix.SysctlUint64("hw.memsize")
if err != nil {
return 0
}
return int64(mem)
}
+14
View File
@@ -0,0 +1,14 @@
//go:build linux
package main
import "syscall"
// totalSystemMemory returns total physical memory in bytes via Sysinfo.
func totalSystemMemory() int64 {
var si syscall.Sysinfo_t
if err := syscall.Sysinfo(&si); err != nil {
return 0
}
return int64(si.Totalram) * int64(si.Unit)
}
@@ -0,0 +1,35 @@
//go:build windows
package main
import (
"syscall"
"unsafe"
)
// memoryStatusEx mirrors the Windows MEMORYSTATUSEX struct.
type memoryStatusEx struct {
length uint32
memoryLoad uint32
totalPhys uint64
availPhys uint64
totalPageFile uint64
availPageFile uint64
totalVirtual uint64
availVirtual uint64
availExtendedVirtual uint64
}
// totalSystemMemory returns total physical memory via GlobalMemoryStatusEx.
func totalSystemMemory() int64 {
kernel32 := syscall.NewLazyDLL("kernel32.dll")
proc := kernel32.NewProc("GlobalMemoryStatusEx")
var ms memoryStatusEx
ms.length = uint32(unsafe.Sizeof(ms)) //nolint:gosec // standard Windows API pattern
r, _, _ := proc.Call(uintptr(unsafe.Pointer(&ms)))
if r == 0 {
return 0
}
return int64(ms.totalPhys)
}