Skip to content
academia.sh

Lesson 08 / 15

Subnets

The same block is split on two axes, and for every layout the usable address count, the reserved share, the unassigned remainder, and the instance count lost to a zone loss are counted; how far the public/private subnet separation cuts the externally reachable item count, and how many extra components and route tables it requires in exchange, is worked out item by item.

Published Updated

In this lesson

  • Splits the same block on two axes — subnet size and zone count — and works out the usable address count and growth headroom for each layout
  • Counts the splitting scheme's failure impact and measures how many instances a zone loss takes by zone count
  • Counts how far the public/private separation cuts the open item count and which components it requires in exchange
Contents

The block is chosen, and it holds a demand of 678 addresses for period eight. For now the block is one piece: 4,096 addresses sit side by side, and none of them can be handed to an instance. An address is distributed from a subnet, a subnet is tied to a zone, and once a subnet boundary is drawn, that boundary stays put.

The splitting decision carries two axes at once. The first is subnet size: a small number of large pieces, or a large number of small pieces. The second is distribution across zones: how many zones the same block spreads across. Both spend the same address count and produce two entirely different results. A third question sits on top of these: does every piece need to be directly reachable from outside.

  • CN7. The splitting schemes and demand distribution belong to the course’s fictional regional measurement network. No real network is split; what is measured is the model built with node. The address arithmetic is real.
  • CN8. The previous lesson’s /20 block and the assumption of five reserved addresses per subnet carry over.
  • CN9. Tier shares are written by hand as percentages: public 8, application 62, data 30. In reality the share varies by workload; it shifts which subnet is busiest without changing the sweep logic.
  • CN10. The split is assumed uniform: every subnet is the same size. In reality mixed sizes are used; it reduces the unassigned remainder, and does not remove the partition-rounding penalty.
  • CN11. Every instance is assumed to listen on four ports. This number was measured separately at the machine level; here it is used only as a multiplier.
  • CN12. A zone loss is counted as a total loss. In reality partial degradation is more common; it lowers the lost instance count without changing its relationship to zone count.

The Splitting Scheme Model

The model takes one thing seriously: splitting a block uniformly requires the piece count to be a power of two. A layout that asks for twelve subnets gets split into sixteen partitions, and four are left unassigned. The partitions function does this rounding, and every other number derives from it.

// split.mjs — the /20 block is split into subnets; no real network is set up, the arithmetic is real
export const BLOCK = 4096;          // /20 chosen in the previous lesson
export const RESERVED = 5;          // provider reserves five addresses in every subnet (CN8)
export const INSTANCES = 612;       // period-8 demand
// Tier shares as percentages; they total 100 (CN9)
export const TIER = [['public', 8], ['application', 62], ['data', 30]];

export const prefixLen = (size) => 32 - Math.log2(size);
export const partitions = (n) => 2 ** Math.ceil(Math.log2(n));   // the scheme rounds to a power of two
export const subnetSize = (n) => BLOCK / partitions(n);
export const usable = (n) => subnetSize(n) - RESERVED;
// The instance count of the busiest tier in one zone
export const densest = (zones) => Math.max(...TIER.map(([, p]) => Math.ceil(INSTANCES * p / 100 / zones)));

export const layout = (zones, tiers = TIER.length) => {
  const n = zones * tiers, k = usable(n);
  const normal = densest(zones), afterLoss = zones > 1 ? densest(zones - 1) : normal;
  return { zones, tiers, n, partitions: partitions(n), subnetSize: subnetSize(n), usable: k,
    total: n * k, reserved: RESERVED * n, unassigned: BLOCK - n * subnetSize(n),
    normal, afterLoss, lost: Math.ceil(INSTANCES / zones), fits: normal <= k,
    fitsAfterLoss: afterLoss <= k, headroom: (k / normal) };
};

The afterLoss field is the model’s second claim: when one zone goes down, its load spreads across the remaining zones and the busiest subnet’s demand grows. A splitting scheme fitting on a normal day does not have to fit on a failure day, because the subnet’s boundary cannot be widened that day.

