Relógios físicos e lógicos

Relógios físicos e lógicos
Sistemas Distribuídos

Você já parou para se perguntar porque o Java tem duas chamadas na classe System que retornam timestamps? Podemos usar o System.currentTimeMillis() para ter o tempo em milissegundos e System.nanoTime() para termos o tempo em nanossegundos. Mas será que essas informações são equivalentes? Quando devemos usar um ou o outro? Em qual informação podemos confiar? Nesse post vou tentar trazer algumas informações sobre relógios, tempo e sistemas distribuídos.

Para ser sincero, eu nunca tinha parado para pensar nisso até o dia 07 de outubro de 2021!!!! E é muito curioso que venho escrever esse post exatamente nessa data! Naquele dia eu estava lendo um capitulo do livro Designing Data-Intensive Applications quando me deparei com essa informação e fiz o fio abaixo.

Mas continue lendo o post que vou trazer mais informações além do que existe no fio. Hoje estava lendo o paper Time, Clocks, and the Ordering of Events in a Distributed System de 1978 e me deparei novamente com esse tema, algumas ideias vão vir desse paper. Se você se perguntar porque eu estava lendo um paper de 1978 em um sábado… Bom, talvez porque eu goste de ler e porque ele é citado nada menos do que 13.816 vezes em outros artigos segundo o Google Scholar.

Imagem do paper Time, Clocks, and the Ordering of Events in a Distributed System

Como eu sei que esse tema é bastante complexo, vou começar do zero tentando definir tempo, depois demonstrando como ele é usando em sistemas computacionais e por fim como pode ser usado em sistemas distribuídos.

O que é o tempo?

Eu não planejo tentar definir o que é tempo filosoficamente falando. Tempo é uma grandeza física que por muito tempo foi considerada como absoluta, mas depois viu-se que é relativa. Na física moderna, o tempo pode varia conforme a velocidade, se você já assistiu ao filme Contato de 1997 saberá do que estou falando. Mas na computação podemos considerar o tempo como uma grandeza absoluta que cresce sequencialmente, ou seja, não nos interessa o tempo da física moderna que varia de acordo com a velocidade, pois o tempo serve para marcar o instante que eventos acontecem.

Todo evento pode ter um tempo associado representando o momento em que ele aconteceu. Por exemplo, você sabe a data que você nasceu e é quase certo que não se lembra mas o seu primeiro dia dia na escola também tem uma data.

Da mesma forma acontece em sistemas computacionais e dessa informação podemos tirar nossa primeira conclusão. Digamos que o momento do seu nascimento é considerado t0 e que o seu primeiro dia na escola é considerado te, logo podemos supor que t0 aconteceu antes de te, ou seja, t0 → te. Vamos usar o caractere para representar que um evento aconteceu antes que outro.

Agora vamos pensar em dois individuos distintos: Alice e Bob. As perguntas que farei parecerão estranhas agora, mas elas farão sentidos posteriormente. Podemos afirmar que o nascimento de Alice (tA0) aconteceu no mesmo momento do nascimento de Bob (tB0)? Não! Mas os dois não tinham exatamente 0 dias de vida? E sobre o primeiro dia na escola de ambos? Também não, mesmo que eles tivessem a mesma idade.

Como um computador mede o tempo?

Todo processador é uma máquina orientada por um relógio interno. O principal componente de um processador é seu clock, que em intervalos regulares de tempo incrementa um valor. Logo, sempre que você inicia um computador p, ele tem o seu tp0.

Esse tempo em nenhum momento irá refletir o tempo do relógio. Ele apenas mede o avanço do tempo, logo tpi e tpj não tem nenhum significado absoluto, mas podemos afirmar que Δtij=tpi - tpj é a diferença de tempo em que os eventos i e j aconteceram.

Todo computador para ajustar seu relógio vai consultar um relógio (ntp.br) e criar umas associação entre seu tempo de clock e um tempo real. Para que esse relógio não fique desatualizado, é mantido uma bateria para que o clock nunca pare, mesmo quando desligado. O tempo do clock vamos chamar de Relógio Monotônico (Monotonic), visto que ele nunca retrocede e nem avança em intervalos constantes. Já o relógio vamos chamar de Relógio Tempo do Dia (Time-of-Day Clocks), pois ele sempre tentará refletir o valor retornado pelo servidor ntp.br. O relógio tempo do dia irá armazenar a diferença entre o momento atual e o inicio do ano de 1970 em GMT, mais conhecido como epoch time.

Para mais informações, leia Java & Clocks .

Assim, o relógio monotônico, pode ser apenas baseado no relógio interno do processador, terá uma precisão de nanossegundos. Já o relógio tempo do dia não terá uma precisão tão alta, sendo de milissegundos pois sempre existirá um atraso que é o tempo de envio da resposta da requisição com o servidor NTP.

Quando, e como, usar cada relógio?

No Java os valores desses relógios são retornados pelas chamadas System.currentTimeMillis(), para o relógio tempo do dia, e System.nanoTime() para o relógio monotônico. Um erro muito comum que vejo em sistemas é usar essas chamadas sem procurar entender os valores que elas retornam, por isso eu criei o código abaixo (veja no repositório github.com/vepo/java-physical-clock) para podermos verificar quais valores essas chamadas retornam. Você acha que os relógios terão os mesmo valores?

System.out.println(Instant.ofEpochMilli(System.currentTimeMillis()));
System.out.println(Instant.ofEpochMilli(Duration.ofNanos(System.nanoTime()).toMillis()));
Thread.sleep(5_000); // 5s
System.out.println(Instant.ofEpochMilli(System.currentTimeMillis()));
System.out.println(Instant.ofEpochMilli(Duration.ofNanos(System.nanoTime()).toMillis()));

