You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Platform: Linux (Ubuntu Server 8 - Jessie), kernel 3.16
Subsystem: dgram.js
UDP sockets seem to have a memory leak. Seems that it's not the first time it had that, so it feels to me that it might be a regression. The following test code reproduces it:
Memory grows approximately at a rate of 1 MB/s in my machine. I assume that a UDP socket should just send the data (no matter if no one is listening on the other side), so there should not be this huge memory consumption.
I believe it's the large number of DNS requests that are happening. Every time you call socket.send() it has to resolve example.org to an IP address. Try this and you should see more stable memory usage:
Very true. I've tested the code and the memory is stable. So it seems the memory leak might be in the DNS resolution? I've checked the dns module and everything seems to be right to me. Could it be on the native side?
The underlying issue here is that dns requests are currently made in the thread pool which has a default size of 4. All requests to use the thread pool are queued when no threads are available. Since a dns request is being made here every ~10ms, these requests start stacking up very quickly because the thread pool can't keep up (and dns requests can take some time to complete -- especially if the nameserver is responding slowly).
What about a host/IP LRU cache? I think it would mitigate the problem. I assume as well that most of the people that is using UDP sockets is not realizing that, if they pass a host to sendTo, a DNS request is going to be made every time.
If a request is being made every 10 ms, a cache of 5-10 seconds should probably kill the problem.
While that may seem like a sensible solution, I suspect that in-memory caching may catch people off guard especially if the results in the cache don't automatically expire. At that point you're having to implement ttl and having to decide whether a custom ttl is used or you use the ttl returned by the nameserver (which IIRC we aren't retrieving from c-ares for one reason or another) and other things.
So IMHO adding transparent caching support can be tricky and is liable to cause problems for other use cases. FWIW there have been discussions in the past about returning ttl and other information for use in userland modules, but that would only help you write a more efficient/"correct" cache in userland, it wouldn't help with this particular issue.
I see the problem with the cache; so probably what looks the most reasonable is to use the server's returned TTL, which is unfortunate we can't get. As I said before, I think most of the people using UDP sockets will fall into the same issue (and, in general, not expecting gethostbyname to queue); so I think it's worth taking a look to that. However, if you want to close this ticket, feel free to do that.
At the very least it may be worth adding some notes to the documentation in the appropriate places to make this more evident. Perhaps either a separate section in the dns docs or modifying existing wording and then linking to that from various places in other modules' documentation that accept hostnames. If you or anyone else wants to submit a PR for something like that, that would be a good start.
I should also point out that net.Socket() already has support for passing in a custom lookup() function that defaults to dns.lookup(). So I suppose something similar could be added for dgram. This would allow you to implement your own caching system in front of dns.lookup() and/or you could even call out to a pure js dns client from the custom lookup() to avoid the thread pool entirely.
I don't think net.Socket suffers this problem, because you do Socket#connect once, and, unless you kill the socket and reconnect, the DNS resolution will only happen once. The problem is more in send, which is able to accept a host name per call.
Here is the same problem in version v6.9.4 (LTS)
Recordet what is happening while running in debug mode https://youtu.be/mYMMZzEDAE4 (to big to upload here)
The first package is sendt (23sec in the movie), the rest is send just before the stack is closing (29sec)
Here you can see how the memory is growing to 1,3GB if i increase the loop to 1000000 https://youtu.be/xAAz56bIcm0
Server:
varPORT=33333;varHOST='0.0.0.0';vardgram=require('dgram');varserver=dgram.createSocket('udp4');server.on('listening',function(){varaddress=server.address();console.log('UDP Server listening on '+address.address+":"+address.port);});server.on('message',function(message,remote){console.log(remote.address+':'+remote.port+' - '+message);});server.bind(PORT,HOST);
Client:
varPORT=33333;varHOST='127.0.0.1';vardgram=require('dgram');varclient=dgram.createSocket('udp4');for(i=0;i<1000000;i++){varmessage=newBuffer('My KungFu is Good! '+i);client.send(message,0,message.length,PORT,HOST,function(err,bytes){if(err)throwerr;if(i==1000000){console.log("done");}});}
@punnerud the problem in your code is caused by the fact that you are scheduling 1000000 messages to be sent before the socket is even ready. This is causing the massive memory allocation spike. You should schedule your messages using a queue and send only a limited number in parallel.
Activity
mscdex commentedon Apr 14, 2016
I believe it's the large number of DNS requests that are happening. Every time you call
socket.send()it has to resolveexample.orgto an IP address. Try this and you should see more stable memory usage:mjesun commentedon Apr 14, 2016
Very true. I've tested the code and the memory is stable. So it seems the memory leak might be in the DNS resolution? I've checked the
dnsmodule and everything seems to be right to me. Could it be on the native side?mscdex commentedon Apr 14, 2016
The underlying issue here is that dns requests are currently made in the thread pool which has a default size of 4. All requests to use the thread pool are queued when no threads are available. Since a dns request is being made here every ~10ms, these requests start stacking up very quickly because the thread pool can't keep up (and dns requests can take some time to complete -- especially if the nameserver is responding slowly).
cjihrig commentedon Apr 14, 2016
This seems to be resolved. Are we good to close this?
mjesun commentedon Apr 14, 2016
What about a host/IP LRU cache? I think it would mitigate the problem. I assume as well that most of the people that is using UDP sockets is not realizing that, if they pass a host to
sendTo, a DNS request is going to be made every time.If a request is being made every 10 ms, a cache of 5-10 seconds should probably kill the problem.
mscdex commentedon Apr 14, 2016
While that may seem like a sensible solution, I suspect that in-memory caching may catch people off guard especially if the results in the cache don't automatically expire. At that point you're having to implement ttl and having to decide whether a custom ttl is used or you use the ttl returned by the nameserver (which IIRC we aren't retrieving from c-ares for one reason or another) and other things.
So IMHO adding transparent caching support can be tricky and is liable to cause problems for other use cases. FWIW there have been discussions in the past about returning ttl and other information for use in userland modules, but that would only help you write a more efficient/"correct" cache in userland, it wouldn't help with this particular issue.
mjesun commentedon Apr 14, 2016
I see the problem with the cache; so probably what looks the most reasonable is to use the server's returned TTL, which is unfortunate we can't get. As I said before, I think most of the people using UDP sockets will fall into the same issue (and, in general, not expecting
gethostbynameto queue); so I think it's worth taking a look to that. However, if you want to close this ticket, feel free to do that.mscdex commentedon Apr 14, 2016
At the very least it may be worth adding some notes to the documentation in the appropriate places to make this more evident. Perhaps either a separate section in the dns docs or modifying existing wording and then linking to that from various places in other modules' documentation that accept hostnames. If you or anyone else wants to submit a PR for something like that, that would be a good start.
I should also point out that
net.Socket()already has support for passing in a customlookup()function that defaults todns.lookup(). So I suppose something similar could be added fordgram. This would allow you to implement your own caching system in front ofdns.lookup()and/or you could even call out to a pure js dns client from the customlookup()to avoid the thread pool entirely.mjesun commentedon Apr 14, 2016
I don't think
net.Socketsuffers this problem, because you doSocket#connectonce, and, unless you kill the socket and reconnect, the DNS resolution will only happen once. The problem is more insend, which is able to accept a host name per call.punnerud commentedon Jan 26, 2017
Here is the same problem in version v6.9.4 (LTS)
Recordet what is happening while running in debug mode
https://youtu.be/mYMMZzEDAE4 (to big to upload here)
The first package is sendt (23sec in the movie), the rest is send just before the stack is closing (29sec)
Here you can see how the memory is growing to 1,3GB if i increase the loop to 1000000
https://youtu.be/xAAz56bIcm0
Server:
Client:
Trott commentedon Jul 16, 2017
@mscdex @cjihrig @mcollina Should this remain open?
mcollina commentedon Jul 17, 2017
@Trott this can be closed, I'm closing it.
@punnerud the problem in your code is caused by the fact that you are scheduling 1000000 messages to be sent before the socket is even ready. This is causing the massive memory allocation spike. You should schedule your messages using a queue and send only a limited number in parallel.
dgram: add custom lookup function in sockets