Procurando por um Padrão de Bloqueio Distribuído


10

Preciso criar um mecanismo \ padrão de bloqueio de objeto recursivo personalizado para um sistema distribuído em C #. Basicamente, eu tenho um sistema com vários nós. Cada nó tem permissões de gravação exclusivas em n- número de partes do estado. O mesmo estado também está disponível no formato somente leitura em pelo menos um outro nó. Algumas gravações / atualizações devem ser atômicas em todos os nós, enquanto outras se tornarão consistentes através de processos de replicação em segundo plano, filas, etc.

Para as atualizações atômicas, estou procurando um padrão ou exemplos que permitam marcar com eficiência um objeto como bloqueado para gravações, para que eu possa distribuir, confirmar, reverter, etc ... Como o sistema possui altos níveis de simultaneidade, eu estou assumindo que precisarei ser capaz de empilhar bloqueios que expirarão ou serão desenrolados quando os bloqueios forem liberados.

As partes da transação ou do sistema de mensagens não são o foco desta pergunta, mas forneci a elas um contexto extra. Com isso dito, fique à vontade para articular quais mensagens você acha que seriam necessárias, se quiser.

Aqui está uma amostra vaga do que eu estava imaginando, embora eu esteja aberto a novas idéias além de implementar produtos totalmente novos

thing.AquireLock(LockLevel.Write);

//Do work

thing.ReleaseLock();

Eu estava pensando em usar métodos de extensão, que podem ser algo como isto

public static void AquireLock(this IThing instance, TupleLockLevel lockLevel)
{ 
    //TODO: Add aquisition wait, retry, recursion count, timeout support, etc...  
    //TODO: Disallow read lock requests if the 'thing' is already write locked
    //TODO: Throw exception when aquisition fails
    instance.Lock = lockLevel;
}

public static void ReleaseLock(this IThing instance)
{
    instance.Lock = TupleLockLevel.None;
}

Para esclarecer alguns detalhes ...

  • Todas as comunicações são TCP / IP usando um protocolo de solicitação / resposta binário
  • Não há tecnologias intermediárias, como filas ou bancos de dados
  • Não há nó principal central. Nesse caso, o arranjo de bloqueio é definido pelo iniciador do bloqueio e pelo parceiro que atenderá à solicitação com alguma forma de tempo limite para controlar seu comportamento

Alguém tem alguma sugestão?


Bloqueios geralmente são um recurso padrão na maioria dos sistemas. Eu acho que está lá para C # também. (Um resultado de pesquisa do Google: albahari.com/threading/part2.aspx ) Você está tentando alcançar algo além do Mutex ou semáforo básico?
Dipan Mehta

2
@DipanMehta Desculpe, eu deveria ter abordado isso mais claramente. Os nós que eu mencionei são máquinas em uma rede. Meu entendimento sobre o Mutex e o Semáforo é que eles são bloqueios em toda a máquina ( por exemplo, processo cruzado ) e não bloqueios que podem se estender entre as máquinas em uma rede.
JoeGeeky

@JoeGeeky Sua pergunta está relacionada ao tópico aqui e seria possivelmente muito teórica para o Stack Overflow . Se você quiser pedir novamente, é possível, mas desejará uma redação mais focada no código.
Adam Lear

Respostas:


4

Obrigado pelos esclarecimentos.

Nesse caso, o que eu recomendaria é usar um modelo de publicação / assinatura. Protocolo de bloqueio distribuído Chubby do Google (uma implementação do Paxos )

Eu nunca usei Paxos (ou Chubby), mas parece haver uma implementação de código aberto aqui .

Se isso não funcionar, você pode implementar sua própria versão do Paxos usando, por exemplo, um dos suspeitos comuns em termos de bibliotecas de mensagens: a biblioteca da fila de mensagens zero , RabbitMQ ou ActiveMQ .


Resposta anterior:

A maioria das sugestões em SO ( [A] , [B] ) é usada para usar uma fila de mensagens para obter o bloqueio entre máquinas.

Seu AcquireLockmétodo enviaria algo que identificasse o objeto de bloqueio para a fila, procurando por instâncias anteriores de bloqueios antes do sucesso. Seu ReleaseLockmétodo removeria o objeto de bloqueio da fila.

O usuário do SO atlantis sugere, neste post , o post de Jeff Key para alguns detalhes.


Obrigado, mas essas soluções não seriam adequadas, pois eu não tenho mestre central, banco de dados ou fila. Atualizei a pergunta com alguns detalhes adicionais para esclarecer alguns desses detalhes.
JoeGeeky

