Zum Inhalt springen

Zellenbasierte Architektur für resiliente verteilte Systeme

Zellenbasierte Architekturen, die Fehler auf kleine Blast Radii isolieren, unabhängige Skalierung erlauben und kaskadierende Ausfälle verhindern.

4 Min. Lesezeit
Diagramm einer zellenbasierten Architektur mit unabhängigen, isolierten Zellen und einer Routing-Schicht, die den Datenverkehr verteilt

Wenn eine einzige Datenbankmigration deine gesamte Plattform lahmlegt oder ein fehlerhaftes Deployment alle Nutzer gleichzeitig betrifft, liegt das Problem nicht bei der Migration oder dem Deployment, sondern bei der Architektur. Die zellenbasierte Architektur löst dieses Problem, indem sie dein System in unabhängige Zellen partitioniert, von denen jede eine Teilmenge der Nutzer mit eigener, isolierter Infrastruktur bedient.

Wenn Zelle 7 ein fehlerhaftes Deployment hat, sind nur die Nutzer betroffen, die an Zelle 7 geroutet werden. Die anderen Zellen arbeiten normal weiter. Dieses Muster treibt einige der zuverlässigsten Systeme im großen Maßstab an.

Was eine Zelle ausmacht

Eine Zelle ist eine vollständige, unabhängige Kopie deines Service-Stacks, die eine Teilmenge des Traffics verarbeitet. Jede Zelle hat eigene Compute-, Storage-, Cache- und Queue-Ressourcen. Zellen teilen zur Laufzeit nichts miteinander.

tstypescript
interface Cell {
  id: string;
  region: string;
  capacity: number; // max tenants or users
  currentLoad: number;
  services: CellService[];
  status: "healthy" | "degraded" | "draining" | "offline";
}
 
interface CellService {
  name: string;
  instances: number;
  database: string;      // Cell-dedicated database
  cache: string;         // Cell-dedicated cache cluster
  messageQueue: string;  // Cell-dedicated queue
}
 
// ❌ Shared infrastructure — single point of failure
const sharedSetup = {
  apiServers: "api-cluster (all users)",
  database: "main-db (single instance, all data)",
  cache: "redis-cluster (shared)",
  // One bad query, one migration, one cache flush → everyone affected
};
tstypescript
// ✅ Cell-isolated infrastructure — blast radius contained
const cells: Cell[] = [
  {
    id: "cell-01",
    region: "us-east-1",
    capacity: 10000,
    currentLoad: 7500,
    status: "healthy",
    services: [
      {
        name: "api",
        instances: 4,
        database: "cell-01-postgres",
        cache: "cell-01-redis",
        messageQueue: "cell-01-sqs",
      },
    ],
  },
  {
    id: "cell-02",
    region: "us-east-1",
    capacity: 10000,
    currentLoad: 6200,
    status: "healthy",
    services: [
      {
        name: "api",
        instances: 4,
        database: "cell-02-postgres",
        cache: "cell-02-redis",
        messageQueue: "cell-02-sqs",
      },
    ],
  },
];

Das zentrale Prinzip: Zellen teilen zur Laufzeit nichts. Keine gemeinsamen Datenbanken, keine gemeinsamen Caches, keine gemeinsamen Queues. Zellübergreifende Kommunikation läuft ausschließlich über explizit dafür vorgesehene asynchrone Kanäle.

Zell-Routing

Eine dünne Routing-Schicht leitet jede Anfrage anhand der Tenant-ID, der User-ID oder eines anderen stabilen Partitionsschlüssels an die richtige Zelle. Dieser Router muss extrem zuverlässig sein, da er die einzige gemeinsam genutzte Komponente ist.

tstypescript
interface CellRouter {
  routingTable: Map<string, string>; // tenantId → cellId
  defaultCell: string;
}
 
class Router {
  private assignments: Map<string, string>;
  private cells: Map<string, Cell>;
 
