Skip to content

DNS Failover: Health Checks, Weighted Routing, Recovery Procedures ​

Tại Sao Điều Này Quan Trọng ​

DNS là điểm mấu chốt giữa clients và actual service location. Nếu primary region fails, DNS phải route clients to secondary region. Nhưng DNS failover không tức thời:

  1. Health check phải detect failure (10–30 seconds)
  2. DNS record phải update (varies by implementation)
  3. Clients phải re-query DNS (respects TTL, 1–5 minutes)

Vì vậy, DNS failover RTO = health check detection time + DNS propagation time + client timeout.


Internal Model: How DNS Failover Works ​

Cloud Load Balancer (Global Load Balancing) ​

Architecture:

Anycast routing:
├─ Backend service in us-central1 (region A)
├─ Backend service in us-east1 (region B)
└─ Global load balancer distributes traffic

Health checks:
├─ Check A every 10 seconds (query service A)
├─ Check B every 10 seconds (query service B)
├─ If check fails 3 times → mark unhealthy

Failover logic:

Healthy endpoints: [A, B]
└─ Route traffic to both (load balance)

A fails health check:
T=0:00  Check fails (attempt 1, A doesn't respond)
T=0:10  Check fails (attempt 2, A still doesn't respond)
T=0:20  Check fails (attempt 3, A is marked unhealthy)

Healthy endpoints: [B]
└─ Route ALL traffic to B

When A recovers:
T=5:00  Check succeeds (A responds)
T=5:10  Check succeeds (A healthy again)

Healthy endpoints: [A, B]
└─ Route traffic to both again

Key point: Failover automatic and transparent. Clients don't need to re-query DNS.

Cloud DNS with Health Checks ​

Architecture (manual configuration):

cloud.example.com → weighted DNS routing

Routes:
├─ 100% to us-central1.example.com (if healthy)
├─ 100% to us-east1.example.com (if us-central1 unhealthy)

Health check:
└─ Monitors us-central1 endpoint every 30 seconds

Failover logic:

us-central1 healthy:
└─ DNS responds: 35.201.1.1 (us-central1 IP)

us-central1 health check fails (3 consecutive failures):
└─ DNS responds: 35.201.2.1 (us-east1 IP)

Clients using cached DNS:
├─ TTL = 300 seconds (5 minutes)
├─ Still use old IP for up to 5 minutes
└─ Might connect to failed region (timeout, retry)

Disadvantage: TTL delay. Clients may not failover until TTL expires.


Health Check Mechanisms ​

HTTP/HTTPS Health Checks ​

Check request:

GET /healthz HTTP/1.1
Host: service.example.com

Check response (healthy):

HTTP/1.1 200 OK
Content-Length: 4
OK

Configuration:

Health check interval: 10 seconds
Timeout: 5 seconds
Unhealthy threshold: 3 failures
Healthy threshold: 1 success (to recover)

Timeline:

T=0:00  Check 1: success ✓
T=0:10  Check 2: success ✓
T=0:20  Check 3: timeout ✗ (service slow)
T=0:30  Check 4: timeout ✗
T=0:40  Check 5: timeout ✗
        Marked unhealthy (3 failures)

Service recovers:
T=1:00  Check 6: success ✓
        Marked healthy (1 success to recover)

Total detection time: 40 seconds (failure) + 20 seconds (recovery) = 60 seconds

TCP/UDP Health Checks ​

Mechanism:

Try to establish TCP connection:
├─ If succeeds: healthy
├─ If timeout: unhealthy
└─ No HTTP request needed (faster)

Configuration: Same as HTTP (interval, timeout, thresholds).

Use case: Services without HTTP endpoint (databases, custom protocols).

GCP Health Check Best Practices ​

1. Choose right check interval:

Fast detection (high cost):
├─ Interval = 5 seconds
├─ Check every region every 5 sec = high API cost
└─ Good for: RTO < 1 minute

Normal detection (balanced):
├─ Interval = 10 seconds
├─ Default for most services
└─ Good for: RTO = 1–2 minutes

Slow detection (cost-effective):
├─ Interval = 30 seconds
├─ Fewer checks = lower cost
└─ Good for: RTO ≥ 5 minutes

2. Set appropriate timeout:

Timeout = 5 seconds
├─ If service responds in 5s, considered healthy
├─ Too low: false positives (slow service marked unhealthy)
├─ Too high: delayed failure detection

3. Unhealthy threshold:

Unhealthy threshold = 2–3
├─ Mark unhealthy after 2–3 consecutive failures
├─ Prevents flaky network from causing thrashing
├─ Lower threshold = faster detection

TTL (Time-To-Live) Impact ​

What is TTL? ​

TTL = how long client can cache DNS response.

DNS response from cloud.example.com:
35.201.1.1 (TTL = 300 seconds)

Client caches this:
T=0:00   Query DNS, get 35.201.1.1
T=0:05   Use cached response (no DNS query)
T=0:10   Use cached response (no DNS query)
...
T=4:59   Use cached response (last second of cache)
T=5:00   Cache expires, query DNS again

TTL Impact on Failover ​

Scenario: Primary fails at T=0:00

Clients with fresh DNS cache (just queried):
├─ Have IP of failed region (35.201.1.1)
├─ Try to connect → timeout/connection refused
├─ Retry with exponential backoff (1s, 2s, 4s, 8s, ...)
├─ Eventually retry DNS (after app timeout, ~30s)
├─ Query succeeds, get secondary IP (35.201.2.1)
└─ Total time: 30–90 seconds (depends on app retry logic)

Clients with old DNS cache (TTL about to expire):
├─ TTL expires, query DNS
├─ Get secondary IP immediately
├─ Connect to secondary (RTO = query latency + connection time)
└─ Total time: < 100ms (fast)

Average RTO = 10–60 seconds (across client base)

TTL Trade-Off ​

TTL = 60 seconds:
├─ Failover faster (clients re-query sooner)
├─ But: higher DNS query load (every 60 sec × clients)
└─ Cost: GCP DNS query volume scales with TTL

TTL = 3600 seconds (1 hour):
├─ Lower DNS query cost
├─ But: failover slower (clients use stale IP for up to 1 hour)
└─ Risk: during failover, some clients still try old region

Typical choice: TTL = 300 seconds (5 minutes)
├─ Balance: reasonable failover speed + acceptable DNS cost
RTO < 1 minute     → TTL = 60 seconds
RTO < 5 minutes    → TTL = 300 seconds
RTO < 30 minutes   → TTL = 600 seconds
RTO > 1 hour       → TTL = 3600 seconds

Global Load Balancer Failover ​

Active-Active Load Balancing ​

Architecture:

Global Load Balancer (anycast IP)
├─ Analyzes client request origin
├─ Routes to nearest healthy backend
└─ Transparent failover (no client-side awareness)

Backends:
├─ us-central1 instance group
├─ us-east1 instance group
├─ both same priority (equal load balancing)

Health checks:
├─ Monitor us-central1 group
├─ Monitor us-east1 group

Failover (automatic):

us-central1 healthy, us-east1 healthy:
└─ Route 50% clients to us-central1, 50% to us-east1

us-central1 fails:
T=0:00  Health check 1 fails
T=0:10  Health check 2 fails
T=0:20  Health check 3 fails, mark us-central1 unhealthy

New routing:
└─ Route 100% clients to us-east1

In-flight connections to us-central1:
├─ Already established, continue
├─ Eventually timeout (connection hangs)
├─ Client retry on timeout, route to us-east1
└─ Total: few hundred ms (connection timeout) + reconnect

Advantage: Automatic, no client awareness, transparent.

Disadvantage: In-flight connections might fail (client must retry).

Weighted Load Balancing ​

For gradual failover:

us-central1 unhealthy: weight = 0% (no traffic)
us-east1 healthy: weight = 100% (all traffic)

Or canary deployment:

us-central1 (new version): weight = 10% (test with 10% traffic)
us-east1 (stable version): weight = 90% (keep on stable)

Manual DNS Failover Procedure ​

For services not using Global Load Balancer:

Manual failover steps:

1. Detect primary failure (monitoring alert)
2. Confirm it's real (not false alarm)
3. Update DNS:
   - Change primary to secondary
   - Update TTL to low value (60 sec) for faster recovery
4. Monitor secondary health
5. Investigate and fix primary
6. DNS update to re-enable primary:
   - Change back to primary
   - Rebalance if needed

Example (Cloud DNS):

bash
# Before failover (normal state)
gcloud dns record-sets list --zone=prod-dns-zone
# OUTPUT:
# NAME: cloud.example.com.
# TYPE: A
# TTL: 300
# RDATA: 35.201.1.1 (us-central1)

# Failover triggered:
gcloud dns record-sets update cloud.example.com. \
  --zone=prod-dns-zone \
  --rrdatas=35.201.2.1 \
  --ttl=60

# Update back to primary (after fix):
gcloud dns record-sets update cloud.example.com. \
  --zone=prod-dns-zone \
  --rrdatas=35.201.1.1 \
  --ttl=300

Health Check Patterns ​

Pattern 1: Simple HTTP Health Check ​

Service: api.example.com
├─ Backend: /api/health
├─ Healthy response: 200 OK, body "OK"
├─ Unhealthy response: 500+ status code

Configuration:
├─ Check interval: 10 seconds
├─ Timeout: 5 seconds
├─ Unhealthy threshold: 2 failures
└─ Healthy threshold: 1 success

Pattern 2: Dependency-Aware Health Check ​

Service: app.example.com
├─ Depends on: database, cache, message queue
├─ Health endpoint checks:
│  ├─ Database connectivity
│  ├─ Cache availability
│  ├─ Message queue connectivity
│  └─ Local service status
└─ Returns 200 only if ALL dependencies healthy

Benefit:
├─ If database down, health check fails
├─ Even though local service running
├─ Prevents routing traffic to broken instance

Pattern 3: Gradual Drain on Unhealthy ​

When service becomes unhealthy:
├─ New connections routed away (via load balancer failover)
├─ Existing connections: connection draining
├─ Close timeouts: 30 seconds to drain
├─ After 30 sec: forcefully close

Benefit:
├─ In-flight requests complete gracefully
├─ No abrupt connection termination
├─ Less client-side error handling needed

Validation: Testing Failover ​

Test 1: Health Check Responsiveness ​

Monthly:

bash
# Check actual health endpoint
curl -v https://api.example.com/health

# Simulate failure (disable health endpoint)
# (manual or via feature flag)

# Verify health check detects within target time
# Expected: detected within 30 seconds
# Actual: 45 seconds (too slow, adjust threshold)

Test 2: DNS Failover Verification ​

Monthly:

bash
# Query DNS before failover
nslookup cloud.example.com
# Result: 35.201.1.1 (primary)

# Trigger failover (simulate primary failure)
# Update DNS to secondary

# Query DNS after failover
nslookup cloud.example.com
# Result: 35.201.2.1 (secondary)

# Measure: time from failover trigger to DNS update
# Expected: < 1 minute for manual, < 30 seconds for automatic

Test 3: Full Failover Drill (Quarterly) ​

1. Disable primary region (or disable health checks)
2. Verify: load balancer routes to secondary
3. Verify: in-flight connections handled gracefully
4. Verify: new connections route to secondary
5. Measure: total failover time (detection + routing)
6. Re-enable primary, verify recovery

Anti-Patterns ​

Anti-pattern 1: "TTL Too High" ​

Symptom: TTL = 1 hour for critical service.

Problem:

  • During failover, clients continue using stale IP for up to 1 hour
  • RTO = 1 hour (not acceptable for critical service)

Right approach: TTL = 60–300 seconds for critical services.

Anti-pattern 2: "Health Check Too Sensitive" ​

Symptom: Unhealthy threshold = 1 (fails on first error).

Problem:

  • Transient network glitch → failover triggered
  • Cascading failures (unnecessary failover)
  • Service flapping (constantly switching regions)

Right approach: Unhealthy threshold = 2–3 (require multiple failures).

Anti-pattern 3: "No Graceful Connection Draining" ​

Symptom: When health check fails, connections immediately closed.

Problem:

  • In-flight requests fail (client sees 500 error)
  • Client retry logic kicks in (thundering herd)
  • Unnecessary errors during failover

Right approach: Enable connection draining (30–60 second timeout) so in-flight requests complete gracefully.


Summary ​

DNS failover combines:

  1. Health checks: Detect failures (10–30 seconds)
  2. DNS updates: Route to secondary region
  3. TTL: Control client cache duration (affects failover speed)
  4. Load balancer: Automatic routing (if using Global LB)

RTO depends on:

  • Health check detection time
  • DNS update time
  • Client TTL cache expiration
  • Client retry logic

Typical RTO: 10–60 seconds (with Global LB), 1–5 minutes (with manual DNS).


References ​