Quick Summary: Troubleshoot intermittent EAI_AGAIN in Node.js 16/18 on RHEL 8 Docker/K8s with ndots:5. Understand how C-Ares, conntrack, and ephemeral ports collide.
Alright, listen up. If you've landed here, you're probably pulling your hair out. You've got a Node.js application, humming along in a Docker container on RHEL 8. It's in Kubernetes, so ndots:5 is stamped all over your resolv.conf. Everything should be fine.
But it's not. Your app is throwing intermittent EAI_AGAIN errors for internal service names, like my-backend-service. But only for the short, unqualified names. If you use the FQDN, my-backend-service.my-namespace.svc.cluster.local, it works every single time. And the kicker? It works fine on Ubuntu hosts, or with older Node.js versions. What the hell gives?
This isn't your grandma's DNS problem. This is a subtle, nasty interaction between Node.js's default internal DNS resolver, RHEL 8's kernel defaults, and the common Kubernetes resolv.conf setup. It’s a ghost in the machine, and it's eating your latency budget. Speaking of latency, if you're working on something where every millisecond counts, you might want to check out our deep dive on Architecting Ultra-Low Latency Trading Systems – though this issue can ruin any such ambition.
The Problem: Intermittent EAI_AGAIN for Unqualified Hostnames
Your Node.js 16/18 application intermittently fails to resolve internal service names when using short, unqualified hostnames (e.g., my-service). Qualified names (e.g., my-service.namespace.svc.cluster.local) work reliably. This manifests as EAI_AGAIN errors in your logs, leading to connection failures and service instability.
This pain typically surfaces in specific environments:
| Operating System | Node.js Version | Container Runtime | resolv.conf snippet | Observed Behavior |
|---|---|---|---|---|
| RHEL 8.x (e.g., 8.4, 8.6, 8.8) | 16.x, 18.x | Docker, containerd (K8s) | search default.svc.cluster.local svc.cluster.local cluster.localndots:5 |
Intermittent EAI_AGAIN for unqualified hostnames (e.g., my-service) |
| Ubuntu 20.04/22.04 | 16.x, 18.x | Docker, containerd (K8s) | (Same as above) | Works reliably |
| RHEL 8.x | 14.x (or older) | Docker, containerd (K8s) | (Same as above) | Works reliably |
The Root Cause
Node.js, by default, uses its own C-Ares based resolver for dns.lookup (which is what http.request and similar high-level APIs use). This C-Ares resolver, particularly in versions 16 and 18, when confronted with a high ndots value (like 5, common in Kubernetes), performs a rather aggressive, parallel query strategy for unqualified hostnames. Instead of sequentially trying search domains, it fires off multiple DNS queries almost simultaneously: my-service. (as an FQDN), my-service.default.svc.cluster.local, my-service.svc.cluster.local, etc.
On RHEL 8.x, this burst of simultaneous, mostly failing DNS queries for unqualified names hits a critical limit. RHEL 8's default kernel settings for net.netfilter.nf_conntrack_max (connection tracking table size) and net.ipv4.ip_local_port_range (ephemeral port range) are often tighter than on Ubuntu or older RHEL versions. This results in:
- Ephemeral Port Exhaustion: Node.js tries to open many UDP sockets for these parallel DNS queries. Quickly, the available ephemeral ports are used up, leading to failures to bind new sockets.
- Conntrack Table Saturation: Each UDP query creates an entry in the conntrack table. A flood of these (often failed) queries can rapidly saturate the table, causing new connections (even TCP) to be dropped.
The intermittent nature comes from the load. Under low load, the system recovers quickly enough. Under high load, the limits are hit, DNS queries fail, and your app gets an EAI_AGAIN. Older Node.js versions or different OS kernels handle this parallel querying differently, or have more generous default limits, hence the environment-specific behavior. It's a subtle resource contention nightmare.
The Fix: Taming Node.js's Resolver Aggression
The solution is to tell Node.js to stop being so aggressive with its DNS queries for unqualified names, forcing it to behave more like the system's getaddrinfo, which respects ndots and search paths more sequentially and efficiently. The easiest way to achieve this is via a Node.js environment variable:
NODE_OPTIONS='--dns-result-order=verbatim'
Explanation:
--dns-result-order=verbatimforces Node.js's internal resolver to return DNS results in the exact order received from the DNS server, rather than reordering them (e.g., preferring IPv4). Crucially, in Node.js 16+, this also subtly influences the C-Ares behavior, making it more compliant withresolv.conf'sndotsandsearchpath logic for unqualified names. It reduces the wasteful, parallel query burst, thus preventing ephemeral port and conntrack exhaustion on RHEL 8.
How to Implement:
In your Dockerfile, K8s deployment YAML, or startup script, set this environment variable:
# In a Dockerfile
ENV NODE_OPTIONS="--dns-result-order=verbatim"
# In Kubernetes deployment YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-node-app
spec:
template:
spec:
containers:
- name: app
image: my-node-image:latest
env:
- name: NODE_OPTIONS
value: "--dns-result-order=verbatim"
# Or, if running directly, just export it
export NODE_OPTIONS="--dns-result-order=verbatim"
node server.js
After applying this change, restart your Node.js application. You should see a drastic reduction, if not complete elimination, of the intermittent EAI_AGAIN errors for your internal unqualified hostnames. This specific issue is a common pitfall in containerized Node.js applications, often discussed alongside other `EAI_AGAIN` mysteries. For more context on Node.js DNS quirks, I highly recommend our previous deep dive on The Phantom EAI_AGAIN: Node.js Internal DNS Hell on RHEL 8 with ndots Misery, which tackles related but distinct problems.
Final Thoughts
This problem highlights the crucial need to understand the nuances of your runtime's network stack, especially in modern containerized and microservice environments. Don't assume everything 'just works' the same across OS versions or even minor runtime upgrades. Dig into the details, because sometimes the ghosts are real, and they cost you production stability.
Comments
Post a Comment