Edit page

Coordenação e Consenso

Exclusão mútua distribuída

Já fomos introduzidos ao problema da exclusão mútua em Sistemas Operativos (SO), que tem como objetivo garantir que dois processos/threads não acedem concorrentemente ao mesmo recurso (ex: ficheiro, bloco de memória, ...), uma vez que tal pode causar incoerências (ao tentarmos ler e escrever no mesmo espaço por ex.).

Em Sistemas Distríbuidos existe o mesmo problema: é necessário coordenar os processos de forma a garantir que não acedem simultaneamente ao mesmo recurso, mas com a diferença de que agora a exclusão mútua é distribuída, baseando-se na troca de mensagens.

Algoritmos de exclusão mútua

Consideramos um sistema de NN processos pi,i=1,2,...,Np_i, i = 1, 2, ..., N, que não partilham variáveis. Os processos acedem a recursos comuns, mas fazem-no numa secção crítica.

Os requisitos essenciais para exclusão mútua são:

  • ME1 (safety): apenas um processo pode estar na secção crítica
  • ME2 (liveness): os pedidos para entrar e sair da secção crítica eventualmente são bem sucedidos (esta condição previne deadlocks e starvation)
  • ME3 (happened-before ordering): se um pedido para entrar na secção crítica ocorreu antes de outro, então a entrada é concedida nessa ordem.

Algoritmo do servidor central

Esta é a forma mais simples de alcançar a exclusão mútua, utilizando um servidor que concede permissão para entrar na secção crítica.

Para entrar na secção crítica, um processo envia um pedido ao servidor e aguarda uma resposta deste. Conceptualmente, a resposta é uma chave que significa permissão para entrar na secção crítica. Se nenhum outro processo tiver a chave, então o servidor responde imediatamente, concedendo a chave. Se a chave estiver detida por outro processo, então o servidor não responde, mas coloca o pedido numa fila de espera. Quando um processo sai da secção crítica, envia uma mensagem ao servidor, devolvendo-lhe a chave.

Algoritmo do servidor central

Desvantagens:

  • Pode existir sobrecarga do servidor.
  • Se o servidor falhar, o sistema fica bloqueado.
  • É necessário entregar a "chave" ao servidor para que este depois a passe a outro cliente (entregar diretamente ao próximo cliente seria muito mais eficiente)

Devemos assim tentar implementar uma solução descentralizada!

Algoritmo baseado em anel

Uma das formas mais simples de estabelecer exclusão mútua entre NN processos sem utilizar um processo adicional é organizá-los num anel lógico. Isto apenas requer que cada processo pip_i tenha um canal de comunicação com o próximo processo no anel, p(i+1)modNp_{(i+1) \op{mod} N}.

A ideia é que a exclusão mútua é concebida passando a chave de processo para processo numa única direção (por exemplo, no sentido horário) ao redor do anel. Se um processo não precisa de entrar na secção crítica quando recebe a chave, encaminha-a para o seu vizinho. Um processo que precise da chave espera até recebê-la (e retém-a após receber). Para sair da secção crítica, o processo encaminha a chave para o seu vizinho.

Algoritmo baseado em anel

Algoritmo de Ricart and Agrawala

Este algoritmo utiliza 3 estados:

  • HELD: significa que temos acesso exclusivo à região crítica
  • WANTED: não temos acesso mas queremos obtê-lo
  • RELEASED: não precisamos de aceder à região crítica

Se um processo deseja aceder à região, deve enviar requests a todos os outros clientes e esperar obter "OK" de todos estes. Se todos responderem afirmativamente, o processo recebe acesso exclusivo à região.

On initialization
  state := RELEASED;

To enter the section
  state := WANTED;
  Multicast request to all processes; // É omitido o processamento dos requests
  T := request’s timestamp;
  Wait until (number of replies received = (N1)); // Espera receber N-1 OK's
  state := HELD; // Temos acesso

On receipt of a request <T_i, p_i> at p_j (i != j)
  // Se (já temos acesso) ou (queremos ter e enviámos o pedido há mais tempo),
  // colocamos os pedidos em espera
  if (state = HELD or (state = WANTED and (T, p_j) < (T_i, p_i)))
  then
      queue request from p_i without replying;
  else
      reply immediately to p_i;
  end if

