Você está na página 1de 16

6.

Sincronização
Os processos em um sistema distribuído devem comunicar-se entre si, sempre no intuito de fazer algo útil, ou
seja, a cooperação entre processos é essencial em um sistema distribuído…

Sincronia:

• Ordem de operações
• Espera pela conclusão
• Recurso compartilhado e controle de acesso exclusivo ou simultâneo

Carol não compreende a razão de tanta hostilidade da parte de Bob.

Os servidores não concordam sobre o mesmo valor de x até às 08:00:00.000 em ponto.


Sincronização baseada em:

- Tempo real (absoluto) => Relógio físico


- Ordem relativa => Relógio lógico

Alerta! Uso da hora local ao invés da hora do servidor

UPDATE …
SET …
Usuario = “pedro”,
DataHora = ?
WHERE …

É possível sincronizar todos os relógios em um sistema distribuído?

Clock/Timer

Cristal de - n (Registrador)
Quartzo

Oscilações => n – 1
12, 11, 10, …, 0 => Ciclo => Interrupção => Procedimento => RAM CMOS (time++)

Mesmo com relógios em perfeita sincronia, com o passar do tempo a sincronização é perdida devido a
diferenças nas oscilações dos cristais de quartzo.

A hora UTC (Coordinated Universal Time) também sofre ajustes anuais (cerca de 1s) para se manter em
sincronia com a rotação da Terra e seu posicionamento na órbita solar.

Desafio!

code1.c code1.o
code2.c code2.o
code3.c compilação code3.o link executável

coden.c coden.o

codek.c é recompilado somente se codek.o for mais antigo, ou seja, data(codek.o) < data(codek.c).

Você consegue pensar em uma solução que independa do relógio da máquina?

GPS – Um sistema distribuído altamente específico


https://www.youtube.com/watch?v=m__KuTjkThI
https://www.youtube.com/watch?v=XMSdrIqSGdI
Algoritmos de sincronização de relógios
Essencialmente, cada sistema computacional possui um timer que gera uma interrupção H vezes por segundo.
Chamemos de C (Clock) o valor atual do relógio em um sistema computacional, ou seja, o número de
interrupções geradas pelo timer deste sistema desde um instante inicial com o qual todos os demais sistemas
concordam.

Quando a hora UTC é t, o valor do relógio na máquina p é Cp(t). Idealmente teríamos Cp(t) = t para toda
máquina p e a qualquer instante t, ou seja, sincronização perfeita.

Teoricamente, timers que geram, por exemplo, 60 interrupções por segundo (H = 60), devem gerar em uma
hora (3600 segundos) 3600 * 60 = 216.000 interrupções. Na prática, porém, o valor pode variar entre
aproximadamente 215.998 e 216.002.

Isso implica que difrenças nos valores de clock podem e, normalmente ocorrem, ainda que todos os relógios em
um sistema distribuído estivessem perfeitamente sincronizados. Os algoritmos de sincronização levam em conta
essa defasagem inerente ao hardware e procuram uma maneira de sincronizar os relógios não apenas uma vez,
mas também periodicamente.

Protocolo de Tempo de Rede – Network Time Protocol (NTP)

A ideia aqui consiste em ter um servidor de tempo (Time Server), de modo que os clientes possam consultar a
hora atual. Contudo, os atrasos na propagação de mensagens fazem com que a hora fornecida se torne
desatualizada e, portanto, não mais correta. A solução adotada pelo protocolo consiste em encontrar uma boa
estimativa para esses atrasos.

1. A envia uma requisição a B com uma marca de tempo T1 (timestamp).


2. B registra o momento de chegada da mensagem: T2.
3. B aguarda alguns instantes e envia uma resposta com uma marca de tempo T3. A resposta contém, além
de T3, a marca de tempo T2.
4. A registra a hora de chegada da resposta: T4.

Não é possível calcular o tempo que a requisição demorou para ser transmitida nem o tempo que a resposta
levou para chegar. Contudo, o tempo total de ida e volta (Round Trip Time) pode ser calculado como:

RTT = (T4 – T1) – (T3 – T2)

O protocolo NTP assume que os atrasos de propagação das mensagens de A até B e de B até A sejam
aproximadamente iguais. Assim sendo, o deslocamento temporal pode ser estimado da seguinte forma:

θ = T2 – (T1 + RTT / 2)
θ = T2 – (T1 + ((T4 – T1) – (T3 – T2)) / 2)
θ = (T2 – T1 + T3 – T4) / 2

