面向服务的设计已被证明是针对各种不同的分布式系统的成功解决方案。 如果使用得当,它会带来很多好处。 但是随着服务数量的增加,了解部署什么以及部署在何处变得更加困难。 而且,由于我们正在构建可靠且高度可用的系统,因此还需要问另一个问题:每个服务有多少实例可用?
在今天的帖子中,我想向您介绍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使用People 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插件 。
翻译自: https://www.javacodegeeks.com/2013/11/coordination-and-service-discovery-with-apache-zookeeper.html