Closed roeschter closed 1 month ago
Will experiment with export GOGC=nnn
For controlling the memory and preventing such OOMs with high load on insufficient resources, you should use GOMEMLIMIT
. Keep in mind that if the limit will be a lot below the needs, it will throttle the throughput a lot, while putting more load on CPU (as the GC will need to work way harder).
GOMEMLIMIT solves the immediate problem (I tried from 256MiB to 1024Mib), but the memory usage reported by the OS still vastly exceeds the limit I set.
Also: I would expect the nats-server to self-limit to a reasonable memory footprint. "Reasonable" would be 50% of system memory when JS is enabled (because we need space with buffers and caches) and 70% for pure Nats Core.
My test system had 2.5GB of 4GB available for NATS when the tests starts
E.g. In this case I set
export GOMEMLIMIT=256MiB
export GOGC=20
Resident memory still exceeds 2GB (consistent with overall system memory report) Performance is not much impacted by the limits above.
Note that this only happens when reading from the stream, footprint is small when writing.
I now created a state of the stream, where nats-server exceeds all limits during startup, recovering the stream and gets killed. export GOMEMLIMIT=256MiB export GOGC=10 Stream size on disk ~3.5GB Resident memory growing to 2.4GB very fast - then OOM killed
Can you get a memory profile when the memory usage is inflated, either through nats server request profile allocs
or https://docs.nats.io/running-a-nats-service/nats_admin/profiling?
Also: I would expect the nats-server to self-limit to a reasonable memory footprint. "Reasonable" would be 50% of system memory when JS is enabled (because we need space with buffers and caches) and 70% for pure Nats Core.
Short of using GOMEMLIMIT
, which is actually a runtime hint to the GC and not an actual hard limit, this is simply not practical to do. Runtime stats can't tell us about the actual RSS size, nor can it tell us about buffers/caches from the kernel, and we also don't have custom allocators in Go to actually enforce those limits. Even if we did, failing to allocate structures would just leave the system in an undefined state, which is worse than crashing.
We would need to figure out from profiles what is using the memory and whether that can be optimised for this case.
Here are the memory profiles: 03_allocs_after_stream_delete_from_client.zip
Observations:
OOI how did you capture these profiles? They're in text rather than binary format which means the pprof visualiser can't parse them. Please make sure to use --level=0
if using the CLI or ?debug=0
if using the pprof HTTP endpoint.
Human readable output via:
Testing under Windows now, as the memory growth happens here as well. (just no OOM kill)
Binary Allocs: allocs.zip
The more I look into the behavior the less deterministic it is. Under Windows is respected: Server shrinks back to the set size. BUT under Linux it didnt. In any case, memory temporarily growth to about the amount consumed during 10seconds by the consumers.
Seems the buffer management relies on: https://pkg.go.dev/sync#Pool Seems this only cleans up after multiple GC cycles. But also, why is the memory not effectively being re-used? Who is not returning it fast enough?
The sync.Pool
s are indeed only purged during a GC run, that is by design. Otherwise they largely rely on the number of gets to roughly match the number of puts in order to stay balanced in their resource consumption.
In this case, looking at the profile, it appears as though lots of blocks are being loaded in from the filestore and held in the block cache. They should be held there for 10 seconds from the last access, after which the buffer will be returned to the pool. That would roughly explain why you see the correlation between the data accessed and the memory growth in that window.
Perhaps exposing the cache expiry as a configurable, i.e. being able to set it to less than 10 seconds, would help reduce memory usage at the expense of potentially more I/O operations. As an experiment, you could potentially test with a custom build that changes defaultCacheBufferExpiration
in filestore.go
to a lower value like time.Second * 2
.
Correct! If the behavior of the block cache would be documented (I can guess it exists, but I incorrectly assumed it to be of fixed size per connection) and its size or timeout configurable I would not have been surprised.
Last post on observed behavior:
It does not happen:
In summary:
I will test with : DefaultCacheBufferExpiration in filestore.go to a lower value like time.Second * 2.
Reducing the buffer timeout to 1s limits the memory consumption to around 600MB in my tests. At teh cost of reduced throughput, which is fine (still about 50MB/s from a workqueue)
Tested with DefaultCacheBufferExpiration = time.Second * 1 Windows and Linux based on 2.11 preview rc3 In combination with GOMEMELIMIT=512MiB
Closing the investigation, but I would like the CacheBuffer configurable as a small enhancement request. Should I try to patch this myself and make it a pull request?
If you could file an issue that would be great.
What is the stream maxBytes that you were using? @roeschter
I was not using maxbytes (unlimited). The effect is purely about the speed of consuming. As as was testing on a 4GB device with 2.5 GB free a 2GB effective stream size was enough to trigger the OOM.
Closing this in favor of a new CR - https://github.com/nats-io/nats-server/issues/5740
Observed behavior
Consuming from a workqueue with multiple consumers (subject segmented) at high speed (150MB/s) result in rapid memory growth and eventually exhaustion. Server will die with a range of symptoms, mostly silently OOM killed, sometimes throwing readloop processing time warning first.
Testing on a small system with 4Gb and 2 CPUs - This is deliberate to simulate an edge node.
This does not seem to be a memory leak per se. Repeated tests with a below threshold message volume do not trigger the effect.
Expected behavior
Self limits memory growth or throughput.
Server and client version
jnats-2.19.1.jar _ Java17 2.10.17 and 2.11-preview-rc3 (self compiled) memory growth is not platform specific. Seen on Windows as well, but crashes observed only on smaller systems with <8GB.
Host environment
Linux - Old Kernel Linux lvps5-35-240-105.dedicated.hosteurope.de 3.10.0-1160.80.1.vz7.191.4 #1 SMP Thu Dec 15 20:31:06 MSK 2022 x86_64 x86_64 x86_64 GNU/Linux
Steps to reproduce
playground.jar.zip nats00_hosteurope.config.txt source.zip
Full eclipse project available in private repo: https://github.com/roeschter/natsPlayground
Run on a system with 4GB (or less) - CPU count >=2 Java 17
/var/lib/java17/bin/java -cp playground.jar:jnats-2.19.1.jar com.synadia.test.config03_performance.C03JetstreamWorQueueSlowDeletes --user user --password dja673b48fh3kqp9 --messages 200000 --messageSize 10000 --fetchsize 10 --subjects 10 --consumemode fetch
--messages 200000 Should be sufficient on a 4GB system. Increase to 1000000 if not crashing. --messageSize 10000 Increasing message size my speed the crash up. Not observable with small messages (throughput too low I presume)
Memory should increase rapidly when the consumers start. Rate should be 10000 msg/s or more