init: account for maximum file descriptors needed by HTTP

This commit is contained in:
Matthew Zipkin
2026-07-10 11:49:46 -04:00
parent cc2acebefb
commit b08662060d
4 changed files with 95 additions and 17 deletions

View File

@@ -31,18 +31,6 @@ If you front `bitcoind` with a reverse proxy or CDN such as Caddy or nginx with
the headers-more module, you can override these defaults there. Keep overrides
scoped to responses you know are safe to cache more aggressively.
Limitations
-----------
There is a known issue in the REST interface that can cause a node to crash if
too many http connections are being opened at the same time because the system runs
out of available file descriptors. To prevent this from happening you might
want to increase the number of maximum allowed file descriptors in your system
and try to prevent opening too many connections to your rest interface at the
same time if this is under your control. It is hard to give general advice
since this depends on your system but if you make several hundred requests at
once you are definitely at risk of encountering this issue.
Supported API
-------------

View File

@@ -16,4 +16,6 @@ Certain HTTP edge cases will observe different behavior to be more RFC-compliant
- Multiple "Content-Length" headers with different values are rejected
A new configuration option `-rpcmaxconnections` (default `16`) limits the
number of simultaneously connected HTTP clients to the server.
number of simultaneously connected HTTP clients to the server. The application
will now attempt to reserve file descriptors for the HTTP server sockets. If your
system has limited resources, consider using a lower setting.

View File

@@ -114,6 +114,7 @@
#include <fstream>
#include <functional>
#include <initializer_list>
#include <limits>
#include <list>
#include <memory>
#include <new>
@@ -1075,18 +1076,58 @@ bool AppInitParameterInteraction(const ArgsManager& args)
const size_t max_private{args.GetBoolArg("-privatebroadcast", DEFAULT_PRIVATE_BROADCAST)
? MAX_PRIVATE_BROADCAST_CONNECTIONS
: 0};
// Reserve enough FDs to account for the bare minimum, plus any manual connections, plus the bound interfaces
int min_required_fds = MIN_CORE_FDS + MAX_ADDNODE_CONNECTIONS + num_p2p_bind;
// Try raising the FD limit to what we need (available_fds may be smaller than the requested amount if this fails)
available_fds = RaiseFileDescriptorLimit(user_p2p_max_connections + max_private + min_required_fds);
// HTTP server listen sockets: by default two (IPv4 and IPv6 loopback), or one per -rpcbind entry
int num_rpc_bind = std::max(args.GetArgs("-rpcbind").size(), size_t(2));
// HTTP server connected client sockets
int user_rpc_max_connections = args.GetArg<int>("-rpcmaxconnections", DEFAULT_MAX_HTTP_CONNECTIONS);
if (user_rpc_max_connections < 1) {
return InitError(Untranslated("-rpcmaxconnections must be greater than zero. Use -server=0 to disable HTTP."));
}
// Reserve enough FDs to account for the bare minimum, plus any manual connections, plus the bound interfaces.
// Every element is an int >= 0 so summing in int64_t cannot overflow.
// RaiseFileDescriptorLimit() accepts an int so we check that limit before casting.
const int64_t total_fds = int64_t{MIN_CORE_FDS} +
MAX_ADDNODE_CONNECTIONS +
num_p2p_bind +
num_rpc_bind +
user_rpc_max_connections +
user_p2p_max_connections +
static_cast<int64_t>(max_private);
if (total_fds > std::numeric_limits<int>::max()) {
return InitError(Untranslated("Too many file descriptors requested. Try lower values for -rpcmaxconnections "
"or -maxconnections, or fewer settings of "
"-rpcbind, -bind and -whitebind"));
}
// Subset of total_fds must also be a safe int
int min_required_fds = MIN_CORE_FDS +
MAX_ADDNODE_CONNECTIONS +
num_p2p_bind +
num_rpc_bind +
user_rpc_max_connections;
// Try raising the FD limit to what the user wants (available_fds may be smaller than the requested amount if this fails)
available_fds = RaiseFileDescriptorLimit(static_cast<int>(total_fds));
// If we are using select instead of poll, our actual limit may be even smaller
#ifndef USE_POLL
available_fds = std::min(FD_SETSIZE, available_fds);
#endif
// The system can't support our bare minimum
if (available_fds < min_required_fds)
return InitError(strprintf(_("Not enough file descriptors available. %d available, %d required."), available_fds, min_required_fds));
// The system can support our minimum but not the full amount the user requested.
if (available_fds < total_fds) {
// If the user is requesting extra HTTP connections, abort. They need to change that.
if (user_rpc_max_connections > DEFAULT_MAX_HTTP_CONNECTIONS) {
return InitError(strprintf(_("Not enough file descriptors available. "
"Try reducing -rpcmaxconnections or using the default value of %d"),
DEFAULT_MAX_HTTP_CONNECTIONS));
}
}
// Trim requested connection counts, to fit into system limitations
num_p2p_max_connections = std::min(available_fds - min_required_fds, user_p2p_max_connections);

