all

Case study

Oporavak RabbitMQ klastera nakon ispada noda: kako je nedovoljno pracenje dovelo do split-brain klastera

Klijent nas je kontaktirao nakon sto je aplikacija pocela povremeno da omasuje pri upisu u RabbitMQ Streams.

Neobicno je bilo to sto RabbitMQ nije bio potpuno nedostupan.

Neki zahtevi su prolazili. Drugi su padali sa greskama slicnim sledecim:

1
Stream coordinator unavailable

RabbitMQ logovi su takodje sadrzali ponovljene timeout-e za coordinator-a:

1
2
new_stream coordinator timeout on rabbitmq-slough-2
coordinator unavailable

Sa strane aplikacije, otkazi su delovali gotovo nasumicno.

Sva tri RabbitMQ poda su takodje bila u stanju Running, pa nije bilo ociglednog ispada na nivou Kubernetes-a.

Pravi problem je bio dublji: RabbitMQ nodovi vise nisu bili saglasni oko stanja klastera, a Stream coordinator je imao vrlo razlicite Raft istorije na razlicitim nodovima.

To je promenilo pitanje oporavka sa:

Koji RabbitMQ nod je ziv?

na:

Koji nodovi sadrze najnovije konzistentno stanje koje mora da se sacuva?

Incident

Klaster se sastojao od tri RabbitMQ noda:

1
2
3
rabbitmq-slough-0
rabbitmq-slough-1
rabbitmq-slough-2

koji su pokretali RabbitMQ 4.1.3 sa Erlang 27.3.4.

Na Kubernetes nivou, sve je u pocetku izgledalo prihvatljivo:

1
2
3
rabbitmq-slough-0   Running   0 restarts
rabbitmq-slough-1   Running   0 restarts
rabbitmq-slough-2   Running   0 restarts

Ali running pod samo dokazuje da je kontejner ziv. Ne dokazuje da broker i dalje ispravno ucestvuje u RabbitMQ klasteru.

Kada smo proverili sam RabbitMQ:

1
rabbitmqctl cluster_status

rabbitmq-slough-0 je prijavio samo sebe kao clana klastera.

Sa njegove tacke gledista nije bilo prijavljenih particija niti alarma.

To je vec bilo sumnjivo: nod koji veruje da je zdrav jednocvorni klaster veoma je razlicit od ocekivanog trocvorog klastera.

  flowchart LR
    APP[Application] --> S[RabbitMQ Service]

    S --> R0[rabbitmq-slough-0]
    S --> R1[rabbitmq-slough-1]
    S --> R2[rabbitmq-slough-2]

    R0 -. different cluster view .-> R1
    R1 <--> R2

    APP --> ERR[Intermittent<br/>Stream errors]

Samo clanstvo u klasteru nije bilo dovoljno

Cinjenica da se klaster podelio i dalje nije govorila koje stanje treba da prezivi oporavak.

Za RabbitMQ Streams, vazan dokaz bio je unutar Stream coordinator-a.

Pregledali smo ga na svakom nodu:

1
rabbitmqctl eval 'rabbit_stream_coordinator:status().'

Kada su nodovi mogli dovoljno da komuniciraju za poredjenje, razlika je bila jasna.

NodeStateTermLog / commitSnapshot
rabbitmq-slough-0pre_vote233,794none
rabbitmq-slough-1leader241,537,3971,531,904
rabbitmq-slough-2follower241,537,3971,531,904

Ovo je bio kljucni dokaz.

rabbitmq-slough-0 je imao commit poziciju od samo:

1
3794

dok su se druga dva noda slagala oko:

1
1537397

Takodje su se slagali oko novijeg Raft term-a:

1
24

i oko istog snapshot-a:

1
1531904

rabbitmq-slough-1 je bio leader, a rabbitmq-slough-2 njegov follower.

  flowchart TB
    R0["rabbitmq-slough-0<br/>pre_vote<br/>term 23<br/>commit 3,794"]

    subgraph AUTH["Newest consistent state"]
        R1["rabbitmq-slough-1<br/>LEADER<br/>term 24<br/>commit 1,537,397"]
        R2["rabbitmq-slough-2<br/>FOLLOWER<br/>term 24<br/>commit 1,537,397"]

        R1 <--> R2
    end

    R0 -. divergent history .-> AUTH

Koje stanje je bilo autoritativno?

Odgovor nije bio odredjen staroscu poda, Kubernetes zdravljem ili time koji je broker slucajno prihvatio klijentsku konekciju.

Najjaci dokaz bilo je slaganje vise Raft glasaca.

Dva noda su nezavisno prijavila:

  • isti trenutni term;
  • istu commit poziciju;
  • isti snapshot;
  • ispravan leader/follower odnos.

Izolovani nod je prijavio dramatično stariju istoriju i ostao u pre_vote.

To je odluku uciniло jednostavnom:

1
2
3
4
5
rabbitmq-slough-1  \
                   > state to preserve
rabbitmq-slough-2  /

rabbitmq-slough-0  -> stale/divergent coordinator state

Ova razlika je vazna tokom oporavka.

Komande kao sto su reset, force_reset, force_boot, offline uklanjanje noda ili ponovo kreiranje persistent storage-a mogu da odbace stanje ili redefinisu koji klaster metadata prezivljava.