To exit the critical section
  state := RELEASED;
  reply to any queued requests;
Exemplo

Exemplo Ricart and Agrawala

Para ilustrar o algoritmo, considere uma situação envolvendo três processos, p1p_1, p2p_2 e p3p_3, conforme mostrado na figura. Vamos supor que p3p_3 não está interessado em entrar na secção crítica, e que p1p_1 e p2p_2 solicitam entrada simultaneamente. O timestamp do pedido de p1p_1 é 41, e o de p2p_2 é 34.

Quando p3p_3 recebe os pedidos, responde imediatamente. Quando p2p_2 recebe o pedido de p1p_1, verifica que o seu próprio pedido tem timestamp menor e, portanto, não responde, mantendo p1p_1 em espera. No entanto, p1p_1 verifica que o pedido de p2p_2 tem um timestamp menor do que o seu próprio pedido e, portanto, responde imediatamente. Ao receber esta segunda resposta, p2p_2 pode entrar na secção crítica. Quando p2p_2 decidir sair da secção crítica, responderá ao pedido de p1p_1 e concederá a sua entrada.

Este algoritmo já permite que a "chave" seja passada diretamente para outro cliente, mas tem 2 problemas:

  • Não é tolerante a faltas
  • Em vez de sobrecarregar o servidor, sobrecarrega todos os processos!

Cuidado

Poderíamos pensar que uma alternativa seria basear este algoritmo em prioridades em vez de timestamps, mas isto não funciona! Vejamos o seguinte exemplo:

Ricart and Agrawala baseado em prioridades

Em (*), o processo com prioridade 0 responde com "OK" apesar de ter feito o pedido antes, visto que a sua prioridade é inferior à do cliente que pediu acesso. Desta forma, ambos recebem N1=2N-1 = 2 OK's e têm acesso "exclusivo" à zona crítica.

Algoritmo de Maekawa

Maekawa observou que, para um processo entrar numa secção crítica, não é necessário que todos os seus peers concedam o acesso (só precisa de obter permissão de um subconjunto destes).

Este algoritmo associa um voting set ViV_i (também chamados quóruns) a cada processo pip_i (i=1,2,...,N)(i = 1,2,...,N), onde Vi{p1,p2,...,pN}V_i \subseteq \{p_1, p_2, ..., p_N\}. Os sets ViV_i são escolhidos de forma a que, para todo i,j=1,2,...,Ni,j = 1,2,...,N:

  • piVip_i \in V_i
  • ViVjV_i \cap V_j \neq \varnothing – há pelo menos um membro comum entre quaisquer dois voting sets
  • Vi=K|V_i| = K – de forma a ser justo, todos os voting sets têm o mesmo tamanho
  • Cada processo pjp_j está contido em MM dos voting sets ViV_i

Nota

Maekawa demonstrou que a solução ótima (que minimiza KK e permite que os processos alcancem a exclusão mútua) tem KNK \sim \sqrt{N} e M=KM = K. Como não é trivial calcular os sets ótimos RiR_i, uma forma simples de derivar estes sets tal que Ri2N|R_i| \sim 2 \sqrt{N}, é colocar os processos numa matriz N\sqrt{N} por N\sqrt{N} e ViV_i ser a união da linha e coluna que contém pip_i.

Cada processo pode votar num pedido de acesso à região crítica, mas não pode votar em mais que um em simultâneo, o que origina a seguinte propriedade:

Propriedade fundamental

Em qualquer par de quóruns, há sempre interseção em pelo menos um processo, o que implica que dois pedidos concorrentes nunca podem ambos receber os votos de quóruns completos.

K = |V_i| ~ sqrt(N)
On initialization
  state := RELEASED;
  voted := FALSE;

For p_i to enter the critical section
  state := WANTED;
  Multicast request to all processes in V_i;
  Wait until (number of replies received = K);
  state := HELD;

On receipt of a request from p_i at p_j
  if (state = HELD or voted = TRUE)
  then
    queue request from p_i without replying;
  else
    send reply to p_i;
    voted := TRUE;
  end if

For p_i to exit the critical section
  state := RELEASED;
  Multicast release to all processes in V_i;