Não poderei usar esses produtos diretamente, pois já existe um protocolo bem definido que devo usar para todas as comunicações entre os nós, mas o Chubby e o Paxos podem ter padrões bem definidos com os quais posso aprender. Vou dar uma olhada.
JoeGeeky

@JoeGeeky Sim, o link Paxos possui diagramas de sequência que podem permitir que você o implemente usando o link de comunicação preferido.
Peter K.

Embora não seja uma resposta direta, a leitura de todo o material Chubby e Paxos me ajudou a definir minha própria solução. Não usei essas ferramentas, mas fui capaz de definir um padrão razoável com base em alguns de seus conceitos. Obrigado.
precisa saber é o seguinte

@ JoeGeeky: É bom ouvir que foi de alguma ajuda, pelo menos. Obrigado pelo carrapato.
Peter K.

4

Parece-me que você tem algumas tecnologias mistas aqui:

  • comunicações (nas quais você essencialmente confia como 100% confiável ... o que pode ser fatal)

  • bloqueio / exclusão mútua

  • tempos limite (para qual finalidade)?

Uma palavra de aviso: Os tempos limite em sistemas distribuídos podem estar repletos de perigos e dificuldades. Se usados, eles devem ser definidos e usados ​​com muito cuidado, porque o uso indiscriminado de intervalos não corrige um problema, apenas adia a catástrofe. (Se você quiser ver como os intervalos devem ser usados, leia e entenda a documentação do protocolo de comunicação HDLC. Este é um bom exemplo de uso adequado e inteligente, em combinação com um sistema de codificação de bits inteligente para permitir a detecção de coisas como a linha IDLE) .

Durante algum tempo, trabalhei em sistemas distribuídos com vários processadores conectados usando links de comunicação (não TCP, outra coisa). Uma das coisas que aprendi foi que, como uma generalização grosseira, existem alguns lugares perigosos de multiprogramação para ir:

  • a dependência de filas geralmente termina em lágrimas (se a fila ficar cheia, você estará com problemas. A menos que você possa calcular um tamanho de fila que nunca será preenchido; nesse caso, você provavelmente poderá usar uma solução sem fila)

  • a dependência do bloqueio é dolorosa, tente e pense se existe outra maneira (se você deve usar o bloqueio, consulte a literatura, o bloqueio distribuído por vários processadores tem sido objeto de muitos artigos acedêmicos das últimas 2-3 décadas)

Se você precisar continuar usando o bloqueio, então:

Assumirei que você usará os tempos limite apenas como um meio de recuperação de último recurso - ou seja, para detectar uma falha do sistema de comunicações subjacente. Assumirei ainda que o seu sistema de comunicação TCP / IP é de alta largura de banda e pode ser considerado baixa latência (idealmente zero, mas isso nunca acontece).

O que eu sugeriria é que cada nó tem uma lista de conectividade de outros nós aos quais pode se conectar. (Os nós não se importam de onde vem uma conexão.) A população das tabelas às quais um nó pode se conectar é deixada como uma coisa separada a ser resolvida, você não disse se isso seria definido estaticamente ou não. Também convenientemente ignorado são coisas como a alocação dos números de porta IP em que as conexões entrariam em um nó - pode haver boas razões para aceitar solicitações em apenas uma única porta ou em várias portas. Isso precisa ser cuidadosamente considerado. Os fatores incluirão filas implícitas, pedidos, uso de recursos, tipo e recursos do sistema operacional.

Depois que os nós souberem com quem se conectam, eles podem enviar para esse nó uma solicitação de bloqueio e devem receber de volta uma resposta de bloqueio desse nó remoto. Você pode agrupar essas duas operações em um wrapper para torná-lo atômico. O efeito disso é que os nós que desejam adquirir um bloqueio farão uma chamada algo como:

if (get_lock(remote_node) == timeout) then
  {
    take some failure action - the comms network is down
  }

/* Lock is now acquired - do work here */

if (release_lock(remote_node) == timeout) then
  {
    take some failure action - the comms network is down
  }

as chamadas get_lock e release_lock devem ser algo como (em princípio):

send_to_remote_node(lock_request)
get_from_remote_node_or_timeout(lock_reply, time)
if (result was timeout) then
  return timeout
else
  return ok

Você precisará tomar muito cuidado, com um sistema de bloqueio distribuído, para que as unidades de trabalho executadas enquanto um bloqueio seja mantido sejam pequenas e rápidas, pois você terá muitos nós remotos potencialmente retidos aguardando o bloqueio. Este é efetivamente um sistema de multiprocessador / comunicação para-e-espera que é robusto, mas não apresenta o melhor desempenho possível.

