Skip to content
academia.sh

Lesson 01 / 16

Domain Name System Design

Designing name resolution as a routing decision: measuring the geolocation, latency-based, and weighted policies on the same request flow, showing through resolver shares why the weighted policy misses its target share, and converting the TTL's determination of the failover window into dropped requests against the introductory course's downtime budget.

Contents

The Introduction to System Design course defined every property, set the thresholds, and chose the failover patterns. One thing was missing: the request had not yet entered the system. A tracking query begins with a domain name resolving, may pass through an edge cache, lands on a load balancer, and reaches the application through a gateway. There, the cache was only a multiplier in the arithmetic, and routing was a one-line choice in the model.

This course takes up that path, starting with name resolution. The Domain Name System itself was built in the How the Internet Works course in the Computer Networks curriculum — hierarchy, resolution chain, record types, and TTL are covered there and not repeated here. The question here is different: when a name has more than one zone behind it, which address gets returned is a design decision with measurable consequences.

The Name Layer Is a Decision Point

The authoritative server need not give the same answer to every query: it can return a different address depending on where the query came from and the state of the zones. This choice is a routing policy, and it takes three forms.

The geolocation policy answers from a coarse, hand-written table keyed on the querier’s location — this client cluster goes to that zone — and the table can be wrong. The latency-based policy picks whichever zone measures closer; because geographic and network proximity are not the same thing, the two policies produce different answers. The weighted policy assigns each zone a target share and distributes answers accordingly; its purpose is capacity control, not proximity.

A fourth use is not a policy but a failure response: when a zone becomes unreachable, the authoritative server removes its address from the answers. This is the name layer’s form of failover, and the second half of the lesson measures its cost.

Setup

The measurement uses an in-process model with one authoritative server, forty resolvers, and three zones — b06 added to the b34 and b35 zones from the Availability Patterns lesson. The request rate comes from the introductory course: peak edge requests/s is 513.89, rounded up to 514. The model adds four new assumptions of its own; they are not folded into the introductory course’s V1–V13 table.

Code Assumption Value Rationale
T1 TTL (baseline) 300 s the duration the name owner chooses, varied here
T2 zone count 3 a backup zone is added to the two-zone failover
T3 target share 0.50 / 0.30 / 0.20 the old zone carries more load, the new zone less
T4 resolver count and share 40, share ~ 1/(j+1) most queries come from a small number of large resolvers
T5 dead-zone detection time 10 s the health check counts a zone dead after this delay

The distance matrix is in abstract units, not measured latency — the same kind of abstraction as the introductory course’s round. The resolvers’ cache phases are spread evenly, since all of them expiring at the same instant is a separate problem, measured in the Caching, Queues and Asynchronous Processing course as the stampede effect.

// name/model.mjs -- name layer model. Distance is an abstract unit, not measured latency;
// TTL, detection time, and outage start are model parameters.
export const ZONE = ["b34", "b35", "b06"];
export const GROUP = ["g1", "g2", "g3", "g4"];
export const DISTANCE = {                      // unit; not measured latency
  g1: { b34: 1, b35: 3, b06: 2 }, g2: { b34: 3, b35: 1, b06: 2 },
  g3: { b34: 2, b35: 4, b06: 1 }, g4: { b34: 2, b35: 3, b06: 4 },
};
export const GEOLOCATION = { g1: "b34", g2: "b35", g3: "b34", g4: "b06" };  // coarse zone table
export const FALLBACK = { b34: "b06", b35: "b34", b06: "b35" };            // the table's fallback
export const PATTERN = ["b34", "b35", "b06", "b34", "b35", "b34", "b35", "b06", "b34", "b34"];
export const WEIGHT = { b34: 0.5, b35: 0.3, b06: 0.2 };  // T3: target share, PATTERN yields this
export const RESOLVERS = 40;      // T4
export const DETECTION = 10;      // T5: health check marks a dead zone (s)
export const REQUESTS_PER_SECOND = 514;  // K01 "peak at the edge 513.89 requests/s", rounded up

// T4: resolver shares are not equal, share ~ 1/(j+1); the per-second requests are divided, remainder goes to the largest fractional parts.
const raw = Array.from({ length: RESOLVERS }, (_, j) => 1 / (j + 1));
const t0 = raw.reduce((a, b) => a + b, 0);
export const REQUESTS = raw.map((p) => Math.floor((p / t0) * REQUESTS_PER_SECOND));
for (const [, j] of raw.map((p, j) => [(p / t0) * REQUESTS_PER_SECOND % 1, j]).sort((a, b) => b[0] - a[0])
  .slice(0, REQUESTS_PER_SECOND - REQUESTS.reduce((a, b) => a + b, 0))) REQUESTS[j] += 1;