On receipt of a release from p_i at p_j
  if (queue of requests is non-empty)
  then
    remove head of queue – from p_k, say;
    send reply to p_k;
    voted := TRUE;
  else
    voted := FALSE;
  end if

Este algoritmo consegue distribuir a carga, ou seja, não existe um processo que recebe todos os pedidos, mas tem um grande problema: sofre de interbloqueio (deadlock-prone).

deadlock-prone

Considere 3 processos, p1p_1, p2p_2 e p3p_3, com V1={p1,p2}V_1 = \{p_1, p_2\}, V2={p2,p3}V_2 = \{p_2, p_3\} e V3={p3,p1}V_3 = \{p_3, p_1\}. Se os três processos solicitarem simultaneamente acesso à seção crítica, então é possível que:

  • p1p_1 responda a si mesmo e meta p2p_2 em espera
  • p2p_2 responda a si mesmo e meta p3p_3 em espera
  • p3p_3 responda a si mesmo e meta p1p_1 em espera

Desta forma, cada processo recebeu apenas uma resposta (de dois pedidos), e nenhum pode prosseguir.

Nota

O algoritmo pode ser adaptado de forma a tornar-se deadlock-free. No protocolo adaptado, os processos colocam na fila de espera pedidos pendentes em ordem happened-before, garantindo assim que o requisito ME3 também seja satisfeito.

(ver o exemplo ilustrativo de Ordem Total baseada em acordo coletivo)

Comparação dos algoritmos

Terminologia:

  • Bandwith usage : total de mensagens trocadas entre enter /exit por um mesmo cliente
  • Client delay : tempo para um processo entrar numa secção crítica livre
  • Synchronization delay : tempo entre exit por um processo e enter por outro que estava à espera
Algoritmo Bandwith usage Client delay Synchronization delay
Centralizado 33 22 22
Ricart and Agrawala 2×(N1)2 \times (N-1) 22 11
Maekawa 3×quorum_size3 \times \text{quorum\_size} 22 2*2 \smartcolor{yellow}{\text{*}}

* assumindo que os 2 quóruns se intercetam em apenas 1 processo

Distribuição de carga:

  • Centralizado: tudo passa pelo servidor, possível sobrecarga
  • Ricart and Agrawala: todos os processos são sobrecarregados
  • Maekawa: cada pedido apenas afeta um subconjunto de processos (quórum)

Tolerância a falhas:

  • Todos assumem rede fiável! Nenhum tolera perdas de mensagens
  • Centralizado: não tolera falha do servidor, mas tolera falha de cliente em estado RELEASED
  • Ricart and Agrawala: nenhum processo pode falhar
  • Maekawa: cada pedido tolera falhas dos processo que não estejam no quórum

Eleição de líder

Tal como vimos anteriormente, muitos algoritmos distribuídos precisam de atribuir cargos especiais a certos processos. Por exemplo, na variante "servidor central" dos algoritmos para exclusão mútua, o servidor é escolhido entre os processos que precisam de utilizar a secção crítica. É necessário um algoritmo de eleição para esta escolha, sendo essencial que todos os processos concordem com a mesma.

É necessário assegurar principalmente 2 propriedades:

  • E1 (safety): todos os processos escolhem o mesmo líder (tipicamente o processo com id maior)
  • E2 (liveness): a execução do algoritmo é finita

Ao longo deste capítulo, iremos assumir que:

  • O detetor de falhas é perfeito, ou seja, nunca diagnostica erradamente um processo como morto
  • Os processos não recuperam, ou seja, não voltam ao ativo depois de morrerem

Eleição em anel

Este algoritmo é adequado para um conjunto de processos organizados num anel lógico. Cada processo pip_i tem um canal de comunicação com o próximo processo no anel, p(i+1)modNp_{(i + 1) \op{mod} N}, e todas as mensagens são enviadas no sentido horário ao redor do anel.

Funcionamento do algoritmo

Quando um processo p decide iniciar uma eleição:

  • Marca-se como participante
  • Prepara uma mensagem election(id(p)) e envia-a para o próximo anel

