1. Введение
Dubbo — это среда RPC и микросервисов с открытым исходным кодом от Alibaba.
Помимо прочего, это помогает улучшить управление услугами и позволяет плавно преобразовать традиционные монолитные приложения в масштабируемую распределенную архитектуру.
В этой статье мы познакомимся с Dubbo и его наиболее важными функциями.
2. Архитектура
Даббо различает несколько ролей:
- Провайдер — где предоставляется услуга; провайдер зарегистрирует свою услугу в реестре
- Контейнер — место, где служба инициируется, загружается и запускается.
- Потребитель — кто вызывает удаленные службы; потребитель подпишется на услугу, необходимую в реестре
- Реестр — где служба будет зарегистрирована и обнаружена
- Мониторить — записывать статистику по сервисам, например, частоту вызова сервиса в заданный интервал времени
(источник: 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;
}
}
Чтобы сделать удаленный вызов процедуры, потребитель должен использовать общий интерфейс с поставщиком услуг, поэтому интерфейс GreetingsService
должен быть общим с потребителем.
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.foreach.dubbo.remote.GreetingsServiceImpl"/>
<dubbo:service interface="com.foreach.dubbo.remote.GreetingsService"
ref="greetingsService"/>
С приведенной выше конфигурацией bean-компонентов мы только что предоставили нашему GreetingsService
URL-адрес под dubbo://127.0.0.1:20880
и зарегистрировали службу на многоадресном адресе, указанном в <dubbo:registry />
.
В конфигурации провайдера мы также объявили метаданные нашего приложения, интерфейс для публикации и его реализацию соответственно с помощью <dubbo:application />
, <dubbo:service />
и <beans />
.
Протокол 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.foreach.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("foreach");
assertNotNull(hiMessage);
assertEquals("hi, foreach", hiMessage);
}
}
Когда удаленный контекст провайдера запускается, Dubbo
автоматически загружает GreetingsService
и регистрирует его в заданном реестре. В данном случае это многоадресный реестр.
Потребитель подписывается на многоадресный реестр и создает прокси-сервер GreetingsService
в контексте. Когда наш локальный клиент вызывает метод sayHi
, он прозрачно вызывает удаленную службу.
Мы упомянули, что реестр является необязательным, что означает, что потребитель может напрямую подключаться к провайдеру через открытый порт:
<dubbo:reference interface="com.foreach.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, файл свойств и аннотации. Однако файл свойств и аннотации применимы только в том случае, если наша архитектура не очень сложна.
Давайте посмотрим, как наши предыдущие XML-конфигурации для многоадресного реестра можно преобразовать в конфигурацию 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();
Теперь, когда сервис уже доступен через многоадресный реестр, давайте воспользуемся им в локальном клиенте:
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("foreach");
Хотя приведенный выше фрагмент работает так же хорошо, как и предыдущий пример конфигурации XML, он немного более тривиален. На данный момент конфигурация XML должна быть первым выбором, если мы намерены в полной мере использовать Dubbo.
5. Поддержка протокола
Фреймворк поддерживает несколько протоколов, включая dubbo
, RMI
, hessian
, HTTP
, веб-сервис
, thrift
, memcached
и redis
. Большинство протоколов выглядит знакомым, за исключением dubbo
. Давайте посмотрим, что нового в этом протоколе.
Протокол dubbo
поддерживает постоянное соединение между поставщиками и потребителями. Длинное соединение и неблокирующая сетевая связь NIO обеспечивают довольно высокую производительность при передаче небольших пакетов данных (<100 КБ).
Существует несколько настраиваемых свойств, таких как порт, количество подключений на одного потребителя, максимально допустимое количество подключений и т. д.
<dubbo:protocol name="dubbo" port="20880"
connections="2" accepts="1000" />
Dubbo также поддерживает одновременное предоставление услуг через разные протоколы:
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:protocol name="rmi" port="1099" />
<dubbo:service interface="com.foreach.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" />
И да, мы можем предоставлять доступ к разным службам, используя разные протоколы, как показано во фрагменте выше. Базовые транспортеры, реализации сериализации и другие общие свойства, относящиеся к сети, также можно настраивать.
6. Кэширование результатов
Встроенное удаленное кэширование результатов поддерживается для ускорения доступа к горячим данным. Это так же просто, как добавить атрибут кеша в ссылку на компонент:
<dubbo:reference interface="com.foreach.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("foreach");
long timeElapsed = System.currentTimeMillis() - before;
assertTrue(timeElapsed > 5000);
assertNotNull(hiMessage);
assertEquals("hi, foreach", hiMessage);
before = System.currentTimeMillis();
hiMessage = greetingsService.sayHi("foreach");
timeElapsed = System.currentTimeMillis() - before;
assertTrue(timeElapsed < 1000);
assertNotNull(hiMessage);
assertEquals("hi, foreach", hiMessage);
}
Здесь потребитель вызывает реализацию специального сервиса, поэтому для завершения вызова в первый раз потребовалось более 5 секунд. При повторном вызове метод sayHi
завершается почти сразу же, так как результат возвращается из кеша.
Обратите внимание, что локальный кеш потока и JCache также поддерживаются.
7. Поддержка кластера
Dubbo помогает нам свободно масштабировать наши услуги благодаря возможности балансировки нагрузки и нескольким стратегиям отказоустойчивости. Здесь предположим, что у нас есть Zookeeper в качестве нашего реестра для управления службами в кластере. Провайдеры могут зарегистрировать свои услуги в Zookeeper следующим образом:
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
Обратите внимание, что нам нужны эти дополнительные зависимости в POM
:
<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>
Последние версии зависимостей 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 секунд на каждом запросе.
После 6 запусков со стратегией циклического перебора мы ожидаем, что среднее время отклика будет не менее 2,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("foreach");
assertNotNull(hiMessage);
elapseList.add(System.currentTimeMillis() - current);
}
OptionalDouble avgElapse = elapseList
.stream()
.mapToLong(e -> e)
.average();
assertTrue(avgElapse.isPresent());
assertTrue(avgElapse.getAsDouble() > 2500.0);
}
Кроме того, применяется динамическая балансировка нагрузки. Следующий пример демонстрирует, что при циклической стратегии потребитель автоматически выбирает нового поставщика услуг в качестве кандидата, когда новый поставщик появляется в сети.
«Медленный провайдер» регистрируется через 2 секунды после старта системы:
@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;
});
}
Потребитель вызывает удаленную службу один раз в секунду. После выполнения 6 раз мы ожидаем, что среднее время отклика будет больше 1,6 секунды:
@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("foreach");
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);
}
Обратите внимание, что балансировщик нагрузки можно настроить как на стороне потребителя, так и на стороне провайдера. Вот пример конфигурации на стороне потребителя:
<dubbo:reference interface="com.foreach.dubbo.remote.GreetingsService"
id="greetingsService" loadbalance="roundrobin" />
7.2. Отказоустойчивость
В Dubbo поддерживается несколько стратегий отказоустойчивости, в том числе:
- отказоустойчивость
- отказоустойчивый
- отказоустойчивый
- отказоустойчивость
- разветвление.
В случае аварийного переключения, когда один поставщик выходит из строя, потребитель может попробовать использовать других поставщиков услуг в кластере.
Стратегии отказоустойчивости настраиваются следующим образом для поставщиков услуг:
<dubbo:service interface="com.foreach.dubbo.remote.GreetingsService"
ref="greetingsService" cluster="failover"/>
Чтобы продемонстрировать отказоустойчивость службы в действии, давайте создадим отказоустойчивую реализацию GreetingsService
:
public class GreetingsFailoverServiceImpl implements GreetingsService {
@Override
public String sayHi(String name) {
return "hi, failover " + name;
}
}
Мы можем вспомнить, что наша специальная реализация службы GreetingsServiceSpecialImpl
спит 5 секунд для каждого запроса.
Когда любой ответ, который занимает более 2 секунд, рассматривается потребителем как сбой запроса, у нас есть сценарий отработки отказа:
<dubbo:reference interface="com.foreach.dubbo.remote.GreetingsService"
id="greetingsService" retries="2" timeout="2000" />
После запуска двух провайдеров мы можем проверить поведение при отказе с помощью следующего фрагмента кода:
@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("foreach");
assertNotNull(hiMessage);
assertEquals("hi, failover foreach", hiMessage);
}
8. Резюме
В этом уроке мы немного откусили от Dubbo. Большинство пользователей привлекает его простота и богатые и мощные функции.
Помимо того, что мы представили в этой статье, у платформы есть ряд функций, которые еще предстоит изучить, таких как проверка параметров, уведомление и обратный вызов, обобщенная реализация и ссылка, удаленная группировка результатов и слияние, обновление службы и обратная совместимость, чтобы назвать только немного.
Как всегда, полную реализацию можно найти на Github .