  constructor(cells: Cell[]) {
    this.assignments = new Map();
    this.cells = new Map(cells.map(c => [c.id, c]));
  }
 
  routeRequest(tenantId: string): string {
    // Check existing assignment
    const assigned = this.assignments.get(tenantId);
    if (assigned) {
      const cell = this.cells.get(assigned);
      if (cell && cell.status === "healthy") {
        return assigned;
      }
      // Cell is unhealthy — don't reroute automatically
      // Drain explicitly to maintain data locality
      if (cell && cell.status === "degraded") {
        return assigned; // Still route, cell is partially working
      }
    }
 
    // New tenant — assign to cell with lowest load ratio
    return this.assignToCell(tenantId);
  }
 
  private assignToCell(tenantId: string): string {
    let bestCell: Cell | null = null;
    let bestRatio = Infinity;
 
    for (const cell of this.cells.values()) {
      if (cell.status !== "healthy") continue;
      const ratio = cell.currentLoad / cell.capacity;
      if (ratio < bestRatio) {
        bestRatio = ratio;
        bestCell = cell;
      }
    }
 
    if (!bestCell) {
      throw new Error("No healthy cells available");
    }
 
    this.assignments.set(tenantId, bestCell.id);
    bestCell.currentLoad++;
    return bestCell.id;
  }
 
  drainCell(cellId: string, targetCellId: string): string[] {
    const movedTenants: string[] = [];
    const targetCell = this.cells.get(targetCellId);
 
    if (!targetCell || targetCell.status !== "healthy") {
      throw new Error(`Target cell ${targetCellId} is not healthy`);
    }
 
    for (const [tenant, cell] of this.assignments) {
      if (cell === cellId) {
        this.assignments.set(tenant, targetCellId);
        targetCell.currentLoad++;
        movedTenants.push(tenant);
      }
    }
 
    const sourceCell = this.cells.get(cellId);
    if (sourceCell) {
      sourceCell.status = "draining";
      sourceCell.currentLoad = 0;
    }
 
    return movedTenants;
  }
}

Die Routing-Schicht selbst muss zustandslos sein und die Zuweisungen aus einem schnellen, replizierten Store lesen. Sie fügt nur minimale Latenz hinzu – einen einzigen Lookup pro Anfrage.

Sichere Deployments mit Zellen

Zellen ermöglichen inkrementelle Deployment-Strategien, die mit geteilter Infrastruktur unmöglich sind. Deploye auf eine Zelle, beobachte sie und rolle dann schrittweise weiter aus.

tstypescript
interface DeploymentPlan {
  version: string;
  stages: DeploymentStage[];
  rollbackTriggers: RollbackTrigger[];
}
 
interface DeploymentStage {
  cells: string[];
  trafficPercentage: number;
  observationPeriod: string;
  successCriteria: SuccessCriterion[];
}
 
interface SuccessCriterion {
  metric: string;
  threshold: number;
  comparison: "less_than" | "greater_than";
}
 
interface RollbackTrigger {
  metric: string;
  threshold: number;
  window: string;
}
 
const deploymentPlan: DeploymentPlan = {
  version: "v2.5.0",
  stages: [
    {
      cells: ["cell-canary"],
      trafficPercentage: 2,
      observationPeriod: "30m",
      successCriteria: [
        { metric: "error_rate", threshold: 0.01, comparison: "less_than" },
        { metric: "p99_latency_ms", threshold: 500, comparison: "less_than" },
      ],
    },
    {
      cells: ["cell-01", "cell-02"],
      trafficPercentage: 20,
      observationPeriod: "1h",
      successCriteria: [
        { metric: "error_rate", threshold: 0.005, comparison: "less_than" },
        { metric: "p99_latency_ms", threshold: 400, comparison: "less_than" },
      ],
    },
    {
      cells: ["cell-03", "cell-04", "cell-05", "cell-06"],
      trafficPercentage: 60,
      observationPeriod: "2h",
      successCriteria: [
        { metric: "error_rate", threshold: 0.005, comparison: "less_than" },
        { metric: "p99_latency_ms", threshold: 400, comparison: "less_than" },
      ],
    },
    {
      cells: ["cell-07", "cell-08", "cell-09", "cell-10"],
      trafficPercentage: 100,
      observationPeriod: "4h",
      successCriteria: [
        { metric: "error_rate", threshold: 0.005, comparison: "less_than" },
        { metric: "p99_latency_ms", threshold: 400, comparison: "less_than" },
      ],
    },
  ],
  rollbackTriggers: [
    { metric: "error_rate", threshold: 0.02, window: "5m" },
    { metric: "p99_latency_ms", threshold: 1000, window: "5m" },
  ],
};