Quando um processo p recebe uma mensagem election(id):

  • Se o id na mensagem é superior ao identificador local: p reencaminha-a ao próximo e marca-se como participante.
  • Se o id na mensagem é inferior e p ainda não participava: substitui o id na mensagem pelo de p, reencaminha-a ao próximo e marca-se como participante.
  • Se o id na mensagem é o de p, então p torna-se o novo líder! Marca-se como não participante e envia mensagem elected(id(p)) ao próximo no anel.

Quando um processo p recebe uma mensagem elected(id):

  • Aprende que o novo líder é aquele indicado na mensagem, reencaminha a mensagem e marca-se como não participante.
  • Se o id na mensagem for p, não faz nada (o algoritmo terminou).
Exemplo

Diagrama de eleição em anel

A execução do algoritmo começou no nó 17 e até agora participaram 3 nós: 17, 24 e 1 (realçados com uma cor ligeiramente diferente). O nó 1 envia election(24) já que o seu id é menor que o da mensagem. Ao receber election(24), o nó 28 envia election(28), pois possui um id maior. Assumindo que o maior id é 28, os restantes nós irão reencaminhar esta mensagem até chegar ao emissor. Após a receção, o nó 28 irá emitir a mensagem elected(28). O algoritmo termina quando esta última mensagem dá uma volta completa ao anel e regressa ao líder (28).

No caso em que apenas um processo dá início ao processo de eleição, podem ser geradas até 3N1\bold{3N - 1} mensagens.

Justificação

Ao começar no processo seguinte ao que possui o maior id:

  • irão ser trocadas N1N-1 mensagens até chegar ao processo com maior id
  • NN mensagens para circular todo o anel com a mensagem election(max(id)) até regressar ao processo com maior id, que irá emitir a mensagem elected(max(id))
  • NN mensagens para circular novamente todo o anel e terminar ao regressar ao emissor

Nota

Se todos os processos decidirem começar o processo de eleição ao mesmo tempo, o algoritmo tem uma complexidade temporal quadrática.

Eleição em anel por torneio

  • Os processos procuram um líder num horizonte que duplica em cada turno
  • Em cada turno o número de competidores vai sendo reduzido para metade
  • Isto resulta na execução de log(n)\log(n) turnos para uma complexidade total de nlog(n)n\log(n)

Algoritmo "Bully"

O objetivo deste algoritmo é, tal como anteriormente, eleger o processo com maior identificador.

Tem alguns pressupostos:

  • existem tempos máximos conhecidos para a comunicação (sistema síncrono; canais fiáveis)
  • os processos podem falhar
  • todos os processos conhecem os identificadores dos restantes

Existem 3 tipos de mensagens trocadas neste algoritmo:

  • election: que assinala o início de uma eleição
  • answer: serve de resposta a uma mensagem election
  • coordinator: assinala o id do processo elegido (o novo coordenador)

Funcionamento do algoritmo

Um processo inicia uma eleição quando se apercebe, através de timeouts, que o coordenador falhou (vários processos podem descobrir isto simultaneamente):

  • se for o processo com id mais alto:
    • elege-se a si próprio e envia uma coordinator message para todos os processos com id mais baixo
  • se não:
    • envia a todos os processos com ids mais altos uma election message:
      • se não receber nenhuma answer message, considera-se o coordenador e envia uma coordinator message a todos os processos com id mais baixo
      • se receber, espera durante um período de tempo por uma mensagem coordinator e caso não receba nenhuma, começa uma nova eleição

Quando um processo pip_i receber uma coordinator message:

  • regista o id recebido na mensagem e passa a tratar esse processo como coordenador

Quando um processo recebe uma election message:

  • envia de volta uma answer message
  • começa uma nova eleição (a não ser que já tenha começado uma)

Quando um novo processo vem substituir um outro crashed:

  • começa uma nova eleição:
    • se tiver o id mais alto: decide que é o líder e anuncia-o (mesmo que o atual líder esteja a funcionar, daí ser chamado bully)
Exemplo

Exemplo de funcionamento do Bully

O processo p1p_1 deteta a falha do líder p4p_4 e começa uma eleição (stage 1).