Se o relógio de A estiver adiantado (θ < 0), significa que A deve atrasar seu relógio. Como isso pode causar
problemas, tal alteração é feita gradativamente pelo sistema reduzindo-se o número de milisegundos que são
acrescentados ao relógio de A a cada interrupção até que o ajuste seja feito.
O algoritmo de Berkeley

No algoritmo de Berkeley não existe a figura de um servidor, e sim um daemon de tempo que consulta todas as
máquinas na rede periodicamente para saber a hora de cada uma. Com base nas respostas, uma média é obtida e
as máquinas são instruídas a adiantarem ou atrasarem seus relógios.

média = (-10 + 25 + 0) / 3 = 5

O ponto chave a se notar aqui é que o algoritmo não se preocupa em manter cada relógia em sincronia com a
hora UTC, mas apenas manter os horários dos computadores na rede sincronizados entre si.

É importante observar que, mesmo que os relógios estejam atualmente sincronizados com a hora UTC, com o
passar do tempo tal sincronia é perdida em razão de fatores como, por exemplo, o posicionamento do globo na
órbita solar. Fica claro então que o propósito do algoritmo de Berkeley é apenas garantir a sincronia entre os
relógios da rede, sem qualquer preocupação com a hora global. Assim sendo, o algoritmo de Berkeley visa a
mera sincronização dos relógios em âmbito interno.
Relógios lógicos de Lamport
Conceitos iniciais…

a → b O evento a ocorre antes do evento b.

Exemplo: a e b são “disparados” por um mesmo processo, sendo que este dispara a e somente
depois dispara b.

Exemplo: a representa um evento de uma mensagem enviada por um processo e b representa um


evento que corresponde a essa mensagem sendo recebida por outro processo.

a → b e b → c implica a → c, ou seja, relação transitiva.

a e b são concorrentes, ou seja, não se pode afirmar que a → b ou b → a, pois ambos ocorrem em
processos diferentes que não trocam mensagens.

Objetivo: Garantir que se a → b, então C(a) < C(b). Onde C (clock) é o instante em que ocorre o evento.
Não necessariamente ligado ao relógio real. Tipicamente um contador local.

Exemplo de funcionamento

Os processos P1, P2 e P3 executam em máquinas diferentes, cada uma com seu próprio relógio e com taxas de
pulsos diferentes, ou seja, pequenas variações nos ciclos de clock.

Cada processo Pi mantém um contador local Ci e procede da seguinte forma:

1. Antes de processar um evento, Pi executa Ci ← Ci + 1.


2. Quando Pi envia uma mensagem m a Pj, Pi ajusta a marca de tempo de m: ts(m) ← Ci.
3. Quando Pj recebe a mensagem m, Pj ajusta seu próprio contador local: Cj ← max(Cj, ts(m)). Em seguida
a etapa 1 é executada.
Exemplo: Multicast totalmente ordenado

Problema: Em um banco de dados replicado, ocorre uma operação de depósito (Atualização 1 = +100) em uma
conta, cujo saldo atual é 1.000, ao mesmo tempo em que o sistema bancário concede um acréscimo de 1% na
mesma conta, porém em uma réplica diferente (Atualização 2 = +1%).

Resultados possíveis:

• Saldo = 1.111,00 (depósito + 1% juro)


• Saldo = 1.110,00 (1% juro + depósito)

De fato, os dois resultados podem ser considerados como corretos, a questão é como garantir que as operações
sejam executadas na mesma ordem em todas as réplicas do banco de dados, ou seja, os dados devem ser
consistentes.

Solução: Relógios lógicos de Lamport

A ideia por trás do multicast totalmente ordenado é garantir que todas as mensagens sejam entregues (à
aplicação = SGBD) na mesma ordem em todos os destinatários.

1. Quando uma atualização é requisitada a um processo, este cria uma mensagem de requisição contendo
sua marca temporal atual e então envia essa mensagem aos outros processos envolvidos na replicação
dos dados: multicast. Além disso, a requisição é incluída na fila local do processo fazendo multicast,
ordenada por sua marca temporal e já marcada como reconhecida (ACK) pelo próprio processo.
2. Quando uma mensagem é recebida por um processo, este a coloca em sua fila local ordenada pela marca
temporal que acompanha a mensagem e já marcada como reconhecida (ACK) pelos processos emissor e
receptor. Logo em seguida, uma mensagem de reconhecimento (ACK) é enviada aos outros processos
do grupo.

