Top 7 Most-Used Distributed System Patterns
Summary of Top 7 Most-Used Distributed System Patterns from ByteByteGo · Published 2023-05-09 · Views: 368,251
This note was generated automatically from the video transcript.
TL;DR
Distributed system patterns such as Ambassador, Circuit Breaker, CQRS, Event Sourcing, Leader Election, Pub/Sub, and Sharding provide reusable solutions for latency, resilience, scalability, and maintainability. Understanding their trade‑offs and real‑world implementations (Envoy, Hystrix, ZooKeeper, etc.) lets you choose the right pattern for a given workload.
Key Insights
- Ambassador isolates cross‑cutting concerns (logging, retries, security) by deploying a sidecar proxy (e.g., Envoy) next to each service.
- Circuit Breaker prevents cascading failures by short‑circuiting calls to an unhealthy service and optionally providing fallback responses (Netflix Hystrix).
- CQRS splits write‑side and read‑side workloads, allowing independent scaling and optimization for each (e.g., high‑read e‑commerce catalog).
- Event Sourcing stores immutable events instead of current state, enabling full audit trails, replay, and time‑travel debugging (Git commits as a canonical example).
- Leader Election guarantees a single node performs exclusive tasks, with failover handled by coordination services like ZooKeeper or etcd.
- Pub/Sub decouples producers and consumers through an asynchronous event bus (Google Cloud Pub/Sub), improving modularity and horizontal scalability.
- Sharding partitions data across many nodes to keep per‑node load low and improve locality (MongoDB, Cassandra).
- Strangler Fig offers a gradual migration path from legacy monoliths to modern services, reducing risk compared to a “big‑bang” cut‑over.
Detailed Breakdown
1. Ambassador Pattern
- Purpose: Acts as a go‑between for an application and the services it calls, handling concerns that are orthogonal to business logic.
- Typical implementation: A sidecar proxy (Envoy, Istio’s Envoy) deployed alongside each microservice container.
- Benefits:
- Centralizes logging, metrics, retries, TLS termination.
- Reduces latency overhead compared to remote libraries because the proxy runs in the same pod/network namespace.
- Improves security by enforcing policies at the proxy layer.
- Example flow:
flowchart LR
app["Application"] --> amb["Ambassador (Envoy)"]
amb --> svcA["Service A"]
amb --> svcB["Service B"]
2. Circuit Breaker
- Purpose: Detects when a downstream service is unhealthy and stops sending requests until it recovers, avoiding cascading failures.
- State machine: Closed → Open after a failure threshold, then Half‑Open after a cooldown to test the service.
- Real‑world library: Netflix Hystrix (now superseded by resilience4j, but the concept remains).
- Diagram:
stateDiagram-v2
[*] --> Closed
Closed --> Open: failureRate > threshold
Open --> HalfOpen: timeout expires
HalfOpen --> Closed: successCount >= successThreshold
HalfOpen --> Open: failureCount > 0
3. CQRS (Command Query Responsibility Segregation)
- Purpose: Separate command (write) paths from query (read) paths so each can be tuned independently.
- Typical layout:
- Write side: receives commands, validates, persists events or state.
- Read side: builds denormalized view models (often via event handlers) optimized for fast queries.
- Typical use case: An e‑commerce catalog where product listings are read millions of times per second, but order placement (writes) occurs far less frequently.
- Flow example:
flowchart LR
client["Client"] --> cmd["Command Service"]
cmd --> eventStore["Event Store"]
eventStore --> proj["Projection Service"]
proj --> query["Query Service"]
client --> query
4. Event Sourcing
- Purpose: Persist every state‑changing event rather than the current snapshot.
- Advantages:
- Complete audit trail (replayability).
- Enables time‑travel debugging and rebuilding state for new read models.
- Analogy: Git commits—each commit is an immutable event that can be replayed to reconstruct any repository state.
- Typical components:
- Event Store (append‑only log).
- Command Handler (validates and writes events).
- Read Model Builder (projects events into query‑optimized tables).
5. Leader Election
- Purpose: Ensure exactly one node performs a critical exclusive task (e.g., distributed lock holder, master scheduler).
- Mechanisms:
- ZooKeeper: creates an EPHEMERAL sequential ZNode; the smallest sequence becomes leader.
- etcd: uses lease‑based lock with
campaignRPC.
- Failover: When the leader crashes, its session expires, causing remaining nodes to run a new election automatically.
- Flow diagram:
flowchart LR
node1["Node 1"] -->|campaign| zk["ZooKeeper"]
node2["Node 2"] -->|campaign| zk
zk --> leader["Elected Leader"]
6. Pub/Sub (Publisher/Subscriber)
- Purpose: Decouple producers from consumers; producers publish messages to a topic, and any number of subscribers receive them asynchronously.
- Key properties: At‑least‑once delivery, horizontal scaling of both publishers and subscribers, and optional message filtering.
- Real‑world service: Google Cloud Pub/Sub, Apache Kafka (though Kafka is technically a log‑based system, it’s often used as Pub/Sub).
- Diagram:
flowchart LR
pub["Publisher"] --> topic["Topic"]
topic --> subA["Subscriber A"]
topic --> subB["Subscriber B"]
7. Sharding
- Purpose: Partition a dataset across many physical nodes (shards) to keep per‑node workload manageable and improve locality.
- Shard key: Determines which shard a record belongs to (e.g., user ID hash).
- Examples:
- MongoDB: range‑based or hashed sharding via a config server.
- Cassandra: token‑ring partitioner distributes rows across nodes.
- Benefits: Linear scalability, reduced network hops for locality‑aware queries.
- Simple illustration:
flowchart LR
client["Client"] --> router["Shard Router"]
router --> shard1["Shard 1"]
router --> shard2["Shard 2"]
router --> shardN["Shard N"]
Bonus: Strangler Fig Pattern
- Purpose: Incrementally replace a legacy monolith by routing new functionality to a fresh service while the old system continues to run.
- Process:
- Identify a bounded context (e.g., user‑profile).
- Build a new microservice for that context.
- Add a router (API gateway or proxy) that forwards relevant requests to the new service, leaving the rest to the legacy code.
- Gradually migrate more contexts until the monolith can be decommissioned.
- Risk reduction: Avoids the “big‑bang” cut‑over that often leads to outages.
Trade-offs and Gotchas
- Ambassador: Adds an extra hop; misconfiguration can become a single point of failure if the sidecar crashes.
- Circuit Breaker: Incorrect thresholds can either mask real failures (too permissive) or cause premature cut‑offs (too aggressive).
- CQRS: Introduces data consistency complexity; eventual consistency between write and read models must be handled.
- Event Sourcing: Event store can grow without bound—needs compaction or snapshotting; debugging requires replaying events.
- Leader Election: Leader becomes a hotspot; if the leader performs heavy work, it may need to be sharded or delegated.
- Pub/Sub: At‑least‑once delivery can cause duplicate processing; subscribers must be idempotent.
- Sharding: Choosing a bad shard key leads to hot spots; rebalancing shards is non‑trivial and may require downtime or complex migration scripts.
- Strangler Fig: Requires a robust routing layer; latency can increase when traffic is split across old and new systems.
Takeaways
- Deploy sidecar proxies (Ambassador) to offload cross‑cutting concerns without touching application code.
- Protect against downstream failures with a properly tuned Circuit Breaker and implement sensible fallback logic.
- Use CQRS when read and write workloads have divergent performance or scaling needs, but plan for eventual consistency.
- Store immutable events if auditability, replay, or time‑travel debugging are critical; manage event store size with snapshots.
- Choose a coordination service (ZooKeeper, etcd) for reliable Leader Election when exclusive access is required.
- Leverage Pub/Sub for asynchronous, many‑to‑many communication, ensuring subscribers are idempotent.
- Partition data via sharding to achieve linear scalability, but invest time in selecting a balanced shard key.
- Adopt the Strangler Fig pattern for low‑risk migration from legacy monoliths to a microservice architecture.
Glossary
- Ambassador: A sidecar or proxy that mediates between a service and its downstream dependencies, handling concerns like retries, metrics, and security.
- Circuit Breaker: A resilience pattern that stops calls to a failing service after a threshold, optionally allowing a test request after a cooldown.
- CQRS (Command Query Responsibility Segregation): Architectural pattern separating write (command) and read (query) responsibilities into distinct models/services.
- Event Sourcing: Persistence strategy where state changes are stored as a sequence of immutable events rather than overwriting the current state.
- Leader Election: Algorithm that designates a single node as the coordinator/owner of a particular task, with automatic failover.
- Pub/Sub (Publisher/Subscriber): Messaging paradigm where publishers emit messages to a topic without knowledge of subscribers; subscribers receive messages of interest asynchronously.
- Sharding: Data partitioning technique that distributes rows/records across multiple physical nodes based on a shard key.
- Strangler Fig Pattern: Incremental migration approach that gradually replaces parts of a legacy system with new services, analogous to a strangler fig tree enveloping its host.
Leave a comment