export function run({ policy, ttl, outageStart = null, duration = 3600, requests = REQUESTS }) {
  const s = { accepted: 0, dropped: 0, upstreamQueries: 0, distance: 0, window: 0 };
  const zoneRequests = Object.fromEntries(ZONE.map((b) => [b, 0]));
  const bound = new Array(RESOLVERS).fill(null);
  // warm-up: cache phases are spread evenly, counting starts at t >= 1
  const expires = Array.from({ length: RESOLVERS }, (_, j) => 1 - ttl + ((j * 13) % ttl));
  let patternIndex = 0;
  const isDown = (b, t) => outageStart !== null && b === "b34" && t >= outageStart;
  const isDetected = (b, t) => isDown(b, t) && t >= outageStart + DETECTION;

  const authoritative = (group, t) => {
    if (policy === "geolocation") {
      const b = GEOLOCATION[group];
      return isDetected(b, t) ? FALLBACK[b] : b;
    }
    if (policy === "latency-based") {
      return ZONE.filter((b) => !isDetected(b, t))
        .reduce((a, b) => (DISTANCE[group][b] < DISTANCE[group][a] ? b : a));
    }
    for (let k = 0; k < PATTERN.length; k++) {      // weighted: next live zone in the pattern
      const b = PATTERN[(patternIndex + k) % PATTERN.length];
      if (!isDetected(b, t)) { patternIndex = (patternIndex + k + 1) % PATTERN.length; return b; }
    }
  };

  for (let t = 1 - ttl; t <= duration; t++) {
    for (let j = 0; j < RESOLVERS; j++) {
      if (t >= expires[j]) {                        // TTL expired: asks the authoritative server
        bound[j] = authoritative(GROUP[j % GROUP.length], t);
        expires[j] = t + ttl;
        if (t >= 1) s.upstreamQueries += 1;
      }
      if (t < 1) continue;
      const n = requests[j], target = bound[j];
      if (isDown(target, t)) { s.dropped += n; s.window = t - outageStart + 1; continue; }
      s.accepted += n;
      s.distance += n * DISTANCE[GROUP[j % GROUP.length]][target];
      zoneRequests[target] += n;
    }
  }
  const share = Object.fromEntries(ZONE.map((b) => [b, zoneRequests[b] / s.accepted]));
  const deviation = ZONE.reduce((a, b) => a + Math.abs(share[b] - WEIGHT[b]), 0) / 2;
  return { ...s, share, deviation, unitDistance: s.distance / s.accepted };
}

Three Policies on the Same Flow

// name/policy.mjs -- three routing policies on the same request flow: zone share, deviation from target share,
// distance unit per request, and how many names resolvers ask the authoritative server for
import { run, REQUESTS, REQUESTS_PER_SECOND, WEIGHT, ZONE } from "./model.mjs";

const TTL = 300, DURATION = 3600;
console.log(`${DURATION} s, ${REQUESTS_PER_SECOND} requests/s, TTL ${TTL} s, ` +
  `largest resolver's share %${((REQUESTS[0] / REQUESTS_PER_SECOND) * 100).toFixed(2)}`);
console.log(`target share: ${ZONE.map((b) => `${b}=%${WEIGHT[b] * 100}`).join(" ")}`);
console.log();
console.log("policy          |   b34   b35   b06 | deviation | unit distance | upstream query");
console.log("----------------|-------------------|-----------|---------------|---------------");
for (const policy of ["geolocation", "latency-based", "weighted"]) {
  const r = run({ policy, ttl: TTL, duration: DURATION });
  console.log(`${policy.padEnd(15)} |` +
    `${ZONE.map((b) => `%${(r.share[b] * 100).toFixed(1)}`.padStart(6)).join("")} | ` +
    `${r.deviation.toFixed(3).padStart(9)} | ${r.unitDistance.toFixed(3).padStart(13)} | ` +
    `${String(r.upstreamQueries).padStart(14)}`);
}
console.log();
for (const ttl of [30, 60, 300, 900]) {
  const r = run({ policy: "weighted", ttl, duration: DURATION });
  console.log(`weighted, ttl ${String(ttl).padStart(3)} s -> deviation ${r.deviation.toFixed(3)}, ` +
    `upstream query ${String(r.upstreamQueries).padStart(4)}`);
}

