5. File-descriptor limitations
- In order to ensure that all incoming connections will successfully be served,
- HAProxy computes at load time the total number of file descriptors that will be
- needed during the process's life. A regular Unix process is generally granted
- 1024 file descriptors by default, and a privileged process can raise this limit
- itself. This is one reason for starting HAProxy as root and letting it adjust
- the limit. The default limit of 1024 file descriptors roughly allow about 500
- concurrent connections to be processed. The computation is based on the global
- maxconn parameter which limits the total number of connections per process, the
- number of listeners, the number of servers which have a health check enabled,
- the agent checks, the peers, the loggers and possibly a few other technical
- requirements. A simple rough estimate of this number consists in simply
- doubling the maxconn value and adding a few tens to get the approximate number
- of file descriptors needed.
-
- Originally HAProxy did not know how to compute this value, and it was necessary
- to pass the value using the "ulimit-n" setting in the global section. This
- explains why even today a lot of configurations are seen with this setting
- present. Unfortunately it was often miscalculated resulting in connection
- failures when approaching maxconn instead of throttling incoming connection
- while waiting for the needed resources. For this reason it is important to
- remove any vestigial "ulimit-n" setting that can remain from very old versions.
-
- Raising the number of file descriptors to accept even moderate loads is
- mandatory but comes with some OS-specific adjustments. First, the select()
- polling system is limited to 1024 file descriptors. In fact on Linux it used
- to be capable of handling more but since certain OS ship with excessively
- restrictive SELinux policies forbidding the use of select() with more than
- 1024 file descriptors, HAProxy now refuses to start in this case in order to
- avoid any issue at run time. On all supported operating systems, poll() is
- available and will not suffer from this limitation. It is automatically picked
- so there is nothing to do to get a working configuration. But poll's becomes
- very slow when the number of file descriptors increases. While HAProxy does its
- best to limit this performance impact (eg: via the use of the internal file
- descriptor cache and batched processing), a good rule of thumb is that using
- poll() with more than a thousand concurrent connections will use a lot of CPU.
-
- For Linux systems base on kernels 2.6 and above, the epoll() system call will
- be used. It's a much more scalable mechanism relying on callbacks in the kernel
- that guarantee a constant wake up time regardless of the number of registered
- monitored file descriptors. It is automatically used where detected, provided
- that HAProxy had been built for one of the Linux flavors. Its presence and
- support can be verified using "haproxy -vv".
-
- For BSD systems which support it, kqueue() is available as an alternative. It
- is much faster than poll() and even slightly faster than epoll() thanks to its
- batched handling of changes. At least FreeBSD and OpenBSD support it. Just like
- with Linux's epoll(), its support and availability are reported in the output
- of "haproxy -vv".
-
- Having a good poller is one thing, but it is mandatory that the process can
- reach the limits. When HAProxy starts, it immediately sets the new process's
- file descriptor limits and verifies if it succeeds. In case of failure, it
- reports it before forking so that the administrator can see the problem. As
- long as the process is started by as root, there should be no reason for this
- setting to fail. However, it can fail if the process is started by an
- unprivileged user. If there is a compelling reason for *not* starting haproxy
- as root (eg: started by end users, or by a per-application account), then the
- file descriptor limit can be raised by the system administrator for this
- specific user. The effectiveness of the setting can be verified by issuing
- "ulimit -n" from the user's command line. It should reflect the new limit.
-
- Warning: when an unprivileged user's limits are changed in this user's account,
- it is fairly common that these values are only considered when the user logs in
- and not at all in some scripts run at system boot time nor in crontabs. This is
- totally dependent on the operating system, keep in mind to check "ulimit -n"
- before starting haproxy when running this way. The general advice is never to
- start haproxy as an unprivileged user for production purposes. Another good
- reason is that it prevents haproxy from enabling some security protections.
-
- Once it is certain that the system will allow the haproxy process to use the
- requested number of file descriptors, two new system-specific limits may be
- encountered. The first one is the system-wide file descriptor limit, which is
- the total number of file descriptors opened on the system, covering all
- processes. When this limit is reached, accept() or socket() will typically
- return ENFILE. The second one is the per-process hard limit on the number of
- file descriptors, it prevents setrlimit() from being set higher. Both are very
- dependent on the operating system. On Linux, the system limit is set at boot
- based on the amount of memory. It can be changed with the "fs.file-max" sysctl.
- And the per-process hard limit is set to 1048576 by default, but it can be
- changed using the "fs.nr_open" sysctl.
-
- File descriptor limitations may be observed on a running process when they are
- set too low. The strace utility will report that accept() and socket() return
- "-1 EMFILE" when the process's limits have been reached. In this case, simply
- raising the "ulimit-n" value (or removing it) will solve the problem. If these
- system calls return "-1 ENFILE" then it means that the kernel's limits have
- been reached and that something must be done on a system-wide parameter. These
- trouble must absolutely be addressed, as they result in high CPU usage (when
- accept() fails) and failed connections that are generally visible to the user.
- One solution also consists in lowering the global maxconn value to enforce
- serialization, and possibly to disable HTTP keep-alive to force connections
- to be released and reused faster.