Introdução ao Dubbo
1. Introdução
Dubbo é um RPC de código aberto e estrutura de microsserviço do Alibaba.
Entre outras coisas, ajuda a aprimorar a governança de serviço e possibilita que um aplicativo monolítico tradicional seja refatorado sem problemas para uma arquitetura distribuída e escalável.
Neste artigo, daremos uma introdução ao Dubbo e seus recursos mais importantes.
2. Arquitetura
Dubbo distingue alguns papéis:
-
Fornecedor - onde o serviço é exposto; um provedor registrará seu serviço no registro
-
Contêiner - onde o serviço é iniciado, carregado e executado
-
Consumidor - que invoca serviços remotos; um consumidor assinará o serviço necessário no registro
-
Registro - onde o serviço será registrado e descoberto
-
Monitor - registre estatísticas de serviços, por exemplo, frequência de chamada de serviço em um determinado intervalo de tempo
As conexões entre um provedor, um consumidor e um registro são persistentes; portanto, sempre que um provedor de serviços está inoperante, o registro pode detectar a falha e notificar os consumidores.
O registro e o monitor são opcionais. Os consumidores poderiam se conectar diretamente aos provedores de serviços, mas a estabilidade de todo o sistema seria afetada.
3. Dependência do Maven
Antes de começarmos, vamos adicionar a seguinte dependência ao nossopom.xml:
com.alibaba
dubbo
2.5.7
A versão mais recente pode ser encontradahere.
4. Bootstrapping
Agora vamos experimentar os recursos básicos do Dubbo.
Essa é uma estrutura minimamente invasiva e muitos de seus recursos dependem de configurações ou anotações externas.
É sugerido oficialmente que devemos usar o arquivo de configuração XML porque ele depende de um contêiner Spring (atualmente Spring 4.3.10).
Vamos demonstrar a maioria de seus recursos usando a configuração XML.
4.1. Registro multicast - provedor de serviços
Para um início rápido, precisaremos apenas de um provedor de serviços, um consumidor e um registro "invisível". O registro é invisível porque estamos usando uma rede multicast.
No exemplo a seguir, o provedor apenas diz "oi" para seus consumidores:
public interface GreetingsService {
String sayHi(String name);
}
public class GreetingsServiceImpl implements GreetingsService {
@Override
public String sayHi(String name) {
return "hi, " + name;
}
}
Para fazer uma chamada de procedimento remoto, o consumidor deve compartilhar uma interface comum com o provedor de serviços, portanto, a interfaceGreetingsService deve ser compartilhada com o consumidor.
4.2. Registro de multicast - Registro de serviço
Vamos agora registrarGreetingsService no registro. Uma maneira muito conveniente é usar um registro multicast se fornecedores e consumidores estiverem na mesma rede local:
Com a configuração de beans acima, acabamos de expor nossoGreetingsService a uma url emdubbo://127.0.0.1:20880e registramos o serviço para um endereço multicast especificado em<dubbo:registry />.
Na configuração do provedor, também declaramos nossos metadados de aplicativo, a interface a ser publicada e sua implementação respectivamente por<dubbo:application />,<dubbo:service />e<beans />
O protocolodubbo é um dos muitos protocolos que o framework suporta. Ele é construído em cima do recurso de não bloqueio Java NIO e é o protocolo padrão usado.
Discutiremos isso com mais detalhes posteriormente neste artigo.
4.3. Registro de multicast - consumidor de serviço
Geralmente, o consumidor precisa especificar a interface a ser invocada e o endereço do serviço remoto, e isso é exatamente o que é necessário para um consumidor:
Agora que está tudo configurado, vamos ver como funcionam em ação:
public class MulticastRegistryTest {
@Before
public void initRemote() {
ClassPathXmlApplicationContext remoteContext
= new ClassPathXmlApplicationContext("multicast/provider-app.xml");
remoteContext.start();
}
@Test
public void givenProvider_whenConsumerSaysHi_thenGotResponse(){
ClassPathXmlApplicationContext localContext
= new ClassPathXmlApplicationContext("multicast/consumer-app.xml");
localContext.start();
GreetingsService greetingsService
= (GreetingsService) localContext.getBean("greetingsService");
String hiMessage = greetingsService.sayHi("example");
assertNotNull(hiMessage);
assertEquals("hi, example", hiMessage);
}
}
Quando oremoteContext do provedor é iniciado, o Dubbo carrega automaticamenteGreetingsService e o registra em um determinado registro. Nesse caso, é um registro multicast.
O consumidor assina o registro multicast e cria um proxy deGreetingsService no contexto. Quando nosso cliente local invoca o métodosayHi, ele invoca um serviço remoto de forma transparente.
Mencionamos que o registro é opcional, o que significa que o consumidor pode se conectar diretamente ao provedor, através da porta exposta:
Basicamente, o procedimento é semelhante ao serviço da Web tradicional, mas o Dubbo apenas o torna claro, simples e leve.
4.4. Registro Simples
Observe que, ao usar um registro multicast “invisível”, o serviço de registro não é autônomo. No entanto, é aplicável apenas a uma rede local restrita.
Para configurar explicitamente um registro gerenciável, podemos usar umSimpleRegistryService.
Após carregar a seguinte configuração de beans no contexto do Spring, um serviço de registro simples é iniciado:
Observe que a classeSimpleRegistryService não está contida no artefato, portanto, copiamossource code diretamente do repositório Github.
Em seguida, ajustaremos a configuração do registro do provedor e consumidor:
SimpleRegistryService pode ser usado como um registro independente durante o teste, mas não é recomendado para ser usado em ambiente de produção.
4.5. Configuração Java
A configuração via API Java, arquivo de propriedades e anotações também são suportadas. No entanto, o arquivo de propriedade e as anotações são aplicáveis apenas se nossa arquitetura não for muito complexa.
Vamos ver como nossas configurações XML anteriores para registro multicast podem ser traduzidas em configuração de API. Primeiro, o provedor é configurado da seguinte maneira:
ApplicationConfig application = new ApplicationConfig();
application.setName("demo-provider");
application.setVersion("1.0");
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("multicast://224.1.1.1:9090");
ServiceConfig service = new ServiceConfig<>();
service.setApplication(application);
service.setRegistry(registryConfig);
service.setInterface(GreetingsService.class);
service.setRef(new GreetingsServiceImpl());
service.export();
Agora que o serviço já está exposto por meio do registro multicast, vamos consumi-lo em um cliente local:
ApplicationConfig application = new ApplicationConfig();
application.setName("demo-consumer");
application.setVersion("1.0");
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("multicast://224.1.1.1:9090");
ReferenceConfig reference = new ReferenceConfig<>();
reference.setApplication(application);
reference.setRegistry(registryConfig);
reference.setInterface(GreetingsService.class);
GreetingsService greetingsService = reference.get();
String hiMessage = greetingsService.sayHi("example");
Embora o trecho acima funcione como um encanto como o exemplo anterior de configuração XML, é um pouco mais trivial. Por enquanto, a configuração XML deve ser a primeira opção, se pretendermos fazer pleno uso do Dubbo.
5. Suporte de protocolo
A estrutura oferece suporte a vários protocolos, incluindodubbo,RMI,hessian,HTTP,web service,thrift,memcached eredis. A maioria dos protocolos parece familiar, exceto paradubbo. Vamos ver o que há de novo neste protocolo.
O protocolodubbo mantém uma conexão persistente entre provedores e consumidores. A conexão longa e a comunicação de rede sem bloqueio da NIO resultam em um desempenho bastante bom ao transmitir pacotes de dados em pequena escala (<100K).
Existem várias propriedades configuráveis, como porta, número de conexões por consumidor, número máximo de conexões aceitas, etc.
O Dubbo também suporta a exposição de serviços através de diferentes protocolos de uma só vez:
E sim, podemos expor serviços diferentes usando protocolos diferentes, conforme mostrado no snippet acima. Os transportadores subjacentes, implementações de serialização e outras propriedades comuns relacionadas à rede também são configuráveis.
6. Cache de resultados
O cache nativo de resultados remotos é suportado para acelerar o acesso a dados quentes. É tão simples quanto adicionar um atributo cache à referência do bean:
Aqui, configuramos um cache usado menos recentemente. Para verificar o comportamento do cache, vamos mudar um pouco na implementação padrão anterior (vamos chamá-la de “implementação especial”):
public class GreetingsServiceSpecialImpl implements GreetingsService {
@Override
public String sayHi(String name) {
try {
SECONDS.sleep(5);
} catch (Exception ignored) { }
return "hi, " + name;
}
}
Depois de inicializar o provedor, podemos verificar do lado do consumidor se o resultado é armazenado em cache ao invocar mais de uma vez:
@Test
public void givenProvider_whenConsumerSaysHi_thenGotResponse() {
ClassPathXmlApplicationContext localContext
= new ClassPathXmlApplicationContext("multicast/consumer-app.xml");
localContext.start();
GreetingsService greetingsService
= (GreetingsService) localContext.getBean("greetingsService");
long before = System.currentTimeMillis();
String hiMessage = greetingsService.sayHi("example");
long timeElapsed = System.currentTimeMillis() - before;
assertTrue(timeElapsed > 5000);
assertNotNull(hiMessage);
assertEquals("hi, example", hiMessage);
before = System.currentTimeMillis();
hiMessage = greetingsService.sayHi("example");
timeElapsed = System.currentTimeMillis() - before;
assertTrue(timeElapsed < 1000);
assertNotNull(hiMessage);
assertEquals("hi, example", hiMessage);
}
Aqui, o consumidor está invocando a implementação do serviço especial; portanto, levou mais de 5 segundos para a invocação ser concluída na primeira vez. Quando invocamos novamente, o métodosayHi é concluído quase imediatamente, pois o resultado é retornado do cache.
Observe que o cache local do encadeamento e o JCache também são suportados.
7. Suporte de cluster
O Dubbo nos ajuda a ampliar nossos serviços livremente com sua capacidade de balanceamento de carga e várias estratégias de tolerância a falhas. Aqui, vamos supor que temos o Zookeeper como nosso registro para gerenciar serviços em um cluster. Os fornecedores podem registrar seus serviços no Zookeeper assim:
Observe que precisamos dessas dependências adicionais emPOM:
org.apache.zookeeper
zookeeper
3.4.11
com.101tec
zkclient
0.10
7.1. Balanceamento de carga
Atualmente, a estrutura suporta algumas estratégias de balanceamento de carga:
-
aleatória
-
round-robin
-
menos ativo
-
hash consistente.
No exemplo a seguir, temos duas implementações de serviço como provedores em um cluster. As solicitações são roteadas usando a abordagem round-robin.
Primeiro, vamos configurar os provedores de serviços:
@Before
public void initRemote() {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> {
ClassPathXmlApplicationContext remoteContext
= new ClassPathXmlApplicationContext("cluster/provider-app-default.xml");
remoteContext.start();
});
executorService.submit(() -> {
ClassPathXmlApplicationContext backupRemoteContext
= new ClassPathXmlApplicationContext("cluster/provider-app-special.xml");
backupRemoteContext.start();
});
}
Agora, temos um "provedor rápido" padrão que responde imediatamente e um "provedor lento" especial que dorme por 5 segundos em cada solicitação.
Depois de executar 6 vezes com a estratégia round-robin, esperamos que o tempo médio de resposta seja de pelo menos 2,5 segundos:
@Test
public void givenProviderCluster_whenConsumerSaysHi_thenResponseBalanced() {
ClassPathXmlApplicationContext localContext
= new ClassPathXmlApplicationContext("cluster/consumer-app-lb.xml");
localContext.start();
GreetingsService greetingsService
= (GreetingsService) localContext.getBean("greetingsService");
List elapseList = new ArrayList<>(6);
for (int i = 0; i < 6; i++) {
long current = System.currentTimeMillis();
String hiMessage = greetingsService.sayHi("example");
assertNotNull(hiMessage);
elapseList.add(System.currentTimeMillis() - current);
}
OptionalDouble avgElapse = elapseList
.stream()
.mapToLong(e -> e)
.average();
assertTrue(avgElapse.isPresent());
assertTrue(avgElapse.getAsDouble() > 2500.0);
}
Além disso, o balanceamento de carga dinâmico é adotado. O próximo exemplo demonstra que, com a estratégia round-robin, o consumidor escolhe automaticamente o novo provedor de serviços como candidato quando o novo provedor fica online.
O "provedor lento" é registrado 2 segundos depois após o sistema ser iniciado:
@Before
public void initRemote() {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> {
ClassPathXmlApplicationContext remoteContext
= new ClassPathXmlApplicationContext("cluster/provider-app-default.xml");
remoteContext.start();
});
executorService.submit(() -> {
SECONDS.sleep(2);
ClassPathXmlApplicationContext backupRemoteContext
= new ClassPathXmlApplicationContext("cluster/provider-app-special.xml");
backupRemoteContext.start();
return null;
});
}
O consumidor chama o serviço remoto uma vez por segundo. Após executar seis vezes, esperamos que o tempo médio de resposta seja maior que 1,6 segundos:
@Test
public void givenProviderCluster_whenConsumerSaysHi_thenResponseBalanced()
throws InterruptedException {
ClassPathXmlApplicationContext localContext
= new ClassPathXmlApplicationContext("cluster/consumer-app-lb.xml");
localContext.start();
GreetingsService greetingsService
= (GreetingsService) localContext.getBean("greetingsService");
List elapseList = new ArrayList<>(6);
for (int i = 0; i < 6; i++) {
long current = System.currentTimeMillis();
String hiMessage = greetingsService.sayHi("example");
assertNotNull(hiMessage);
elapseList.add(System.currentTimeMillis() - current);
SECONDS.sleep(1);
}
OptionalDouble avgElapse = elapseList
.stream()
.mapToLong(e -> e)
.average();
assertTrue(avgElapse.isPresent());
assertTrue(avgElapse.getAsDouble() > 1666.0);
}
Observe que o balanceador de carga pode ser configurado tanto no lado do consumidor quanto no lado do provedor. Aqui está um exemplo de configuração do lado do consumidor:
7.2. Tolerância ao erro
Várias estratégias de tolerância a falhas são suportadas no Dubbo, incluindo:
-
failover
-
à prova de falhas
-
rápido
-
retorno
-
bifurcação.
No caso de failover, quando um provedor falha, o consumidor pode tentar com outros fornecedores de serviços no cluster.
As estratégias de tolerância a falhas são configuradas da seguinte maneira para os provedores de serviços:
Para demonstrar o failover de serviço em ação, vamos criar uma implementação de failover deGreetingsService:
public class GreetingsFailoverServiceImpl implements GreetingsService {
@Override
public String sayHi(String name) {
return "hi, failover " + name;
}
}
Podemos lembrar que nossa implementação de serviço especialGreetingsServiceSpecialImpl dorme 5 segundos para cada solicitação.
Quando qualquer resposta que leva mais de 2 segundos é vista como uma falha de solicitação para o consumidor, temos um cenário de failover:
Após iniciar dois provedores, podemos verificar o comportamento de failover com o seguinte trecho:
@Test
public void whenConsumerSaysHi_thenGotFailoverResponse() {
ClassPathXmlApplicationContext localContext
= new ClassPathXmlApplicationContext(
"cluster/consumer-app-failtest.xml");
localContext.start();
GreetingsService greetingsService
= (GreetingsService) localContext.getBean("greetingsService");
String hiMessage = greetingsService.sayHi("example");
assertNotNull(hiMessage);
assertEquals("hi, failover example", hiMessage);
}
8. Sumário
Neste tutorial, demos uma pequena mordida no Dubbo. A maioria dos usuários é atraída por sua simplicidade e recursos ricos e poderosos.
Além do que introduzimos neste artigo, a estrutura ainda tem vários recursos a serem explorados, como validação de parâmetros, notificação e retorno de chamada, implementação e referência generalizada, agrupamento e mesclagem remota de resultados, atualização de serviço e compatibilidade com versões anteriores, para citar apenas um pouco.
Como sempre, a implementação completa pode ser encontradaover on Github.