Статьи

Координация и поиск услуг с Apache Zookeeper

Сервисно-ориентированный дизайн оказался успешным решением для огромного разнообразия распределенных систем. При правильном использовании он имеет много преимуществ. Но с ростом числа сервисов становится все труднее понять, что и где развернуто. И поскольку мы создаем надежные и высокодоступные системы, возникает еще один вопрос: сколько экземпляров каждого сервиса доступно в настоящее время?

В сегодняшнем посте я хотел бы познакомить вас с миром Apache ZooKeeper — высоконадежного сервиса распределенной координации. Количество функций, предоставляемых ZooKeeper, просто поразительно, поэтому давайте начнем с очень простой проблемы, которую нужно решить: у нас есть служба JAX-RS без сохранения состояния, которую мы развертываем на любом количестве JVM / хостов, сколько захотим. Клиенты этой службы должны иметь возможность автоматически обнаруживать все доступные экземпляры и просто выбирать один из них (или все) для выполнения вызова REST .

Похоже, очень интересный вызов. Там может быть много способов решить эту проблему, но позвольте мне для этого выбрать Apache ZooKeeper . Первым шагом является загрузка Apache ZooKeeper (текущая стабильная версия на момент написания статьи 3.4.5) и распаковка. Далее нам нужно создать файл конфигурации. Простой способ сделать это — скопировать conf / zoo_sample.cfg в conf / zoo.cfg . Чтобы запустить, просто выполните:

1
2
Windows: bin/zkServer.cmd
Linux: bin/zkServer

Отлично, теперь Apache ZooKeeper работает и прослушивает порт 2181 (по умолчанию). Apache ZooKeeper сам по себе стоит книги, чтобы объяснить его возможности. Но краткий обзор дает картину очень высокого уровня, достаточную, чтобы начать работу.

Apache ZooKeeper имеет мощный Java API, но он довольно низкоуровневый и не простой в использовании. Вот почему Netflix разработал и открыл замечательную библиотеку под названием Curator, чтобы превратить нативный API-интерфейс Apache ZooKeeper в более удобный и простой в интеграции фреймворк (теперь это проект инкубатора Apache ).

Теперь давайте сделаем немного кода! Мы разрабатываем простой сервис JAX-RS 2.0, который возвращает список людей. Поскольку он не будет сохранять состояние, мы можем запускать множество экземпляров на одном или нескольких хостах, в зависимости, например, от загрузки системы. Потрясающие Apache CXF и Spring Framework поддержат нашу реализацию. Ниже приведен фрагмент кода для PeopleRestService :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.rs;
 
import java.util.Arrays;
import java.util.Collection;
 
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
 
import com.example.model.Person;
 
@Path( PeopleRestService.PEOPLE_PATH )
public class PeopleRestService {
    public static final String PEOPLE_PATH = "/people";
 
    @PostConstruct
    public void init() throws Exception {
    }
 
    @Produces( { MediaType.APPLICATION_JSON } )
    @GET
    public Collection< Person > getPeople( @QueryParam( "page") @DefaultValue( "1" ) final int page ) {
        return Arrays.asList(
            new Person( "Tom", "Bombadil" ),
            new Person( "Jim", "Tommyknockers" )
        );
    }
}

Очень простая и наивная реализация. Метод init пуст по намерению, он очень скоро пригодится. Кроме того, давайте предположим, что каждый разрабатываемый нами сервис JAX-RS 2.0 поддерживает некоторое понятие управления версиями, класс RestServiceDetails служит для этой цели:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.config;
 
import org.codehaus.jackson.map.annotate.JsonRootName;
 
@JsonRootName( "serviceDetails" )
public class RestServiceDetails {
    private String version;
 
    public RestServiceDetails() {
    }
 
    public RestServiceDetails( final String version ) {
        this.version = version;
    }
 
    public void setVersion( final String version ) {
        this.version = version;
    }
 
