Skip to content
academia.sh

Lesson 13 / 20

Deployment Stamps and Geo-Replicas

Splitting the service into self-contained, replicable units: counting the links that cross the stamp boundary, comparing each component's blast radius across three placements, showing that the scope of the single resource shared across stamps stays independent of stamp count, and converting the load per stamp into K01 numbers.

Contents

The previous lesson spread replicas across zones and regions, but what got spread was always the same whole: one service, one dataset. Surviving capacity coming out to 66.7 percent or 50 percent at the loss of one unit assumed that the lost unit carried a fraction of the service. In most arrangements this assumption does not hold: the units keep something in common, and the loss of that point takes all of them at once.

A deployment stamp is a self-contained, replicable copy of the service: the application, its store, its cache, and its queue together. The same structure is called a scale unit when it is used as a scaling unit — adding capacity means opening a new stamp. The term is not confused with the version stamp from the Application Layer and Service Interaction course; that is a marker showing which version a record was written in, while this is a deployment unit.

What the Stamp Boundary Is

A stamp is self-contained only if no link crosses its boundary. Every link that crosses the boundary requires something outside the stamp to stay up, and when that thing goes down, the stamp’s independence ends. This is why this lesson’s first measure is the number of links crossing the stamp.

The setup below is a model: nine components, three flows, and three placements. Components are tagged as stamp-local or shared across stamps; a flow is the list of components it touches in order. No real cluster, region, or container is set up.

// stamp/model.mjs — a deployment stamp is a MODEL: components are tagged as stamp-local or
// shared across stamps, flows are component arrays. No real cluster is set up.
export const STAMPS = 4;
export const FLOW = {                          // components each flow touches, in order
  tracking: ["routing", "edge", "tracking-service", "cache", "state-store"],
  event: ["routing", "edge", "event-queue", "state-store", "number-dispenser"],
  billing: ["billing-job", "state-store", "tariff-store"],
};
export const LOAD = { tracking: 8, event: 2, billing: 1 };   // requests per round, close to K01's ratio

const ALL = [...new Set(Object.values(FLOW).flat())];
export const PLACEMENT = {                      // anything not marked shared is stamp-local
  "single-piece": ALL,
  "shared-stamp": ["routing", "cache", "number-dispenser", "tariff-store"],
  "self-contained": ["routing"],
};
export const COMPONENTS = ALL;

export const scope = (placement, c) => (PLACEMENT[placement].includes(c) ? "shared" : "stamp");

export function stampCount(placement) {         // the single-piece arrangement has no stamp concept
  return placement === "single-piece" ? 1 : STAMPS;
}

export function crossingLinks(placement) {      // how many times the stamp boundary is crossed
  let n = 0;
  for (const path of Object.values(FLOW))
    for (let i = 0; i + 1 < path.length; i += 1)
      if (scope(placement, path[i]) !== scope(placement, path[i + 1])) n += 1;
  return n;
}

export function blastRadius(placement, downComponent) {   // requests and stamps affected when one component drops
  const d = stampCount(placement);
  const total = Object.values(LOAD).reduce((a, b) => a + b, 0) * d;
  let affected = 0;
  for (const [name, path] of Object.entries(FLOW)) {
    if (!path.includes(downComponent)) continue;
    affected += scope(placement, downComponent) === "shared" ? LOAD[name] * d : LOAD[name];
  }
  const stamps = scope(placement, downComponent) === "shared" ? d : 1;
  return { affected, total, ratio: affected / total, stamps, d };
}
// stamp/measure.mjs — crossing links in three placements and each component's blast radius
import { FLOW, PLACEMENT, COMPONENTS, crossingLinks, blastRadius, stampCount, scope } from "./model.mjs";

const s = (x, n) => String(x).padStart(n);
console.log(`${COMPONENTS.length} components, ${Object.keys(FLOW).length} flows, ` +
  `8 tracking + 2 event + 1 billing request per stamp.`);
console.log();
console.log(`${"placement".padEnd(15)} | ${s("stamps", 6)} | ${s("shared components", 18)} | ${s("stamp-local", 11)} | ${s("crossing links", 14)}`);
console.log(`${"-".repeat(15)}-|-${"-".repeat(6)}-|-${"-".repeat(18)}-|-${"-".repeat(11)}-|-${"-".repeat(14)}`);
for (const p of Object.keys(PLACEMENT)) {
  const shared = COMPONENTS.filter((c) => scope(p, c) === "shared").length;
  console.log(`${p.padEnd(15)} | ${s(stampCount(p), 6)} | ${s(shared, 18)} | ` +
    `${s(COMPONENTS.length - shared, 11)} | ${s(crossingLinks(p), 14)}`);
}