Sweeping Two Axes

// run1.mjs — the same /20 block is split on two axes: subnet size and zone count
import { BLOCK, INSTANCES, RESERVED, prefixLen, densest, layout } from './split.mjs';

const ZONES = 4, TIERS = 3, N = ZONES * TIERS;
console.log(`${BLOCK}-address block, ${ZONES} zones x ${TIERS} tiers = ${N} subnets, ${INSTANCES} instances`);
console.log(`${'subnet'.padEnd(7)} ${'parts'.padStart(6)} ${'usable'.padStart(14)} ` +
  `${'total'.padStart(7)} ${'reserved'.padStart(9)} ${'unassigned'.padStart(11)} ${'busiest'.padStart(9)}  status`);
for (const b of [1024, 512, 256, 128, 64, 32]) {
  const d = BLOCK / b, k = b - RESERVED, y = densest(ZONES), fits = d >= N;
  console.log(`/${String(prefixLen(b)).padEnd(6)} ${String(d).padStart(6)} ${String(k).padStart(14)} ` +
    `${String(fits ? N * k : '-').padStart(7)} ${String(fits ? RESERVED * N : '-').padStart(9)} ` +
    `${String(fits ? BLOCK - N * b : '-').padStart(11)} ${String(y).padStart(9)}  ` +
    (!fits ? `${N} subnets cannot be built, at most ${d}` : y <= k ? `fits, ${(k / y).toFixed(1)}x headroom` : `busiest tier does not fit`));
}

console.log(`\nsame block, ${TIERS} tiers fixed, zone count is swept:`);
console.log(`${'zones'.padEnd(6)} ${'subnets'.padStart(8)} ${'parts'.padStart(6)} ${'usable'.padStart(14)} ` +
  `${'busiest'.padStart(9)} ${'headroom'.padStart(12)} ${'lost on zone loss'.padStart(18)} ${'post-loss'.padStart(14)}`);
for (const a of [2, 3, 4, 6, 8]) {
  const d = layout(a);
  console.log(`${String(a).padEnd(6)} ${String(d.n).padStart(8)} ${String(d.partitions).padStart(6)} ` +
    `${String(d.usable).padStart(14)} ${String(d.normal).padStart(9)} ${`${d.headroom.toFixed(1)}x`.padStart(12)} ` +
    `${`${d.lost} instances`.padStart(18)} ${`${d.afterLoss}/${d.usable}`.padStart(14)}`);
}
4096-address block, 4 zones x 3 tiers = 12 subnets, 612 instances
subnet   parts         usable   total  reserved  unassigned   busiest  status
/22          4           1019       -         -           -        95  12 subnets cannot be built, at most 4
/23          8            507       -         -           -        95  12 subnets cannot be built, at most 8
/24         16            251    3012        60        1024        95  fits, 2.6x headroom
/25         32            123    1476        60        2560        95  fits, 1.3x headroom
/26         64             59     708        60        3328        95  busiest tier does not fit
/27        128             27     324        60        3712        95  busiest tier does not fit

same block, 3 tiers fixed, zone count is swept:
zones   subnets  parts         usable   busiest     headroom  lost on zone loss      post-loss
2             6      8            507       190         2.7x      306 instances        380/507
3             9     16            251       127         2.0x      204 instances        190/251
4            12     16            251        95         2.6x      153 instances        127/251
6            18     32            123        64         1.9x      102 instances         76/123
8            24     32            123        48         2.6x       77 instances         55/123

The top table’s first two rows have nothing to do with address count. The /23 subnet has 507 usable addresses, more than the total demand of twelve subnets — but the block only splits into eight partitions, and the product of four zones and three tiers does not fit into eight. The block’s limit binds on partition count, not address count.