Uma mensagem é removida da fila e encaminhada para processamento após a confirmação (ACK) de
recebimento de todos os processos envolvidos e somente se a mensagem for a primeira na fila.

Impasses podem ocorrer quando os contadores lógicos dos processos forem iguais durante duas requisições
simultâneas e, neste caso, uma abordagem simples do tipo Pid.contador pode ser utilizada para resolver o
problema de ordem.
What about Alice, Bob, and Carol… :^(

Alice Bob Carol


queue: 6. Bob smells, ABC queue: 6. Bob smells, ABC queue: 6. Bob smells, ABC
36. Fuck you!, ABC 36. Fuck you!, ABC 36. Fuck you!, ABC
5
33
6 6. Bob smell
s
34
8
35
ACK 6
6 36
K
AC
36
6.
36 ! Bo
6. Fu ck you b sm
36. F
uc
3 ell k yo
37 s u!

38 37
38
3 6
ACK 36 ACK
39 39
AC
AC

K 36 39
K
36

40
ACK 6 ACK
6
41 41

43 41
Relógios vetoriais
Em um sistema distribuído, os relógios lógicos de Lamport garantem o seguinte: se um evento a ocorre antes de
um evento b, então C(a) < C(b), ou seja, os eventos são totalmente ordenados.

Exemplos:

send(m1) ocorre antes de rec(m1), logo: Tsend(m1) < Trec(m1)


send(m2) ocorre antes de rec(m2), logo: Tsend(m2) < Trec(m2)
send(m3) ocorre antes de rec(m3), logo: Tsend(m3) < Trec(m3)

ou seja, para qualquer mensagem mi, Tsend(mi) < Trec(mi)

Com base no exemplo acima, podemos também afirmar o seguinte: se Trec(m1) < Tsend(m3), então m3 foi enviada
após o recebimento de m1, mas isto apenas porque estes são eventos que ocorreram em um mesmo processo: P2.
Neste caso pode-se dizer que há uma relação de causalidade, ou seja, o envio de m3 dependeu, ou teve como
causa, o recebimento de m1. Ou ainda, o que foi recebido em m1 desencadeou (influenciou) a mensagem m3.

Entretanto, não podemos afirmar que, se Trec(m1) < Tsend(m2), então m2 foi enviada após o recebimento de m1, ou
seja, não podemos concluir que há uma relação de causalidade entre as duas mensagens: não podemos afirmar
que m2 só foi enviada por causa do conteúdo de m1.

De fato, pela representação gráfica apresentada, observa-se que o envio de m2 ocorre no mesmo instante (hora
real) que o recebimento de m1. Poderia inclusive ter acontecido um pouco antes (digamos, send(m2) = 19) e
ainda assim a relação temporal Trec(m1) < Tsend(m2) seria mantida, mais uma vez confirmando que não é possível
tirar conclusões de ordem sobre eventos não interligados pela troca direta de mensagens.

Logo, os relógios lógicos de Lamport garantem a ordem total entre eventos que ocorrem em um mesmo
processo ou entre eventos que ocorrem entre processos que se comunicam por mensagem. Ou ainda, se C(a) <
C(b), então não é possível afirmar que o evento a ocorreu antes do evento b, a não ser que ambos os eventos
tenham ocorrido em um mesmo processo ou os eventos sejam o resultado da comunicação entre dois processos.
Análise de m1 considerando-se os relógios lógicos de Lamport…

Trec(m1) = 16
Tsend(m2) = 20
Tsend(m3) = 32
Tsend(m4) = 60
Tsend(m5) = 69

Resultado Relação de causalidade


com relógios lógicos de Lamport
Trec(m1) < Tsend(m2) verdadeiro incorreto
Trec(m1) < Tsend(m3) verdadeiro correto
Trec(m1) < Tsend(m4) verdadeiro correto
Trec(m1) < Tsend(m5) verdadeiro correto

Mas como obter essa relação de causalidade? Como saber se o conteúdo de uma mensagem que foi recebida
influenciou ou poderia ter influenciado (causalidade potencial) no conteúdo de uma mensagem enviada?
Resposta: relógios vetoriais.

Se VC(a) < VC(b), então o evento a precede por causalidade o evento b, ou seja, o evento a pode ter influência
no evento b.

Exemplo de como funciona com 3 processos distribuídos…

Processo P1: VC1 = [n1, ?, ?] → n1 é o número de eventos ocorridos em P1 (seu relógio lógico local)
Processo P2: VC2 = [?, n2, ?] → n2 é o número de eventos ocorridos em P2 (seu relógio lógico local)
Processo P3: VC3 = [?, ?, n3] → n3 é o número de eventos ocorridos em P3 (seu relógio lógico local)

Notar que cada processo também conhece, ainda que defasado, o relógio lógico dos outros processos. Esse
contador defasado é atualizado sempre que uma mensagem é recebida por um processo, isso porque o relógio
vetorial do processo emissor sempre é enviado com a mensagem:

1. Antes de processar qualquer evento (exemplo: enviar uma mensagem), Pi executa VCi[i] ← VCi[i] + 1.
2. Quando um processo Pi envia uma mensagem m, esta é enviada com uma marca de tempo vetorial,
ts(m), igual ao relógio vetorial de Pi, o qual já teve seu relógio local incrementado na etapa anterior:
ts(m) ← VCi.
3. Ao receber uma mensagem m, Pj ajusta seu próprio vetor fixando VCj[k] ← max{VCj[k], ts(m)[k]} para
cada k. Em seguida executa a primeira etapa e entrega a mensagem à aplicação.

Neste caso, se a marca de tempo de um evento a for ts(a), então ts(a)[i] – 1 é o número de eventos ocorridos em
Pi que precedem a por causalidade.

Assim sendo, quando Pj recebe uma mensagem de Pi com marca de tempo ts(m), ele sabe o número de eventos
que ocorreram em Pi e que precedem por causalidade o envio de m. Porém, o mais importante é que Pj também
é informado de quantos eventos ocorreram em outros processos, antes de Pi enviar a mensagem m. Em outras
palavras, a marca de tempo ts(m) informa ao receptor quantos eventos ocorreram em outros processos antes do
envio de m e dos quais m pode depender por causalidade.
Voltemos ao nosso exemplo inicial, desta vez utilizando relógios vetoriais…

VC(rec(m1)) = [2, 1, 0]
VC(send(m2)) = [0, 0, 1]
VC(send(m3)) = [2, 4, 1]
VC(send(m4)) = [2, 4, 3]
VC(send(m5)) = [2, 6, 3]

Resultado Relação de causalidade


com relógios vetoriais
VC(rec(m1)) < VC(send(m2)) falso correto
VC(rec(m1)) < VC(send(m3)) verdadeiro correto
VC(rec(m1)) < VC(send(m4)) verdadeiro correto
VC(rec(m1)) < VC(send(m5)) verdadeiro correto

A relação VC(e1) < VC(e2) implica que marca temporal do evento e1 é menor ou igual a marca temporal do
evento e2 (para cada posição do vetor).

https://levelup.gitconnected.com/distributed-systems-physical-logical-and-vector-clocks-7ca989f5f780
https://www.youtube.com/channel/UCekZUWSJkX9kHuvfPbt_gvg (Professora Lindsey Kuper)
Exclusão mútua
Quando um processo obtém acesso a um recurso, os outros processos são impedidos de acessar o recurso até
que este seja liberado pelo processo que o detém. A exclusão mútua é uma forma simples de se impedir que um
recurso seja levado a um estado inconsistente.

Em um sistema não distribuído, essa é uma questão fácil de se resolver, mas em sistemas distribuídos a
exclusão mútua requer uma análise mais aprofundada.

1. Soluções baseadas em token (ficha)

O processo que detém o token pode, se desejar, acessar o recurso ou simplesmente passar o token adiante.

Vantagens:

- Evita starvation (inanição: quando um processo espera para sempre)


- Evita deadlock (impasse)

Desvantagem:

Se o processo que detém o token falha, um “complicado” procedimento para disponibilizar um novo token deve
ter início e, principalmente, esse token deve ser único.

Exemplo: Algoritmo Token Ring – Um anel lógico é contruído em software e a cada processo é designada uma
posição no anel.
2. Soluções baseadas em solicitação de permissão

Algoritmo centralizado: O processo que deseja acesso a um recurso compartilhado deve requisitar ao
coordenador (servidor responsável pelo controle de acesso ao recurso) o recurso desejado.

Vantagens:

- Simples de implementar.
- Justo, pois as permissões são concedidas na ordem em que as requisições foram recebidas.
- Não há inanição, ou seja, nenhum processo espera para sempre.

Desvantagens:

- Single point of failure (ponto de falha único).


- O processo que detém o recurso atualmente não pode falhar.
- O coordenador pode se tornar um gargalo de desempenho.

Algoritmo descentralizado: O processo que deseja acesso a um recurso compartilhado deve enviar um pedido
de acesso a todos os coordenadores responsáveis pelo recurso. Supondo n coordenadores, o requisitante espera
apenas por uma resposta majoritária m: m > n / 2.

Vantagens:

- Não há um ponto de falha único.


- Ameniza o problema de gargalo de um coordenador, mas não resolve.

Desvantagens:

- O processo que detém o recurso atualmente não pode falhar.


- Em caso de alta concorrência, pode haver problema de inanição.

Algoritmo distribuído: Os relógios lógicos de Lamport podem ser utilizados para se obter exclusão mútua por
meio das marcas de tempo da seguinte forma…

Quando um processo quer acessar um recurso compartilhado, este envia uma mensagem a todos os processos
na rede (inclusive ele mesmo, conceitualmente) contendo o nome do recurso desejado, seu número de processo
e sua marca de tempo atual. Ao receber a mensagem de requisição, cada processo receptor age da seguinte
forma:

1. Se o receptor não estiver acessando o recurso e também não tiver interesse em acessá-lo, então
simplesmente devolve uma mensagem OK ao remetente.
2. Se o receptor não estiver acessando o recurso, mas tiver interesse em acessá-lo, então ele compara a
marca de tempo que veio junto com a mensagem de requisição com a marca de tempo que ele mesmo
enviou como parte de sua requisição ao recurso. A mais baixa vence. Se a mais baixa for do processo
solicitante, o receptor devolve uma mensagem OK, caso contrário o receptor coloca a requisição na fila
e nada responde ao processo requisitante.
3. Se o receptor já tiver acesso ao recurso, simplesmente não responde. Em vez disso, coloca a requisição
na fila.

O acesso ao recurso é concedido quando um processo requisitante recebe uma mensagem OK (permissão
concedida) de todos os processos na rede. Após utilizar o recurso, o processo envia mensagens OK a todos os
processos requisitantes que estão atualmente em sua fila.
Vantagens:

- Não há deadlock.
- Não há inanição.

Desvantagem:

- Infelizmente, o ponto de falha único foi substituído por n (processos) pontos de falha. Se qualquer processo
falhar, não responderá as requisições e esse silêncio será interpretado (incorretamente) como recusa de
permissão.

É importante destacar também que, se antes com uma abordagem centralizada, era ruim ter que pedir permissão
a um único nó da rede, com uma abordagem distribuída ter que pedir permissão a todos é pior ainda.
Algoritmos de eleição
Dado um conjunto de nós em um sistema distribuído, como eleger um líder?

Estratégia comumente adotada: Selecionar o processo com o maior ID.

O que os processos não sabem é, quais estão funcionando e quais estão inativos no momento em que é preciso
escolher um líder. Assim sendo, o problema principal consiste em fazer com que todos os processos concordem
com o líder escolhido, mesmo quando alguns nós estão inativos.

Algoritmo do valentão

Quando um processo P “percebe” que o líder atual não está mais respondendo, P inicia uma eleição:

• P envia uma mensagem “ELEIÇÃO” a todos os processos com ID maior que o seu.
• Se nenhum destinatário responder, P vence a eleição e se torna o líder.
• Se um processo com ID maior responder, P desiste da eleição e o processo destinatário toma o poder.

Quando uma mensagem “ELEIÇÃO” chega a um destinatário, este responde com uma mensagem “OK”
informando ao processo P que ele irá assumir o controle (Valentão) iniciando sua própria candidatura, caso
ainda não o tenha feito.

O processo que vence a eleição comunica a todos os nós que agora ele é o líder.
Algoritmo do anel

Quando um processo P “percebe” que o líder atual não está mais respondendo, P inicia uma eleição:

• P envia uma mensagem “ELEIÇÃO + [PID]” a seu sucessor. Caso o sucessor não esteja ativo, P submete
a mensagem ao sucessor seguinte na cadeia.
• Cada nó que recebe a mensagem adiciona a si mesmo como candidato inserindo seu próprio ID na
mensagem: “ELEIÇÃO + [PID, QID, …]”.
• Quando o nó que iniciou a eleição (P) recebe a mensagem de volta após um ciclo, este reconhece seu
próprio ID na cadeia e envia novamente uma mensagem, desta vez passando apenas pelos sucessores
identificados na cadeia e comunicando o novo líder eleito: o maior ID presente no ciclo.

Você também pode gostar