Skip to content

Commit 63c4002

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 9fb09dd commit 63c4002

File tree

3 files changed

+46
-3
lines changed

3 files changed

+46
-3
lines changed

cmd/ipfs/daemon.go

Lines changed: 42 additions & 1 deletion
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"
@@ -33,13 +35,14 @@ import (
3335
p2pcrypto "github.com/libp2p/go-libp2p/core/crypto"
3436
pnet "github.com/libp2p/go-libp2p/core/pnet"
3537
sockets "github.com/libp2p/go-socket-activation"
36-
3738
options "github.com/ipfs/boxo/coreiface/options"
39+
"github.com/dustin/go-humanize"
3840
cmds "github.com/ipfs/go-ipfs-cmds"
3941
mprome "github.com/ipfs/go-metrics-prometheus"
4042
goprocess "github.com/jbenet/goprocess"
4143
ma "github.com/multiformats/go-multiaddr"
4244
manet "github.com/multiformats/go-multiaddr/net"
45+
"github.com/pbnjay/memory"
4346
prometheus "github.com/prometheus/client_golang/prometheus"
4447
promauto "github.com/prometheus/client_golang/prometheus/promauto"
4548
)
@@ -197,6 +200,42 @@ func defaultMux(path string) corehttp.ServeOption {
197200
}
198201
}
199202

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

261+
setMemoryLimit()
262+
222263
managefd, _ := req.Options[adjustFDLimitKwd].(bool)
223264
if managefd {
224265
if _, _, err := utilmain.ManageFdLimit(); err != nil {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,4 +225,4 @@ require (
225225
nhooyr.io/websocket v1.8.7 // indirect
226226
)
227227

228-
go 1.18
228+
go 1.19

test/sharness/t0060-daemon.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ test_expect_success "ipfs daemon output looks good" '
9393
echo "WebUI: http://'$API_ADDR'/webui" >>expected_daemon &&
9494
echo "Gateway server listening on '$GWAY_MADDR'" >>expected_daemon &&
9595
echo "Daemon is ready" >>expected_daemon &&
96-
test_cmp expected_daemon actual_daemon
96+
grep -q "^Set GOMEMLIMIT to" actual_daemon &&
97+
grep -v "^Set GOMEMLIMIT to" actual_daemon > actual_daemon_filtered &&
98+
test_cmp expected_daemon actual_daemon_filtered
9799
'
98100

99101
test_expect_success ".ipfs/ has been created" '

0 commit comments

Comments
 (0)