// T4 sensitivity: how much the same policy would deviate if resolver shares were equal
const EQUAL = Array.from({ length: 40 }, (_, j) => (j < 34 ? 13 : 12));   // totals 514
const e = run({ policy: "weighted", ttl: TTL, duration: DURATION, requests: EQUAL });
console.log(`\nT4 sensitivity: with equal shares (ttl ${TTL}) deviation ${e.deviation.toFixed(3)}`);
3600 s, 514 requests/s, TTL 300 s, largest resolver's share %23.35
target share: b34=%50 b35=%30 b06=%20

policy          |   b34   b35   b06 | deviation | unit distance | upstream query
----------------|-------------------|-----------|---------------|---------------
geolocation     | %57.6 %25.3 %17.1 |     0.076 |         1.710 |            480
latency-based   | %55.1 %25.3 %19.6 |     0.051 |         1.171 |            480
weighted        | %50.6 %27.8 %21.6 |     0.022 |         2.144 |            480

weighted, ttl  30 s -> deviation 0.082, upstream query 4800
weighted, ttl  60 s -> deviation 0.195, upstream query 2400
weighted, ttl 300 s -> deviation 0.022, upstream query  480
weighted, ttl 900 s -> deviation 0.037, upstream query  160

T4 sensitivity: with equal shares (ttl 300) deviation 0.000

These numbers belong to the measurement class: they come out of a model run on this machine. The distance unit depends on the model’s parameters; the request counts do not.

The first table shows two metrics moving in opposite directions. The latency-based policy finds the closest placement, 1.171 distance units per request, against 1.710 for geolocation and 2.144 for weighted — 1.83 times the distance of the closest-zone policy, a ratio independent of unit choice since both runs share the same matrix. But weighted gives the distribution closest to the target share: deviation 0.022, against 0.076 for geolocation. Proximity and share control are not achieved together in the same policy.

Geolocation’s 1.710 comes from the hand-written table: in two of the four client clusters it misses the nearest zone. Cluster g3’s entry says b34, but the nearest zone in the matrix is b06; g4’s says b06, but the nearest is b34. When geographic and network proximity diverge, the policy silently sends traffic to the wrong zone — visible only once it is measured.

The second table exposes the weighted policy’s real problem: deviation does not improve as the TTL shortens — 0.082, 0.195, 0.022, 0.037, oscillating without a pattern — while upstream queries climb from 160 to 4,800, thirtyfold. A short TTL multiplies the price paid by thirty without bringing the result any closer to the target share.

The last line gives the reason: when resolver shares are equalized, the same policy’s deviation drops to 0.000. The problem is not the TTL but the control unit — the policy distributes share per resolution, while load arrives per request. The single largest resolver alone carries 23.35% of all requests, more than zone b06’s entire 20% target share; whichever zone it is bound to overshoots. The name layer is not a request-level control instrument.

The Failover Window

When a zone becomes unreachable, the authoritative server removes it from the answers, but the answers already sitting in caches at that moment stay valid. The failover window therefore has two parts: the zone being marked dead (T5) and the cached answer’s TTL running out (T1).

// name/failover.mjs -- b34 becomes unreachable at second 100. TTL determines the failover
// window and the requests dropped in it; the budget figures are taken from the K01 computations.
import { run, DETECTION, REQUESTS_PER_SECOND } from "./model.mjs";

const OUTAGE = 100, DURATION = 1200;
const MONTHLY_BUDGET_S = (1 - 99.9 / 100) * 30 * 24 * 3600;  // K01: 99.9% -> 43.2 min/month
const K01_PASSIVE_DROPPED = 1541.67;                          // K01: active-passive, 3 s window

console.log(`b34 goes down at second ${OUTAGE}, the health check marks it ${DETECTION} s later.`);
console.log(`monthly downtime budget = ${(MONTHLY_BUDGET_S / 60).toFixed(1)} min (99.9%)`);
console.log();
console.log("ttl | window | dropped req | equiv. downtime | budget share | upstream query");
console.log("----|--------|-------------|------------------|---------------|---------------");
for (const ttl of [30, 60, 300, 900]) {
  const r = run({ policy: "geolocation", ttl, outageStart: OUTAGE, duration: DURATION });
  const equivalent = r.dropped / REQUESTS_PER_SECOND;
  console.log(`${String(ttl).padStart(3)} | ${String(r.window).padStart(4)} s | ` +
    `${String(r.dropped).padStart(11)} | ${equivalent.toFixed(2).padStart(14)} s | ` +
    `${`%${((equivalent / MONTHLY_BUDGET_S) * 100).toFixed(2)}`.padStart(13)} | ` +
    `${String(r.upstreamQueries).padStart(15)}`);
}
const r = run({ policy: "geolocation", ttl: 300, outageStart: OUTAGE, duration: DURATION });
console.log();
console.log(`ttl 300: window ${r.window} s, equivalent downtime ` +
  `${(r.dropped / REQUESTS_PER_SECOND).toFixed(2)} s -> ` +
  `%${((r.dropped / (r.window * REQUESTS_PER_SECOND)) * 100).toFixed(1)} of the window's traffic was dropped`);
