Ir para o conteúdo

Blogue ·

Duas encomendas, uma máquina

Um servidor dedicado vende-se exatamente uma vez. Bloquear todo o conjunto de candidatas custou em silêncio uma reserva, e a cura foi bloquear uma de cada vez.

Por Provibr

Um plano de VPS vende a mais de propósito e ninguém perde o sono com isso. Uma máquina dedicada não pode: há uma, e se duas encomendas acreditarem ambas que ficaram com ela, numa segunda-feira de manhã um cliente vai ouvir que aquilo que pagou na sexta pertence a outra pessoa.

O requisito cabe numa frase. Reservar antes dos efeitos secundários e devolver capacidade de forma transacional depois de uma queda ou de uma falha. Construí-lo produziu duas medições que mudaram o desenho, e uma delas custou-nos uma máquina que estava ali, livre.

O árbitro é um índice

A regra de que duas encomendas nunca podem reter a mesma máquina vive num índice único parcial sobre o identificador do equipamento, restringido às reservas no estado retida. Não na aplicação, não num bloqueio que o código tome, não num comentário.

Essa é de propósito a mesma forma do índice de ocupação sobre unidades de bastidor. A camada de aplicação produz uma recusa legível antes da colisão, e a base de dados decide quem ganha de facto. Só uma das duas pode ser autoridade, e tem de ser aquela que nenhum caminho de código acrescentado para o ano consegue contornar.

Retida é o único estado que retém alguma coisa, e isso está expresso numa tabela ao lado do índice e não numa coluna booleana, porque um booleano ao lado do estado é uma segunda verdade que só está certa enquanto todos os escritores se lembrarem dela. Um teste compara a tabela com o índice.

Há quatro estados e dois deles formam de propósito um par: retida, convertida, expirada, cancelada. A simplificação tentadora é uma única libertada para as duas últimas, e deixaria sem resposta a pergunta de saber se alguém a cancelou ou se ficou simplesmente ali, que é justamente a pergunta que aparece quando um cliente telefona por causa de uma encomenda que não avançou. Uma varredura que escreve em silêncio o mesmo estado que uma pessoa apaga a diferença entre uma decisão e um prazo esgotado.

Bloquear todo o conjunto custou uma reserva

A primeira versão selecionava as máquinas candidatas e bloqueava-as numa única consulta com FOR UPDATE SKIP LOCKED, saltando o que já estivesse tomado. Lê-se bem e está errado, e só um teste de concorrência o mostrou.

Oito encomendas simultâneas sobre três máquinas elegíveis deviam produzir três reservas. Produziram duas. A primeira transação agarrou as três linhas, usou uma e reteve as outras duas até confirmar; todos os restantes passaram ao lado das três e não encontraram nada. O conjunto não estava esgotado, estava refém.

A cura é bloquear uma candidata de cada vez: pega na primeira e, se outra pessoa a tiver, passa à seguinte. O mesmo teste outra vez: três reservas sobre três máquinas diferentes e cinco recusas honestas. Duas encomendas sobre uma única máquina dão uma reserva e uma recusa.

Essa cura traz o seu próprio risco, que é o comprimento do ciclo. Com cem encomendas simultâneas, a última transação percorreria cem candidatas antes de ganhar, e uma transação longa segura todas as outras. Por isso a varredura para às vinte e cinco candidatas, e acima disso «volte a tentar daqui a pouco» é a resposta honesta.

Repare que saltar e falhar são contados em separado. Uma candidata retida por outra pessoa é saltada, e isso não é uma tentativa, porque não correu nada mal. Uma candidata que perdeu uma corrida de inserção é uma tentativa, e dessas há no máximo cinco.

Uma violação de unicidade envenena a transação

Esta é a armadilha em torno da qual o ciclo de repetição foi construído, e é fácil escrever código que parece correto e cai na primeira colisão a sério.

Dentro de uma transação PostgreSQL, uma violação de chave única aborta a transação. Toda a instrução seguinte é então recusada com outro erro, «a transação atual está abortada», até alguém reverter. Um ciclo que apanha a violação e tenta a candidata seguinte não funciona de todo: a inserção da segunda candidata falha por um motivo que nada tem a ver com essa segunda candidata.

Cada tentativa corre por isso dentro de um ponto de salvaguarda, que no nosso construtor de consultas é simplesmente uma transação encaixada. Reverter até ao ponto de salvaguarda, a transação exterior está de novo saudável, candidata seguinte.

Há um pormenor associado sobre como a violação é apanhada. O nosso construtor de consultas embrulha os erros do controlador num tipo próprio e pendura o verdadeiro na causa, portanto testar um código no nível de topo não vê nada. É preciso percorrer a cadeia de causas, e é preciso olhar também para o nome da restrição, porque esta tabela tem duas restrições de unicidade e dizem coisas opostas: uma diz que a máquina está tomada, a outra diz que esta encomenda já tem uma reserva e aqui está ela.

A recusa tem de dizer de que espécie é

O atribuidor constrói a lista de candidatas duas vezes, e a diferença entre os dois resultados é uma mensagem para um cliente.

A primeira passagem é a lista completa, com um motivo para cada máquina que ficou de fora: sede errada, fabricante errado, não pronta, e assim por diante. A segunda passagem é aquilo que naquele instante foi realmente possível bloquear. Se a lista completa deu uma vencedora e a lista bloqueável não, então havia algo para vender e alguém chegou uma fração de segundo antes. Essa é uma frase diferente de «não há nada»: uma significa volte a tentar, a outra significa compre mais ferro. Merecem botões diferentes.