    public String getVersion() {
        return version;
    }   
}

Наш класс конфигурации Spring AppConfig создает экземпляр сервера JAX-RS 2.0 с сервисом REST people, который будет размещен в контейнере Jetty :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.example.config;
 
import java.util.Arrays;
 
import javax.ws.rs.ext.RuntimeDelegate;
 
import org.apache.cxf.bus.spring.SpringBus;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
 
import com.example.rs.JaxRsApiApplication;
import com.example.rs.PeopleRestService;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
 
@Configuration
public class AppConfig {
    public static final String SERVER_PORT = "server.port";
    public static final String SERVER_HOST = "server.host";
    public static final String CONTEXT_PATH = "rest";
 
    @Bean( destroyMethod = "shutdown" )
    public SpringBus cxf() {
        return new SpringBus();
    }
 
    @Bean @DependsOn( "cxf" )
    public Server jaxRsServer() {
        JAXRSServerFactoryBean factory = RuntimeDelegate.getInstance().createEndpoint( jaxRsApiApplication(), JAXRSServerFactoryBean.class );
        factory.setServiceBeans( Arrays.< Object >asList( peopleRestService() ) );
        factory.setAddress( factory.getAddress() );
        factory.setProviders( Arrays.< Object >asList( jsonProvider() ) );
        return factory.create();
    }
 
    @Bean
    public JaxRsApiApplication jaxRsApiApplication() {
        return new JaxRsApiApplication();
    }
 
    @Bean
    public PeopleRestService peopleRestService() {
        return new PeopleRestService();
    }
 
    @Bean
    public JacksonJsonProvider jsonProvider() {
        return new JacksonJsonProvider();
    }
}

А вот класс ServerStarter, который запускает встроенный сервер Jetty . Поскольку мы хотели бы разместить много таких серверов на каждом хосте, порт должен быть не жестко закодирован, а представлен в качестве аргумента:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example;
 
import org.apache.cxf.transport.servlet.CXFServlet;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
 
import com.example.config.AppConfig;
 
public class ServerStarter {
    public static void main( final String[] args ) throws Exception {
        if( args.length != 1 ) {
            System.out.println( "Please provide port number" );
            return;
        }
 
        final int port = Integer.valueOf( args[ 0 ] );
        final Server server = new Server( port );
 
        System.setProperty( AppConfig.SERVER_PORT, Integer.toString( port ) );
        System.setProperty( AppConfig.SERVER_HOST, "localhost" );
 
        // Register and map the dispatcher servlet
        final ServletHolder servletHolder = new ServletHolder( new CXFServlet() );
        final ServletContextHandler context = new ServletContextHandler();  
        context.setContextPath( "/" );
        context.addServlet( servletHolder, "/" + AppConfig.CONTEXT_PATH + "/*" ); 
        context.addEventListener( new ContextLoaderListener() );
 
        context.setInitParameter( "contextClass", AnnotationConfigWebApplicationContext.class.getName() );
        context.setInitParameter( "contextConfigLocation", AppConfig.class.getName() );
 
        server.setHandler( context );
        server.start();
        server.join();
    }
}

Хорошо, в этот момент скучная часть закончилась. Но где Apache ZooKeeper и сервис обнаружения вписываются в эту картину? Вот ответ: всякий раз, когда развертывается новый экземпляр службы PeopleRestService , он публикует (или регистрирует) себя в реестре Apache ZooKeeper , включая URL-адрес, к которому он доступен, и версию службы, которую он размещает. Клиенты могут запросить Apache ZooKeeper , чтобы получить список всех доступных сервисов и позвонить им. Единственное, что сервисы и их клиенты должны знать, это то, где работает Apache ZooKeeper . Поскольку я развертываю все на своей локальной машине, мой экземпляр находится на локальном хосте . Давайте добавим эту константу в класс AppConfig :

1
private static final String ZK_HOST = "localhost";