Ao receber a mensagem election do p1p_1, os processos p2p_2 e p3p_3 enviam answer's de volta e começam as suas próprias eleições. p3p_3 envia uma answer ao p2p_2 mas não recebe do uma p4p_4 (stage 2).

p3p_3 decide assim que é o líder, mas antes de enviar a mensagem coordinator a p1p_1 e p2p_2, falha (stage 3). Quando o timeout de p1p_1 dispara (assumimos que é menor que o de p2p_2), este começa uma nova eleição, já que não recebeu uma mensagem coordinator. O p2p_2 vai enviar mensagens a p3p_3 e p4p_4 e ao não obter resposta elege-se como líder, notificando p1p_1 (stage 4).

No melhor caso, o processo com o segundo maior identificador percebe a falha do coordenador e elege-se imediatamente, e envia N2N - 2 mensagens coordinator.

No pior caso, o processo com o identificador mais baixo é o primeiro a detetar a falha do coordenador. Assim, N1N - 1 processos iniciam eleições simultaneamente, cada um enviando mensagens para os processos com identificadores mais altos. Desta forma, o algoritmo requer O(N2)O(N^{2}) mensagens.

Nota

O algoritmo não garante cumprir a condição E1 se os processos que falharam forem substituídos por outros com os mesmos identificadores, visto que é possível que dois processos anunciem simultaneamente que são coordenadores. Além disso, esta condição pode ser violada se os valores de timeout forem imprecisos.

Algoritmo do "Luís"

O professor desenhou um algoritmo bastante simples, que consiste em ter um detetor de falhas que notifica todos os processos sempre que algum falha:

  • Inicialização:

    • ativos=p0,p1,p2,...,pnativos = {p_0, p_1, p_2, ..., p_n}
    • lıˊder=maxid(ativos)líder = max_{id}(ativos)
    • output(lıˊder)output(líder)
  • Quando um processo (pip_i) falha:

    • ativos=ativos{ pi }ativos = ativos \setminus \{~p_i~\}
    • lıˊder=maxid(ativos)líder = max_{id}(ativos)
    • output(lıˊder)output(líder)

Bully vs "Luís":

Algoritmo do Luís:

  • simples
  • modular
  • menos eficiente pois obriga a detetar falhas em processos que não são candidatos a líder

Algoritmo Bully:

  • Mistura deteção de falhas com eleição de líder
  • Cada processo apenas precisa de detetar falhas de outros com id maior

Deteção de falhas

Todos estes algoritmos têm um grande problema: assumem que a deteção de falhas é perfeita. Só assim conseguem garantir que todos os nós funcionais elegem o mesmo líder (ou seja, garantir a safety do algoritmo).

Uma deteção de falhas perfeita implica que:

  • um processo funcional nunca é diagnosticado como falhado
  • a falha é sempre detetada

Mas para se fornecer tais garantias é preciso que:

  • o sistema seja síncrono, para que se possa detetar as falhas com exatidão com recurso a temporizadores
  • não hajam falhas na rede

Por outro lado, se a deteção de falhas não for perfeita:

  • os processos podem discordar sobre a identidade do líder
  • podem existir vários líderes ao mesmo tempo

O ideal seria construirmos algoritmos que são tolerantes a falhas, de forma a não dependerem de uma deteção perfeita.

Deteção de falhas "alguma-vez" perfeita

O detetor pode temporariamente errar, mas há um momento a partir do qual volta a estar correto (por ex. declarando um processo como falhado mas mais tarde reconhecendo que está ativo). Um detetor com estas características é designado por "alguma-vez" (do inglês "eventually") perfeito.

Problema do Consenso

O consenso é um dos problemas mais difíceis e estudados de Sistemas Distribuídos. Foi provado que, num sistema assíncrono em que podem ocorrer falhas, este problema não tem solução (resultado conhecido como FLP).

Definição de Consenso

Dado um conjunto de NN processos:

  1. Cada processo propõe um valor (input)
  2. Todos os processos decidem o mesmo valor (output)

Notas:

  • O valor decidido deve ser um dos valores propostos
    • invalidando assim uma solução que decide sempre um valor por omissão independentemente dos inputs dados
  • Pode ser qualquer um dos valores propostos:
    • Não tem de ser o valor proposto por mais processos
    • Não existe qualquer tipo de hierarquia de processos ou critério de qualidade que distinga os valores (i.e. não existem valores nem processos melhores que os outros)

