Dissecting GitHub's ZooKeeper Outage: Split-Brain Consensus and Fallback Strategies

Arpit Bhayani

Arpit Bhayani

Jul 01, 2022 • 8 min read

Play

Dissecting GitHub’s ZooKeeper Outage: Split-Brain Consensus and Fallback Strategies

Distributed systems are inherently prone to obscure edge cases. During a routine infrastructure maintenance window, GitHub suffered an outage originating from a subtle failure mode: a set of nodes in an Apache ZooKeeper cluster ran an unscheduled election and elected a second active leader. This formed two distinct logical clusters within the same physical environment, cascading into Apache Kafka and causing write failures across GitHub’s background job infrastructure.

Despite the severity of this split-brain scenario, GitHub suffered zero data loss. This breakdown analyzes the root cause of the incident, how the failure cascaded through Kafka, and the architectural patterns that mitigated what could have been catastrophic data corruption.


1. ZooKeeper’s Role in an Apache Kafka Cluster

To understand how the failure propagated, one must examine how Apache Kafka traditionally relies on ZooKeeper as its central coordination engine and source of truth.

                    +----------------------------+
                    |     ZooKeeper Ensemble     |
                    |  (Metadata, Leader State)  |
                    +--------------+-------------+
                                   |
        +--------------------------+--------------------------+
        |                                                     |
        v                                                     v
+---------------+                                     +---------------+
| Kafka Broker 1|                                     | Kafka Broker 2|
|  [Controller] |                                     |   [Follower]  |
+-------+-------+                                     +-------+-------+
        |                                                     |
        +--------------------------+--------------------------+
                                   |
                                   v
                      +--------------------------+
                      | Topics, Partitions, ACLs |
                      +--------------------------+

(Note: While Kafka version 2.8+ introduced KRaft to manage consensus internally without ZooKeeper, the principles of consensus, metadata propagation, and split-brain resolution remain universal).

In classical deployments, ZooKeeper fulfills four critical responsibilities for Kafka:

  1. Controller Election: Kafka designates one broker as the cluster controller. This controller manages state transitions for partitions, tracks broker availability, and commands replica reassignments. ZooKeeper handles the election of this controller.
  2. Cluster Membership: ZooKeeper tracks which brokers are alive, joining, or leaving via ephemeral nodes (znode heartbeats).
  3. Topic and Partition Metadata: Information on topic definitions, partition counts, leader replicas, and in-sync replicas (ISR) is stored directly in ZooKeeper.
  4. Access Control Lists (ACLs) and Quotas: Operational permissions and throughput constraints for clients are maintained within ZooKeeper nodes.

If ZooKeeper produces conflicting or split-brain state, Kafka brokers and client libraries lose the ability to establish a single source of truth for message ingestion.


2. The Root Cause: Rapid Reprovisioning and Second Leader Election

During routine operations—such as OS patch cycles, kernel updates, or security rotations—ZooKeeper nodes must be replaced. In this incident, new ZooKeeper nodes were reprovisioned too quickly.

The Autonomous Bootstrap Race Condition

ZooKeeper nodes are autonomous. When a new node bootstraps into a cluster, it executes a self-discovery process:

  1. It attempts to locate the existing leader and sync state.
  2. If it cannot contact or discover an existing leader within a given timeout, it assumes the cluster is leaderless.
  3. It initiates a Fast Leader Election (FLE) based on quorum voting.

When a large batch of fresh nodes was introduced simultaneously, these new nodes faced network contention or initialization delays, preventing them from discovering the pre-existing leader in time. Because these new nodes formed a numerical majority amongst themselves relative to their immediate discovery scope, they reached a quorum independently.

Physical Infrastructure
+-------------------------------------------------------------------------+
|                                                                         |
|   [Logical Cluster A]                            [Logical Cluster B]    |
|   +-------------------+                          +-------------------+  |
|   | Old Leader        |                          | Newly Elected     |  |
|   | + Older Followers |                          | Leader + Followers|  |
|   +---------+---------+                          +---------+---------+  |
|             |                                              |            |
|             v                                              v            |
|   Kafka Brokers (1..N-1)                         Kafka Broker N         |
|   (Pointed to Cluster A)                         (Pointed to Cluster B) |
+-------------------------------------------------------------------------+

The result: A split-brain state where two distinct logical ZooKeeper ensembles operated within the same physical environment, each convinced it was the authoritative leader.


3. How the Failure Cascaded to Kafka

Once the two logical ZooKeeper clusters coexisted, the corruption trickled down to the messaging tier:

  1. A Rogue Kafka Controller: A single Kafka broker joined or re-established its connection. Instead of discovering the primary ZooKeeper ensemble, it connected to the newly formed second ZooKeeper ensemble.
  2. Self-Appointed Control: The second ZooKeeper cluster had an empty or partial state and indicated that no Kafka controller existed. The single broker created the ephemeral controller node and appointed itself the cluster controller.
  3. Conflicting State Propagation: With two active Kafka controllers operating against two disjoint ZooKeeper databases, the system state bifurcated.
  4. Client-Side Conflicting Metadata: When clients attempted to write data, they fetched metadata regarding which broker led a particular topic-partition. Depending on which ZooKeeper/Kafka node answered the discovery request, clients received contradictory routing tables.
