使用Apache 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";@PostConstructpublic void init() throws Exception {}@Produces( { MediaType.APPLICATION_JSON } )@GETpublic 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();}@Beanpublic 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 servletfinal 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 。 要查看實際情況,讓我們構建并運行幾個人服務實例。
在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.0http://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
總結
以上是生活随笔為你收集整理的使用Apache Zookeeper进行协调和服务发现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 相爱相杀的意思 怎么理解相爱相杀的意思
- 下一篇: 蚊子灭绝了世界会怎样 蚊子灭绝了会怎样