Каждый клиент поддерживает постоянное соединение с сервером Apache ZooKeeper . Всякий раз, когда клиент умирает, соединение также прерывается, и Apache ZooKeeper может принять решение о доступности этого конкретного клиента. Чтобы подключиться к Apache ZooKeeper , нам нужно создать экземпляр класса CuratorFramework :

1
2
3
4
@Bean( initMethod = "start", destroyMethod = "close" )
public CuratorFramework curator() {
    return CuratorFrameworkFactory.newClient( ZK_HOST, new ExponentialBackoffRetry( 1000, 3 ) );
}

Следующим шагом является создание экземпляра класса ServiceDiscovery, который позволит публиковать информацию об услуге для обнаружения в Apache ZooKeeper с использованием только что созданного экземпляра CuratorFramework (мы также хотели бы представить RestServiceDetails в качестве дополнительных метаданных вместе с каждой регистрацией службы):

01
02
03
04
05
06
07
08
09
10
11
@Bean( initMethod = "start", destroyMethod = "close" )
public ServiceDiscovery< RestServiceDetails > discovery() {
    JsonInstanceSerializer< RestServiceDetails > serializer =
        new JsonInstanceSerializer< RestServiceDetails >( RestServiceDetails.class );
 
    return ServiceDiscoveryBuilder.builder( RestServiceDetails.class )
        .client( curator() )
        .basePath( "services" )
        .serializer( serializer )
        .build();       
}

Внутри Apache ZooKeeper хранит все свои данные в виде иерархического пространства имен, так же, как это делает стандартная файловая система. Путь сервисов будет основным (корневым) путем для всех наших сервисов. Каждый сервис также должен выяснить, какой хост и порт он работает. Мы можем сделать это, создав спецификацию URI, которая включена в класс JaxRsApiApplication ( {порт} и {схема} будут решаться инфраструктурой Curator в момент регистрации службы):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.rs;
 
import javax.inject.Inject;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
 
import org.springframework.core.env.Environment;
 
import com.example.config.AppConfig;
import com.netflix.curator.x.discovery.UriSpec;
 
@ApplicationPath( JaxRsApiApplication.APPLICATION_PATH )
public class JaxRsApiApplication extends Application {
    public static final String APPLICATION_PATH = "api";
 
    @Inject Environment environment;
 
    public UriSpec getUriSpec( final String servicePath ) {
        return new UriSpec(
            String.format( "{scheme}://%s:{port}/%s/%s%s",
                environment.getProperty( AppConfig.SERVER_HOST ),
                AppConfig.CONTEXT_PATH,
                APPLICATION_PATH,
                servicePath
            ) );  
    }
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Inject private JaxRsApiApplication application;
@Inject private ServiceDiscovery< RestServiceDetails > discovery;
@Inject private Environment environment;
 
@PostConstruct
public void init() throws Exception {
    final ServiceInstance< RestServiceDetails > instance =
        ServiceInstance.< RestServiceDetails >builder()
            .name( "people" )
            .payload( new RestServiceDetails( "1.0" ) )
            .port( environment.getProperty( AppConfig.SERVER_PORT, Integer.class ) )
            .uriSpec( application.getUriSpec( PEOPLE_PATH ) )
            .build();
 
    discovery.registerService( instance );
}

Вот что мы сделали:

