4 pontos por GN⁺ 2024-10-21 | 1 comentários | Compartilhar no WhatsApp
  • O Redlock, baseado em Redis, tem como objetivo oferecer locks distribuídos tolerantes a falhas, mas não é seguro o suficiente para tarefas em que a correção importa e é complexo demais para uso como otimização de eficiência
  • Em locks distribuídos, é preciso primeiro distinguir entre o objetivo de eficiência, que reduz trabalho duplicado, e o de correção, que protege estado compartilhado; o critério é se uma falha aumenta custos ou causa corrupção de dados
  • Mesmo com um serviço de lock perfeito, pausas longas de GC, suspensão de processos e atrasos de rede podem fazer com que uma escrita antiga seja executada após a expiração do lease, tornando necessário um fencing token
  • O Redlock não consegue criar um token monotonicamente crescente a cada aquisição de lock, e a expiração de chaves do Redis depende do relógio do sistema baseado em gettimeofday, de modo que sua segurança pode ser quebrada em situações de salto de relógio ou atraso
  • Para locks que exigem correção, use um sistema de consenso como ZooKeeper com verificação de fencing token; restrinja locks em um único nó Redis a usos aproximados e não críticos

O ponto de partida para avaliar o Redlock

  • Redlock é um algoritmo que implementa locks distribuídos tolerantes a falhas — mais precisamente, leases — sobre Redis
  • Já existem mais de 10 implementações independentes, e não se sabe quem depende desse algoritmo, portanto vale a pena fazer uma avaliação pública
  • O próprio Redis é adequado para compartilhar dados temporários, aproximados e que mudam rapidamente entre servidores
    • Ex.: contadores de requisições por endereço IP, conjuntos de IPs únicos por ID de usuário
  • O ponto preocupante é a tendência de o Redis ser usado cada vez mais em áreas de gerenciamento de dados que esperam consistência e durabilidade mais fortes; locks distribuídos são uma dessas áreas

Objetivo do lock: eficiência ou correção?

  • Em aplicações distribuídas, um lock é um mecanismo para garantir que, quando vários nós tentam executar a mesma tarefa, apenas um a execute por vez
  • Os motivos para usar locks se dividem em dois grandes grupos
    • Eficiência: é uma otimização para evitar fazer o mesmo cálculo caro duas vezes; se falhar, no máximo o custo da AWS aumenta um pouco ou a mesma notificação por e-mail é enviada duas vezes
    • Correção: é um mecanismo para impedir que processos concorrentes corrompam o mesmo estado; se falhar, podem ocorrer problemas graves como corrupção de arquivos, perda de dados, inconsistência permanente ou administração incorreta de medicamentos
  • Para locks com objetivo de eficiência, o custo e a complexidade do Redlock, que usa 5 servidores Redis e verificação por maioria, são desnecessários
    • É mais adequado usar uma única instância Redis e, se necessário, replicação assíncrona
    • Nesse caso, uma falha de energia ou um problema em um nó Redis pode causar a perda de alguns locks, mas, se for uma otimização não crítica, é uma falha aceitável
  • Por usar 5 réplicas e maioria, o Redlock parece adequado para locks em que a correção é importante, mas na prática ele não serve para esse propósito

Leases por si só não protegem recursos com segurança

  • Locks em sistemas distribuídos são diferentes de mutexes em aplicações multithread e são mais complexos porque nós e redes podem falhar independentemente
  • Um fluxo típico para atualizar um arquivo em armazenamento compartilhado é adquirir o lock, ler o arquivo, modificá-lo, escrevê-lo de volta e liberar o lock
    • O lock serve para evitar que dois clientes executem read-modify-write ao mesmo tempo e percam atualizações
  • Se um cliente parar por muito tempo enquanto mantém o lock, o lease pode expirar
    • O GC pode intervir e pausar o cliente por um longo período
    • O lease é um bom desenho para impedir que um cliente que sofreu crash mantenha o lock para sempre, mas, se o tempo de pausa for maior que o tempo de expiração, o cliente pode executar uma escrita perigosa sem saber que o lease expirou
  • Esse problema não é um caso teórico; o HBase já teve um problema semelhante no passado
    • Houve casos em que pausas de GC “stop-the-world” duraram vários minutos
    • Mesmo GCs “concurrent”, como o CMS da JVM HotSpot, às vezes precisam parar a aplicação
  • Verificar se o lock expirou imediatamente antes da escrita não resolve
    • O GC pode pausar a thread em execução em qualquer ponto, inclusive entre a última verificação e a operação de escrita

Pausas de processo e atrasos de rede são um modelo de ameaça comum

  • Mesmo usando um runtime sem pausas longas de GC, processos podem parar por vários motivos
    • Ler um endereço que não está na memória pode causar um page fault
    • Se o disco for EBS, a leitura de uma variável pode virar uma requisição síncrona pela rede da Amazon
    • Contenção de CPU, atrasos do escalonador e até um SIGSTOP enviado por engano também podem pausar um processo
  • Atrasos de rede criam o mesmo problema
    • A aplicação enviou uma requisição de escrita, mas o pacote pode atrasar e chegar ao servidor de armazenamento depois que o lease expirar
    • Em uma falha do GitHub, pacotes de rede atrasaram cerca de 90 segundos
  • Redes de pacotes como Ethernet e IP podem atrasar pacotes arbitrariamente, e isso de fato acontece
  • Portanto, mesmo em redes bem administradas, não é possível presumir timing; código simples baseado em leases é fundamentalmente inseguro, independentemente do serviço de lock usado

É preciso bloquear escritas antigas com fencing tokens

  • A solução é incluir um fencing token em toda requisição de escrita ao armazenamento
  • Um fencing token é um número que aumenta toda vez que um cliente adquire o lock
    • Ex.: o cliente 1 obtém um lease com token 33, depois fica parado por muito tempo e o lease expira
    • O cliente 2 obtém um novo lease com token 34 e envia uma requisição de escrita ao armazenamento
    • Mais tarde, o cliente 1 acorda e envia uma escrita com token 33; como o armazenamento já processou o token 34, ele rejeita a requisição com token 33
  • Para ser seguro, o servidor de armazenamento deve verificar ativamente o token e rejeitar escritas cujo valor de token retrocedeu
  • Se o serviço de lock gerar tokens estritamente monotonicamente crescentes, é possível tornar o lock seguro
    • Ao usar o ZooKeeper como serviço de lock, é possível usar o zxid ou o número de versão do znode como fencing token
  • O grande problema do Redlock é que ele não tem funcionalidade de geração de fencing token
    • O valor aleatório único do Redlock não fornece a monotonicidade crescente necessária
    • Um contador em um único nó Redis não é suficiente, pois esse nó pode falhar
    • Contadores em vários nós podem divergir entre si
    • Mesmo para gerar fencing tokens, provavelmente será necessário um algoritmo de consenso

O Redlock depende de premissas de tempo para sua segurança

  • Em algoritmos distribuídos, o modelo prático é o modelo assíncrono com detectores de falha não confiáveis
    • Processos podem parar por um período arbitrário
    • Pacotes podem ser atrasados arbitrariamente na rede
    • Relógios podem estar arbitrariamente errados
    • Ainda assim, o algoritmo deve tomar decisões corretas
  • Relógios só podem ser usados para criar timeouts a fim de não esperar para sempre quando um nó cai
    • O timeout não precisa ser exato, e o fato de uma requisição ter dado timeout não significa que o nó remoto esteja necessariamente fora do ar
    • Também pode ser atraso de rede ou erro no relógio local
  • O Redis usa gettimeofday, não um monotonic clock, para decidir a expiração de chaves
    • gettimeofday pode sofrer saltos descontínuos no horário do sistema
    • Se o NTP ajustar o relógio ou um administrador alterar a hora manualmente, a expiração de chaves do Redis pode acontecer muito mais cedo ou muito mais tarde do que o esperado
  • Algoritmos no modelo assíncrono normalmente preservam a segurança sem premissas de timing, e detectores de falha como timeouts afetam apenas a vivacidade
    • Se o timing ficar caótico, o desempenho pode piorar, mas o algoritmo não deve tomar decisões incorretas
  • O Redlock, diferentemente disso, depende de várias premissas de timing para sua segurança
    • Todos os nós Redis devem manter as chaves por aproximadamente o período correto
    • O atraso de rede deve ser suficientemente menor que o tempo de expiração
    • As pausas de processo devem ser muito mais curtas que o tempo de expiração