console.log();
console.log(`${"component down".padEnd(16)} | ${s("single-piece", 15)} | ${s("shared-stamp", 16)} | ${s("self-contained", 14)}`);
console.log(`${"-".repeat(16)}-|-${"-".repeat(15)}-|-${"-".repeat(16)}-|-${"-".repeat(14)}`);
for (const c of COMPONENTS) {
  const cell = Object.keys(PLACEMENT).map((p) => {
    const r = blastRadius(p, c);
    return `${(100 * r.ratio).toFixed(1)}% (${r.stamps}/${r.d})`;
  });
  console.log(`${c.padEnd(16)} | ${s(cell[0], 15)} | ${s(cell[1], 16)} | ${s(cell[2], 14)}`);
}
9 components, 3 flows, 8 tracking + 2 event + 1 billing request per stamp.

placement       | stamps |  shared components | stamp-local | crossing links
----------------|--------|--------------------|-------------|---------------
single-piece    |      1 |                  9 |           0 |              0
shared-stamp    |      4 |                  4 |           5 |              6
self-contained  |      4 |                  1 |           8 |              2

component down   |    single-piece |     shared-stamp | self-contained
-----------------|-----------------|------------------|---------------
routing          |     90.9% (1/1) |      90.9% (4/4) |    90.9% (4/4)
edge             |     90.9% (1/1) |      22.7% (1/4) |    22.7% (1/4)
tracking-service |     72.7% (1/1) |      18.2% (1/4) |    18.2% (1/4)
cache            |     72.7% (1/1) |      72.7% (4/4) |    18.2% (1/4)
state-store      |    100.0% (1/1) |      25.0% (1/4) |    25.0% (1/4)
event-queue      |     18.2% (1/1) |       4.5% (1/4) |     4.5% (1/4)
number-dispenser |     18.2% (1/1) |      18.2% (4/4) |     4.5% (1/4)
billing-job      |      9.1% (1/1) |       2.3% (1/4) |     2.3% (1/4)
tariff-store     |      9.1% (1/1) |       9.1% (4/4) |     2.3% (1/4)

What the Blast Radius Is a Result Of

The first table gives stamping’s measure on paper. The single-piece arrangement has no stamp boundary, so it has no crossing link either — but it also has no independent unit. When four stamps are opened, the shared component count drops to four and the crossing link count becomes 6; once the shared components are moved into the stamp, the crossing link count drops to 2. Stamp count does not give independence; shared component count does.

The second table counts the counterpart of this for each component separately. For stamp-local components, the scope is divided by four: the edge component’s loss goes from 90.9 percent to 22.7 percent, the state store’s loss goes from 100 percent to 25 percent, and the affected stamp count drops from 4 to 1. This column is the pattern’s promise.

For shared components, stamping does nothing. When the cache stays shared, its loss is the same as the single-piece arrangement: 72.7 percent and 4/4 stamps. Moved into the stamp, it becomes 18.2 percent and 1/4. The same pattern holds for the number dispenser (18.2 percent to 4.5 percent) and the tariff store (9.1 percent to 2.3 percent). As long as a component stays outside the stamp, increasing stamp count does not change its blast radius.

The routing map row is the same across all three columns: 90.9 percent and 4/4. This is the single source that knows which shipment is in which stamp, and it cannot be moved into a stamp — if it were, there would be no way to know which stamp a request should go to. This is the pattern’s limit: stamping does not zero out the blast radius, it reduces it to the single point that remains shared. That point’s service availability is the upper bound for the whole system, and K01’s weakest-link rule applies here without modification.

What Stamp Count Changes

// stamp/load.mjs — converting the stamp count into K01 loads and the failure-free day's cost
const PEAK_EDGE = 513.89;     // K01: peak requests/s at the edge
const PEAK_WRITE = 97.22;     // K01: peak write requests/s
const STORE_REQ = 138.89;     // K01: requests/s reaching the store
const SHIPMENTS_DAY = 400_000; // K01: daily shipments
const STAMP_LOCAL = 8;        // from the model: stamp-local component count in the self-contained placement
const b = (x, n = 2) => x.toFixed(n);

console.log(`${"stamps".padStart(6)}${"edge per stamp".padStart(17)}${"store per stamp".padStart(19)}` +
  `${"writes per stamp".padStart(20)}${"blast radius".padStart(17)}${"daily shipments".padStart(16)}`);
for (const n of [1, 2, 4, 8, 16]) {
  console.log(`${String(n).padStart(6)}${b(PEAK_EDGE / n).padStart(17)}${b(STORE_REQ / n).padStart(19)}` +
    `${b(PEAK_WRITE / n).padStart(20)}${(b(100 / n) + "%").padStart(17)}` +
    `${(SHIPMENTS_DAY / n).toLocaleString("en-US").padStart(16)}`);
}

console.log(`\n${"stamps".padStart(7)}${"total components".padStart(18)}${"with geo-replicas".padStart(19)}` +
  `${"stamps touched by billing".padStart(30)}`);
