A replicação fornece tolerância a falhas e aumenta o desempenho para uma topologia eXtreme Scale distribuída. A replicação é ativada associando mapas de apoio a um conjunto de mapas.
Um conjunto de mapas é uma coleção de mapas que são categorizados por uma chave da partição. Esta chave da partição é derivada da chave no mapa individual obtendo o número de partições de seu modulo de hash. Se um grupo de mapas dentro do conjunto de mapas tiver a chave da partição X, esses mapas serão armazenados em uma partição X correspondente na grade de dados. Se um outro grupo possuir a chave da partição Y, todos os mapas serão armazenados na partição Y e assim por diante. Os dados dentro dos mapas são replicados com base na política definida no conjunto de mapas. A replicação ocorre nas topologias distribuídas.
Os conjuntos de mapas são designados ao número de partições e a uma política de replicação. A configuração da replicação do conjunto de mapas identifica o número de shards de réplica síncrona e assíncrona para o conjunto de mapas, além do shard primário. Por exemplo, se uma réplica síncrona e uma assíncrona existirem, todos os BackingMaps que foram designados ao conjunto de mapas terão um shard de réplica cada distribuído automaticamente dentro do conjunto de servidores de contêineres disponíveis para a grade de dados. A configuração de replicação também deve ativar clientes para ler dados a partir de servidores replicados de maneira síncrona. Isto pode propagar o carregamento para pedidos de leitura sobre servidores adicionais no eXtreme Scale. A replicação tem um impacto no modelo de programação somente ao pré-carregar os mapas de apoio.
void preloadMap(Session session, BackingMap backingMap) throws LoaderException;
Os mapas podem ser particionados em N partições. Portanto, os mapas podem ser divididos em vários servidores, com cada entrada identifica por uma chave que é armazenada apenas em um destes servidores. Mapas muitos grandes podem ser mantidos em um eXtreme Scale porque o aplicativo não está mais limitado pelo tamanho de heap de uma JVM única para conter todas as entradas de um Mapa. Aplicativos que desejam pré-carregar com o método preloadMap da interface do Utilitário de Carga deve identificar o subconjunto de dados que ele pré-carrega. Sempre existe um número fixo de partições. É possível determinar este número utilizando o seguinte exemplo de código:
int numPartitions = backingMap.getPartitionManager().getNumOfPartitions();
int myPartition = backingMap.getPartitionId();
Este exemplo de
código mostra que um aplicativo pode identificar o subconjunto dos dados
a serem pré-carregados a partir do banco de dados. Os aplicativos sempre devem utilizar estes métodos mesmo
quando o mapa não é inicialmente particionado. Estes métodos permitem flexibilidade:
Se o mapa for posteriormente particionado pelos administradores, então, o utilitário
de carga continua a funcionar corretamente.O aplicativo deve emitir consultas para recuperar o subconjunto myPartition a partir do backend. Se um banco de dados for utilizado, então, pode ser mais fácil ter uma coluna com o identificador de partições para um determinado registro, a menos que haja alguma consulta natural que permita que os dados na tabela sejam particionados facilmente.
A implementação de pré-carregamento copia dados do backend para o mapa, armazenando vários objetos no mapa em uma única transação. O número ideal de registros a serem armazenados por transação depende de vários fatores, incluindo complexidade e tamanho. Por exemplo, após a transação incluir blocos de mais de 100 entradas, o benefício do desempenho diminui conforme você aumenta o número de entradas. Para determinar o número ideal, comece com 100 entradas e, em seguida, aumente o número até que não sejam mais percebidos ganhos de desempenho. Transações maiores resultam em melhor desempenho de replicação. Lembre-se, apenas o primário executa o código de pré-carregamento. Os dados pré-carregados são replicados do primário para quaisquer réplicas que estão on-line.
Se o aplicativo usar um conjunto de mapas com diversos mapas, cada mapa terá seu próprio carregador. Cada utilitário de carga possui um método preload. Cada mapa é carregado serialmente pelo eXtreme Scale. Pode ser mais eficiente pré-carregar todos os mapas, projetando um único mapa como o mapa de pré-carregamento. Esse processo é uma convenção do aplicativo. Por exemplo, dois mapas, department e employee, podem utilizar o Utilitário de Carga de department para pré-carregar os mapas department e employee. Isto assegura que, transacionalmente, se um aplicativo desejar um departamento, os funcionários desse departamento estarão no cache. Quando o Utilitário de Carga do departamento pré-carregar um departamento do backend, ele também buscará os funcionários para esse departamento. O objeto department e seus objetos employee associados são, então, incluídos no mapa utilizando uma transação única.
Alguns clientes têm conjuntos de dados muito grandes que precisam ser armazenados em cache. O pré-carregamento de dados pode consumir muito tempo. Às vezes, o pré-carregamento deve ser concluído antes de o aplicativo ficar on-line. É possível beneficiar-se ao tornar o pré-carregamento recuperável. Supunha que haja um milhão de registros para pré-carregar. O primário está pré-carregando-os e falha no registro de número 800.000. Normalmente, a réplica escolhida para ser o novo primário limpa qualquer estado replicado e começa do início. O eXtreme Scale pode utilizar uma interface ReplicaPreloadController. O utilitário de carga para o aplicativo também precisa implementar a interface ReplicaPreloadController. Este exemplo inclui um método único no Utilitário de Carga: Status checkPreloadStatus(Session session, BackingMap bmap);. Este método é chamado pelo tempo de execução do eXtreme Scale antes do método preload da interface do Utilitário de Carga ser chamada normalmente. O eXtreme Scale testa o resultado deste método (Status) para determinar seu comportamento sempre que uma réplica é promovida para um primário.
Valor do Status Retornado | Resposta do eXtreme Scale |
---|---|
Status.PRELOADED_ALREADY | O eXtreme Scale não chama o método preload porque este valor do status indica que o mapa foi totalmente pré-carregado. |
Status.FULL_PRELOAD_NEEDED | O eXtreme Scale limpa o mapa e chama o método preload normalmente. |
Status.PARTIAL_PRELOAD_NEEDED | O eXtreme Scale deixa o mapa no estado em que se encontra e chama o pré-carregamento. Essa estratégia permite que o Utilitário de Carga do aplicativo continue o pré-carregamento desse ponto em diante. |
Claramente, enquanto um primário está pré-carregando o mapa, ele deve deixar algum estado em um mapa no conjunto de mapas que está sendo replicado para que a réplica determine qual status retornar. É possível utilizar um mapa extra denominado, por exemplo, RecoveryMap. Este RecoveryMap deve fazer parte do mesmo conjunto de mapas que está sendo pré-carregado para assegurar que o mapa seja replicado de forma consistente com os dados que estão sendo pré-carregados. A seguir, está uma implementação sugerida.
À medida que ocorre o commit de cada bloco de registros, o processo também atualiza um contador ou valor no RecoveryMap como parte de tal transação. Os dados pré-carregados e os dados de RecoveryMap são replicados atomicamente para as réplicas. Quando a réplica é promovida para o primário, ela pode verificar o RecoveryMap para saber o que aconteceu.
O RecoveryMap pode conter uma única entrada com a chave de estado. Se nenhum objeto existir para esta chave, será necessário um pré-carregamento (checkPreloadStatus returns FULL_PRELOAD_NEEDED) integral. Se um objeto existir para esta chave de estado e o valor for COMPLETE, o pré-carregamento é concluído e o método checkPreloadStatus retorna PRELOADED_ALREADY. Caso contrário, o objeto de valor indica onde o pré-carregamento reinicia e o método checkPreloadStatus retorna: PARTIAL_PRELOAD_NEEDED. O utilitário de carga pode armazenar o ponto de recuperação em uma variável de instância para o utilitário de carga para que, quando o pré-carregamento for chamado, ele saiba o ponto de partida. O RecoveryMap também pode conter uma entrada por mapa se cada mapa for pré-carregado de maneira independente.
O tempo de execução doeXtreme Scale é projetado para não perder dados com commit quando o primário falha. A seção a seguir mostra os algoritmos utilizados. Estes algoritmos se aplicam apenas quando um grupo de replicação utiliza a replicação síncrona. Um utilitário de carga é opcional.
O tempo de execução do eXtreme Scale pode ser configurado para replicar todas as alterações a partir de um primário para as réplicas de maneira síncrona. Quando uma réplica síncrona é posicionada ela recebe uma cópia dos dados existentes no shard primário. Durante este tempo, o primário continua recebendo transações e as copia na réplica assincronamente. A réplica não é considerada como estando on-line neste período.
Depois de a réplica capturar o primário, ela entre no modo peer e começa a replicação síncrona. Cada transação consolidada no primário é enviada às réplicas síncronas e o primário aguarda por uma resposta de cada réplica. Uma sequência de consolidação síncrona com um utilitário de carga no primário se parece com o conjunto e etapas a seguir:
Etapa com o Utilitário de Carga | Etapa sem o Utilitário de Carga |
---|---|
Obter bloqueios para entradas | igual |
Limpar alterações no utilitário de carga | no-op |
Salvar alterações no cache | igual |
Enviar mudanças para réplicas e aguardar confirmação | igual |
Confirmar para o utilitário de carga por meio do Plug-in TransactionCallback | commit do Plug-in chamado, mas não faz nada |
Liberar bloqueios para entradas | igual |
Observe que as alterações são enviadas para a réplica antes de serem confirmadas para o utilitário de carga. Para determinar quando ocorre o commit das alterações na réplica, revise esta sequência: No momento da inicialização, inicialize as listas tx no primário, conforme abaixo.
CommitedTx = {}, RolledBackTx = {}
Durante o processamento de confirmação síncrona, utilize a seguinte sequência:
Etapa com o Utilitário de Carga | Etapa sem o Utilitário de Carga |
---|---|
Obter bloqueios para entradas | igual |
Limpar alterações no utilitário de carga | no-op |
Salvar alterações no cache | igual |
Enviar mudanças com uma transação confirmada, retroceder transação para réplica e aguardar confirmação | igual |
Limpar lista de transações confirmadas e de transações que receberam rollback | igual |
Confirmar o utilitário de carga por meio do plug-in TransactionCallBack | A confirmação do plug-in TransactionCallBack ainda é chamada mas, geralmente, não faz nada |
Se a confirmação for bem-sucedida, inclua a transação nas transações confirmadas; caso contrário, inclua nas transações que receberam rollback | no-op |
Liberar bloqueios para entradas | igual |
Para processamento de réplica, utilize a seguinte sequência:
Observe que, na réplica, não existem interações do Utilitário de Carga enquanto ele está no modo de réplica. O primário deve enviar todas as alterações por meio do Utilitário de Carga. A réplica não faz nenhuma mudança. Um efeito secundário deste algoritmo é que a réplica sempre tem as transações, mas elas não são confirmadas, até que a próxima transação primária envie o status de confirmação destas transações. Elas são então confirmadas ou recebem rollback na réplica. Mas, até então, as transações não são confirmadas. É possível incluir um cronômetro no primário que envia o resultado da transação após um pequeno período (alguns segundos). Esse cronômetro limita, mas não elimina, qualquer deterioração desse espaço de tempo. Este staleness é um problemas apenas ao utilizar o modo de leitura de réplica. Do contrário, a deterioração não tem impacto sobre o aplicativo.
Quando o primário falha, é provável que poucos commits ou rollback tenham ocorrido nas transações no primário, mas a mensagem nunca fez isto para a réplica com estas saídas. Quando uma réplica for promovida para o novo primário, uma de suas primeiras ações será manipular esta condição. Cada transação pendente é processada novamente junto ao novo conjunto de mapas do primário. Se houver um Utilitário de Carga, então, cada transação é fornecida para o Utilitário de Carga. Estas transações são aplicadas na ordem FIFO (primeiro a entrar, primeiro a sair) estrita. Se uma transação falhar, ela será ignorada. Se três transações estiverem pendentes, A, B e C, A poderá ser confirmada, B poderá ser retrocedida e C também poderá ser confirmada. Nenhuma transação tem impacto sobre as outras. Suponha que elas sejam independentes.
Um utilitário de carga talvez queira utilizar uma lógica um pouco diferente quando no modo recuperação de failover versus modo normal. O utilitário de carga pode saber facilmente quando está em modo de recuperação de failover, implementando a interface ReplicaPreloadController. O método checkPreloadStatus é chamado apenas quando a recuperação de failover é concluída. Portanto, se o método apply da interface do Utilitário de Carga for chamado antes do checkPreloadStatus, ele será uma transação de recuperação. Depois que o método checkPreloadStatus for chamado, a recuperação de failover estará concluída.
O eXtreme Scale, a menos que configurado de outra forma, envia todos os pedidos de leitura e gravação para o servidor primário para um determinado grupo de replicação. O primário deve atender todos os pedidos de clientes. É possível permitir que pedidos de leitura sejam enviados para réplicas do primário. Enviar pedidos de leitura para as réplicas permite que o carregamento dos pedidos de leitura seja compartilhado por várias Java Virtual Machines (JVM). No entanto, utilizar réplicas para pedidos de leitura pode resultar em respostas inconsistentes.
Geralmente, o balanceamento de carga por meio de réplicas é utilizado apenas quando clientes estão armazenando dados em cache que estão sendo alterados sempre ou quando os clientes estão utilizando bloqueio pessimista.
Se os dados estiverem sendo alterados continuamente e sendo invalidados no cliente, caches locais e o primário deverão ver uma taxa relativamente alta de pedidos get de clientes como resultado. Da mesma forma, no modo de bloqueio pessimista, não existe cache local, portanto, todos os pedidos são enviados para o primário.
Se os dados forem relativamente estáticos ou se o modo pessimista não for usado, o envio de solicitações de leitura à réplica não terá um grande impacto no desempenho. A frequência de pedidos get dos clientes com caches ativos não será alta.
Quando um cliente é iniciado pela primeira vez, seu near cache está vazio. Os pedidos de cache para esse cache vazio são redirecionados para o primário. O cache cliente obtém dados com o tempo, causando a eliminação deste carregamento de pedido. Se muitos clientes iniciarem simultaneamente, o carregamento poderá ser significativo e a leitura da réplica poderá ser uma opção de desempenho apropriada.
Com o eXtreme Scale, é possível replicar um mapa de servidor em um ou mais clientes utilizando a replicação assíncrona. Um cliente pode solicitar uma cópia local somente leitura de um mapa do lado do servidor usando o método ClientReplicableMap.enableClientReplication.
void enableClientReplication(Mode mode, int[] partitions, ReplicationMapListener listener) throws ObjectGridException;
O primeiro parâmetro é o modo de replicação. Esse modo pode ser uma replicação contínua ou uma replicação de captura instantânea. O segundo parâmetro é uma matriz de IDs de partição que representa as partições a partir das quais replicar os dados. Se o valor for nulo ou uma matriz vazia, os dados são replicados a partir de todas as partições. O último parâmetro é um listener para receber eventos de replicação de cliente. Consulte ClientReplicableMap e ReplicationMapListener na documentação da API para obter detalhes.
Depois de ativada a replicação, então o servidor começa a replicar o mapa para o cliente. O cliente eventualmente está apenas algumas transações atrás do servidor em questão de tempo.