Casos em que o Redlock quebra com timing ruim

  • Com 5 nós Redis A, B, C, D, E e clientes 1 e 2, se o relógio de um nó saltar para a frente, ambos os clientes podem acreditar que possuem o lock
    • O cliente 1 obtém o lock em A, B, C e não consegue alcançar D, E devido a um problema de rede
    • O relógio de C salta para a frente, e o lock expira
    • O cliente 2 obtém o lock em C, D, E e não consegue alcançar A, B devido a um problema de rede
    • Como resultado, os clientes 1 e 2 concluem que ambos são donos do lock
  • Um problema semelhante pode ocorrer se C sofrer crash e reiniciar imediatamente antes de persistir o lock em disco
    • A documentação do Redlock recomenda atrasar a reinicialização de um nó que sofreu crash por pelo menos o maior TTL de lock
    • Esse atraso de reinicialização também depende de medição de tempo razoavelmente precisa e pode falhar se o relógio saltar
  • Pausas no processo cliente também podem quebrar o Redlock
    • O cliente 1 solicita o lock a A, B, C, D, E
    • Enquanto as respostas estão em trânsito, o cliente 1 entra em um GC stop-the-world
    • Os locks em todos os nós Redis expiram
    • O cliente 2 obtém o lock em A, B, C, D, E
    • O cliente 1 termina o GC e recebe as respostas de sucesso que estavam no buffer de rede do kernel
    • Ambos os clientes acreditam possuir o lock
  • O fato de o Redis ser escrito em C e não ter GC não ajuda
    • O problema ocorre em sistemas em que o cliente pode sofrer pausas de GC
    • Para ser seguro, é preciso impedir o trabalho do cliente 1 depois que o cliente 2 obtém o lock, por meio de algo como fencing tokens
  • Atrasos longos de rede podem ter o mesmo efeito que pausas de processo
    • Se o TCP user timeout for configurado para ser muito menor que o TTL do Redis, talvez pacotes atrasados sejam ignorados, mas seria preciso analisar a implementação específica do TCP para ter certeza
    • Mesmo nesse caso, voltamos ao problema da precisão da medição de tempo

As premissas de sistema síncrono exigidas pelo Redlock

  • O Redlock só funciona corretamente em um modelo de sistema síncrono com as seguintes propriedades
    • Há um limite superior garantido para o atraso de rede
    • O tempo de pausa de processos é limitado
    • O erro de relógio é limitado
  • Um modelo síncrono não significa que os relógios sejam exatamente sincronizados, mas sim que há limites superiores fixos e conhecidos para atraso de rede, pausas e drift de relógio
  • O Redlock assume que atraso, pausa e drift são todos pequenos em comparação com o TTL do lock
    • Se um problema de timing for tão grande quanto o TTL, o algoritmo falha
  • Em ambientes típicos de datacenter, essas premissas de timing podem ser satisfeitas na maior parte do tempo; isso é chamado de sistema parcialmente síncrono
  • Se a correção depende do lock, “na maior parte do tempo” não é suficiente
    • No momento em que uma premissa de timing é quebrada, o Redlock pode violar a segurança, por exemplo concedendo um lease a outro cliente antes de o lease de um cliente expirar
    • O caso de atraso de pacotes de 90 segundos no GitHub é evidência de que é difícil assumir um modelo de sistema síncrono em ambientes reais
  • Raft, Viewstamped Replication, Zab e Paxos pertencem à categoria de algoritmos de consenso projetados para o modelo de sistema parcialmente síncrono ou para o modelo assíncrono com detectores de falha
    • Esses algoritmos devem abandonar premissas de timing, e é preciso ter cuidado para não presumir que redes, processos e relógios de sistemas distribuídos sejam mais confiáveis do que realmente são

Conclusões e opções recomendadas

  • O Redlock é desnecessariamente pesado e caro para locks de otimização de eficiência, e não é seguro o suficiente para locks em que a correção importa
  • Em especial, ele presume na prática um sistema síncrono com limites superiores para atraso de rede e tempo de execução de operações, e pode violar a segurança quando essa premissa é quebrada
  • Ele também não tem funcionalidade de geração de fencing tokens para proteger o sistema contra atrasos longos de rede ou processos pausados
  • Se você precisa de um lock de otimização de eficiência em regime de melhor esforço, é melhor usar o algoritmo de lock de nó único do Redis
    • Adquirir o lock com um set-if-not-exists condicional
    • Liberar o lock apagando-o atomicamente apenas quando o valor coincidir
    • Documentar claramente no código que o lock é aproximado e pode falhar ocasionalmente
    • Não é necessário configurar um cluster de 5 nós Redis
  • Para locks que exigem correção, não use Redlock; use um sistema de consenso como ZooKeeper
    • Se possível, use Curator recipes, que implementam locks
    • No mínimo, é possível usar um banco de dados como PostgreSQL, que oferece garantias transacionais razoáveis
    • Todo acesso a recursos protegidos pelo lock deve impor a verificação de fencing token
  • O Redis é uma ferramenta útil quando usada para os fins pretendidos; toda ferramenta tem limites, e é preciso conhecê-los e planejar em torno deles
  • Na atualização de 9 de fevereiro de 2016, Salvatore, o autor original do Redlock, publicou uma réplica, mas a conclusão se mantém

