«1. Введение

Dubbo — это платформа RPC и микросервисов с открытым исходным кодом от Alibaba.

Помимо прочего, это помогает улучшить управление услугами и позволяет плавно преобразовать традиционные монолитные приложения в масштабируемую распределенную архитектуру.

В этой статье мы познакомимся с Dubbo и его наиболее важными функциями.

2. Архитектура

Dubbo различает несколько ролей:

  1. Provider – where service is exposed; a provider will register its service to registry
  2. Container – where the service is initiated, loaded and run
  3. Consumer – who invokes remote services; a consumer will subscribe to the service needed in the registry
  4. Registry – where service will be registered and discovered
  5. Monitor – record statistics for services, for example, frequency of service invocation in a given time interval

(источник: http://dubbo.io/images/dubbo-architecture.png)

Связи между поставщиком, потребителем и реестром являются постоянными, поэтому всякий раз, когда поставщик услуг не работает, реестр может обнаружить сбой и уведомить потребителей.

Реестр и монитор необязательны. Потребители могут напрямую подключаться к поставщикам услуг, но это повлияет на стабильность всей системы.

3. Зависимость Maven

Прежде чем мы углубимся, давайте добавим следующую зависимость в наш pom.xml:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>dubbo</artifactId>
    <version>2.5.7</version>
</dependency>

Последнюю версию можно найти здесь.

4. Начальная загрузка

Теперь давайте попробуем основные возможности Dubbo.

Это минимально инвазивный фреймворк, и многие его функции зависят от внешних конфигураций или аннотаций.

Официально предлагается использовать файл конфигурации XML, поскольку он зависит от контейнера Spring (в настоящее время Spring 4.3.10).

Мы продемонстрируем большинство его функций, используя XML-конфигурацию.

4.1. Реестр многоадресной рассылки — поставщик услуг

Для начала нам понадобится только поставщик услуг, потребитель и «невидимый» реестр. Реестр невидим, потому что мы используем многоадресную сеть.

В следующем примере провайдер только говорит «привет» своим потребителям:

public interface GreetingsService {
    String sayHi(String name);
}

public class GreetingsServiceImpl implements GreetingsService {

    @Override
    public String sayHi(String name) {
        return "hi, " + name;
    }
}

Чтобы сделать удаленный вызов процедуры, потребитель должен использовать общий интерфейс с поставщиком услуг, поэтому интерфейс делиться с потребителем.

4.2. Многоадресный реестр — регистрация службы

Давайте теперь зарегистрируем GreetingsService в реестре. Очень удобным способом является использование многоадресного реестра, если и поставщики, и потребители находятся в одной и той же локальной сети:

<dubbo:application name="demo-provider" version="1.0"/>
<dubbo:registry address="multicast://224.1.1.1:9090"/>
<dubbo:protocol name="dubbo" port="20880"/>
<bean id="greetingsService" class="com.baeldung.dubbo.remote.GreetingsServiceImpl"/>
<dubbo:service interface="com.baeldung.dubbo.remote.GreetingsService"
  ref="greetingsService"/>

С приведенной выше конфигурацией bean-компонентов мы только что предоставили нашему GreetingsService URL-адрес под dubbo://127.0. 0.1:20880 и зарегистрировал службу на групповой адрес, указанный в \u003cdubbo:registry /\u003e.

В конфигурации провайдера мы также объявили метаданные нашего приложения, интерфейс для публикации и его реализацию соответственно с помощью \u003cdubbo:application /\u003e, \u003cdubbo:service /\u003e и \u003cbeans /\u003e.

Протокол dubbo — один из многих протоколов, поддерживаемых фреймворком. Он построен на основе неблокирующей функции Java NIO и используется по умолчанию.

Мы обсудим это более подробно позже в этой статье.

4.3. Многоадресный реестр — потребитель службы

Как правило, потребителю необходимо указать интерфейс для вызова и адрес удаленной службы, и это именно то, что нужно потребителю:

<dubbo:application name="demo-consumer" version="1.0"/>
<dubbo:registry address="multicast://224.1.1.1:9090"/>
<dubbo:reference interface="com.baeldung.dubbo.remote.GreetingsService"
  id="greetingsService"/>

Теперь все настроено, давайте посмотрим как они работают в действии:

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("baeldung");

        assertNotNull(hiMessage);
        assertEquals("hi, baeldung", hiMessage);
    }
}

Когда запускается remoteContext провайдера, Dubbo автоматически загружает GreetingsService и регистрирует его в заданном реестре. В данном случае это многоадресный реестр.

Потребитель подписывается на многоадресный реестр и создает прокси-сервер GreetingsService в контексте. Когда наш локальный клиент вызывает метод sayHi, он прозрачно вызывает удаленную службу.

Мы упомянули, что реестр является необязательным, что означает, что потребитель может напрямую подключаться к провайдеру через открытый порт:

<dubbo:reference interface="com.baeldung.dubbo.remote.GreetingsService"
  id="greetingsService" url="dubbo://127.0.0.1:20880"/>

По сути, процедура аналогична традиционной веб-службе, но Dubbo делает ее более простой. , простой и легкий.

4.4. Простой реестр

Обратите внимание, что при использовании «невидимого» многоадресного реестра служба реестра не является автономной. Однако это применимо только к ограниченной локальной сети.

Чтобы явно настроить управляемый реестр, мы можем использовать SimpleRegistryService.

После загрузки следующей конфигурации bean-компонентов в контекст Spring запускается простая служба реестра:

<dubbo:application name="simple-registry" />
<dubbo:protocol port="9090" />
<dubbo:service interface="com.alibaba.dubbo.registry.RegistryService"
  ref="registryService" registry="N/A" ondisconnect="disconnect">
    <dubbo:method name="subscribe">
        <dubbo:argument index="1" callback="true" />
    </dubbo:method>
    <dubbo:method name="unsubscribe">
        <dubbo:argument index="1" callback="true" />
    </dubbo:method>
</dubbo:service>

<bean class="com.alibaba.dubbo.registry.simple.SimpleRegistryService"
  id="registryService" />

«

Обратите внимание, что класс SimpleRegistryService не содержится в артефакте, поэтому мы скопировали исходный код непосредственно из репозитория Github.

<dubbo:registry address="127.0.0.1:9090"/>

Затем мы настроим конфигурацию реестра провайдера и потребителя:

SimpleRegistryService можно использовать как отдельный реестр при тестировании, но не рекомендуется использовать его в производственной среде.

4.5. Конфигурация Java

Конфигурация через Java API, файл свойств и аннотации также поддерживаются. Однако файл свойств и аннотации применимы только в том случае, если наша архитектура не очень сложна.

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<GreetingsService> service = new ServiceConfig<>();
service.setApplication(application);
service.setRegistry(registryConfig);
service.setInterface(GreetingsService.class);
service.setRef(new GreetingsServiceImpl());

service.export();

Давайте посмотрим, как наши предыдущие XML-конфигурации для многоадресного реестра можно преобразовать в конфигурацию API. Во-первых, провайдер настроен следующим образом:

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<GreetingsService> reference = new ReferenceConfig<>();
reference.setApplication(application);
reference.setRegistry(registryConfig);
reference.setInterface(GreetingsService.class);

GreetingsService greetingsService = reference.get();
String hiMessage = greetingsService.sayHi("baeldung");

Теперь, когда сервис уже доступен через многоадресный реестр, давайте воспользуемся им в локальном клиенте:

Хотя приведенный выше фрагмент работает как шарм как и предыдущий пример конфигурации XML, он немного более тривиален. На данный момент конфигурация XML должна быть первым выбором, если мы намерены в полной мере использовать Dubbo.

5. Поддержка протоколов

Фреймворк поддерживает несколько протоколов, включая dubbo, RMI, гессиан, HTTP, веб-сервис, thrift, memcached и Redis. Большинство протоколов выглядит знакомым, за исключением dubbo. Давайте посмотрим, что нового в этом протоколе.

Протокол dubbo поддерживает постоянное соединение между поставщиками и потребителями. Длинное соединение и неблокирующая сетевая связь NIO обеспечивают довольно высокую производительность при передаче небольших пакетов данных (\u003c100 КБ).

<dubbo:protocol name="dubbo" port="20880"
  connections="2" accepts="1000" />

Существует несколько настраиваемых параметров, таких как порт, количество подключений на потребителя, максимально допустимое количество подключений и т. д.

<dubbo:protocol name="dubbo" port="20880" />
<dubbo:protocol name="rmi" port="1099" />

<dubbo:service interface="com.baeldung.dubbo.remote.GreetingsService"
  version="1.0.0" ref="greetingsService" protocol="dubbo" />
<dubbo:service interface="com.bealdung.dubbo.remote.AnotherService"
  version="1.0.0" ref="anotherService" protocol="rmi" />

Dubbo также поддерживает одновременный доступ к службам через разные протоколы:

И да, мы можем предоставлять разные сервисы, используя разные протоколы, как показано во фрагменте выше. Базовые транспортеры, реализации сериализации и другие общие свойства, относящиеся к сети, также можно настраивать.

6. Кэширование результатов

<dubbo:reference interface="com.baeldung.dubbo.remote.GreetingsService"
  id="greetingsService" cache="lru" />

Встроенное удаленное кэширование результатов поддерживается для ускорения доступа к горячим данным. Это так же просто, как добавить атрибут кеша в ссылку на бин:

public class GreetingsServiceSpecialImpl implements GreetingsService {
    @Override
    public String sayHi(String name) {
        try {
            SECONDS.sleep(5);
        } catch (Exception ignored) { }
        return "hi, " + name;
    }
}

Здесь мы настроили кеш, который использовался наименее недавно. Чтобы проверить поведение кэширования, мы немного изменим предыдущую стандартную реализацию (назовем ее «специальной реализацией»):

@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("baeldung");

    long timeElapsed = System.currentTimeMillis() - before;
    assertTrue(timeElapsed > 5000);
    assertNotNull(hiMessage);
    assertEquals("hi, baeldung", hiMessage);

    before = System.currentTimeMillis();
    hiMessage = greetingsService.sayHi("baeldung");
    timeElapsed = System.currentTimeMillis() - before;
 
    assertTrue(timeElapsed < 1000);
    assertNotNull(hiMessage);
    assertEquals("hi, baeldung", hiMessage);
}

После запуска провайдера мы можем проверить на стороне потребителя, что результат кэшируется при вызове более одного раза:

Здесь потребитель вызывает реализацию специальной службы, поэтому для завершения вызова в первый раз потребовалось более 5 секунд. При повторном вызове метод sayHi завершается почти сразу же, так как результат возвращается из кеша.

Обратите внимание, что локальный кеш потока и JCache также поддерживаются.

7. Поддержка кластеров

<dubbo:registry address="zookeeper://127.0.0.1:2181"/>

Dubbo помогает нам свободно масштабировать наши услуги благодаря возможности балансировки нагрузки и нескольким стратегиям отказоустойчивости. Здесь предположим, что у нас есть Zookeeper в качестве нашего реестра для управления службами в кластере. Провайдеры могут зарегистрировать свои сервисы в Zookeeper следующим образом:

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.11</version>
</dependency>
<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.10</version>
</dependency>

Обратите внимание, что нам нужны эти дополнительные зависимости в POM:

Последние версии зависимости zookeeper и zkclient можно найти здесь и здесь.

7.1. Балансировка нагрузки

    В настоящее время фреймворк поддерживает несколько стратегий балансировки нагрузки:

случайный циклический наименее активный постоянный хэш.

В следующем примере у нас есть две реализации службы в качестве поставщиков в кластере. Запросы маршрутизируются с использованием циклического подхода.

@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();
    });
}

Сначала настроим сервис-провайдеров:

Теперь у нас есть стандартный «быстрый провайдер», который отвечает сразу, и специальный «медленный провайдер», который спит 5 секунд на каждый запрос.

@Test
public void givenProviderCluster_whenConsumerSaysHi_thenResponseBalanced() {
    ClassPathXmlApplicationContext localContext
      = new ClassPathXmlApplicationContext("cluster/consumer-app-lb.xml");
    localContext.start();
    GreetingsService greetingsService
      = (GreetingsService) localContext.getBean("greetingsService");

    List<Long> elapseList = new ArrayList<>(6);
    for (int i = 0; i < 6; i++) {
        long current = System.currentTimeMillis();
        String hiMessage = greetingsService.sayHi("baeldung");
        assertNotNull(hiMessage);
        elapseList.add(System.currentTimeMillis() - current);
    }

    OptionalDouble avgElapse = elapseList
      .stream()
      .mapToLong(e -> e)
      .average();
    assertTrue(avgElapse.isPresent());
    assertTrue(avgElapse.getAsDouble() > 2500.0);
}

После 6 запусков со стратегией циклического перебора мы ожидаем, что среднее время отклика будет не менее 2,5 секунд:

«

@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;
    });
}

«Кроме того, применяется динамическая балансировка нагрузки. Следующий пример демонстрирует, что при циклической стратегии потребитель автоматически выбирает нового поставщика услуг в качестве кандидата, когда новый поставщик появляется в сети.

@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<Long> elapseList = new ArrayList<>(6);
    for (int i = 0; i < 6; i++) {
        long current = System.currentTimeMillis();
        String hiMessage = greetingsService.sayHi("baeldung");
        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);
}

«Медленный провайдер» регистрируется через 2 секунды после запуска системы:

<dubbo:reference interface="com.baeldung.dubbo.remote.GreetingsService"
  id="greetingsService" loadbalance="roundrobin" />

Потребитель вызывает удаленную службу один раз в секунду. После 6 запусков мы ожидаем, что среднее время отклика будет больше 1,6 секунды:

Обратите внимание, что балансировщик нагрузки можно настроить как на стороне потребителя, так и на стороне провайдера. Вот пример конфигурации на стороне потребителя:

    7.2. Отказоустойчивость

В Dubbo поддерживается несколько стратегий отказоустойчивости, в том числе:

отказоустойчивость отказоустойчивость отказоустойчивость отказоустойчивость разветвление отказоустойчивости.

<dubbo:service interface="com.baeldung.dubbo.remote.GreetingsService"
  ref="greetingsService" cluster="failover"/>

В случае отработки отказа, когда один провайдер выходит из строя, потребитель может попробовать использовать других провайдеров в кластере.

public class GreetingsFailoverServiceImpl implements GreetingsService {

    @Override
    public String sayHi(String name) {
        return "hi, failover " + name;
    }
}

Стратегии отказоустойчивости настроены для сервис-провайдеров следующим образом:

Чтобы продемонстрировать отказоустойчивость службы в действии, давайте создадим отказоустойчивую реализацию GreetingsService:

<dubbo:reference interface="com.baeldung.dubbo.remote.GreetingsService"
  id="greetingsService" retries="2" timeout="2000" />

Мы можем вспомнить что наша специальная реализация службы GreetingsServiceSpecialImpl спит 5 секунд для каждого запроса.

@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("baeldung");

    assertNotNull(hiMessage);
    assertEquals("hi, failover baeldung", hiMessage);
}

Когда любой ответ, который занимает более 2 секунд, рассматривается как ошибка запроса для потребителя, у нас есть сценарий отработки отказа:

После запуска двух провайдеров мы можем проверить поведение отработки отказа с помощью следующий фрагмент:

8. Резюме

В этом уроке мы немного поработали над Dubbo. Большинство пользователей привлекает его простота и богатые и мощные функции.