Further down, addresses are enough, partitions fit, and the layout still fails to hold: the /26 subnet’s fifty-nine usable addresses cannot take the ninety-five-instance application tier. The window is two rows wide. The /24 layout gives the busiest subnet 2.6x headroom; in the /25 layout that headroom drops to 1.3x — one growth step later the subnet fills, and the subnet boundary cannot be widened that day. The reserved sixty addresses stay the same in every row, not the weight: 2% of the 3,012 addresses assigned in the /24 layout, 8% of the 768 in the /26 layout. The unassigned column climbs from 1,024 to 3,712; those addresses are not lost, but they are locked to the chosen partition size — a plan that picks /26 can only add /26 in the future.

The lower table measures the second axis and turns up an unexpected penalty. When the zone count is a power of two, growth headroom stays above 2.6x: 2.7x at two zones, 2.6x at four, 2.6x at eight. Three and six zones use the same block worse — 2.0x and 1.9x. The cause is not address demand but rounding: nine subnets round up to sixteen partitions, eighteen subnets to thirty-two, and the partitions that get no return shrink the subnet size by a notch. Pulling the zone count down to three is a decision made for resilience, and it charges its cost against the address plan.

The last two columns separate out the failure impact. A zone loss takes 306 instances at two zones and 77 instances at eight zones: same block, same address count, a fourfold difference. In the post-loss column, though, every layout fits — the load spread across the remaining zones cannot fill the busiest subnet. On a failure day, splitting schemes are told apart not by address count but by how many instances stay standing.

Public and Private Separation

// run2.mjs — the same 12 subnets in three access layouts; the externally visible surface is counted
import { INSTANCES, TIER } from './split.mjs';

const ZONES = 4, PORTS = 4;                    // ports each instance listens on (CN11)
const count = (p) => Math.ceil(INSTANCES * p / 100 / ZONES) * ZONES;
const perTier = Object.fromEntries(TIER.map(([name, p]) => [name, count(p)]));
const TOTAL = Object.values(perTier).reduce((a, b) => a + b, 0);
console.log(`${ZONES} zones x ${TIER.length} tiers: ` +
  TIER.map(([name]) => `${name} ${perTier[name]}`).join(', ') + `, total ${TOTAL} instances, ${PORTS} ports per instance`);

// [name, public-addressed tiers, extra balancer, gateway per zone]
const LAYOUT = [
  ['no separation', ['public', 'application', 'data'], 0, 0],
  ['public tier only', ['public'], 0, 0],
  ['none public', [], 1, 1],
];
console.log(`\n${'layout'.padEnd(20)} ${'public'.padStart(9)} ${'private'.padStart(8)} ${'public addr'.padStart(12)} ` +
  `${'open item'.padStart(11)} ${'not public'.padStart(11)} ${'extra parts'.padStart(12)} ${'tables'.padStart(7)}`);
for (const [name, pub, extraLB, gw] of LAYOUT) {
  const publicSubnets = pub.length * ZONES, privateSubnets = (TIER.length - pub.length) * ZONES;
  const publicInstances = pub.reduce((a, k) => a + perTier[k], 0);
  const item = publicInstances * PORTS + extraLB;      // extra balancer listens on a single port
  const parts = 1 + extraLB + gw * ZONES;              // internet gateway + balancer + gateway per zone
  const tables = gw ? 1 + ZONES : publicSubnets && privateSubnets ? 2 : 1;
  console.log(`${name.padEnd(20)} ${String(publicSubnets).padStart(9)} ${String(privateSubnets).padStart(8)} ` +
    `${String(publicInstances).padStart(12)} ${String(item).padStart(11)} ${String(TOTAL - publicInstances).padStart(11)} ` +
    `${String(parts).padStart(12)} ${String(tables).padStart(7)}`);
}
console.log(`\ninside the block every instance can reach every instance: ${TOTAL} x ${TOTAL - 1} directions, same in all three layouts`);
console.log(`externally open items: ${TOTAL * PORTS} -> ${perTier.public * PORTS} -> 1`);
4 zones x 3 tiers: public 52, application 380, data 184, total 616 instances, 4 ports per instance

layout                  public  private  public addr   open item  not public  extra parts  tables
no separation               12        0          616        2464           0            1       1
public tier only             4        8           52         208         564            1       2
none public                  0       12            0           1         616            6       5