Uma sugestão é adotar uma abordagem completamente diferente. Você pode usar uma chamada de procedimento remoto em que cada chamada RPC carrega um pacote de informações que podem ser tratadas pelo destinatário e que remove as necessidades de bloqueios?


Ao reler a pergunta, parece que você realmente não quer se preocupar com o lado da comunicação, apenas deseja resolver seu problema de bloqueio.

Minha resposta pode, portanto, parecer um pouco fora de tópico, no entanto, acredito que você não pode resolver seu problema de bloqueio sem acertar as peças abaixo. Analogia: Construir uma casa com fundações ruins faz com que ela caia ... Eventualmente.


11
A semântica de tempo limite está em grande parte para lidar com nós que desaparecem da rede ou para lidar com grandes atrasos em pilhas de bloqueio ... Isso limitará o tempo gasto bloqueado enquanto espera a aquisição de um bloqueio e fornecerá aos solicitantes do bloqueio uma oportunidade para iniciar outros processos em meio a atrasos inesperados, falhas, etc ... Além disso, isso impediria que algo fosse bloqueado para sempre, caso algo falhasse. Agradeço suas preocupações, embora, neste momento, não encontre alternativas, pois algo acabará por falhar
JoeGeeky

Para falar com alguns de seus outros comentários, não estou usando filas em si (no sentido de comunicação assíncrona), embora eu esperasse que os bloqueios fossem empilhados e liberados com base no padrão FIFO. Ainda não concordei como isso funcionará em termos do padrão de solicitação / resposta exigido, além do que isso precisará bloquear de alguma forma e fazer parte de um aperto de mão maior. No momento, estou trabalhando no mecanismo de bloqueio empilhado em um único nó e como ele funcionará no cenário distribuído. Vou ler um pouco mais, como você sugeriu. Obrigado
JoeGeeky

@ JoeGeeky - um FIFO é uma fila. Cuidado com as filas. Pense nesse lado com muito cuidado. Parece que você não vai conseguir algo "pronto", mas terá que pensar cuidadosamente no seu problema e solução.
quickly_now

Eu entendo ... Eu estava tentando esclarecer a diferença entre uma fila FIFO usada em processos assíncronos ( por exemplo, um processo enfileira e outro enfileira ). Nesse caso, as coisas precisarão ser gerenciadas em ordem, mas o processo que entra na fila não sairá até que (a) obtenham o bloqueio, (b) seja negado um bloqueio ou (c) atinjam o tempo limite e deixem a linha. Mais como ficar na fila do caixa eletrônico. Isso se comporta como um padrão FIFO no caso de sucesso, mas os processos podem sair de ordem antes de chegar à frente da linha. Quanto à prateleira? Não, mas isso não é um problema novo
JoeGeeky

0

Sua pergunta pode ser facilmente implementada usando um cache distribuído como o NCache. O que você precisa é de um mecanismo de bloqueio pessimista, no qual você pode adquirir um bloqueio usando um objeto. Em seguida, execute suas tarefas e operações e libere a trava para outros aplicativos consumirem posteriormente.

Dê uma olhada no código a seguir;

Aqui você adquire um bloqueio em uma chave específica e, em seguida, executa tarefas (variando de uma ou mais operações) e, finalmente, libera o bloqueio quando terminar.

// Instance of the object used to lock and unlock cache items in NCache
LockHandle lockHandle = new LockHandle();

// Specify time span of 10 sec for which the item remains locked
// NCache will auto release the lock after 10 seconds.
TimeSpan lockSpan = new TimeSpan(0, 0, 10); 

try
{
    // If item fetch is successful, lockHandle object will be populated
    // The lockHandle object will be used to unlock the cache item
    // acquireLock should be true if you want to acquire to the lock.
    // If item does not exists, account will be null
    BankAccount account = cache.Get(key, lockSpan, 
    ref lockHandle, acquireLock) as BankAccount;
    // Lock acquired otherwise it will throw LockingException exception

    if(account != null && account.IsActive)
    {
        // Withdraw money or Deposit
        account.Balance += withdrawAmount;
        // account.Balance -= depositAmount;

        // Insert the data in the cache and release the lock simultaneously 
        // LockHandle initially used to lock the item must be provided
        // releaseLock should be true to release the lock, otherwise false
        cache.Insert("Key", account, lockHandle, releaseLock); 
        //For your case you should use cache.Unlock("Key", lockHandle);
    }
    else
    {
        // Either does not exist or unable to cast
        // Explicitly release the lock in case of errors
        cache.Unlock("Key", lockHandle);
    } 
}
catch(LockingException lockException)
{
    // Lock couldn't be acquired
    // Wait and try again
}

Retirado do link: http://blogs.alachisoft.com/ncache/distributed-locking/

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.