diff --git a/cmd/ipfs/daemon.go b/cmd/ipfs/daemon.go index 1bfb9d6f1137..0c30113dc88b 100644 --- a/cmd/ipfs/daemon.go +++ b/cmd/ipfs/daemon.go @@ -4,11 +4,13 @@ import ( "errors" _ "expvar" "fmt" + "math" "net" "net/http" _ "net/http/pprof" "os" "runtime" + "runtime/debug" "sort" "sync" "time" @@ -32,12 +34,14 @@ import ( "github.com/ipfs/kubo/repo/fsrepo/migrations/ipfsfetcher" sockets "github.com/libp2p/go-socket-activation" + "github.com/dustin/go-humanize" cmds "github.com/ipfs/go-ipfs-cmds" mprome "github.com/ipfs/go-metrics-prometheus" options "github.com/ipfs/interface-go-ipfs-core/options" goprocess "github.com/jbenet/goprocess" ma "github.com/multiformats/go-multiaddr" manet "github.com/multiformats/go-multiaddr/net" + "github.com/pbnjay/memory" prometheus "github.com/prometheus/client_golang/prometheus" promauto "github.com/prometheus/client_golang/prometheus/promauto" ) @@ -205,6 +209,42 @@ func defaultMux(path string) corehttp.ServeOption { } } +// setMemoryLimit a soft memory limit to enforce running the GC more often when +// we are about to run out. +// This allows to recoop memory when it's about to run out and cancel the +// doubled memory footprint most go programs experience, at the cost of more CPU +// usage in memory tight conditions. This does not increase CPU usage when memory +// is plenty available, it will use more CPU and continue to run in cases where Kubo +// would OOM. +func setMemoryLimit() { + // From the STD documentation: + // A negative input does not adjust the limit, and allows for retrieval of the currently set memory limit. + if currentMemoryLimit := debug.SetMemoryLimit(-1); currentMemoryLimit != math.MaxInt64 { + fmt.Printf("GOMEMLIMIT already set to %s, leaving as-is.\n", humanize.IBytes(uint64(currentMemoryLimit))) + // only update the memory limit if it wasn't set with GOMEMLIMIT already + return + } + + // this is a proportional negative-rate increase curve fitted equation to thoses points: + // 0GiB -> 0GiB + // 4GiB -> 0.5GiB + // 6GiB -> 0.75GiB + // 12GiB -> 1GiB + // 256GiB -> 2GiB + totalMemory := memory.TotalMemory() + memoryMargin := int64(213865e4 - 209281e4*math.Pow(math.E, (-588918e-16*float64(totalMemory)))) + // if memory is extremely small this approximation / is useless + if memoryMargin <= 0 { + // then don't bother setting a limit and rely on GOGC + fmt.Println("TotalMemory is too tight, continuing without GOMEMLIMIT.") + return + } + + remainingMemory := totalMemory - uint64(memoryMargin) + debug.SetMemoryLimit(int64(remainingMemory)) + fmt.Printf("Set GOMEMLIMIT to %s.\n", humanize.IBytes(remainingMemory)) +} + func daemonFunc(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) (_err error) { // Inject metrics before we do anything err := mprome.Inject() @@ -227,6 +267,8 @@ func daemonFunc(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment // print the ipfs version printVersion() + setMemoryLimit() + managefd, _ := req.Options[adjustFDLimitKwd].(bool) if managefd { if _, _, err := utilmain.ManageFdLimit(); err != nil { diff --git a/go.mod b/go.mod index fdb2efa083d8..4f272ae85d10 100644 --- a/go.mod +++ b/go.mod @@ -255,4 +255,4 @@ require ( lukechampine.com/blake3 v1.1.7 // indirect ) -go 1.18 +go 1.19 diff --git a/test/sharness/t0060-daemon.sh b/test/sharness/t0060-daemon.sh index d448e035b466..08736787cb23 100755 --- a/test/sharness/t0060-daemon.sh +++ b/test/sharness/t0060-daemon.sh @@ -82,7 +82,9 @@ test_expect_success "ipfs daemon output looks good" ' echo "WebUI: http://'$API_ADDR'/webui" >>expected_daemon && echo "Gateway (readonly) server listening on '$GWAY_MADDR'" >>expected_daemon && echo "Daemon is ready" >>expected_daemon && - test_cmp expected_daemon actual_daemon + grep -q "^Set GOMEMLIMIT to" actual_daemon && + grep -v "^Set GOMEMLIMIT to" actual_daemon > actual_daemon_filtered && + test_cmp expected_daemon actual_daemon_filtered ' test_expect_success ".ipfs/ has been created" '