-
-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Measuring memory usage #5530
Comments
pprof shows only space used on the allocated heap. |
Thanks for the quick response. That makes sense. What's the best way to get a feel for what's actually using all the memory? I'm looking to run IPFS on systems that don't have 1GB available for IPFS, and I'm wondering if there's low hanging fruit for me to tackle. |
Some of it will be stacks (go routines). However, those should generally only use on the order of a few megabytes in total. I have a sneaking suspicion that the rest is memory fragmentation. Basically:
At this point, we're not using the entire page but go can't give it back to the OS. Go will use the rest of the space in these pages, but we may not need them (and/or the gaps in allocations are too small to be useful). This is exacerbated by:
TL;DR: reduce allocation frequency and use memory pools (see |
Thanks for the insight! Maybe that actually makes it easier to fix, because no core logic has to be changed, just the creation/deletion of objects. However, investigating this issue seems to be tough. Ignore the following. I think I confirmed your suspicion. Although the numbers dont quite reconcile.
|
Okay, I've been starting to make some progress on this. It turns out that MemStats from the runtime package might have the info we want, and that go-ipfs is kind enough to expose it (I'll submit a PR adding it to the documentation). However, I'm slightly concerned that the numbers don't reconcile to ps aux. For example, I'm surprised that MemStats is reporting less heap than the total RES size. Also, according to the MemStats documentation,
|
So, that's the upper bound on memory that can't be reused (I think?). However, there may be some that can be reused but isn't and also can't be returned (maybe?). Unfortunately, the documentation isn't very clear on this point. I really don't know what's going on with those numbers but I'm guessing it has to do with the fact that the OS is free to do whatever it wants and go can't accurately guess. |
Thanks for the response @Stebalien. I think there are currently a lot of unknowns. It's possible that OS ambiguity is a cause here, although I'm not 100% convinced yet, but even if that's the case it's not the only cause. pprof says the heap is using 80MB but MemStats reports that HeapInUse is 180MB. In general my desire is to understand why actual memory usage is so much higher than the pprof heap figure plus the MemStat's stack size (~100MB). Or, to narrow it a bit, why MemStat's HeapSys is so much bigger than the pprof heap figure. There is very little information about investigating what Go's memory allocator and garbage collector is actually doing. I am currently investigating using |
Is it okay if I use this issue for a log of my continuing investigation and experiments? I'm also happy to take any thoughts/suggestions that people have. I hacked SummaryGolang's
Note that Go has told the OS that 82MB of this 332MB the memory isn't strictly being used, which is probably why Conclusion and Next StepsWhile fragmentation is certainly an issue, it seems that high allocation/deallocation velocity might be a bigger issue. The Go runtime is hoarding a lot of memory, and I would suggest that There are two things I'd like to look into next:
Incidentally, I was hoping that Detailed ExplanationIn summary I think that the heap usage is built up in the following way:
However,
However, there's the memory fragmentation issue that was raised in this thread, and also Go's memory allocator is based on fixed size class chunks, which area always slightly bigger than the underlying data. In this case there is 80.2MB of wasted space.
But Go keeps hold of some other memory to reduce how often it needs to go to the OS to ask for more. In theory this is fine, but in this example there's really no need for it to keep hold of 151MB when the underlying memory requirement of
HOWEVER, Go tries to be nice about the memory its holding on to, and tries to tell the OS about memory its holding on to but isn't actually being used, so that the OS can release the physical RAM supporting it. In this case 82MB. There are two issues with relying on this:
|
So I've been playing with prometheus, and grafana, because I think that this graph confirms the 2nd item that I wanted to confirm. I.e. Go runtime's hoarding is the biggest contributor of heap waste. I think that @Stebalien might've been right on the money in his first reply.
|
Progress:
Also, I just noticed #3318 about reducing [edit]: Ignore the below. This doc gives more info on Pacer and it seems it's irrelevant. I think Pacer is only concerned with giving sufficient resources/time to the GC to prevent GOGC being breached.
|
With respect to libp2p/go-libp2p-peerstore#15, I've just reverted that in libp2p/go-libp2p-peerstore#39 and introduced naive garbage collection of stale peer addresses. I'm hoping that'll help significantly (and that the GC will offset the additional memory usage) but we'll see. I've also been fixing some issues with our multiaddrs:
It also looks like bitswap is causing quite a bit of allocation. We really need to get sessions #3786 working, that'll reduce the number of bitswap "wantlists" each peer gets. |
Also, this is truly amazing progress! |
We need to be slightly careful when looking at the above numbers. I had a suspicion, which I think I've confirmed, that Prometheus itself also has a high allocation velocity (when hitting it once a second). In particular, its generating a lot of garbage in its calls to flate.NewWriter and MetricFamilyToText. There are a few different ways of solving this. But my gut is telling me the simplest is to run Prometheus in a seperate process. The alternative is to run our own seperate pprof heap scraper, but then we lose:
|
That's... annoying. I wonder if we could optimize Prometheus. We could consider adding a metrics plugin interface for running stuff like this in a separate process but that's a non-trivial undertaking. |
I'm hesitant to start optimising Promotheus lest we end up saying "To optimise ipfs let's optimise Prometheus. To optimise Promotheus lets optimise [a dependancy]. Etc. Etc" There might be a sneaky way to run Promotheus in a seperate tightly bound process, e.g. fork ipfs with an argument that kicks off a seperate codepath, but I don't have capacity to investigate this right now. |
@rob-deutsch - Apologizes for the off-topic question, but I'm doing some digging myself and am curious how you're visualizing / grokking the output of |
@sanderpick - If I recall correctly, I wrote a small Go program to parse the heap dump according to this doc. I did not end up using this in my analysis, because it just didn't have the depth of information I wanted/needed. My analysis here was done using runtime.MemStats, pprof, and Prometheus. |
Okay cool, thanks for the info 👍 |
I have a question about measuring and understanding go-ipfs' memory usage - because I'm getting conflicting signs.
I'm doing this because go-ipfs is using a lot of memory on my docker instance (~1GB) and I want to understand why.
I've been following the instructions in debug-guide.md but I'm getting different results to what my OS is telling me.
My OS is telling me that I'm using around 256MB:
But pprof tells me that it's only using 76.38MB:
What am I doing wrong?
The text was updated successfully, but these errors were encountered: