zookeeper_使用Apache Zookeeper进行协调和服务发现

zookeeper_使用Apache Zookeeper进行协调和服务发现_第1张图片

zookeeper

事实证明,面向服务的设计是针对各种不同的分布式系统的成功解决方案。 如果使用得当,它会带来很多好处。 但是随着服务数量的增加,了解部署什么以及部署在何处变得更加困难。 而且由于我们正在构建可靠且高度可用的系统,因此还需要问另一个问题:每个服务有多少实例可用?

在今天的帖子中,我想向您介绍Apache ZooKeeper的世界-一种高度可靠的分布式协调服务。 ZooKeeper提供的功能之多令人惊讶,因此让我们从一个非常简单的问题开始解决:我们有一个无状态的JAX-RS服务,我们可以根据需要在任意数量的JVM /主机上进行部署。 该服务的客户端应该能够自动发现所有可用实例,而只需选择其中一个(或全部)以执行REST调用即可。

听起来像是一个非常有趣的挑战。 解决它的方法可能很多,但是让我选择Apache ZooKeeper 。 第一步是下载Apache ZooKeeper (撰写本文时,当前的稳定版本是3.4.5)并解压缩。 接下来,我们需要创建一个配置文件。 做到这一点的简单方法是将conf / zoo_sample.cfg复制到conf / zoo.cfg中。 要运行,只需执行:

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

太好了,现在Apache ZooKeeper已启动并正在运行,正在端口2181上侦听(默认)。 Apache ZooKeeper本身值得一本书来解释其功能。 但是简短的概述给出了一个非常高级的图片,足以使我们入门。

Apache ZooKeeper具有强大的Java API,但是它是一个很底层的工具,并且不容易使用。 这就是为什么Netflix开发并开源了一个很棒的库,称为Curator,用于将本机Apache ZooKeeper API包装到更方便,更易于集成的框架中(现在是Apache孵化器项目)。

现在,让我们做一些代码! 我们正在开发简单的JAX-RS 2.0服务,该服务返回人员列表。 由于它将是无状态的,因此我们能够在单个主机或多个主机中运行许多实例,例如,取决于系统负载。 出色的Apache CXF和Spring框架将支持我们的实现。 以下是PeopleRestService的代码段:

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

非常基本和天真的实现。 方法初始化有意为空,很快将非常有帮助。 同样,让我们​​假设我们正在开发的每个JAX-RS 2.0服务都支持某种版本控制概念,类RestServiceDetails可以达到以下目的:

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使用人REST服务创建JAX-RS 2.0服务器的实例,该实例将由Jetty容器托管:

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

这是运行嵌入式Jetty服务器的ServerStarter类。 由于我们希望每个主机托管许多这样的服务器,因此端口不应该硬编码,而应作为参数提供:

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的运行位置。 当我在本地计算机上部署所有内容时,实例在localhost上。 让我们将此常量添加到AppConfig类中:

private static final String ZK_HOST = "localhost";

每个客户端都维护与Apache ZooKeeper服务器的持久连接。 每当客户端死亡时,连接也会断开, Apache ZooKeeper可以决定此特定客户端的可用性。 要连接到Apache ZooKeeper ,我们必须创建一个CuratorFramework类的实例:

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

下一步是创建ServiceDiscovery类的实例,该实例将允许使用刚刚创建的CuratorFramework实例将服务信息发布到Apache ZooKeeper中以供发现(我们还希望将RestServiceDetails作为附加元数据与每个服务注册一起提交):

@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像标准文件系统一样,将其所有数据存储为分层名称空间。 服务路径将成为我们所有服务的基本(根)路径。 每个服务还需要弄清楚它正在运行哪个主机和端口。 我们可以通过构建JaxRsApiApplication类中包含的URI规范来做到这一点( {port}{scheme}将在服务注册时由Curator框架解析):

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方法在这里起作用:

@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 )
  • 端口设置为该实例正在运行的实际值
  • 设置此特定REST服务端点的URI规范
  • 此外,还附加了带有服务版本的有效负载( RestServiceDetails )(尽管未使用,但它演示了传递更多详细信息的能力)

我们正在运行的每个新服务实例都将在以下位置发布/ services / people路径Apache ZooKeeper 。 要查看实际情况,让我们构建并运行几个人服务实例。

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将有所不同):

动物园管理员
zookeeper_使用Apache Zookeeper进行协调和服务发现_第2张图片

让两个服务实例启动并运行,让我们尝试使用它们。 从服务客户端的角度来看,第一步是完全相同的:应该按照上面的方法创建CuratorFrameworkServiceDiscovery的实例(配置类ClientConfig声明那些Bean),而无需进行任何更改。 但是,除了注册服务,我们将查询可用的服务:

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调用(使用令人敬畏的JAX-RS 2.0客户端API),并且还将询问服务版本(因为有效负载包含RestServiceDetails类的实例)。 让我们针对先前部署的两个实例构建并运行客户端:

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

控制台输出应显示对两个不同端点的两次调用:

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可以完成的工作,我强烈建议大家探索其功能(分布式锁,缓存,计数器,队列等)。

值得一提的是来自LinkedIn的Apache ZooKeeper上另一个名为Norbert的出色项目。 对于Eclipse开发人员,还可以使用Eclipse插件。

  • 所有资源都可以在GitHub上找到。
参考: Andriy Redko {devmind}博客上的JCG合作伙伴Andrey Redko与Apache Zookeeper的协调和服务发现。

翻译自: https://www.javacodegeeks.com/2013/11/coordination-and-service-discovery-with-apache-zookeeper.html

zookeeper

你可能感兴趣的:(分布式,java,zookeeper,大数据,spring)