console.log(`K01's active-passive failover dropped ${K01_PASSIVE_DROPPED} requests in 3 s; ` +
  `this failover drops ${(r.dropped / K01_PASSIVE_DROPPED).toFixed(1)} times that`);
b34 goes down at second 100, the health check marks it 10 s later.
monthly downtime budget = 43.2 min (99.9%)

ttl | window | dropped req | equiv. downtime | budget share | upstream query
----|--------|-------------|------------------|---------------|---------------
 30 |   39 s |        6666 |          12.97 s |         %0.50 |            1600
 60 |   69 s |        9486 |          18.46 s |         %0.71 |             800
300 |  305 s |       57906 |         112.66 s |         %4.35 |             160
900 |  905 s |      190206 |         370.05 s |        %14.28 |              64

ttl 300: window 305 s, equivalent downtime 112.66 s -> %36.9 of the window's traffic was dropped
K01's active-passive failover dropped 1541.67 requests in 3 s; this failover drops 37.6 times that

The window tracks the TTL almost one to one — 39, 69, 305, 905 seconds — so the TTL stops being merely a cache setting and becomes the failover duration itself. The number the name owner picks to reduce query load is the same number that decides how long traffic keeps hitting a dead address once a zone fails.

The gap between dropped requests and equivalent downtime confirms a distinction from the introductory course: at a 300-second TTL the window is 305 seconds, but the dropped requests’ equivalent is only 112.66 — 36.9% of the window’s traffic, not all of it. The Availability in Numbers lesson said time-based and request-based measurement diverge; a zone failover is exactly where that lands. Someone counting by time sees 305 seconds of downtime, someone counting by requests sees 112.66 — both describing the same event.

The budget share column settles the decision. At a 99.9% target, the monthly budget is 43.2 minutes; a single zone failover spends 14.28% of it at a 900-second TTL, 4.35% at 300 seconds, 0.50% at 30 seconds. The cost of the same event changes twenty-eightfold, and the only thing that changed is a number in the name record.

The comparison closes on the last line: the active–passive failover in the Availability Patterns lesson dropped 1,541.67 requests in a three-second window; the name layer’s failover, at the baseline TTL, drops 37.6 times that many. The name layer is the slowest failover instrument available — seconds at the replica level become minutes here. Design consequence: use the name layer for zone selection, not as the sole failure response — the stop that closes the window is the subject of the following lessons.

Summary

  • Name resolution is a routing decision: the geolocation policy answers from a coarse table, the latency-based policy from measured proximity, and the weighted policy from a target share.
  • On the same flow, distance units per request were 1.171 for latency-based, 1.710 for geolocation, and 2.144 for weighted; weighted gave the distribution closest to the target share (deviation 0.022).
  • Writing the geolocation table by hand is a measurable flaw: in two of the four client clusters, the table did not point to the nearest zone.
  • The weighted policy’s deviation did not improve as the TTL shortened (0.082–0.195, oscillating), but upstream queries rose thirtyfold; equalizing shares dropped the deviation to 0.000 — the control unit is the resolution, but load arrives per request.
  • The failover window tracks the TTL (39, 69, 305, 905 s), and a single zone failover takes up anywhere from 0.50% to 14.28% of the monthly downtime budget.
  • At a 300-second TTL the window is 305 seconds but the equivalent downtime is 112.66 seconds; the name layer’s failover drops 37.6 times as many requests as the replica-level one.

Next Step

The name layer chose which zone the request would go to, paying for that choice in the distance unit and the failover window. But what it chose was an address — the request still has not reached the application once it arrives there. In that gap sits a stop that can answer the request without letting it reach the application. Caching was built in the Caching, Queues and Asynchronous Processing course, but there the cache sat next to the application, as the introductory course’s arithmetic assumed too: a 90% hit rate brought reads behind cache/s down to 41.67. The next lesson relocates the same cache — next to the user instead of the application — and measures what happens to the requests reaching the application, the bytes crossing the boundary, and the introductory course’s computations.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close