View File

@@ -7,6 +7,7 @@ from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import os
import platform
import re
import shutil
import signal
import subprocess
@@ -362,6 +363,51 @@ class InitTest(BitcoinTestFramework):
self.log.info("Testing node startup with fd limit above INT_MAX")
self.restart_node_with_fd_limit(1 << 31)
def init_fd_overflow_test(self):
node = self.nodes[1]
if node.running:
self.stop_node(1)
# A value larger than any possible int saturates to INT_MAX during arg parsing.
# Adding in other file descriptor requirements is guaranteed to overflow,
# so expect an InitError before RaiseFileDescriptorLimit() is called.
self.log.info("Checking -rpcmaxconnections setting that would overflow int is rejected")
node.assert_start_raises_init_error(
extra_args=[f"-rpcmaxconnections={2**64}"],
expected_msg="Error: Too many file descriptors requested.",
match=ErrorMatch.PARTIAL_REGEX
)
if self.RLIM_INFINITY is not None:
# Get the platform's file descriptor limit, if possible
import resource
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
# Lower the hard limit so RaiseFileDescriptorLimit() has a ceiling.
# The hard limit can not be raised again without root privilges,
# so this test should always be left for last in the process.
try:
resource.setrlimit(resource.RLIMIT_NOFILE, (soft, soft))
except (ValueError, OSError):
self.log.info(f"Skipping rlimit test: cannot reduce hard limit (soft={soft}, hard={hard})")
return
self.log.info("Checking that large -maxconnections setting gets adjusted for available file descriptors")
# Note this prints a message to the log and stderr but does not abort the process
with node.assert_debug_log(expected_msgs=[f"Reducing -maxconnections from {soft} "]):
self.restart_node(1, extra_args=[f"-maxconnections={soft}"])
self.stop_node(1, expected_stderr=re.compile(fr"Reducing -maxconnections from {soft} "))
# From httpserver.h
DEFAULT_MAX_HTTP_CONNECTIONS = 16
self.log.info("Checking -rpcmaxconnections gets blamed if available file descriptors are insufficient")
node.assert_start_raises_init_error(
extra_args=[f"-rpcmaxconnections={DEFAULT_MAX_HTTP_CONNECTIONS + 1}", f"-maxconnections={soft}"],
expected_msg="Not enough file descriptors available. Try reducing -rpcmaxconnections",
match=ErrorMatch.PARTIAL_REGEX
)
def run_test(self):
self.init_pid_test()
self.init_stress_test_interrupt()
@@ -370,6 +416,7 @@ class InitTest(BitcoinTestFramework):
self.init_empty_test()
self.init_rlimit_test()
self.init_rlimit_large_test()
self.init_fd_overflow_test()
if __name__ == '__main__':