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将有所不同):
让两个服务实例启动并运行,让我们尝试使用它们。 从服务客户端的角度来看,第一步是完全相同的:应该按照上面的方法创建CuratorFramework和ServiceDiscovery的实例(配置类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上找到。
翻译自: https://www.javacodegeeks.com/2013/11/coordination-and-service-discovery-with-apache-zookeeper.html
zookeeper