Pela execução, podemos o relógio monotônico está bem atrasado em relação ao relógio tempo do dia, estando próximo ao epoch time. Mas, considerando que a diferença entre as duas execuções são de exatamente 5s, não podemos garantir que o segundo valor do relógio do dia seja exatamente 5s maior que o primeiro, pois entre as duas execuções poderia haver uma atualização do valor desse relógio, podendo até o segundo valor ser inferior ao primário. Mas podemos afirmar categoricamente que o relógio monotônico estaria ao menos 5s a frente na segunda execução.

2023-10-07T22:57:04.332Z
1970-01-04T16:34:56.622Z
2023-10-07T22:57:09.338Z
1970-01-04T16:35:01.624Z

Um erro comum é usar System.nanoTime() como forma de saber o momento em que um evento aconteceu, pois, como visto, esse relógio não reflete o instante presente. System.nanoTime() deve ser usado somente como forma de se calcular intervalos de tempo dentro de um mesmo processo. Vejamos o caso abaixo, onde quero calcular o tempo que uma execução demorou, nesse caso faz sentido usar e deve ser usado System.nanoTime().

Random rnd = new SecureRandom();
long start = System.nanoTime();
for (int i = 0; i < 1_000; ++i) {
    Thread.sleep(rnd.nextInt(5));
}
System.out.println("It took: " + DurationFormatUtils.formatDuration(Duration.ofNanos(System.nanoTime() - start).toMillis(), "HH:mm:ss.SSS", true));

Observe que se estou escolhendo um número aleatório entre 0 e 5, o tempo médio de execução deve ser aproximadamente 2.5s. Esse valor não seria garantido se usássemos System.currentTimeMillis().

It took: 00:00:02.465

Isso não implica que devemos sempre usar System.currentTimeMillis() para termos informações de tempo em Java. Existem várias outras classes que implementam diversas funções de tempo, como as já usadas no exemplo Instant e Duration. Eu recomendo também que se use as classes LocalDate, LocalDateTime, OffsetTime, OffsetDateTime e ZonedDateTime. Caso não tenha o hábito de usar essas classes, reserve um tempo para ler a documentação delas.

Como usar em Sistemas Distribuídos?

Lembra que eu falei sobre a data de nascimento de Alice e Bob? Cada um poderia ter registrado seu momento de nascimento como t0=0, mas com certeza os dois não nasceram no mesmo momento. O mesmo acontece em sistemas distribuídos.

Para compreender melhor, vamos supor vários processos distintos Pi rodando em processadores I distintos e que o tempo de comunicação entre Pi e o servidor NTP é diferente entre os vários processos. A primeira dificuldade de sincronização entre esses diversos sistemas é que mesmo o relógio estando correto cada relógio irá apresentar uma atraso diferente em relação ao servidor NTP. Assim, se fosse possível que todos os processos executassem System.currentTimeMillis() sincronamente, cada um teria uma resposta diferente.

Imagem mostrando a comunicação entre Pi e Pj com o servidor NTP. Cada processo envia uma requisião que é respondida com um atraso único.

Agora vamos supor outro cenário, dois eventos A e B aconteceram em dois processos distintos. Ambos os eventos recebem um timestamp ta e tb respectivamente e são enviados a um servidor comum, conforma a figura abaixo. Como podemos afirmar que A → B? Devemos considerar ta ou ta’?

3 timelines representando Pi, Pj e um servidor. Um evento A surge em ta em Pi, depois um evento B surge em Pj em tb, mas ao ser enviado ao servidor chegam na ordem trocadas

Não é possível estabelecer relação de precedência entre A e B ou entre ta e tb. Mas podemos estabelecer que ta → ta’, tb → tb’ e tb’ → ta’. No artigo Time, Clocks, and the Ordering of Events in a Distributed System, Lamport define que podemos usar os timestamps de cada processo para ordenar esses eventos mesmo sem existir relação de precedência e em caso de empate devemos usar uma propriedade dos processos. Segundo Lamport, logicamente podemos fazer duas inferências:

A questão que poderiamos levantar é como criar uma regra de ordenação que possa ser usada em algoritmos distribuídos? Mesmo que os timestamps não possam estabelecer precedência, podem ser usados para ordenação. Logo se os eventos A e B foram gerados em momentos distintos, podem ser ordenados, mas caso ta=tb deve se usar uma propriedade secundária como regra de desempate, que pode ser o identificar de cada processo.

Vale notar que se dois eventos acontecem no mesmo processo e são enviados para um servidor usando a mesma conexão, ambos chegarão em ordem ao servidor e os timestamps deles podem ser usados para estabelecer uma relação de precedência.

Conclusão

É preciso saber como funcionam os relógios de um computador para usa-los corretamente. Cada computador tem ao menos dois relógios, os monotônicos e o relógio com tempo do dia, o primeiro serve para medir intervalos de tempo, enquanto o segundo pode ser usado para se estabelecer o momento em que eventos aconteceram.

Tempos não podem ser comparados para se estabelecer relações de precedência em processos distintos, mas existindo uma regra clara de como fazer a ordenação e possível criarmos algoritmos usando essa regra.

Licença Creative Commons
Este obra está licenciado com uma Licença Creative Commons Atribuição-NãoComercial-CompartilhaIgual 4.0 Internacional .
Escrito em 06/outubro/2023