Repository navigation
Memory leak when calling fs.write() in long synchronous loop #11289
Description
Activity
I think this is probably expected. You're continuously queuing up write requests to the thread pool, which has a fixed number of threads available.
If you write synchronously (
fs.writeSync()) instead, or you limit the number of outstanding write requests (e.g. equal to the size of the thread pool -- which is 4 by default IIRC), you should not run into this problem.- addedfsIssues and PRs related to file-system APIs and the fs module.Issues and PRs related to file-system APIs and the fs module.questionIssues asking questions about Node.js.Issues asking questions about Node.js.
on Feb 10, 2017 @mscdex I understand that some structures may accumulate in memory as long as the buffer is not flushed but note that the memory leak is still present AFTER everything has been wrote to the file and the file was closed.
From:
Note that it is unsafe to use fs.write multiple times on the same file without waiting for the callback. For this scenario, fs.createWriteStream is strongly recommended.
What unsafe means here? Should the documentation be more specific?
Reacted by Marcelo RochaQuite possibly a variation on #8871 (comment) (summary: memory fragmentation, not memory leak) but could also be #11077.
Is this close-able at this point or should this remain open?
I don't think there is anything actionable. I'll close it out. Thanks for the bug report, @aalexgabi.
I think it's actionable if it's an issue with fragmentation.
@aalexgabi's gist should work with Node. Sure, it's pushing the edge with so many temporary buffers but it's a reasonable stress test. Node should be able to handle these kinds of edge cases without being fragile.
This could be fixed through a better allocator.
Reacted by Marcelo RochaThere are two things here:
- The process runs out of memory because it queues up 2 million fs.write() requests.
- RSS doesn't go down after the test completes.
Point 1 is not a bug but a peculiarity: since the test case doesn't pass a callback to
fs.write()(which is deprecated), node.js callsprocess.emitWarning()2 million times, exacerbated by 2 million calls toError.captureStackTrace()that cause running time and memory usage to explode. If you add a callback, the test completes in seconds flat.(Should
process.emitWarning()do rate limiting? Maybe, maybe not; that's a complexity/commonality trade-off. I don't expect you would normally hit such pathological behavior in a real-world program.)A non-deprecated calling pattern shows #8871 (comment), memory fragmentation. You say "could be fixed through a better allocator", I say that's not our bug to fix. Take it up with glibc or experiment with
MALLOC_MMAP_THRESHOLD_and friends.EDIT: To be clear, a custom allocator is not out of the question but for other reasons than as a workaround for a glibc deficiency.
- RSS doesn't go down after the test completes.
Yes, this is the thing.
Take it up with glibc or experiment with MALLOC_MMAP_THRESHOLD_ and friends.
Perhaps jemalloc would be another way forward.
EDIT: To be clear, a custom allocator is not out of the question but for other reasons than as a workaround for a glibc deficiency.
Antirez has some comments on moving to jemalloc from glibc a few years back: http://oldblog.antirez.com/post/everything-about-redis-24.html).
The jemalloc affair is one of our most fortunate use of external code ever. If you used to follow the Redis developments you know I'm not exactly the kind of guy excited to link some big project to Redis without some huge gain. We don't use libevent, our data structures are implemented in small .c files, and so forth. But an allocator is a serious thing. Since we introduced the specially encoded data types Redis started suffering from fragmentation. We tried different things to fix the problem, but basically the Linux default allocator in glibc sucks really, really hard. Including jemalloc inside of Redis (no need to have it installed in your computer, just download the Redis tarball as usually and type make) was a huge win. Every single case of fragmentation in real world systems was fixed by this change, and also the amount of memory used dropped a bit.Also, Jason Evans from 2011 (https://www.facebook.com/notes/facebook-engineering/scalable-memory-allocation-using-jemalloc/480222803919):
The relation between active memory and RAM usage must be consistent. In other words, practical bounds on allocator-induced fragmentation are critical. Consider that if fragmentation causes RAM usage to increase by 1 GiB per day, an application that is designed to leave only a little head room will fail within days.I'm not rabidly opposed to jemalloc (libc interop issues aside) but the "other reasons" I alluded to are exploiting known facts about allocations; e.g., that most memory is allocated and freed on the main thread (no synchronization needed), or that memory for a fs request should come from a different bin because it stays around longer than the temp storage used for flattening a string. That kind of thing.
@aalexgabi Yes, I mentioned that in #11289 (comment).
To be honest I'm disappointed with the closure of this issue.
The first imagined "real world" example that comes to mind it's a logger used by a service with a highly variable workload. This means that during intense workload if the cpu is faster than the disk for a given period of time, all the memory queued log write calls will generate leaks that will never be recovered in the lifetime of the service.
In a way this means that node applications are "required" to be restarted/killed from time to time if there is no way to implement a back-pressure mechanism for handling IO writes.

I think I found a memory leak when using fs.write() from a long synchronous loop. Here is the script that I used to reproduce:
With node v0.12.9:
After the process closed the file, there is 1.1 GB used by the process:

The file contains all the lines:
The file has 12 MB so I would eventually expect the node process to take around 70 MB of memory given that it takes 17 with an empty vm:
If I don't call the gc manually I get:

With node v7.5.0:
I have also noticed that v7.5.0 is several times slower when writing to file.