Zato je bezbedan redosled sledeci:

  1. prikupiti stanje sa svakog noda;
  2. uporediti Raft term, commit index, ulogu i snapshot;
  3. utvrditi koji nodovi se slazu oko najnovije istorije;
  4. zastititi to stanje;
  5. tek onda uskladiti zastareli nod i obnoviti normalno clanstvo u klasteru.

Klaster koji se uspesno formira oko pogresnog stanja i dalje je neuspesan oporavak.

Zasto su padali samo neki upisi?

Pocetni simptom aplikacije sada je imao smisla.

Procesi brokera su bili zivi, ali RabbitMQ vise nije radio sa jednim konzistentnim stanjem Stream coordinator-a.

Neke operacije su i dalje mogle da se zavrse kroz dostupne puteve.

Druge su zahtevale aktivnost coordinator-a i padale sa:

1
Stream coordinator unavailable

Zato je incident izgledao povremeno, a ne kao potpuni RabbitMQ ispado.

To takodje objasnjava zasto su obicne infrastrukturne provere to propustile.

Monitoring:

1
2
3
pod is Running
process exists
TCP port is open

nije bio dovoljan.

Kvar je bio u distribuiranom stanju RabbitMQ klastera.

Korenski uzrok: degradacija klastera nije bila otkrivena

Oporavak ispravnog RabbitMQ stanja resio je trenutni incident.

Ali to nije bio kompletan fix.

Produkcioni RabbitMQ klaster sa tri noda ne bi trebalo da neprimetno degradira u razdvojene prikaze klastera i da tako ostane dok aplikacije ne pocnu da vracaju greske.

Ovaj incident je trebalo mnogo ranije da bude otkriven.

  flowchart LR
    A[Inter-node connectivity fails] --> B[RabbitMQ cluster degrades]

    B --> C{Cluster alerting?}

    C -->|Yes| D[Alert]
    D --> E[Investigate before<br/>application impact]

    C -->|No| F[Failure remains unnoticed]
    F --> G[Coordinator problems]
    G --> H[Application write errors]

Nedostajala je kontrola pracenja RabbitMQ cluster connectivity, a ne samo dostupnosti Kubernetes poda.

Upozori kada RabbitMQ nod izgubi svoje peerove

RabbitMQ izloži stanje Erlang distribution konekcija izmedju nodova kroz Prometheus metrike.

Za fiksni trocvorni klaster, svaki broker bi normalno trebalo da vidi dve peer konekcije u up stanju.

Alarm zato moze da proveri ovu invarijantu.

Na primer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
groups:
  - name: rabbitmq-cluster
    rules:
      - alert: RabbitMQClusterPeerConnectivityDegraded
        expr: |
          (
            max by (namespace, pod) (
              rabbitmq_identity_info{job="rabbitmq"}
            )
            unless on (namespace, pod)
            (
              count by (namespace, pod) (
                erlang_vm_dist_node_state{job="rabbitmq"} == 3
              ) >= 2
            )
          ) == 1
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "RabbitMQ cluster peer connectivity degraded"
          description: >-
            RabbitMQ node {{ $labels.pod }} does not see all expected
            cluster peers. A three-node cluster should have two active
            Erlang distribution peer links per node.

Tacne labele zavise od Prometheus konfiguracije, ali princip je vazniji od samog upita.

Alarm bi trebalo da odgovori na pitanje:

Da li svaki RabbitMQ broker i dalje moze da komunicira sa svim ocekivanim cluster peers?

umesto samo:

Da li tri RabbitMQ poda rade?

Za ovo okruzenje, pracena invarijanta postaje:

1
2
3
Expected RabbitMQ nodes:        3
Expected peer links per node:   2
Persistent deviation allowed:   0

Kratak for interval sprecava da prolazni restarti izazovu nepotrebne alarme, a i dalje otkriva stvarni split klastera mnogo pre nego sto stigne do aplikacionog saobracaja.

Prakticna lekcija

Prilikom oporavka distribuiranog sistema, nod koji je ziv nije nuzno onaj cije stanje treba da prezivi.

Za RabbitMQ Streams, uporedite samo distribuirano stanje.

U ovom incidentu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
rabbitmq-slough-0
  term:     23
  commit:   3,794
  state:    pre_vote

rabbitmq-slough-1
  term:     24
  commit:   1,537,397
  state:    leader
  snapshot: 1,531,904

rabbitmq-slough-2
  term:     24
  commit:   1,537,397
  state:    follower
  snapshot: 1,531,904

Slaganje izmedju nodova 1 i 2 dalo nam je dokaz potreban da sacuvamo ispravno stanje umesto da slepo rebuild-ujemo klaster oko bilo kog brokera koji je slucajno bio dostupan.

Ali oporavak je bio samo pola resenja.

Dublji operativni problem bio je to sto je RabbitMQ klaster vec degradirao bez generisanja alarma.

Oporavak je resio incident. Pracenje zdravlja klastera je resilo korenski uzrok.

Za produkcione incidente gde Kubernetes resursi izgledaju zdravo, ali aplikacije i dalje dozivljavaju povremene infrastrukturne otkaze, pogledajte nasu DevOps troubleshooting uslugu.