O motivo congelado da máquina escolhida vai para a linha da reserva, porque um mês depois cada equipamento está noutro estado e já não é possível reconstruir porque é que o atribuidor tinha escolhido aquela. A linha di-lo então por si.

A chave de idempotência é perguntada duas vezes, e essa segunda vez é ainda uma coisa que só um teste de concorrência encontrou. Cinco pedidos simultâneos com a mesma chave, sobre quatro máquinas, produziram uma reserva e quatro recusas, incluindo uma de uma transação que não conseguiu bloquear nada e por isso nunca chegou à inserção nem ao ramo de repetição, enquanto naquele momento existia simplesmente uma reserva com a sua chave. Perguntar outra vez mesmo antes de recusar resolve isso, e funciona porque no nosso nível de isolamento cada instrução tira um instantâneo novo: um irmão acabado de confirmar é portanto visível.

A garantia que sobrevive a tudo isto é estreita e merece ser escrita assim: cinco pedidos com a mesma chave dão uma reserva e quatro vezes «alguém foi mais rápido». Quem perde ainda não vê quem ganhou, porque naquele instante ainda não tinha confirmado. Não existe versão disto em que seja diferente sem que uns esperem pelos outros, e esperar transforma cada minuto movimentado numa fila.

A reserva torna-se a ligação

Quando a encomenda se conclui, a máquina tem de passar de reservada a vendida. A implementação óbvia liberta a reserva e depois escreve a ligação de serviço, e contém uma janela de alguns milissegundos em que a máquina está livre. Num dia movimentado, essa janela é encontrada.

Por isso a conversão escreve a ligação e passa a reserva a convertida numa única transação, e a linha continua retida até a ligação existir. Em cada instante dessa transação, uma segunda encomenda vê ou a reserva ou a ligação, e ambas a excluem. Não há instante livre porque nenhuma instrução cria um. A mesma forma que promover uma reserva de bastidor a localização, uma camada acima.

A varredura é uma limpeza e não um guarda

Uma reserva expira ao fim de quinze minutos por omissão, e um trabalhador varre as expiradas a cada minuto.

Esse número não são as quatro horas que uma reserva de bastidor recebe, e a diferença está naquilo por que cada uma espera. Uma reserva de bastidor espera por uma pessoa com um carro: alguém tem de ir à sala, montar calhas, mudar cabos. Uma reserva de máquina espera por uma compra: ecrã de pagamento, controlo de fraude, confirmação. Entretanto, o inventário mais caro que um alojador tem fica invendável. Quinze minutos são generosos para um pagamento interrompido que alguém retoma, e curtos o suficiente para que uma tempestade de encomendas abandonadas não deixe a prateleira vazia meio dia.

O que importa é onde o prazo é imposto. É verificado dentro da conversão, não pela varredura. Se a varredura fosse o único sítio, uma reserva expirada entre duas passagens ainda se converteria, e um guarda que passa uma vez por minuto não guarda nada. A varredura põe a administração em ordem e devolve a máquina ao inventário; isso é trabalho de limpeza.

Além disso não tem interruptor de funcionalidade, e é uma exceção deliberada. Existe um interruptor de lançamento sobre todo o módulo de centro de dados, e repeti-lo aqui significaria que desligar o módulo deixa máquinas reservadas para sempre para encomendas que já ninguém consegue ver. Um interruptor cujo efeito secundário é desligar um prazo não é um interruptor, é uma fuga.

O que registaríamos

  1. Ponha o árbitro na base de dados. A aplicação deve produzir a recusa legível; o índice deve decidir o vencedor.
  2. Bloqueie uma candidata de cada vez. Bloquear um conjunto retém linhas que não vai usar, e o sintoma é uma venda perdida e não um erro.
  3. Limite a varredura. O bloqueio por candidata troca um conjunto refém por um ciclo longo, e esse ciclo também precisa de um limite.
  4. Uma violação de unicidade aborta a transação. Repetir sem ponto de salvaguarda produz um erro que nomeia a coisa errada.
  5. Olhe para o nome da restrição. Uma tabela tem muitas vezes várias unicidades, e para o utilizador significam coisas diferentes.
  6. «Não há nada» e «alguém foi mais rápido» são duas mensagens. Construir a lista de candidatas duas vezes é a maneira de as distinguir.
  7. Promova em vez de libertar e reescrever. Qualquer sequência com um instante livre acaba por ser encontrada.
  8. Imponha o prazo no ponto de utilização. Uma varredura periódica é lida de casa, e lida de casa não é uma regra.

Para ser claro sobre o que isto é: o trabalho de logística de centro de dados está atrás de um interruptor de lançamento desligado por omissão, com uma definição por organização por cima, portanto nada disto está hoje à venda. Duas coisas não estão demonstradas. A varredura de expiração foi medida como função e não como trabalhador em execução. E não foi tentado um pedido cru contra as ações de servidor com o interruptor fechado.

Do trabalho de plataforma da Provibr. Essa mesma promoção em vez de reescrita, aplicada a unidades de bastidor, está em a reserva torna-se a localização, e porque é que unidades livres num bastidor não são o mesmo que capacidade vendável está em o espaço em bastidor não é capacidade.

Comece com um host e um agente

Crie uma conta, instale o agente com um único comando e ligue o seu primeiro host Proxmox. O período experimental dura 14 dias e não pede quaisquer dados de pagamento.