Skip to content
academia.sh

Lesson 15 / 17

Spine–Leaf Design

In a design where every leaf connects to every spine, the leaf-to-leaf hop count stays constant at 2, independent of spine count, leaf pair, and break; the cost of that constancy is table rows, and with four spines the single-path row count climbs from 90 to 156 and the multi-path row count from 90 to 468.

Contents

The previous lesson measured three designs on the same nine access switches and left an inconsistency in the three-layer design: the hop count between the same two endpoints depended on which block they fell into. A packet inside a block spent two hops, a packet between blocks spent four. The layered design’s implicit assumption was that most traffic flows upward — leaving the host and crossing out of the network, north-south traffic. When that assumption holds, the four inter-block hops are rarely paid.

The assumption does not always hold. If what a host talks to is not outside the network but another host on the same network, traffic flows sideways rather than upward. This lesson’s question is: when most traffic runs leaf to leaf, which design makes the hop count independent of where the endpoints sit, and what does that cost?

East-West Traffic

Traffic between two endpoints on the network that never leaves the network is called east-west traffic. Its counterpart is north-south traffic: flow that leaves the host, crosses the core, and exits the network.

Layered design is built for north-south traffic. A block’s distribution pair collects all of the block’s traffic and hands it to the core; the core carries it outward. In this design, east-west traffic is handled as an exception: every packet between two blocks climbs to its own distribution pair, rises to the core, descends to the other distribution pair, and reaches the leaf from there. The previous lesson’s measurement counted this as four hops.

The problem is not the hop count alone. When an intra-block packet spends two hops and an inter-block packet spends four, the cost between two endpoints depends on where they plug in. Moving a host to a different leaf changes its latency without changing who it talks to.

Spine and Leaf

Spine–leaf design removes this dependency structurally. There are two kinds of device. The leaf is the switch hosts connect to. The spine only connects leaves, and no host attaches to it. There is a single rule: every leaf connects to every spine, spines do not connect to each other, and leaves do not connect to each other.

This rule has a direct consequence: every path from one leaf to another is exactly two hops — leaf, spine, leaf. No path of any other length exists, because no other link exists.

# taught transcript, not run

spine–leaf, 3 spines
  spine:  o1        o2        o3
  leaf:   y1 y2 y3 y4 y5 y6 y7 y8 y9
  rule:   every y connects to every o  (9 x 3 = 27 links)
          no o-o link, no y-y link

  paths for y3 -> y8:  y3-o1-y8   y3-o2-y8   y3-o3-y8
  all 2 hops, all equal cost

Because all three paths are equal in length, the leaf must choose one. The choice is made by the equal-cost multipath rule: the leaf keeps every equal-cost next hop toward a destination in its table and picks one for each flow. Every packet of the same flow gets the same choice. The rule that a flow is never split was established in the Link Aggregation lesson; there, the choice was among the legs of a link bundle, and here the same rule operates over a path.

The assumptions the measurement rests on:

  • TD9 — The nine leaves (y1-y9) are the previous lesson’s nine access switches; the forty pairs are generated only among these. What is measured is therefore entirely east-west traffic.
  • TD10 — Spine count is tried at 1, 2, 3, and 4; leaf count is fixed in every trial. As a comparison row, the previous lesson’s three-layer design is rebuilt with the same nine leaves.
  • TD11 — The oracle gives all of the first hops of the shortest path for every destination; equal-cost paths are not reduced to one.
  • TD12 — Flow selection is made by the remainder of the flow number divided by the number of equal-cost candidates. In a real deployment, the choice is made by hashing header fields; what matters for the measurement is that the choice is constant per flow and spread evenly across the paths.
  • TD13 — Every link costs one; what is measured is hops.
  • TD14 — The “single-path row” count counts one record per destination, the “multi-path row” count counts a separate record for each equal-cost next hop. The two are two accountings of the same table.
  • TD15 — The break is applied the same way in every design: a leaf’s link to the first spine is cut (y7-o1). In the three-layer design, the counterpart is the same leaf’s distribution link (d5-y7).
  • TD16 — In a set of forty packets, the smallest measurable difference is 1/40 = 0.025; the hop limit is 12.

The Measurement

"""Spine-leaf design: the constancy of the leaf-to-leaf hop and its cost in table rows.

Part 1 - hops, links, and table rows as spine count changes.
Part 2 - the moment of failure and the converged state when a leaf-spine link breaks.
"""
SEED = 20260810
HOP_LIMIT = 12
LEAF = [f"y{i}" for i in range(1, 10)]
BLOCK = {"d1": LEAF[0:3], "d2": LEAF[0:3], "d3": LEAF[3:6],
         "d4": LEAF[3:6], "d5": LEAF[6:9], "d6": LEAF[6:9]}
THREE_LAYER = (LEAF + sorted(BLOCK) + ["c1", "c2"],
               [(y, d) for d, ys in BLOCK.items() for y in ys]
               + [(d, c) for d in sorted(BLOCK) for c in ("c1", "c2")]
               + [("c1", "c2")])


def make_rng(seed):
    state = seed % 2147483646 + 1

    def rand(n):
        nonlocal state
        state = (state * 48271) % 2147483647
        return state % n
    return rand


def neighbors(nodes, links):
    k = {u: set() for u in nodes}
    for x, y in links:
        k[x].add(y)
        k[y].add(x)
    return k


def distances(nodes, links):
    k, dist = neighbors(nodes, links), {}
    for source in nodes:
        seen, front, d = {source}, [source], 0
        dist[(source, source)] = 0
        while front:
            d, new = d + 1, []
            for u in front:
                for v in sorted(k[u]):
                    if v not in seen:
                        seen.add(v)
                        dist[(source, v)] = d
                        new.append(v)
            front = new
    return dist


def oracle(nodes, links):
    """All (equal-cost) first hops of the shortest path, for every destination."""
    k, dist, table = neighbors(nodes, links), distances(nodes, links), {}
    for u in nodes:
        for h in nodes:
            if u == h or (u, h) not in dist:
                continue
            table[(u, h)] = [v for v in sorted(k[u])
                              if dist.get((v, h), 99) == dist[(u, h)] - 1]
    return table


def forward(flow, source, dest, tables, nodes, links):
    """One of the equal-cost paths is chosen by the flow number."""
    k, u, crossed, hops = neighbors(nodes, links), source, [], 0
    while u != dest:
        if hops >= HOP_LIMIT:
            return "loop", hops
        candidates = [v for v in tables.get((u, dest), []) if v in k[u]]
        if not candidates:
            return "black hole", hops
        nxt = candidates[flow % len(candidates)]
        if (u, nxt) in crossed:
            return "loop", hops
        crossed.append((u, nxt))
        u, hops = nxt, hops + 1
    return "reached", hops


def pairs(count=40, seed=SEED):
    rand, out = make_rng(seed), []
    while len(out) < count:
        x, y = LEAF[rand(9)], LEAF[rand(9)]
        if x != y:
            out.append((x, y))
    return out


def measure(tables, nodes, links):
    counts, hops, lengths = {"reached": 0, "loop": 0, "black hole": 0}, 0, []
    for i, (x, y) in enumerate(pairs()):
        fate, a = forward(i, x, y, tables, nodes, links)
        counts[fate] += 1
        hops += a
        if fate == "reached":
            lengths.append(a)
    return counts, hops, (min(lengths), max(lengths)) if lengths else (0, 0)


def max_load(tables, nodes, links):
    """How many of the forty flows use each link; the busiest link's count."""
    k, count = neighbors(nodes, links), {}
    for i, (x, y) in enumerate(pairs()):
        u, hops = x, 0
        while u != y and hops < HOP_LIMIT:
            candidates = [v for v in tables.get((u, y), []) if v in k[u]]
            if not candidates:
                break
            v = candidates[i % len(candidates)]
            link = tuple(sorted((u, v)))
            count[link] = count.get(link, 0) + 1
            u, hops = v, hops + 1
    return max(count.values())


def spine_leaf(spines):
    s = [f"o{i}" for i in range(1, spines + 1)]
    return LEAF + s, [(y, sp) for y in LEAF for sp in s]


DESIGN = {f"spine–leaf, {s} spine{'s' if s != 1 else ''}": spine_leaf(s) for s in (1, 2, 3, 4)}
DESIGN["three-layer (lesson 01)"] = THREE_LAYER

print(f"{'design':<26s} {'nodes':>5s} {'links':>5s} {'reached':>7s} {'hops':>5s} "
      f"{'range':>6s} {'busiest link':>12s} {'single-path':>11s} {'multi-path':>10s}")
for name, (D, B) in DESIGN.items():
    t = oracle(D, B)
    s, a, (mn, mx) = measure(t, D, B)
    print(f"{name:<26s} {len(D):5d} {len(B):5d} {s['reached']:7d} {a:5d} "
          f"{f'{mn}–{mx}':>6s} {max_load(t, D, B):12d} {len(t):11d} "
          f"{sum(len(v) for v in t.values()):10d}")

print()
print(f"{'design':<26s} {'cut':>7s} {'at failure':>18s} {'converged':>18s} "
      f"{'hops':>5s}")
print(f"{'':<26s} {'':>7s} {'reached black hole':>18s} {'reached black hole':>18s}")
for name, (D, B) in DESIGN.items():
    cut = ("y7", "o1") if "spine" in name else ("d5", "y7")
    remaining = [b for b in B if tuple(sorted(b)) != tuple(sorted(cut))]
    old, new = oracle(D, B), oracle(D, remaining)
    s0 = measure(old, D, remaining)[0]
    s1, a1, _ = measure(new, D, remaining)
    print(f"{name:<26s} {cut[0] + '–' + cut[1]:>7s} {s0['reached']:11d}"
          f"{s0['black hole']:7d} {s1['reached']:11d}{s1['black hole']:7d} {a1:5d}")
