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 processos , 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.
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 processos sem utilizar um processo adicional é organizá-los num anel lógico. Isto apenas requer que cada processo tenha um canal de comunicação com o próximo processo no anel, .
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 de Ricart and Agrawala
Este algoritmo utiliza 3 estados:
HELD
: significa que temos acesso exclusivo à região críticaWANTED
: não temos acesso mas queremos obtê-loRELEASED
: 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 = (N – 1)); // 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
Para ilustrar o algoritmo, considere uma situação envolvendo três processos, , e , conforme mostrado na figura. Vamos supor que não está interessado em entrar na secção crítica, e que e solicitam entrada simultaneamente. O timestamp do pedido de é 41, e o de é 34.
Quando recebe os pedidos, responde imediatamente. Quando recebe o pedido de , verifica que o seu próprio pedido tem timestamp menor e, portanto, não responde, mantendo em espera. No entanto, verifica que o pedido de tem um timestamp menor do que o seu próprio pedido e, portanto, responde imediatamente. Ao receber esta segunda resposta, pode entrar na secção crítica. Quando decidir sair da secção crítica, responderá ao pedido de 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:
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 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 (também chamados quóruns) a cada processo , onde . Os sets são escolhidos de forma a que, para todo :
- – há pelo menos um membro comum entre quaisquer dois voting sets
- – de forma a ser justo, todos os voting sets têm o mesmo tamanho
- Cada processo está contido em dos voting sets
Nota
Maekawa demonstrou que a solução ótima (que minimiza e permite que os processos alcancem a exclusão mútua) tem e . Como não é trivial calcular os sets ótimos , uma forma simples de derivar estes sets tal que , é colocar os processos numa matriz por e ser a união da linha e coluna que contém .
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, , e , com , e . Se os três processos solicitarem simultaneamente acesso à seção crítica, então é possível que:
- responda a si mesmo e meta em espera
- responda a si mesmo e meta em espera
- responda a si mesmo e meta 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 | |||
Ricart and Agrawala | |||
Maekawa |
* 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 tem um canal de comunicação com o próximo processo no anel, , 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 dep
, reencaminha-a ao próximo e marca-se como participante. - Se o id na mensagem é o de
p
, entãop
torna-se o novo líder! Marca-se como não participante e envia mensagemelected(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
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é mensagens.
Justificação
Ao começar no processo seguinte ao que possui o maior id:
- irão ser trocadas mensagens até chegar ao processo com maior id
- mensagens para circular todo o anel com a mensagem
election(max(id))
até regressar ao processo com maior id, que irá emitir a mensagemelected(max(id))
- 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 turnos para uma complexidade total de
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çãoanswer
: serve de resposta a uma mensagemelection
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
- elege-se a si próprio e envia uma
- 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 umacoordinator
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
- se não receber nenhuma
- envia a todos os processos com ids mais altos uma
Quando um processo 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
O processo deteta a falha do líder e começa uma eleição (stage 1).
Ao receber a mensagem election
do , os processos e enviam
answer
's de volta e começam as suas próprias eleições. envia uma answer
ao mas não recebe do uma (stage 2).
decide assim que é o líder, mas antes de enviar a mensagem coordinator
a
e , falha (stage 3).
Quando o timeout de dispara (assumimos que é menor que o de ),
este começa uma nova eleição, já que não recebeu uma mensagem coordinator
.
O vai enviar mensagens a e e ao não obter resposta elege-se
como líder, notificando (stage 4).
No melhor caso, o processo com o segundo maior identificador percebe a falha
do coordenador e elege-se imediatamente, e envia mensagens coordinator
.
No pior caso, o processo com o identificador mais baixo é o primeiro a detetar a falha do coordenador. Assim, processos iniciam eleições simultaneamente, cada um enviando mensagens para os processos com identificadores mais altos. Desta forma, o algoritmo requer 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:
-
Quando um processo () falha:
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.
Referências
- Coulouris et al - Distributed Systems: Concepts and Design (5th Edition)
- Secções 15.1, 15.2 e 15.3
- Departamento de Engenharia Informática - Slides de Sistemas Distribuídos (2023/2024)
- SlidesTagus-Aula03a
- SlidesTagus-Aula04