Tipos específicos de consenso

Note que já utilizámos consenso anteriormente:

  • na exclusão mútua, os processos concordam sobre o processo que pode entrar na secção crítica
  • na eleição de líder, os processos concordam sobre o processo eleito
  • no multicast totalmente ordenado, os processos concordam sobre a ordem de entrega das mensagens

Existem diversos protocolos para tipos específicos de consenso. No entanto, nesta secção vamos considerar formas mais gerais de consenso, analisando características e soluções comuns.

Podemos concluir assim três propriedades do Consenso:

  1. Terminação: todos os processos correctos decidem ("alguma-vez")
  2. Acordo uniforme: se dois processos decidem, decidem o mesmo valor
  3. Integridade: o valor decidido (output) foi proposto por um processo

Quanto a soluções para este problema em sistemas:

  • síncronos: ou seja, onde é possível concretizar um detetor de falhas perfeito, iremos abordar o algoritmo FloodSet
  • assíncronos: ou seja, onde é possível concretizar um detetor de falhas "alguma-vez" perfeito, não iremos abordar nenhum algoritmo, mas podem consultar o algoritmo "Paxos" do Lamport

Falhas bizantinas

Os processos podem falhar de formas arbitrárias (falhas bizantinas), enviando valores aleatórios para os restantes processos (estes valores aleatórios podem resultar de bugs ou operações maliciosas).

Nesta cadeira não iremos estudar algoritmos que têm este tipo de falhas em consideração. Se tiveres interesse em aprender mais recomendamos o vídeo introdutório do Martin Kleppmann ao "Byzantine generals problem" e o PBFT Consensus Algorithm.

Floodset Consensus

A ideia essencial deste algoritmo é cada processo enviar para todos os outros o seu valor (input), de forma a que no fim todos conheçam todos os valores possíveis e possam tomar a mesma decisão de forma determinística.

O funcionamento do algoritmo é baseado em rondas:

  • em cada ronda, cada processo faz broadcast do seu valor
  • ao receber um valor de outro processo, adiciona-o ao seu conjunto
  • ao fim de f+1f \op{+} 1 rondas, é escolhido o output com base num critério determinístico utilizado por todos os processos
    • em que ff é o número de processos que pode falhar
Pseudocódigo

Algorithm for process Pig;algorithm proceeds in f+1 rounds\text{Algorithm for process } P_i \in g; \text{algorithm proceeds in } f \op{+} 1 \text{ rounds}

On initialization\text{On initialization}\\ Valuesi1:={vi};Valuesi0={};\qquad Values_i^1 := \set{v_i} ; Values_i^0 = \set{};

In round r (1rf+1)\text{In round } r~(1 \leq r \leq f \op{+} 1)\\ B-multicast(g,ValuesirValuesir1);\qquad \text{B-multicast}(g, Values_i^r \op{—} Values_i^{r-1}); // Send only values that have not been sent Valuesir+1:=Valuesir;\\ \qquad Values_i^{r+1} := Values_i^r;\\ while (in round r) {\qquad \text{while } (\text {in round } r)~\{\\ On B-deliver(Vj) from some pj\qquad \qquad \text{On B-deliver}(V_j) \text{ from some } p_j\\ Valuesir+1:=Valuesir+1Vj\qquad \qquad \qquad Values_i^{r+1} := Values_i^{r+1} \cup V_j\\ }\qquad \}

After (f+1) rounds\text{After } (f \op{+} 1) \text{ rounds}\\ Assign di=minimum(Valuesif+1);\qquad \text{Assign } d_i = minimum(Values_i^{f+1});

NOTA: o critério utilizado neste algoritmo para a escolha do output foi encontrar o valor mínimo, mas pode ser qualquer critério!

Exemplo

Exemplo de execução com f=1f = 1:

Diagrama de execução