design                     nodes links reached  hops  range busiest link single-path multi-path
spine–leaf, 1 spine           10     9      40    80    2–2           14          90         90
spine–leaf, 2 spines          11    18      40    80    2–2            9         110        198
spine–leaf, 3 spines          12    27      40    80    2–2            6         132        324
spine–leaf, 4 spines          13    36      40    80    2–2            5         156        468
three-layer (lesson 01)       17    31      40   138    2–4           11         272        500

design                         cut         at failure          converged  hops
                                   reached black hole reached black hole
spine–leaf, 1 spine          y7–o1          26     14          26     14    52
spine–leaf, 2 spines         y7–o1          38      2          40      0    80
spine–leaf, 3 spines         y7–o1          38      2          40      0    80
spine–leaf, 4 spines         y7–o1          39      1          40      0    80
three-layer (lesson 01)      d5–y7          38      2          40      0   138

The Hop Is Constant

The range column reads 2–2 in all four spine–leaf rows: the shortest and the longest of the forty packets are both two hops. Total hops are 80 in all four designs, and this never changes with spine count.

This constancy holds in three separate dimensions, and all three can be read from the table. Independent of the leaf pair: the forty pairs were generated among nine leaves, and none of them found a path longer than two hops. Independent of spine count: going from one spine to four, link count rose from nine to thirty-six, and the hop count stayed the same. Independent of the break: in the bottom table, after y7–o1 is cut, the converged hop total is again 80.

The comparison row shows the difference. The three-layer design carries the same forty packets in 138 hops, with a range of 2–4. The entire 58-hop gap comes from inter-block traffic climbing to the core. For east-west traffic, the layered design is not only slower, it is unpredictable: its cost is determined by which block the endpoints fall into.

The source of this constancy is not an optimization but an absence. Because spines do not connect to each other and leaves do not connect to each other, a three-hop path cannot be constructed. The design does not select the short path; it does not let the long one exist.

The Cost of Constancy Is Table Rows

The two right-hand columns show the cost, and the two must be read separately.

The single-path row count counts one record per destination: 90 with one spine, 110 with two, 132 with three, 156 with four. The increase comes from adding nodes: every new spine is itself a destination, and it adds one row to everyone’s table.

The multi-path row count is the real cost. A leaf must keep every equal-cost next hop toward a destination; with two spines, every remote destination has two records, with four spines, four. The count climbs from 90 to 468 — as spine count quadruples, rows grow 5.2 times. And the hop count never changes.

The rule is this: in a spine–leaf design, scale is paid in rows, not hops. In layered design, growth lengthens the hop count and caps the table at the block boundary; in spine–leaf, growth never changes the hop count and widens every leaf’s table. The two designs charge the same resource to two different accounts.

The comparison row favors spine–leaf here: the three-layer design holds 272 single-path and 500 multi-path rows, the two-spine design 110 and 198. The three-layer design’s excess comes from the intermediate layer’s own nodes — six distribution and two core devices, all present in each other’s tables.

What Spine Count Changes

If the hop count never changes, why build more than two spines? The answer is in the busiest link column. The link most loaded by the forty flows carries 14 flows with one spine; 9 with two, 6 with three, 5 with four. Adding a spine does not reduce latency, it reduces density — flows spread across parallel paths.

The second answer is in the bottom table. A single-spine design is not a design: when y7–o1 is cut, 14 packets fall into a black hole, and after the tables converge, 14 remain, because y7 has no other path. From two spines onward, the same break leaves 0 black holes after convergence; the 2 packets remaining at the moment of failure are the flows that had chosen o1 at that instant. With four spines, this number drops to 1: the fraction of flows using the cut link shrinks with spine count.

A caveat is necessary. The measurement counts every link’s cost as one and never accounts for bandwidth. In a real deployment, a leaf’s total downward-facing capacity can exceed its total upward-facing capacity; in that case, the busiest link column does not show flow count but where flows fail to fit. This lesson does not do that accounting; it only counts how many links a flow spreads across.

Summary

  • East-west traffic is traffic that stays inside the network; layered design is built for north-south traffic and forces east-west packets to climb to the core.
  • In a spine–leaf design, every leaf connects to every spine, and spines and leaves do not connect among themselves; every leaf-to-leaf path is therefore 2 hops.
  • The constancy holds in three dimensions — independent of leaf pair, spine count, and break — for a total of 80 hops; the three-layer design gives 138 hops and a 2–4 range for the same forty packets.
  • The cost is table rows: going from one spine to four, the single-path row count climbs from 90 to 156, and the multi-path row count from 90 to 468.
  • Adding a spine reduces density, not hops — the busiest link drops from 14 flows to 5; in a single-spine design, a break permanently loses 14 packets, while from two spines onward the loss converges to 0.

Next Step

In a spine–leaf design, redundancy is structural: no single device owns a path, and the loss of a spine only changes how flows are distributed. But a host attached to a leaf sends its outbound packet not to a path but to an address — the default gateway’s address. That address sits on a single device at any moment, and when that device fails, redundancy stops being structural and turns into a failover: the address’s owner must change, and everyone must hear about the change. The next lesson counts the packet’s fate during the failover window, and measures a second time that partial information produces a loop.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close