inside the block every instance can reach every instance: 616 x 615 directions, same in all three layouts
externally open items: 2464 -> 208 -> 1

The layout with no separation does not look like a choice, because no one picks it; it happens because the public-address-assignment behavior was left open when the subnet was created. Its result is 2,464 items: all four ports of all 616 instances are reachable from outside. Separating out the public tier cuts that number to 208 — a twelvefold drop — and its only cost is the route table count going from one to two.

The third row is the separation taken all the way. The only externally reachable item is the port the load balancer listens on. All 616 instances still listen on four ports; none of them can be called from outside. Its cost sits in the two right-hand columns: the component count rises from one to six, and the route table count from one to five. The five tables come from each of the four zones having to look at its own gateway, and that number is a warning of a new burden — every new subnet now has to be written into the right table, not just a table.

The last two lines say what the separation does not measure. Inside the block, all 616 instances can reach one another, and that number is the same in all three layouts: subnet separation does not touch the internal surface. It is also true that all 616 of the 616 instances can no longer get outside on their own; getting out needs four of the six components counted above. These two gaps are the subject of the next two lessons: first, in which table and in which order the rules are written; second, how a closed subnet reaches outside. Both bills are opened by the separation made in this lesson — the decision that cuts the externally open item count from 2,464 to 1 writes a debt of five tables and four gateways at the same time.

This lesson’s item stays with the team in five pieces: partition size, zone count, tier count, which tier is public, and which zone each subnet attaches to. Three items are handed off and never even asked: the five addresses cut per subnet, the rounding of the piece count to a power of two in a uniform split, and a subnet’s inability to change zone once it is created. Two of the three handed off produce the numbers in the tables above directly — the sixty-address cut and the growth headroom dropping to 2.0x at three zones. An item sitting with the provider does not make it powerless, only invisible.

The irreversible item here is the subnet boundary. Once a subnet is created and an instance is placed inside it, its prefix cannot be narrowed and it cannot be moved to another zone; whether it can be widened depends on the provider — some allow growing the prefix, some do not allow any change at all. This lesson treats the prefix as fixed. The partition size choice sits on top of this: a plan that picks /24 can only add /24 in the future. Zone assignment is permanent too — the cost of choosing three zones is a 2.0x growth headroom, and once that headroom fills, the fix is adding a new subnet, not growing the existing one.

Summary

  • The same 4,096-address block, the same 12 subnets: the /22 and /23 layouts fail not because of address count but partition count, and in the /26 and /27 layouts the busiest tier’s 95 instances do not fit into 59 and 27 usable addresses. The window is two rows wide.
  • The 60 reserved addresses are the same in every layout, not the weight: 2% of what is assigned in the /24 layout, 8% in the /26 layout. The unassigned remainder climbs from 1,024 to 3,712 and locks to the chosen partition size.
  • When the zone count is a power of two, growth headroom is 2.6–2.7x; at 3 and 6 zones it drops to 2.0x and 1.9x. The penalty comes from partition rounding: 9 subnets round up to 16 partitions, 18 subnets to 32, shrinking the subnet size by a notch.
  • A zone loss takes 306 instances at 2 zones and 77 at 8 zones — a fourfold difference. After a failure, addresses are enough in every layout; layouts are told apart by how many instances stay standing.
  • Public and private separation cuts the externally open item count from 2,464 to 208, then to 1, and pulls the public address count from 616 to 0. Its price is 6 components and 5 tables in place of 1 and 1; the internal surface — 616 instances reaching one another — does not change across the three layouts.

Next Step

Five route tables passed as a single number in the last run, yet that is where the real load sits. The four zones’ private subnets look at their own gateway, and public subnets look somewhere else, and which traffic goes where is written inside those tables. The next lesson’s question works these rules out item by item: given a destination address, which row wins, and under which conditions does the longest prefix match pick a path that runs against intuition; how many subnets does a single row, written short or written wide, leave unreachable, and what does routing traffic out an unexpected path add to the egress fee; how many tables does the same rule repeat across, and how many tables have to be touched when a thirteenth subnet is added.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close