sequenceDiagram
    autonumber
    participant Client as Client Application
    participant ZK1 as ZooKeeper Cluster A
    participant ZK2 as ZooKeeper Cluster B
    participant B1 as Broker 1 (Controller A)
    participant BN as Broker N (Controller B)

    Client->>ZK1: Query partition metadata
    ZK1-->>Client: Route to Broker 1 (Leader)
    Client->>ZK2: Query partition metadata (subsequent call)
    ZK2-->>Client: Route to Broker N (Leader)
    Note over Client: Conflicting metadata detected!
    Client->>Client: Abort Write / Drop Request (Safety Measure)

When production clients received conflicting states across discovery attempts, strict consistency checks triggered write failures. Approximately 10% of requests to GitHub’s internal background job processing service failed.

Fortunately, because only a single broker connected to the rogue cluster, widespread disjoint writes across two sets of functioning partition leaders did not occur, preventing silent data divergence.


4. Mitigation and the Zero-Data-Loss Architecture

Despite write failures on the primary path, GitHub avoided message loss through robust defensive design: the Dead-Letter Queue (DLQ) and Secondary Job Processing Pipeline.

                               +-------------------------+
                               |   Client Request / Job  |
                               +------------+------------+
                                            |
                                            v
                               +-------------------------+
                               | Try Write to Kafka Main |<-----+ (Retries w/ Backoff)
                               +------------+------------+      |
                                            |                   |
                      +---------------------+-------------------+ 
                      | Failure                                 | Success
                      v                                         v
          +-----------------------+                    +-----------------+
          | Dead-Letter Queue     |                    | Kafka Cluster   |
          | (Secondary Broker)    |                    +--------+--------+
          +-----------+-----------+                             |
                      |                                         v
                      v                                +-----------------+
          +-----------------------+                    | Primary Workers |
          | Secondary Workers     |                    +-----------------+
          +-----------------------+

The Mechanics of the Fallback Queue

  1. Bounded Retries with Backoff: When a background job producer attempts to push to Kafka and encounters a transient failure, it retries 3 to 4 times with exponential backoff.
  2. Fallback Diversion: If retries are exhausted (e.g., due to contradictory routing responses), the client diverts the payload to an isolated secondary queuing mechanism—a Dead-Letter Queue.
  3. Throughput vs. Durability Trade-off:
    • Primary Message Stream (Kafka): Optimized for high throughput, ordered partitions, and high-volume consumers.
    • Secondary Fallback (DLQ): Typically built on a simpler, decoupled broker (such as RabbitMQ, SQS, or Redis-backed queues) that prioritizes durability and immediate availability over raw multi-partition throughput.
  4. Queue Backup (Lag) without Loss: As primary writes failed, the secondary system absorbed the load. Because the secondary system was not provisioned for the sheer concurrency of the primary stream, jobs backed up, increasing latency. However, every payload was persisted. Once ZooKeeper was remediated, the secondary workers drained the backlog without missing a single job.

5. Recovery: Self-Healing vs. Manual Intervention

ZooKeeper has internal convergence routines, but relying on autonomous healing during split-brain states carries risks:

  • Autonomous Re-convergence: In theory, as discovery timeouts settle and network links stabilize, an ensemble can detect invalid quorums and force nodes into follower states. However, this convergence is non-deterministic and can prolong metadata thrashing.
  • Manual Remediation: To resolve the outage swiftly, engineers intervened:
    1. Identified the rogue ZooKeeper nodes via configuration discrepancies and metric anomalies.
    2. Severed connections to the second logical ensemble.
    3. Terminated the rogue ZooKeeper instances and their self-appointed Kafka controller broker.
    4. Allowed the original ZooKeeper leader to re-assert its single source of truth across the fleet.

6. Key Architectural Takeaways

This incident highlights several core distributed systems patterns applicable to modern infrastructure design:

1. Provision Infrastructure with Jitter

Never spin up or tear down distributed nodes simultaneously in a short window. Always introduce jitter (randomized staggered delays) when rolling out configurations, upgrades, or node replacements. Staggering transitions allows new instances to discover the established leader before triggering unnecessary fallback election timeouts.

2. Idempotent Consumers Are Mandatory

When routing tables fail and fallbacks activate, messages are frequently re-transmitted. Both primary and secondary queue consumers must be designed as strictly idempotent. Process(m)=Process(Process(m))\text{Process}(m) = \text{Process}(\text{Process}(m)) Whether through unique idempotency keys, database upserts, or version checks, processing duplicate deliveries must produce the same end-state.

3. Implement Client-Side Defensive Retries

Clients must never treat message brokers as zero-latency, 100% available networks. Use bounded retries with exponential backoff and jitter. If an operational cluster is flapping, rapid naive retries act as a self-inflicted Denial of Service (thundering herd).

4. Always Maintain a Secondary Fallback Pipeline

A primary queue failure should never mean dropping data. Designing an explicit fallback strategy—diverting unroutable payloads to an alternate datastore or simpler queue—ensures that infrastructure degradations result in temporary lag, not data loss.

Arpit Bhayani

Principal Engineer II at Razorpay - building Agent Studio, Ex-staff engg at GCP Memorystore & Dataproc, Creator of DiceDB, ex-Amazon Fast Data, ex-Director of Engg. SRE and Data Engineering at Unacademy. I spark engineering curiosity through my no-fluff engineering videos on YouTube and my courses