for (const n of [1, 2, 4, 8, 16]) {
  const components = STAMP_LOCAL * n + 1;        // +1: the shared routing map
  console.log(`${String(n).padStart(7)}${String(components).padStart(18)}` +
    `${String(STAMP_LOCAL * n * 2 + 1).padStart(19)}${String(n).padStart(30)}`);
}
console.log(`the shared single resource's blast radius is independent of stamp count: always 90.9%`);
stamps   edge per stamp    store per stamp    writes per stamp     blast radius daily shipments
     1           513.89             138.89               97.22          100.00%         400,000
     2           256.94              69.44               48.61           50.00%         200,000
     4           128.47              34.72               24.30           25.00%         100,000
     8            64.24              17.36               12.15           12.50%          50,000
    16            32.12               8.68                6.08            6.25%          25,000

 stamps  total components  with geo-replicas     stamps touched by billing
      1                 9                 17                             1
      2                17                 33                             2
      4                33                 65                             4
      8                65                129                             8
     16               129                257                            16
the shared single resource's blast radius is independent of stamp count: always 90.9%

The first table is stamping’s scaling face: at four stamps, the edge load per stamp is 128.47 requests/s, the load reaching the store is 34.72 requests/s, and the daily shipment count is 100,000. All of K01’s calculations become re-readable per stamp, and once a single stamp’s capacity is known, the number of stamps needed follows by division. The blast radius column is the same division’s resilience face, and the two are the same number — the scale unit and the failure isolation unit coincide here.

The second table gives the failure-free day’s cost. Total component count grows linearly with stamp count: 9 at one stamp, 129 at sixteen stamps. If a geo-replica of each stamp is kept in a second region, the count doubles: 257. A geo-replica is the whole stamp replicated in another region; the mechanics of replication were established in the Scaling the Data Layer course and are not repeated here — the novelty here is that what gets replicated is not a record but a deployment unit.

The last column is stamping’s least visible cost. End-of-day billing must touch every shipment, and shipments are split across stamps; at sixteen stamps, the job has to go to sixteen places and merge the results. While the load per stamp shrinks, distribution is a cost for work that has to cross the stamp. This job depends on K01’s four-hour window, and the window does not change; what changes is how many pieces the job is assembled from.

The Numbers for Two Days

The failure-free day’s cost is three line items. Component count climbs from 9 to 33 (four stamps), to 65 with geo-replicas — each one a unit set up, upgraded, and monitored separately. End-of-day billing goes to four places instead of one. And a query that concerns two stamps at once becomes an operation with no place in the design, because it crosses the stamp boundary; in the self-contained placement, only 2 links remained crossing the boundary, and both of them went to the routing map.

The failing day’s gain is in the second table’s stamp-local rows. The same failure — the state store’s loss — affects 100 percent of requests in the single-piece arrangement, 25 percent in the four-stamp arrangement, and 6.25 percent in the sixteen-stamp arrangement. The edge component’s loss goes from 90.9 percent to 22.7 percent. A failure’s duration does not change; what changes is how many users are affected during that duration, and K01’s request-based availability measure counts exactly that.

Comparing the two days also gives a limit. Increasing stamp count shrinks the blast radius by 1/n1/n while growing component count by nn. The single shared resource’s scope, though, stays at 90.9 percent and never shrinks; the pattern’s gain is limited to the share left above this floor.

Summary

  • A deployment stamp is a self-contained, replicable copy of the service; it is called a scale unit when used as a scaling unit, and it is a separate concept from the version stamp.
  • The measure of independence is not stamp count but the number of links crossing the stamp: 6 in the four-stamp arrangement with shared components, 2 once the shared components are moved into the stamp.
  • For stamp-local components, blast radius is divided by four (the state store goes from 100 percent to 25 percent); for shared components, it does not change at all (the cache stays at 72.7 percent, 4/4 stamps).
  • The routing map cannot be moved into a stamp, and its scope is 90.9 percent across all three placements: stamping does not zero out the blast radius, it reduces it to the single point that remains shared.
  • Stamp count directly divides K01’s loads: at four stamps, 128.47 requests/s edge load, 34.72 requests/s store load, and 100,000 daily shipments per stamp; blast radius is the same division.
  • The failure-free day’s cost is component count: from 9 to 33 at four stamps, to 65 with geo-replicas; end-of-day billing also has to go to as many places as there are stamps.

Next Step

This topic built a recovery arrangement over five lessons: the failover threshold and failback policy, the health endpoint’s content and interval, the recovery time and data loss objectives, zone and region placement, stamps and geo-replicas. Every lesson’s numbers came from a model or arithmetic, and all of them assumed one thing: the plan will work as written. The failover threshold will trigger correctly, the backup replica will really be current, the second region will carry the load, every component in the stamp will really sit inside the stamp. None of these assumptions has been tested. The next lesson tests the plan with a controlled failure trial: how much room the drill itself takes from the outage budget, what the gap is between the targeted recovery time and what the drill measures, and which assumptions an untested arrangement’s first real failure breaks all at once.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close