  • создал экземпляр службы с именем people (полное имя будет / services / people )
  • установить порт на фактическое значение, на котором работает этот экземпляр
  • установить спецификацию URI для этой конкретной конечной точки службы REST
  • Кроме того, прилагается полезная нагрузка ( RestServiceDetails ) с версией сервиса (хотя она не используется, она демонстрирует возможность передавать больше деталей)

Каждый новый экземпляр службы, который мы запускаем, будет публиковаться под
/ Услуги / Люди Путь в
Apache ZooKeeper . Чтобы увидеть все в действии, давайте создадим и запустим несколько сервисных экземпляров.

1
2
3
mvn clean package
java -jar jax-rs-2.0-service\target\jax-rs-2.0-service-0.0.1-SNAPSHOT.one-jar.jar 8080
java -jar jax-rs-2.0-service\target\jax-rs-2.0-service-0.0.1-SNAPSHOT.one-jar.jar 8081

Из Apache ZooKeeper это может выглядеть следующим образом (обратите внимание, что UUID сессии будут отличаться):

Работник зоопарка

Запустив и запустив два экземпляра службы, давайте попробуем использовать их. С точки зрения клиента службы, первый шаг точно такой же: должны быть созданы экземпляры CuratorFramework и ServiceDiscovery (класс конфигурации ClientConfig объявляет эти компоненты), как мы делали это выше, никаких изменений не требуется. Но вместо регистрации сервиса мы будем запрашивать доступные:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.example.client;
 
import java.util.Collection;
 
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import com.example.config.RestServiceDetails;
import com.netflix.curator.x.discovery.ServiceDiscovery;
import com.netflix.curator.x.discovery.ServiceInstance;
 
public class ClientStarter {
    public static void main( final String[] args ) throws Exception {
        try( final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( ClientConfig.class ) ) {
            @SuppressWarnings("unchecked")
            final ServiceDiscovery< RestServiceDetails > discovery =
                context.getBean( ServiceDiscovery.class );
            final Client client = ClientBuilder.newClient();
 
            final Collection< ServiceInstance< RestServiceDetails > > services =
                discovery.queryForInstances( "people" );
            for( final ServiceInstance< RestServiceDetails > service: services ) {
                final String uri = service.buildUriSpec();
 
                final Response response = client
                    .target( uri )
                    .request( MediaType.APPLICATION_JSON )
                    .get();
 
                System.out.println( uri + ": " + response.readEntity( String.class ) );
                System.out.println( "API version: " + service.getPayload().getVersion() );
 
                response.close();
            }
        }
    }
}

После извлечения экземпляров службы выполняется вызов REST (с использованием удивительного клиентского API JAX-RS 2.0 ) и дополнительно запрашивается версия службы (поскольку полезная нагрузка содержит экземпляр класса RestServiceDetails ). Давайте создадим и запустим наш клиент для двух экземпляров, которые мы развернули ранее:

1
2
mvn clean package
java -jar jax-rs-2.0-client\target\jax-rs-2.0-client-0.0.1-SNAPSHOT.one-jar.jar

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

1
2
3
4
5
http://localhost:8081/rest/api/people: [{"email":null,"firstName":"Tom","lastName":"Bombadil"},{"email":null,"firstName":"Jim","lastName":"Tommyknockers"}]
API version: 1.0
 
http://localhost:8080/rest/api/people: [{"email":null,"firstName":"Tom","lastName":"Bombadil"},{"email":null,"firstName":"Jim","lastName":"Tommyknockers"}]
API version: 1.0

Если мы остановим один или все экземпляры, они исчезнут из реестра Apache ZooKeeper . То же самое применимо, если какой-либо экземпляр аварийно завершает работу или перестает отвечать на запросы.

Превосходно! Я думаю, что мы достигли нашей цели, используя такой замечательный и мощный инструмент, как Apache ZooKeeper . Спасибо его разработчикам, а также парням- кураторам за упрощение использования Apache ZooKeeper в ваших приложениях. Мы только что рассмотрели возможности использования Apache ZooKeeper , и я настоятельно рекомендую всем изучить его возможности (распределенные блокировки, кэши, счетчики, очереди и т. Д.).

Стоит упомянуть еще один замечательный проект, построенный поверх Apache ZooKeeper из LinkedIn, который называется Norbert . Для разработчиков Eclipse также доступен плагин Eclipse .

  • Все источники доступны на GitHub .

Справка: координация и поиск услуг с Apache Zookeeper от нашего партнера по JCG Андрея Редько в блоге Андрея Редько .