1 comentários

 
GN⁺ 2024-10-21
Opiniões do Hacker News
  • No trabalho usamos Temporal e implementamos um lock distribuído com workflows e signals dedicados.
    Até agora tem funcionado bem, e deixar a parte de processamento distribuído do lock a cargo dos recursos do Temporal torna a implementação bastante simples.

    • Acabei de conhecer o Temporal, e ele parece uma espécie de Santo Graal dos workflows, oferecendo gerenciamento de tarefas de alto nível de forma muito clara sobre uma infraestrutura complexa.
      Fico curioso para saber se o Temporal é único nessa área, ou se existem alternativas em nível semelhante.
      Como ele foi separado da Uber e é usado por grandes fornecedores, parece suficientemente validado em produção.
    • Interessante. Pode explicar com um pouco mais de detalhes? Quero tentar construir algo parecido com Temporal.
    • Tenho vontade de experimentar o Temporal, mas ouvi dizer que ele pode ser instável. Na prática, funcionou bem?
  • Para locks distribuídos, normalmente uso PostgreSQL advisory lock.
    Mesmo que a tarefa não tenha relação com o banco de dados, se você iniciar uma transação e adquirir um advisory lock, o lock é mantido até que a aplicação o libere explicitamente ou até que a transação termine, por exemplo por um crash.
    Até agora isso me parecia bastante seguro, mas acabei de perceber que nunca verifiquei se a conexão com o banco de dados ainda está saudável.
    Se for uma operação relacionada ao banco de dados, a consulta falharia e a tarefa também falharia; caso contrário, você pode nem perceber que já perdeu o lock.
    Se você quer correção absoluta sem fencing tokens ou operações atômicas, no fim talvez seja necessário usar commit em duas fases para todas as operações.

    • Advisory locks têm muitas armadilhas. Veja [0].
      Para fazer corretamente aquilo que provavelmente era a intenção, talvez seja preciso usar “EXCLUSIVE” ou “ACCESS EXCLUSIVE”, ou então garantir commit em duas fases ou idempotência para a tarefa.
      [0] https://www.postgresql.org/docs/current/explicit-locking.htm...
    • O ponto a observar em locks é que, pelo que sei, eles são por conexão.
      A maioria das bibliotecas normalmente usa pool de conexões, então é preciso reservar uma conexão dedicada ao lock, e as verificações periódicas do lock também precisam ser feitas necessariamente por essa mesma conexão.
  • Seria bom ler meu comentário que deixei nos comentários desse blog no passado, e a resposta que escrevi no meu blog.
    Em ordem aleatória: o autor deixou passar pontos essenciais sobre como o algoritmo funciona e depois rejeitou o algoritmo com base nos argumentos mais fracos que sobraram.
    Também não é verdade que, em computadores e APIs modernos, seja impossível esperar aproximadamente pelo tempo correto. Pausas de GC são bounded, e relógios monotônicos funcionam, então são premissas aceitáveis.
    Criticar o mecanismo de liberação automática em si porque ele expõe uma condição de corrida potencial é diferente de criticá-lo dentro dos objetivos do algoritmo e do modelo de sistema.
    Redlock foi usado com sucesso por muitos anos em muitos casos de uso, e, se você definir o timeout como muito maior que o tempo de conclusão da tarefa e que pausas arbitrárias que podem ocorrer em sistemas operacionais comuns, fica muito difícil induzir uma condição de corrida.
    Claro, se você definir o timeout de liberação automática pequeno demais e a tarefa puder facilmente levar esse tempo, isso é erro de projeto, mas não é um problema do Redlock em si.

    • Sinceramente, desde antes aquele post de resposta no blog nunca fez muito sentido para mim. Talvez esta pergunta ajude a encontrar um ponto comum.
      Você usaria RedLock em uma situação em que o timeout é bem curto (por exemplo, 1–2 segundos), a tarefa normalmente usa cerca de 90% desse timeout, e a operação feita enquanto o lock do RedLock está mantido jamais pode executar simultaneamente com outro detentor do lock?
      Para mim, a resposta correta aqui é sempre “não”. O risco de o lease expirar antes de o cliente terminar a tarefa é muito alto.
      Como o RedLock não consegue garantir exclusão mútua em todas as situações, é preciso tornar a tarefa idempotente, e esse tipo de caso costuma ser melhor implementado com lock otimista.
    • Pode passar o link?
  • Estou reforçando de novo meus conhecimentos de baixo nível e de algoritmos; quais seriam bons livros para este tema? Tenho o livro escrito pelo autor.
    Quero criar algo por diversão, mas os materiais são ou em nível de brinquedo ou complexos demais.

    • Recomendo System Design Interview I, II, de Alex Xu.
      Escolha um tema e implemente de verdade.
  • No passado escrevi um post de blog sobre lock distribuído com base neste material: https://medium.com/sahibinden-technology/an-easy-integration...

  • A explicação de que “locks têm timeout (ou seja, são leases)” soa estranha para mim.
    Primeiro, se o cliente crasha, mesmo sem um timed lease, o sistema operacional ou o supervisor deveria liberar o lock; se ambos morrem, a conexão acabará caindo, e o sistema de rede deveria detectar isso por reset, timeout, ausência de heartbeat etc., invalidar a conexão e então liberar o lock.
    Segundo, se o problema é um cliente que, por bug, não crasha e segura o lock por tempo demais, algum supervisor não deveria detectar isso e matar o cliente antes de liberar o lock para os outros?
    Terceiro, se usamos locks com timeout para lidar com esses corner cases, o programa real não deveria ser avisado por algum mecanismo como exceção, signal ou encerramento? E não deveria esperar a confirmação de que o programa recebeu a notificação antes de liberar o lock?
    A própria ideia de permitir que o programa continue executando seu fluxo de controle normal mesmo depois do timeout parece ser a causa raiz do problema, e não entendo por que todo mundo passa por cima disso. Estou deixando passar algum motivo óbvio?

    • Isso não é um mutex, e sim a versão de sistemas distribuídos dele.
      Quem invalida o lock do próprio lado é o serviço de armazenamento, e, sem garantias adicionais que o Redlock não oferece, o cliente não consegue detectar sozinho o próprio problema.
    • A suposição de que o servidor sempre receberá RST ou FIN do cliente está errada.
      Em alguns casos esses pacotes são descartados, e o cliente na máquina remota já pode ter morrido enquanto o servidor ainda mantém uma conexão aberta.
      A propósito, não fui eu que dei downvote.
  • Implementei um lock distribuído com o Deno KV, hospedado pelo Deno e pelo Deno Deploy.
    Internamente, ele usa o FoundationDB, um banco de dados distribuído, e as instâncias do Deno rodando em dispositivos locais se conectam ao mesmo Deno KV para adquirir o lock.
    Com PostgreSQL também funciona usando SELECT FOR UPDATE, mas o banco de dados em si não é distribuído.

  • Em 2018, avaliamos Redis para nosso caso de uso, mas acabamos escolhendo uma solução menos chamativa, e ela realmente nunca falhou uma vez sequer.
    O caso de uso era distribuir, um a um, tickets com identificador a partir de um conjunto finito de tickets de uma campanha, parecido com o Ticketmaster atribuindo assentos em uma casa de shows.
    Quando uma requisição chegava, precisávamos oferecer um ticket disponível, anexar os metadados da requisição ao ticket atribuído e depois excluí-lo das requisições futuras.
    Como no passado houve campanhas com falhas como alocação em excesso, alocação insuficiente e alocação duplicada, a correção era essencial.
    Também tentamos uma implementação simples com Redis para adquirir o lock, verificar o lock, executar a operação e liberar o lock, mas na época a carga operacional era grande para nós, e foi uma sorte não termos seguido por esse caminho.
    A escolha final foi Postgres. Nosso “lock distribuído” estava mais para uma instrução UPDATE composta usando recursos nativos do Postgres, transformando a requisição em uma espécie de operação de conjunto para que o banco de dados retornasse um registro de sucesso ou uma indicação de falha. As transações ACID venceram.
    Depois de resolver a correção, analisamos escala e desempenho; não precisávamos de milhões de requisições por segundo, mas tínhamos critérios para picos momentâneos.
    Otimizamos as instâncias de banco de dados de leitura e escrita dentro do cluster, posicionamos estrategicamente campanhas maiores ou de maior demanda em sistemas designados e continuamos otimizando por 2 anos, mas não houve uma única campanha com falha na distribuição de tickets.
    Não sou especialista em tecnologia de locks distribuídos; apenas nos concentramos no problema a resolver, tentamos algumas coisas e encontramos a solução adequada.

    • Está correto dizer que, com até 50 mil transações atômicas curtas por segundo, basta usar Postgres.
      Como uma transação UPDATE dura apenas alguns microssegundos, dá para centralizar o problema, e isso é mais simples, mais rápido e mais seguro.
      Mas, como o texto explica, isso não é um problema distribuído.
      Locks em sistemas distribuídos são diferentes de mutexes em apps multithread e são mais complexos porque múltiplos nós e a rede podem falhar de várias maneiras, independentemente uns dos outros.
      Um lock distribuído é necessário quando uma transação pode levar de segundos a horas e a máquina envolvida pode falhar enquanto mantém o lock.
    • O ponto importante que este caso mostra é que o necessário não é um lock, mas sim restrições.
      Neste caso, a restrição é “não vender mais do que N tickets”, e a maior parte das escalas de tráfego reais desse tipo de problema pode ser resolvida com o comportamento transacional de bancos de dados relacionais tradicionais, deixando o gerenciamento interno de locks para o banco de dados.
      Eu gostaria que desenvolvedores não pulassem tão rápido para “vou criar um lock distribuído”. Quase sempre há uma resposta melhor, mas essa resposta varia conforme a aplicação.
    • No fim, a resposta era que, ao contrário do que se pensa, na maioria dos casos não é preciso um lock distribuído. :)
    • Isso parece um problema embaraçosamente fácil de paralelizar, já que dá para fazer sharding por show em instâncias diferentes.
      Talvez seja um caso adequado para algo como o novo SQLite da Cloudflare.
    • Esta é a melhor abordagem e, na prática, a única forma razoável de encarar o problema.
      O primeiro lugar onde li sobre isso foi aqui: https://code.flickr.net/2010/02/08/ticket-servers-distribute...
  • Muitos engenheiros não se preocupam de verdade com problemas de correção até ser tarde demais. É parecido com segurança.
    Mesmo quando se preocupam, muitas vezes não verificam se o que estão fazendo está correto.
    Por exemplo, na minha área, microsserviços, atores e processos trocam mensagens pela rede, e mais de 95% das implementações que vejo têm casos de borda em que mensagens podem ser perdidas ou processadas fora de ordem.
    Mas os incentivos não estão alinhados o suficiente para corrigir isso. A estrutura de remuneração de executivos e engenheiros não está alinhada com os resultados que seriam melhores para clientes e acionistas.

    • Os próprios microsserviços muitas vezes são um sintoma desse problema.
      As pessoas querem colocar uma fronteira de rede entre chamadas de função sem um bom motivo e, em seguida, criam sem parar servidores e clientes HTTP, serialização e desserialização de JSON para essas chamadas de função; com sorte, usam gRPC, e então tentam reimplementar coisas como transações distribuídas através dessa fronteira de rede.
      No fim, isso só cria trabalho inútil para lidar com as inevitáveis “interações assustadoras à distância”.
    • Para corrigir, primeiro é preciso medir e monitorar, depois estabelecer objetivos de nível de serviço que representem a experiência do cliente.
      As equipes de produto e engenharia precisam concordar com isso e, quando um SLO for violado, o foco deve mudar para a estabilidade do sistema.
      Como é difícil convencer todo mundo, é necessária uma boa liderança.
      Quando bugs começam a aparecer, novos recursos ficam lentos ou quase inexistentes e clientes começam a ir embora, fica muito fácil justificar que qualidade precisa fazer parte do processo.
      Líderes maduros se antecipam a essa etapa o quanto antes.
    • Esse tipo de problema pode mandar pessoas inocentes para a prisão ou até levá-las à morte.
      [0] https://en.wikipedia.org/wiki/British_Post_Office_scandal
    • Acho que existe algum alinhamento de incentivos. Esses casos de borda são difíceis e há uma boa chance de os desenvolvedores terem que lidar com muitos tickets de suporte, o que não é bom para ninguém.
      Mas não vejo muito bem como convencer os gestores de ontem a dar tempo para fazer direito.
  • Isso complica demais as coisas
    Se existir algo como o token de fencing mencionado no texto, não é necessário um lock
    O token nem precisa ser monotonicamente crescente; basta ser um valor único passivo que o cliente e o armazenamento possuam em conjunto
    Chamando isso de token de versão, ele pode ser um valor monotonicamente crescente, e um UUID, que geralmente é mais fácil de gerar, também funciona. Tecnicamente, até um hash de todos os dados do armazenamento seria possível, mas não é prático
    O fluxo é este. O cliente busca no armazenamento o token de versão atual junto com os dados a serem modificados, e o armazenamento consulta os dados e o token de forma atômica, garantindo que aquele token pertença àquela versão dos dados
    Depois, o cliente envia de volta o token de versão junto com as alterações, e o armazenamento só aceita a alteração se o token atual corresponder ao token enviado, gerando atomomicamente um novo token de versão
    Pode-se introduzir um lock por outros motivos, mas, em sistemas distribuídos, ele deve ser independente da integridade do armazenamento
    Também não gosto muito do termo “lock”. Como é temporário e não garantido, lease ou reserva talvez comunique melhor o sentido

    • O que você está descrevendo é comparar e trocar (CAS), e é uma boa solução
      É uma forma de empurrar a complexidade para o lado do banco de dados, mas é preciso lembrar que aqui estamos falando de lock distribuído
      Se for um único banco de dados, é simples até o banco de dados cair e você não saber quais escritas CAS foram de fato refletidas
      Em sistemas grandes que exigem alta disponibilidade e backup em múltiplos data centers, cenários em torno de falhas de nós podem fazer essa abordagem também quebrar, então ela fica bastante complexa
      Normalmente usa-se um log de transações em alguma forma de Paxos. Não se deve presumir que exista uma solução fácil em sistemas distribuídos. É sempre uma dor de cabeça
    • Você interpretou o problema incorretamente e está propondo uma solução para outro problema
    • Isso é conhecido como lock otimista. Mas eu não chamaria de mecanismo de lock distribuído
    • Essa explicação ignora o primeiro motivo do texto para usar locks
      Do ponto de vista da eficiência, ao adquirir um lock você evita fazer desnecessariamente a mesma tarefa duas vezes. Por exemplo, um cálculo caro
      Se o lock falhar e dois nós fizerem a mesma tarefa, isso pode ser algo trivial se o resultado for apenas um pequeno aumento de custo ou a duplicação da mesma notificação por e-mail
      Mas acho que vários nós fazendo a mesma tarefa é muito pior do que o exemplo sugere, porque isso pode atrapalhar o próprio processamento distribuído escalável
    • Se você não usar tokens monotonicamente crescentes, isso não leva a um estado inconsistente?
      Suponha que o sistema de armazenamento tenha dois nós e que dois processos de leitura-modificação-escrita estejam em execução. Os processos 1 e 2 obtêm o primeiro token abc
      O processo 1 faz commit, o token muda para cde e a alteração é transmitida por streaming para o nó 2, mas, por causa de atraso na rede, ela chega tarde ao nó 2
      Nesse meio-tempo, se o processo 2 fizer commit no nó 2 com o token abc, o nó 2 ainda não terá recebido a mensagem do nó 1, então aceitará a alteração e o sistema ficará em estado inconsistente
      Com um token de fencing monotonicamente crescente, isso não acontece. Esse requisito obriga os nós a chegarem a um consenso sobre a ordem total das operações antes de fornecerem um token