Skip to content

Commit 6b3e242

Browse files
committed
feat: daemon: automatically set GOMEMLIMIT if it is unset
I have a rather big collection of profiles where someone claims that Kubo is ooming on XGiB. Then you open the profile and it is using half of that, this is due to the default GOGC=200%. That means, go will only run the GC once it's twice as being as the previous alive set. This situation happen more than it should / almost always because many parts of Kubo are memory garbage factories. Adding a GOMEMLIMIT helps by trading off more and more CPU running GC more often when memory is about to run out, it's not healthy to run at the edge of the limit because the GC will continously run killing performance. So this doesn't double the effective memory usable by Kubo, but we should expect to be able to use ~1.5x~1.75x before performance drastically falling off. Closes: #8798
1 parent 0976595 commit 6b3e242

File tree

3 files changed

+46
-2
lines changed

3 files changed

+46
-2
lines changed

cmd/ipfs/daemon.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import (
44
"errors"
55
_ "expvar"
66
"fmt"
7+
"math"
78
"net"
89
"net/http"
910
_ "net/http/pprof"
1011
"os"
1112
"runtime"
13+
"runtime/debug"
1214
"sort"
1315
"sync"
1416
"time"
@@ -32,12 +34,14 @@ import (
3234
"github.com/ipfs/kubo/repo/fsrepo/migrations/ipfsfetcher"
3335
sockets "github.com/libp2p/go-socket-activation"
3436

37+
"github.com/dustin/go-humanize"
3538
cmds "github.com/ipfs/go-ipfs-cmds"
3639
mprome "github.com/ipfs/go-metrics-prometheus"
3740
options "github.com/ipfs/interface-go-ipfs-core/options"
3841
goprocess "github.com/jbenet/goprocess"
3942
ma "github.com/multiformats/go-multiaddr"
4043
manet "github.com/multiformats/go-multiaddr/net"
44+
"github.com/pbnjay/memory"
4145
prometheus "github.com/prometheus/client_golang/prometheus"
4246
promauto "github.com/prometheus/client_golang/prometheus/promauto"
4347
)
@@ -205,6 +209,42 @@ func defaultMux(path string) corehttp.ServeOption {
205209
}
206210
}
207211

212+
// setMemoryLimit a soft memory limit to enforce running the GC more often when
213+
// we are about to run out.
214+
// This allows to recoop memory when it's about to run out and cancel the
215+
// doubled memory footprint most go programs experience, at the cost of more CPU
216+
// usage in memory tight conditions. This does not increase CPU usage when memory
217+
// is plenty available, it will use more CPU and continue to run in cases where Kubo
218+
// would OOM.
219+
func setMemoryLimit() {
220+
// From the STD documentation:
221+
// A negative input does not adjust the limit, and allows for retrieval of the currently set memory limit.
222+
if currentMemoryLimit := debug.SetMemoryLimit(-1); currentMemoryLimit != math.MaxInt64 {
223+
fmt.Printf("GOMEMLIMIT already set to %s, leaving as-is.\n", humanize.IBytes(uint64(currentMemoryLimit)))
224+
// only update the memory limit if it wasn't set with GOMEMLIMIT already
225+
return
226+
}
227+
228+
// this is a proportional negative-rate increase curve fitted to thoses points:
229+
// 0GiB -> 0GiB
230+
// 4GiB -> 0.5GiB
231+
// 6GiB -> 0.75GiB
232+
// 12GiB -> 1GiB
233+
// 256GiB -> 2GiB
234+
totalMemory := memory.TotalMemory()
235+
memoryMargin := int64(213865e4 - 209281e4*math.Pow(math.E, -588918e-16*float64(totalMemory)))
236+
// if memory is extremely small this approximation / is useless
237+
if memoryMargin <= 0 {
238+
// then don't bother setting a limit and rely on GOGC
239+
fmt.Println("TotalMemory is too tight, continuing without GOMEMLIMIT.")
240+
return
241+
}
242+
243+
remainingMemory := totalMemory - uint64(memoryMargin)
244+
debug.SetMemoryLimit(int64(remainingMemory))
245+
fmt.Printf("Set GOMEMLIMIT to %s.\n", humanize.IBytes(remainingMemory))
246+
}
247+
208248
func daemonFunc(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) (_err error) {
209249
// Inject metrics before we do anything
210250
err := mprome.Inject()
@@ -227,6 +267,8 @@ func daemonFunc(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment
227267
// print the ipfs version
228268
printVersion()
229269

270+
setMemoryLimit()
271+
230272
managefd, _ := req.Options[adjustFDLimitKwd].(bool)
231273
if managefd {
232274
if _, _, err := utilmain.ManageFdLimit(); err != nil {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,4 @@ require (
255255
lukechampine.com/blake3 v1.1.7 // indirect
256256
)
257257

258-
go 1.18
258+
go 1.19

test/sharness/t0060-daemon.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ test_expect_success "ipfs daemon output looks good" '
8282
echo "WebUI: http://'$API_ADDR'/webui" >>expected_daemon &&
8383
echo "Gateway (readonly) server listening on '$GWAY_MADDR'" >>expected_daemon &&
8484
echo "Daemon is ready" >>expected_daemon &&
85-
test_cmp expected_daemon actual_daemon
85+
grep -q "^Set GOMEMLIMIT to" actual_daemon &&
86+
grep -v "^Set GOMEMLIMIT to" actual_daemon > actual_daemon_filtered &&
87+
test_cmp expected_daemon actual_daemon_filtered
8688
'
8789

8890
test_expect_success ".ipfs/ has been created" '

0 commit comments

Comments
 (0)