Algumas notas acerca do algoritmo:

  • Pressupõe um sistema síncrono
    • se um processo pip_i não recebe o valor de outro processo pjp_j no turno nn então o processo pjp_j falhou de certeza (e não participa nos próximos turnos)
  • É possível adaptar o algoritmo de forma a utilizar um detetor de falhas perfeito:
    • Caso um processo pip_i não tenha recebido o valor de um processo pjp_j num turno nn, apenas avança para o turno n+1n \op{+} 1 caso o detetor de falhas declare pjp_j como falhado
  • Em alguns casos, caso não ocorram falhas, é possível terminar em menos turnos

Propriedade

Qualquer algoritmo desenhado para resolver o consenso permitindo até ff falhas, requer pelo menos f+1f + 1 rondas de trocas de mensagens, independentemente da forma como foi construído.

Problemas relacionados

Iremos agora abordar dois exemplos de problemas que são semelhantes ao problema do Consenso e que podem ser utilizados para o resolver ou utilizá-lo na construção da sua solução.

Coerência Interativa

O problema da coerência interativa é outra variante de consenso, na qual cada processo propõe um único valor. O objetivo do algoritmo é fazer com que os processos corretos concordem com um vetor de valores, um para cada processo. Por exemplo, o objetivo poderia ser para cada processo de um grupo obter a mesma informação sobre os estados respectivos (de cada processo).

  • Conjunto de NN processos
  • Cada processo pip_i propõe um valor (inputi\text{input}_i)
  • Todos os processo decidem o mesmo vetor VV (output\text{output})
  • O vetor VV decidido tem uma entrada por cada processo em que:
    • ou V[i]=inputiV[i] = \text{input}_i
    • ou V[i]=nullV[i] = null

Propriedades:

  1. Terminação: todos os processos correctos decidem ("alguma-vez")
  2. Acordo uniforme: se dois processos decidem, decidem o mesmo vetor VV
  3. Integridade: se o processo pip_i não falhar, V[i]=inputiV[i] = \text{input}_i
Pseudocódigo das implementações

Consenso usando Coerência Interativa:

Quando Consenso.propoe(valor):
  CoerenciaInteractiva.propoe(valor)

Quando CoerenciaInteractiva.decide(vector):
  valor = primeiraEntradaDiferenteDeNull(vector);
  Consenso.decide(valor)

Coerência Interativa usando consenso:

// este codigo é executado por todos os processos

fun PRONTO(vector):
  se para todo o p_x: p_x não pertence a falhados e tivermos vector_proposta[x] != null
    retorna VERDADEIRO
  caso contrário
    retorna FALSO

Init:
  falhados = {}
  para cada valor de i < N
    vector_proposta[i] = null;

Quando falha(p_x):
  falhados = falhados U {p_x}

Quando IC.propoe(valor_i)
  DifusaoFiavel.envia(p_i, valor_i)

Quando DifusaoFiavel.entrega(p_j, valor_j)
  vector_proposta[j] = valor_j;

Quando PRONTO(vector_proposta)
  Consenso.propoe(vector_proposta) // só propõe uma vez

Quando Consenso.decide(vector)
  IC.decide(vector)

Derivar Consenso a partir de Coerência Interativa

Por vezes é possível derivar uma solução para um problema utilizando uma solução para outro. Esta propriedade é muito útil porque aumenta a nossa compreensão dos problemas e economiza esforço de implementação.

Suponha que existem as seguintes soluções para o consenso (C) e para a consistência interativa (IC):

  • Ci(v1,v2,...,vN)C_i(v_1, v_2, ..., v_N) retorna o valor de decisão do processo pip_i numa execução da solução para o problema do consenso, onde v1,v2,...,vNv_1, v_2, ..., v_N são os valores propostos pelos processos
  • ICi(v1,v2,...,vN)[j]{IC}_i(v_1, v_2, ..., v_N)[j] retorna o j-ésimo valor no vetor de decisão do processo pip_i numa execução da solução para o problema da consistência interativa, onde v1,v2,...,vNv_1, v_2, ..., v_N são os valores propostos pelos processos

Caso a maioria dos processos estejam corretos, construímos uma solução executando IC para produzir um vetor de valores em cada processo, e depois aplicando uma certa função sobre os valores do vetor para derivar um único valor:

Ci(v1,...,vN)=majority(ICi(v1,...,vN)[1],...,ICi(v1,...,vN)[N])C_i(v_1, ..., v_N) = majority({IC}_i(v_1, ..., v_N)[1], ..., {IC}_i(v_1, ..., v_N)[N])

