Em sistemas distribuídos, a alta disponibilidade é crucial para garantir que os serviços permaneçam acessíveis mesmo diante de falhas. Este artigo explora diversas abordagens para construir e manter a resiliência em aplicações, focando em mecanismos de tolerância a falhas, balenceamento de carga e padrões de disjuntores.
Resiliência do Dubbo em Caso de Falha do Zookeeper
Uma preocupação comum em arquiteturas baseadas em microsserviços é a dependência de um registro de serviço central, como o Zookeeper. Uma pergunta frequente em entrevistas é: "Se o Zookeeper falhar, os consumidores de serviço ainda podem invocar os provedores?" A resposta é afirmativa, graças ao cache local do Dubbo.
O Dubbo foi projetado com robustez em mente. Se o centro de monitoramento ficar offline, a operação do serviço não é afetada, embora alguns dados de telemetria possam ser perdidos. Em caso de falha do banco de dados, o registro de serviço pode continuar a fornecer listas de serviços a partir do cache, embora novos serviços não possam ser registrados. Com um cluster de registro de serviço, a falha de um nó leva à comutação automática para outro.
Mais importante ainda, se todos os nós do registro de serviço falharem, os provedores e consumidores de serviço ainda podem se comunicar usando seus caches locais. Isso garante que a funcionalidade existente não seja interrompida imediatamente. Os provedores de serviço são geralmente sem estado; a falha de um não impacta o uso geral. No entanto, se todos os provedores de um serviço específico falharem, os consumidores não conseguirão utilizá-lo e tentarão reconexões contínuas até que o serviço seja restaurado.
Para cenários onde um registro de serviço não está disponível ou para fins de depuração, o Dubbo permite a conexão direta entre o consumidor e o provedor. Isso pode ser configurado no consumidor, especificando o URL direto do provedor:
// Exemplo de configuração de referência direta
@Reference(url="dubbo://127.0.0.1:20880")
private MyService myService;
Configuração de Balanceamento de Carga em Clusters Dubbo
Em um ambiente de cluster, onde programas e funcionalidades idênticas são implantados em múltiplas máquinas, o balanceamento de carga é essencial para distribuir requisições e otimizar a utilização dos recursos. O Dubbo oferece diversas estratégias de balanceamento de carga, com a estratégia random sendo a padrão.
O objetivo do balanceamento de carga é distribuir uniformemente as requisições, evitando que alguns servidores fiquem sobrecarregados enquanto outros permanecem ociosos. Isso garante que cada servidor opere dentro de sua capacidade, prevenindo timeouts e melhorando a performance geral do sistema.
Estratégias de Balanceamento de Carga do Dubbo
As principais estratégias incluem:
1. Random LoadBalance (Aleatório)
- Distribuição aleatória com base em pesos.
- A probabilidade de colisão é maior em uma única janela de tempo, mas a distribuição tende a ser mais uniforme com um grande volume de chamadas.
- Permite ajuste dinâmico do peso dos provedores.
- Como funciona: Cada servidor tem um peso. Esses pesos são mapeados para um intervalo numérico. Um número aleatório é gerado, e o servidor cujo intervalo contém esse número é selecionado. Servidores com maior peso ocupam um intervalo maior e, portanto, têm maior probabilidade de serem escolhidos.
- É a implementação padrão devido à sua simplicidade e eficiência, apesar de poder apresentar desequilíbrios com poucas chamadas.
2. RoundRobin LoadBalance (Round-Robin)
- Distribuição sequencial, com pesos ajustando a proporção.
- Pode levar a um acúmulo de requisições em provedores lentos, pois um provedor lento pode prender a fila de processamento até que sua requisição seja concluída.
- Como funciona: As requisições são distribuídas sequencialmente para cada servidor. Com ponderação, a quantidade de requisições que cada servidor recebe é proporcional ao seu peso. Por exemplo, com pesos 5:2:1 para A, B, C, em 8 requisições, A receberia 5, B receberia 2 e C receberia 1.
3. LeastActive LoadBalance (Menor Atividade)
- Seleciona o provedor com o menor número de chamadas ativas. Em caso de empate, escolhe aleatoriamente entre eles, considerando pesos.
- Favorece provedores mais rápidos, pois estes completam requisições mais rapidamente, diminuindo seu contador de atividade.
- Como funciona: Cada provedor mantém um contador de "requisições ativas". Quando uma requisição é recebida, o contador é incrementado; ao finalizar, é decrementado. Provedores mais rápidos têm seus contadores reduzidos mais rapidamente, tornando-os mais propensos a serem escolhidos para novas requisições. Pesos são aplicados em caso de empate na contagem de atividades.
4. ConsistentHash LoadBalance (Hash Consistente)
- Garante que requisições com os mesmos parâmetros sempre sejam enviadas para o mesmo provedor.
- Quando um provedor falha, as requisições que seriam direcionadas a ele são distribuídas para outros provedores de forma mais suave, utilizando nós virtuais, minimizando interrupções.
- Como funciona: Hash dos nós do provedor (e nós virtuais) e dos parâmetros da requisição em um anel de hash. Uma requisição é direcionada para o primeiro nó do provedor que é maior ou igual ao hash da requisição no anel. Nós virtuais são usados para garantir uma distribuição mais uniforme e evitar desequilíbrios de dados.
- Pode ser configurado para usar parâmetros específicos para o hash (
hash.arguments) e a quantidade de nós virtuais (hash.nodes).
Configuração do Balanceamento de Carga
As estratégias de balanceamento de carga podem ser configuradas em diferentes níveis:
Nível de Serviço (Provedor)
<!-- XML -->
<dubbo:service interface="..." loadbalance="roundrobin" />
// Anotação
@Service(loadbalance="roundrobin")
Nível de Serviço (Consumidor)
<!-- XML -->
<dubbo:reference interface="..." loadbalance="roundrobin" />
// Anotação
@Reference(loadbalance="roundrobin")
Nível de Método (Provedor)
<dubbo:service interface="...">
<dubbo:method name="myMethod" loadbalance="roundrobin"/>
</dubbo:service>
Nível de Método (Consumidor)
<dubbo:reference interface="...">
<dubbo:method name="myMethod" loadbalance="roundrobin"/>
</dubbo:reference>
Integração com Hystrix para Disjuntores e Degradação de Serviço
Em um cenário de alta carga, a degradação de serviço é uma técnica estratégica para manter a funcionalidade principal do sistema. Ela envolve a desabilitação seletiva ou o tratamento simplificado de serviços e funcionalidades não essenciais, liberando recursos do servidor para garantir que as operações críticas continuem funcionando de forma eficiente.
O Dubbo permite a degradação de serviço configurando regras de override dinâmicas no registro. Por exemplo, para que um consumidor retorne null imediatamente sem invocar um serviço remoto:
// Exemplo de configuração para degradação de serviço
RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null"));
mock=force:return+null: O consumidor retornanulldiretamente, sem tentativas de chamada remota. Útil para isolar serviços não críticos.mock=fail:return+null: O consumidor tenta a chamada, mas se falhar, retornanullem vez de lançar uma exceção.
Modos de Tolerância a Falhas em Clusters
Quando uma chamada em cluster falha, o Dubbo oferece várias estratégias de tolerância a falhas, com failover sendo a padrão.
1. Failover Cluster (Falha e Retentativa)
- Se uma falha ocorrer, o sistema automaticamente tenta outros servidores.
- Comum para operações de leitura, mas pode introduzir maior latência devido às retentativas.
- O número de retentativas (excluindo a primeira tentativa) pode ser configurado com
retries="2".
2. Failfast Cluster (Falha Rápida)
- Realiza apenas uma chamada e, se falhar, reporta o erro imediatamente.
- Ideal para operações de escrita não idempotentes (ex: criar um novo registro).
3. Failsafe Cluster (Tolerância a Falhas)
- Ignora exceções e erros.
- Adequado para operações como registro de auditoria, onde a falha não deve impactar o fluxo principle.
4. Failback Cluster (Falha e Recuperação)
- Registra requisições falhas em segundo plano e as reenvia periodicamente.
- Usado para operações de notificação ou tarefas assíncronas que podem ser retentadas mais tarde.
5. Forking Cluster (Chamada Paralela)
- Invoca múltiplos servidores em paralelo e retorna assim que uma das chamadas é bem-sucedida.
- Recomendado para operações de leitura com alta exigência de tempo real, mas consome mais recursos do servidor.
- O número máximo de chamadas paralelas pode ser definido com
forks="2".
6. Broadcast Cluster (Chamada em Broadcast)
- Chama todos os provedores em sequência. Um erro em qualquer provedor resulta em uma falha geral.
- Típico para operações de notificação em massa, como atualização de cache ou logs em todos os provedores.
Configuração do Modo de Cluster
O modo de cluster pode ser configurado tanto no lado do provedor quanto do consumider:
<!-- Nível de Serviço -->
<dubbo:service cluster="failsafe" />
<dubbo:reference cluster="failsafe" />
<!-- Nível de Método (exemplo para retries) -->
<dubbo:reference>
<dubbo:method name="findFoo" retries="2" />
</dubbo:reference>
Integração com Hystrix
Hystrix, parte do ecossistema Netflix OSS, oferece um poderoso mecanismo de disjuntores e isolamento para serviços. Ele é projetado para controlar pontos de acesso a sistemas remotos e bibliotecas de terceiros, proporcionando tolerância a falhas robusta contra latência e falhas.
1. Adicionar Dependência Spring Cloud Hystrix
Para integrar Hystrix em uma aplicação Spring Boot, adicione a dependência spring-cloud-starter-netflix-hystrix no pom.xml:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>1.4.4.RELEASE</version> <!-- Usar a versão compatível com seu Spring Boot -->
</dependency>
Em seguida, habilite o Hystrix na classe principal da aplicação Spring Boot com @EnableHystrix:
@SpringBootApplication
@EnableHystrix
public class ApplicationProvider {
public static void main(String[] args) {
SpringApplication.run(ApplicationProvider.class, args);
}
}
2. Configurar o Provedor (Provider) com Hystrix
No provedor de serviços Dubbo, adicione a anotação @HystrixCommand ao método que você deseja proteger. Isso fará com que as chamadas a esse método passem pelo proxy Hystrix.
@Service(version = "1.0.0")
public class ProductCatalogService implements ProductService {
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), // Número mínimo de requisições em um período para o disjuntor avaliar
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000") // Timeout da execução
})
@Override
public String getProductDetails(String productId) {
// Simula uma falha para demonstrar o Hystrix
throw new RuntimeException("Falha simulada no serviço de produto.");
}
}
3. Configurar o Consumidor (Consumer) com Hystrix e Fallback
No lado do consumidor, você pode encapsular a chamada ao serviço Dubbo dentro de um método anotado com @HystrixCommand. O atributo fallbackMethod especifica um método a ser executado caso a chamada principal falhe ou o disjuntor seja acionado.
@Reference(version = "1.0.0")
private ProductService productService;
@HystrixCommand(fallbackMethod = "getDefaultProductDetails")
public String retrieveProductInformation(String productId) {
return productService.getProductDetails(productId);
}
// Método de fallback que será chamado se o serviço falhar
public String getDefaultProductDetails(String productId) {
return "Detalhes do produto não disponíveis. Retornando valor de fallback.";
}