Przetwarzanie rozproszone

Problemy konsensusu

dr inż. Arkadiusz Danilecki

Plan wykładu

  1. Raft

Raft

Raft

Leslie Lamport w 1989 zaproponował algorytm Paxos, który doczekał się kilku implementacji (używany jest przez google, microsoft, amazon) ... oraz artykułów próbujących wyjaśnić laikom, jak Paxos działa.

Z uwagi na skomplikowanie Paxos, w 2013 Ongaro i Ousterhout zaproponowali prostszy algorytm (Raft), którego jednym z celów było to, by był bardziej zrozumiały. Paxos zostanie omówiony na przedmiocie Systemy Wysokiej Niezawodności.

Ważne - zarówno Paxos, jak i Raft zapewniają własność bezpieczeństwa, ale w ogólności nie zapewniają postępu!

Raft

Założenia

Awarie są rzadkie (i krótkotrwałe)

System jest częściowo synchroniczny - zegary są mniej-więcej zsynchronizowane, czasy przesyłania wiadomości są znane i można odpowiednio dobrać okresy elekcji, pulsu itd wspomniane dalej w algorytmie.

Kanały są niezawodne.

Raft

Skład grupy się nie zmienia i jest znany wszystkim procesom oraz wszystkim klientom. Używany model to crash-recovery.

Przyjmujemy, że procesy można modelować jako maszyny stanów.

Raft

Oryginalny algorytm używa RPC niby "bo są łatwiejsze". Pseudokod dalej został przerobiony na używanie wiadomości.

Raft nie używa detektorów błędów, ale my pokażemy jak zapisać algorytm z użyciem tej abstrakcji.

Wytłumaczymy Rafta po kawałku, by było łatwiej go zrozumieć.

Raft

Raft pracuje w dwóch trybach - normalnym oraz elekcji.

W normalnym trybie istnieje jeden lider, pozostałe procesy są wykonawcami (ang. follower). Tylko lider wykonuje zlecenia klienta.

Do trybu elekcji przechodzimy, gdy podejrzewamy awarię lidera. W trybie elekcji procesy mogą przyjąć rolę kandydata na lidera. Po wyborze nowego lidera wracamy do normalnego trybu.

Raft

Szkic stanów procesów

Raft

Klient

Klienci zawsze komunikują się z liderem. Procesy nie będące liderem przekierowują żądania do lidera lub powiadamiają klienta, kto jest liderem. Klient powtarza żądania, aż otrzyma odpowiedź i ignoruje kolejne odpowiedzi. W dalszym opisie zignorujemy żądania nie zmieniające stanu.

Lider odpowiada klientowi dopiero, gdy zatwierdzi i wykona jego żądanie - co będzie wymagało wymiany wiadomości z wykonawcami (zobaczymy to później).

Raft

Klient

Chcemy zapewnić semantykę exactly-once.

Żądania klientów są rozróżnialne (np. mają identyfikator). Lider nie aplikuje ponownie starych żądań. Jeżeli żądanie z danym identyfikatorem już istnieje w logu, i nie jest zatwierdzone, lider nic nie robi. Jeżeli jest już zatwierdzone, odpowiada natychmiast.

Odpowiedzi mogą być przechowywane w pamięci podręcznej i na stare żądanie lider może odesłać odpowiedź z pamięci podręcznej.

Raft

Każdy proces utrzymuje log zawierający sekwencję poleceń, oraz wskaźnik na ostatnią zatwierdzoną pozycję.

Po otrzymaniu żądania, lider dopisuje je do logu w pamięci stałej. (actually tymczasowo może być w pamięci ulotnej)

Lider regularnie wywołuje AppendEntries, czyli wysyła wykonawcom listę poleceń do zatwierdzenia. Lider na razie nie wykonuje trzymanych w logu, otrzymanych od klienta poleceń (tj. nie zmienia stanu) i niczego na razie jeszcze zatwierdza.

Raft

Każdy proces posiada numer kadencji term, zwiększany przy każdej elekcji.

Z każdym wpisem w logu zapamiętany jest numer kadencji term w którym dokonano wpisu, oraz index.

Każda wiadomość wysyłana przez proces zawiera jego aktualny term.

Raft

Raft

Raft

Raft

Podsumowując - kiedy odpowiadamy klientowi?

  1. Większość procesów potwierdziła otrzymanie
  2. Zatwierdziliśmy operację, wykonaliśmy ją (zmieniając stan)
  3. W przyszłości powiadomimy wykonawców, by też zatwierdzili i wykonali operację (jak?)
  4. Jest jeszcze jeden detal, ale powiemy o nim po elekcji

Raft

Elekcja

Jeżeli nie otrzymamy AppendEntries (czyli de facto pulsu) od lidera, zwiększamy term i proponujemy siebie jako lidera, prosząc innych o głosy. Wygram, gdy otrzymam zgodę od większości procesów.

Jeżeli nie uda się zdobyć większości w ciągu jakiegoś czasu, odczekuję chwilę i próbuję jeszcze raz (chyba że ktoś inny w międzyczasie został liderem.

Raft

Elekcja

Chcemy wybrać kogoś, kto będzie miał:

  1. Największy term
  2. Najbardziej aktualny log
  3. Najwięcej szczęścia

Raft

Elekcja

Największy term

Każda wiadomość (AppendEntries, RequestVote, odpowiedzi..) zawiera term.

Po otrzymaniu wiadomości z term większym od naszego:

  1. Ustalamy własny term na otrzymany
  2. Zmieniamy stan na wykonawca (także gdy byliśmy kandydatem lub liderem)
  3. Jeżeli braliśmy udział w elekcji głosując na kogoś, zapominamy o tym (czyli możemy głosować ponownie)

Raft

Elekcja

Najbardziej aktualny log

Po otrzymaniu prośby o głos z hisTerm, hisLogTerm,hisLogIndex: zgodzę się, jeżeli:

  1. Mój term (po aktualizacji) musi być równy hisTerm (de facto nie był większy przed aktualizacją)
  2. Mój log.Term musi być albo mniejszy od hisLogTerm, albo...
  3. ... albo mój log.Term jest równy hisLogTerm, ale log.Index ≤ hisLogIndex

Raft

Elekcja

Najwięcej szczęścia

Po otrzymaniu prośby o głos zgadzam się tylko jeżeli wcześniej jeszcze na nikogo nie zagłosowałem.

Restart głosowania, gdy nikt nie zdobędzie większości - losowy timeout i próbuję jeszcze raz.

Odpowiedź zawiera mój term i zgodę/niezgodę.

Raft

Elekcja

Najwięcej szczęścia

Po otrzymaniu prośby o głos zgadzam się tylko jeżeli wcześniej jeszcze na nikogo nie zagłosowałem.

A co jeżeli zgodziłem się oddać głos na A po czym uległem awarii i po wznowieniu poprosi mnie o głos proces B?

Przed wysłaniem głosu zapisuję go w trwałej pamięci.

Raft

Raft

Raft

Elekcja

Gdy otrzymamy AppendEntries od procesu A z większym hisTerm, ustawiamy term na hisTerm, uznajemy A za lidera.

Gdy otrzymamy AppendEntries od procesu A z mniejszym hisTerm, powiadamiamy A że obecny term jest już większy, a wywołanie AppendEntries się nie powiedzie. Proces A widząc, że my mieliśmy większy term zrezygnuje z funkcji lidera.

Raft

Pamiętacie ten slajd?

Podsumowując - kiedy odpowiadamy klientowi?

  1. Większość procesów potwierdziła otrzymanie
  2. Zatwierdziliśmy operację, wykonaliśmy ją (zmieniając stan)
  3. W przyszłości powiadomimy wykonawców, by też zatwierdzili i wykonali operację (jak?)
  4. Jest jeszcze jeden detal, ale powiemy o nim po elekcji

Raft

Jest jeszcze jeden detal, ale powiemy o nim TERAZ

Przenalizujmy sytuację na następnym slajdzie. Liderem jest proces A. Czy może wysłać klientowi odpowiedź na żądanie trzecie i czy to żądanie inne repliki mogą spokojnie zaaplikować?

Raft

Raft

Podsumowując - kiedy odpowiadamy klientowi?

  1. Większość procesów potwierdziła otrzymanie.
  2. Zatwierdziliśmy operację, wykonaliśmy ją (zmieniając stan)
  3. W przyszłości powiadomimy wykonawców, by też zatwierdzili i wykonali operację (jak?)
  4. Co najmniej jeden wpis z bieżącej kadencji (od bieżącego lidera) musi być potwierdzona przez większość procesów

Raft

Elekcja

Pytanie za sto punktów (czyście nie spali na wykładzie?) - co z własnością postępu?

NIE MA

... ale w praktyce działa.

Raft

Raft

Bieżący lider zrezygnuje z funkcji gdy tylko się dowie o nowej elekcji z większym term, ale to wcale nie znaczy, że nowym liderem zostanie Bonifacy.

Co ze spójnością logów po elekcji? Obecny lider mógł zacząć rozsyłać już jakieś operacje do wykonawców, może już jakieś zatwierdził...

Raft

Co ze spójnością logów po elekcji? Obecny lider mógł zacząć rozsyłać już jakieś operacje do wykonawców, może już jakieś zatwierdził...

Pamiętajmy, że procesy nie zagłosują na kogoś z mniej aktualnym logiem, a przy decyzji który log jest bardziej aktualny ważny jest nie term kandydata, ale logTerm ostatniego wpisu w logu.

  • Mój log.Term musi być albo mniejszy od hisLogTerm ... albo mój log.Term jest równy hisLogTerm, ale log.Index ≤ hisLogIndex
  • Raft

    Pamiętajmy, że procesy nie zagłosują na kogoś z mniej aktualnym logiem, a przy decyzji który log jest bardziej aktualny ważny jest nie term kandydata, ale logTerm ostatniego wpisu w logu.

    Czy Bonifacy mógł mieć wpis w logu od poprzedniego lidera, który umarł przed wyborem Archibalda, którego nie miał nikt inny? Czy ten wpis mógł być zatwierdzony?

    Raft

    A co jeżeli Bonifacy był liderem, został odcięty od pozostałych, i pozostała trójka wybierze nowego lidera? Będziemy mieli dwóch liderów! Czy to wpłynie na spójność logów? Czy klienci będą otrzymywali różne odpowiedzi?

    Czy Bonifacy będzie mógł zatwierdzić jakiekolwiek żądanie od klienta? Nie , bo nie zdobędzie potrzebnej większości.

    Raft

    Czas na pytania sprawdzające!

    Czy jest możliwe, by w systemie z czterema procesami miały one takie logi?

    Raft

    Raft

    Który log jest najświeższy, abstrahując od tego, czy takie logi są możliwe?

    Jeżeli procesy ulegały awariom i były wznawiane, w jakiej kolejności mogły być wybierane na lidera?

    Czy jest możliwe, by w systemie z czterema procesami miały one takie logi?

    Które wartości mogą być już zatwierdzone?

    Raft

    Raft

    W jakiej kolejności procesy były wybierane na liderów w przeszłości?

    Które z procesów mogą zostać wybrane w głosowaniu, gdyby zaczęło się przy takim stanie logów?

    Czy zmieni coś w odpowiedzi, jeżeli D będzie miał term największy ze wszystkich innych procesów?

    Raft

    Raft

    Po awarii lidera (lub liderów) logi procesów mogą różnić się zawartością.

    Logi są uspójniane w leniwy sposób przy wywoływaniu AppendEntries; lider optymistycznie zakłada, że logi są spójne, a gdy wykonawca powiadomi go o niespójności, lider wysyła temu wykonawcy coraz większe "łatki".

    Log lidera jest zawsze decydujący. Wpisy w innych logach, których nie ma w logu lidera, są porzucane.

    Raft

    Raft

    Raft

    Oryginalnie lider regularnie wywołuje "AppendEntries", co jest traktowane jako heartbeat; używane są też budziki ("tajmery") do ograniczania czasu elekcji, wykrywania awarii oraz minimalizowania prawdopodobieństwa konfliktów podczas elekcji.

    Raft

    Używane wiadomości

    
    message ~~addEntries~~ is a struct of 
            ~~\langle \mbox{int } term, \mbox{processId } leader, \mbox{int } prevLogIdx, \mbox{int } prevLogTerm,~~
            ~~\mbox{set of requests } entries, \mbox{int } leaderCommitIdx \rangle~~
    message ~~result~~ is a struct of 
    	~~\langle\mbox{bool } result, \mbox{int } term \rangle~~
    message ~~requestVotes~~ is a struct of 
    	~~\langle~~int ~~term, \mbox{int } lastLogIdx, \mbox{int } lastLogTerm \rangle~~
    message ~~vote~~ is a struct of ~~\langle~~int ~~value \rangle~~
    				    

    Raft

    Zmienne w pamięci trwałej

    
    persistent state $state_i$ := $nil$
    persistent map of int and struct of $\langle \mbox{request}, \mbox{int } term\rangle$ $log_i$ := $nil$
    persistent int $lastLogIdx_i$ := 0
    persistent int $lastLogTerm_i$ := 0
    persistent int $commitIdx_i$ := 0
    persistent set of response $cache_i$ := $\emptyset$
    				    

    Raft

    Pozostałe zmienne

    
    				    local int $term_i$ := 1
    				    local enum of $\left\{ \mbox{follower},\mbox{candidate},\mbox{leader}\right\}$ $mode_i$ := follower
    				    local processId $leader_i$ := $nil$
    				    local processId $votedFor_i$ := $nil$
    				    local int $timer_i$ := RANDOM
    				    local set of processId $correct_i$ := $\emptyset$
    				    local map of int and set of processId $committed_i$ := $\emptyset$
    				    local set of processId $votesGranted_i$ := $\emptyset$
    				    local set of processId $votesNotGranted_i$ := $\emptyset$
    				    local array of int $nextIdx\left[1 \ldots n\right]$ := $\{ 0, \ldots 0\}$
    				    local array of int $matchIdx\left[1 \ldots n\right]$ := $\{ 0, \ldots 0\}$
    				    

    Raft

    Obsługa żądań klienta

    
    when a message ~~request~~ arrives from client ~~C_i~~ at process ~~P_i~~ do
        if ~~leader_i \neq P_i~~ then
           redirect ~~request~~ to ~~leader_i~~
        else
           if ~~request \in cache_i~~ then
              send ~~cache_i\left[request\right]~~ to ~~C_i~~
           else if ~~request \not\in requests_i~~ then
              ~~requests_i \xleftarrow{append} request~~ 
              ~~log_i\left[ lastLogIdx \right]~~ := ~~\langle request, term_i \rangle~~
              ~~committed_i\left[ lastLogIdx_i \right]~~ := ~~\emptyset~~
              ~~lastLogIdx_i~~ := ~~lastLogIdx_i~~ + 1
           end if 
        end if
    end when
    				    

    Raft

    Obsługa żądań klienta

    
    when ~~mode_i~~ = leader at each period ~~t_i~~ at process ~~P_i~~ do
       for each process ~~P_j \in \mathcal{P}~~ do
          if ~~matchIdx_i\left[ P_j\right]~~ = ~~lastLogIdx_i~~ then
              continue
          end if
          for each ~~entry~~ in ~~log_i\left[ nextIdx_i\left[ P_j\right] \ldots lastLogIdx_i\right]~~ do
              ~~entries \leftarrow entry~~
          end for
          ~~prevLogIdx~~ := ~~nextIdx_i\left[ P_j \right]~~ - 1
          ~~prevLogTerm~~ := ~~log_i\left[ prevLogIdx \right].term~~ 
          send ~~addEntries\langle term_i, prevLogIdx, prevLogTerm, entries, commitIdx_i\rangle~~ to ~~P_j~~   
       end for
    end when
    				    

    Raft

    Obsługa żądań klienta

    
    when ~~mode_i~~ = leader and a message ~~result~~ arrives at process ~~P_i~~ from ~~P_j~~ do
      if ~~result.result~~ = true then
        for ~~N~~ from ~~matchIdx_i\left[ P_j\right]+1~~ to ~~result.last~~ do
          ~~committed_i\left[N\right] \xleftarrow{append} P_j~~
          if ~~\left|committed_i\left[ N \right]\right|\gt n/2~~ and
                          ~~log_i\left[ N\right].term~~ = ~~term_i~~ then
              for ~~k~~ from ~~commitIdx_i~~+1 to ~~N~~ do
                  apply ~~log_i[ k ].request~~ to ~~state_i~~
                  ~~commitIdx_i~~ := ~~commitIdx_i~~ + 1
                  ~~cache_i \xleftarrow{append} response~~
                  send ~~response~~ to ~~C_k~~ which originated the ~~request~~
              end for
          end if
        end for
    				    

    Raft

    Obsługa żądań klienta

    
          if ~~matchIdx_i\left[ P_j\right] \lt result.last~~ then
             ~~matchIdx_i\left[ P_j \right]~~ := ~~result.last~~
          end if
          if ~~nextIdx_i\left[ P_j\right] \lt result.last + 1~~ then
             ~~nextIdx_i\left[ P_j \right]~~ := ~~nextIdx_i\left[ P_j \right]~~ +1
          end if
       else if ~~result.term \leq term_i~~
          ~~nextIdx_i\left[ P_j \right]~~ := MAX(~~1, nextIdx_i\left[ P_j \right]~~ - 1)
       else
          ~~mode_i~~ := follower
       end if 
       if ~~term_i \lt result.term~~ then
          ~~term_i~~ := ~~result.term~~
       end if
    end when
    				    

    Raft

    Procedura AppendEntries

    
    when a message ~~addEntries~~ arrives at ~~{P}_{i}~~ from ~~{P}_{j}~~ do
       if ~~mode_i~~ = candidate or 
                 (~~mode_i~~ = leader and ~~term_i\lt addEntries.term~~) then
          ~~mode_i~~ := follower
       end if
       if ~~addEntries.term\lt term_i~~ or
          send ~~result\langle term_i, false, lastLogIdx_i\rangle~~
       else if ~~addEntries.prevLogIdx \gt lastLogIdx_i~~ or
               (~~addEntries.prevLogIdx \leq lastLogIdx_i~~ and
               ~~addEntries.prevLogTerm \neq log_i[addEntries.prevLogIdx].term~~ then
          send ~~result\langle term_i, false, lastLogIdx_i\rangle~~
       else
    				    

    Raft

    
       else
          for each ~~entry~~ in ~~addEntries.entries~~ do
              ~~log_i\left[ entry.index \right]~~ := ~~entry~~
              ~~last~~ := ~~entry.index~~
          end for
          for ~~k~~ from ~~last~~ to ~~lastLogIdx_i~~ do
              ~~log_i\left[ k \right]~~ := $nil$
          end for
          ~~lastLogIdx_i~~ := ~~last~~
    				    

    Raft

    
          if ~~addEntries.commitIdx \gt commitIdx_i~~ then
              for ~~k~~ from ~~commitIdx_i~~+1 to ~~addEntries.commitIdx~~ do
                  apply ~~log_i[ k ].request~~ to ~~state_i~~
                  ~~commitIdx_i~~ := ~~commitIdx_i~~ + 1
              end for
    	  // to chyba niepotrzebne - ~~lastLogIdx_i~~ musi być}
    	  //~~commitIdx_i~~ := MIN(~~addEntries.commitIdx, lastLogIdx_i~~)
          end if
          send ~~result\langle term_i, true, lastLogIdx_i\rangle~~
       end if
    end when
    				    

    Raft

    W oryginale każdy proces oczekuje na wywołania procedury AppendEntries, traktując ją jako heartbeat, z innymi czasami dla każdego procesu.

    
    when $FD_i$ starts suspecting $P_j$ do
        $correct_i$ := $correct_i \setminus P_j$
        if $leader_i$ = $P_j$ then
           $leader_i$ := $nil$
        end if
    end when
    when $FD_i$ stops suspecting $P_j$ do
        $correct_i \xleftarrow{append} P_j$
        if $leader_i$ = $P_j$ then
           $leader_i$ := $nil$
        end if
    end when
    				    

    Raft

    Elekcja

    
    when ~~leader_i~~ = ~~nil~~ and ~~mode_i~~ = follower at process ~~{P}_{i}~~ do
        ~~mode_i~~ := candidate
        ~~term_i~~ := ~~term_i~~ + 1
        ~~votedFor_i~~ := ~~P_i~~
        ~~votedNotGranted_i~~ := ~~\emptyset~~
        broadcast ~~requestVotes\langle term_i, lastLogIdx_i, lastLogTerm_i\rangle~~ to ~~\mathcal{P}~~ 
    end when
    				    

    Raft

    Elekcja

    
    when ~~mode_i = \mbox{candidate }~~ and 
         ~~correct_i\subseteq votesGranted_i\cup votesNotGranted_i~~ at process ~~P_i~~ do
        if ~~\left|votesGranted_i\right|\gt n/2~~ then
            ~~leader_i~~ := ~~P_i~~
            ~~mode_i~~ := leader
            for each ~~P_k~~ in ~~\mathcal{P}~~ do
                ~~nextIdx_k\left[ P_k \right]~~ := ~~lastLogIdx_i~~ + 1
                ~~matchIdx\left[ P_k \right]~~ := 0
            end for
        else
            ~~mode_i~~ := nil
            wait random time 
            ~~mode_i~~ := follower
        end if
    end when
    				    

    Raft

    Elekcja

    
    when a message ~~requestVote~~ arrives at ~~{P}_{i}~~ from ~~{P}_{j}~~ do 
       if ~~requestVote.term \geq term_i~~ then 
          if ~~mode_i\neq \mbox{leader }~~ and ~~votedFor_i = nil~~ then
             if ~~requestVote.lastLogTerm\gt lastLogTerm_i~~ or 
                  (~~requestVote.lastLogTerm = lastLogTerm_i~~ and 
                   ~~requestVote.lastLogIdx \geq lastLogIdx_i~~) then
                 ~~term_i~~ := ~~requestVote.term~~
                 ~~votedFor_i~~ := ~~requestVote.candidateId~~ 
                 send ~~vote\langle term: term_i, voteGranted: true\rangle~~ to ~~P_j~~
             end if
          end if
       end if
       if ~~votedFor_i \neq P_j~~ then
          send ~~vote\langle term: term_i, voteGranted: false\rangle~~ to ~~P_j~~
       end if 
    end when
    				    

    Raft

    Elekcja

    
    when ~~mode_i~~ = candidate and a message ~~vote~~ arrives at ~~{P}_{i}~~ from ~~{P}_{j}~~ do 
       if ~~vote.term \gt term_i~~ then
          ~~mode_i~~ = follower
       else if ~~vote.granted~~ then 
          ~~votesGranted_i \xleftarrow{append} P_j~~
       else
          ~~votesNotGranted_i \xleftarrow{append} P_j~~
       end if 
    end when
    				    

    Rekonfiguracja

    Zmiana konfiguracji mogłaby doprowadzić do sytuacji, w której różne procesy widzą różne konfiguracje, co mogłoby doprowadzić do dwóch różnych większości.

    Rozwiązanie: dwufazowy algorytm zmiany konfiguracji

    Rekonfiguracja - faza pierwsza

    Lider dostaje polecenie zmiany konfiguracji z $C_{old}$ na $C_{new}$. Polecenie to jest zapisywane w logu i wysyłane do wszystkich.

    Do czasu zatwierdzenia tej operacji, wszystkie operacje wymagają zatwierdzenia przez większość zarówno z $C_{old}$ oraz $C_{new}$ (ang. joint consensus).

    Rekonfiguracja - faza druga

    Po zatwierdzeniu operacji zmiany konfiguracji przez wszystkie procesy, lider dodaje $C_{new}$ do logu i wysyła tę operację do wszystkich procesów.

    Modyfikacja - nowa rola "learner" dla nowych węzłów w pierwszej fazie (i ewentualnie starych węzłów w starej fazie).

    Rekonfiguracja - a co jeżeli...

    Powiedzmy, że większość węzłów ze starej konfiguracji uległa awarii i dodajemy nowe węzły na miejsce tych starych. Czy ten algorytm rekonfiguracji poradzi sobie z tą sytuacją?

    Powiedzmy, że uległ awarii tylko lider starej konfiguracji. Czy ten algorytm rekonfiguracji poradzi sobie z tą sytuacją?

    Czy serializacja operacji jest zawsze potrzebna?

    1. Co jeżeli mamy operacje komutatywne (np. inkrementacja)?
    2. Operacje na różnych fragmentach współdzielonej struktury.
    3. Operacje bez znaczenia lub gdy można automatycznie je uspójnić (dopisz nowy akapit... skasuj cały rozdział).

    Raft is the biggest, the best, better than the rest.

    Czy naprawdę jest taki prosty?

    Czasami rozwiązanie typu fail-over + zewnętrzna usługa konsensus może być lepsze.

    Raft vs Paxos.

    Raft Paxos
    RethinkDB, CockroachDB, mongoDB Cassandra, Amazon DynamoDB
    etcd (używany np. w Kubernetes) Google Chubby, Spanner
    Consul (hashicorp), TiDB Microsoft Azure CosmosDB
    Vault, RabbitMQ, YugabyteDB Azure Storage
    NATS JetStream Neo4j
    Camunda, Ceph ScyllaDB
    Apache Druid Apache Zookeeper
    Raft-rs (Rust), PandaRaft, JRaft, Rafty, BRaft LibPaxos

    Tutaj kończymy...

    ... i przechodzimy do wykładu powtórkowego. Przerwa?