Nota

Em sistemas com falhas, resolver o consenso é equivalente a resolver o multicast confiável e totalmente ordenado: dada uma solução para um, podemos resolver o outro. Implementar o consenso com base em operações de multicast confiável e totalmente ordenado (RTO-multicast)\text{(RTO-multicast)} é trivial.

Dado um grupo de processos gg, para alcançar o consenso, cada processo pip_i executa RTO-multicast(g,vi)\text{RTO-multicast}(g, v_i). Em seguida, cada processo pip_i escolhe di=mid_i = m_i, onde mim_i é o primeiro valor que pip_i RTO-delivers\text{RTO-delivers}.

Difusão com terminação

  • Conjunto de NN processos
  • Um processo pré-definido ss envia uma mensagem mm
  • Se o processo ss é correto, todos os processos corretos entregam mm
  • Se o processo ss falha, os processos entregam mm ou nullnull
  • Todos os processos corretos entregam o mesmo valor (ou mm ou nullnull)

Propriedades:

  1. Terminação: todos os processos correctos decidem ("alguma-vez")
  2. Acordo uniforme: se dois processos decidem, decidem o mesmo valor vv
  3. Integridade: se o processo ss não falhar, v=mv = m
Pseudocódigo das implementações

Difusão com Terminação usando Consenso:

Quando ConsensoPropoe(v) no processo i
  i.DifusaoComTerminacaoEnvia(v)

Para todo o i
  i.DifusaoComTerminacaoEntrega(v_i)

Escolhe v_final como sendo o menor v_i: v_i != null
  ConsensoDecide(v_final)

Consenso usando Difusão com Terminação:

No emissor:
  Quando DifusaoComTerminacao(m)
    DifusaoFiavelEnvia(m)
    ConsensoPropoe(m)


No restantes processos, executa um e apenas um destes passos:
  Quando DifusaoFiavelEntrega(m)
    ConsensoPropoe(m)
  Quando suspeita a falha do processo "s"
    ConsensoPropoe(null)

Em todos os processos:
  Quando ConsensoDecide(v)
    DifusaoComTerminacaoEntrega(m)

Impossibilidade em sistemas assíncronos

As soluções para o consenso que abordámos assumem que o sistema é síncrono, ou seja, assumem que um processo falhou se não lhes enviou uma certa mensagem dentro da ronda desejada (atraso máximo excedido).

Fischer et al. [1985] provaram que nenhum algoritmo pode garantir alcançar o consenso num sistema assíncrono, visto que os processos podem responder a mensagens com latências arbitrárias, fazendo com que um processo que realmente falhou seja indistinguível de um lento.

Note que este resultado não significa que os processos não podem alcançar o consenso distribuído num sistema assíncrono. Este permite que o consenso possa ser alcançado com alguma probabilidade maior que zero, confirmando o que sabemos na prática (por exemplo, existem sistemas de transações assíncronos que têm alcançado o consenso regularmente há anos).

Ainda assim, existem diversas técnicas (não abordadas em aula) para contornar o resultado da impossibilidade. Por exemplo:

  • Mascarar falhas: envolve técnicas como o uso de armazenamento persistente e replicação de componentes para ocultar falhas, permitindo que os processos continuem a funcionar corretamente
  • Consenso usando detetores de falhas: envolve o uso de detetores de falhas (não perfeitos) em sistemas assíncronos para alcançar o consenso, seja considerando processos não responsivos como falhados ou permitindo que processos suspeitos continuem a participar no consenso
  • Consenso usando randomização: envolve introduzir aleatoriedade no comportamento dos processos para neutralizar os efeitos negativos dos sistemas assíncronos, permitindo que o consenso seja alcançado em tempo (esperado) finito

Referências

  • Coulouris et al - Distributed Systems: Concepts and Design (5th Edition)
    • Secções 15.1-15.5
  • Departamento de Engenharia Informática - Slides de Sistemas Distribuídos (2023/2024)
    • SlidesTagus-Aula03a
    • SlidesTagus-Aula04
    • SlidesAlameda-Aula08