Wenn die Canary-Zelle erhöhte Fehlerraten zeigt, rollst du eine einzige Zelle zurück, während 98 % der Nutzer keine Auswirkungen spüren. Vergleiche das mit dem Rollback eines monolithischen Deployments, das alle betrifft.

Zellübergreifende Datenmuster

Der schwierige Teil der Zellarchitektur ist der Umgang mit Daten, die Zellgrenzen überschreiten. Kommunikation zwischen Nutzern, gemeinsame Referenzdaten und Analytics erfordern zellübergreifende Strategien.

tstypescript
interface CrossCellPattern {
  pattern: string;
  useCase: string;
  tradeoff: string;
}
 
const crossCellPatterns: CrossCellPattern[] = [
  {
    pattern: "Async replication of reference data",
    useCase: "Product catalog, feature flags, configuration",
    tradeoff: "Eventually consistent — cells may see stale data briefly",
  },
  {
    pattern: "Event bus for cross-cell notifications",
    useCase: "User A in cell-01 messages User B in cell-03",
    tradeoff: "Adds latency vs direct call, but preserves isolation",
  },
  {
    pattern: "Global read replica for analytics",
    useCase: "Cross-cell reporting and dashboards",
    tradeoff: "Read-only aggregate view, not suitable for transactional queries",
  },
];
 
// Event-based cross-cell communication
interface CrossCellEvent {
  sourceCell: string;
  targetCell: string;
  eventType: string;
  payload: Record<string, unknown>;
  timestamp: Date;
  idempotencyKey: string;
}
 
function publishCrossCellEvent(event: CrossCellEvent): void {
  // Publish to global event bus (SNS, EventBridge, Kafka)
  // Target cell's consumer picks it up independently
  // Idempotency key prevents duplicate processing
  globalEventBus.publish({
    topic: `cross-cell.${event.eventType}`,
    message: event,
    deduplicationId: event.idempotencyKey,
  });
}

Die wichtigsten Erkenntnisse

Die zellenbasierte Architektur tauscht betriebliche Einfachheit gegen Resilienz. Jede Zelle ist eine in sich geschlossene Einheit mit eigenen Datenspeichern, Compute und Queues – zur Laufzeit wird nichts geteilt. Eine dünne Routing-Schicht weist Tenants dauerhaft Zellen zu und schafft so Datenlokalität und Fehlerisolation. Deployments laufen schrittweise durch die Zellen: zuerst der Canary, dann immer breitere Wellen, mit automatischem Rollback, wenn sich die Health-Metriken verschlechtern. Zellübergreifende Belange wie Messaging und Analytics fließen über asynchrone Kanäle, niemals über gemeinsame Datenbanken. Das Ergebnis ist ein System, in dem der Blast Radius jedes Fehlers – fehlerhaftes Deployment, Datenbankproblem, Infrastrukturausfall – auf eine einzelne Zelle begrenzt ist statt auf die gesamte Plattform. Fang mit zwei Zellen und einem Router an. Die Disziplin, von Tag eins an echte Isolation durchzuhalten, ist das, was das Muster funktionieren lässt.

Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX