Zookeeper介绍

ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

安装与配置

单机版

下载

官方下载

目前最新的稳定版本是3.4.10,压缩包解压后会出现如下目录。

bin目录是存放脚本的目录,其中包括windows与linux两个平台的启动与CLI客户端脚本;

conf目录是存放配置文件的目录,其中包括日志配置、zoo_sample.cfg样例配置;

contrib目录是其它语言对zk支持的工具包;

lib目录是zk的依赖包;

recipes目录是zk某些用法的示例代码;

src源代码目录

配置

config目录存放了一个名为zoo_sample.cfg的样例配置文件,将它重命名为zoo.cfg,并修改其中的配置选项,配置选项以键值对的形式存在。

tickTime=2000  // 时长单位为毫秒,是后续配置的基本单位,1 * tickTime是客户端与zk服务端的心跳时间,2 * tickTime是客户端会话的超时时间
clientPort=2181  // zk服务进程监听的TCP端口,默认情况下,服务端会监听2181端口
dataDir=F://ideaWorkspace//zookeeper-3.4.6//data
dataLogDir=F://ideaWorkspace//zookeeper-3.4.6//logs // 配置存储快照与事务日志的文件目录

启动

进入到bin目录,windows环境下直接双击zkServer.cmdlinux环境下执行命令./zkServer.sh start将服务启动。另外还提供了stop/status/restart参数。

CLI

CLI是指zookeeper提供的类似命令行的维护接口。

连接

  • 如果默认连本机的话,在bin目录下执行./zkCli.sh即可
  • 如要需要连其它远程机器的话,使用-server参数,例如./zkCli.sh -server 10.10.107.26:2181

常用命令

  • 如果不知道ZK提供了哪些命令,可在进入CLI后输入help回车,会将支持的命令列表列出来
  • 查看节点数据,使用命令ls /ls2 /
  • 创建节点,使用命令create /test "hello"create /test ""
  • 删除节点,使用命令delete /testrmr /test,表示删除节点或子节点
  • 获取节点内容,使用命令get /test
  • 修改节点内容,使用命令set /test "ohmygod"

nc四字命令

当在没有CLI的情况下,可以通过nctelnet的方式从zk那里获取相关的信息。为什么叫四字命令是由于输入的指令为四个字符,命令例如echo conf | nc 127.0.0.1 2181

  • conf输出相关服务配置的详细信息
  • cons列出所有连接到服务器的客户端的完全的连接 / 会话的详细信息。包括“接受 / 发送”的包数量、会话 id 、操作延迟、最后的操作执行等等信息
  • dump列出未经处理的会话和临时节点
  • envi输出关于服务环境的详细信息(区别于 conf 命令)
  • reqs列出未经处理的请求
  • ruok测试服务是否处于正确状态。如果确实如此,那么服务返回“ imok ”,否则不做任何相应
  • stat输出关于性能和连接的客户端的列表
  • wchs列出服务器 watch 的详细信息
  • wchc通过 session 列出服务器 watch 的详细信息,它的输出是一个与 watch 相关的会话的列表
  • wchp通过路径列出服务器 watch 的详细信息。它输出一个与 session 相关的路径

伪分布式

分布式是多个服务跑在不同的机器上,伪分布式是指在资源有限的情况下,多个服务跑在一台机器上。

配置

  • conf目录下的zoo.cfg中增加如下配置,且每个服务的clientPort要选择不一样。
// initLimit配置follower与leader之间建立连接后进行同步的最长时间为initLimit*tickTime
initLimit=10
// 配置follower和leader之间发送消息,请求和应答的最大时间长度为syncLimit*tickTime
syncLimit=5
// server.id=host:port1:port2
// id为手动给zk服务的编号,port1表示follower和leader交换消息所使用的端口,port2表示选举leader所使用的端口
server.1=10.10.107.104:2887:3887
server.2=10.10.107.104:2888:3888
server.3=10.10.107.104:2889:3889  
  • 增加myid配置,如果是分布式部署,需要在data目录下增加一个myid的配置文件,里面分别填好该进程服务在zoo.cfg文件中分配的编号

启动

  • 分布式模式启动跟单机启动一样,只需要分别启动即可
  • CLI连接时,可以使用命令./zkCli.sh -server 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183,它会随机选择一个服务,从日志中可以具体看出来当前连在哪个服务上

服务注册与发现

在分布式系统开发过程中,服务与服务这间的调用是跨进程或跨机器的,前面讲到的RPC或RESTFul也好,均需要有一个统一的服务管理中心,因为每个服务都存在多个服务实例,也就是常说的服务治理模块,主要负责服务的注册与发现,另外还有配置管理、负载均衡等功能。主要的示意图如下。

解决方案

早期方案

各系统组件角色如下:

  • 服务消费者通过本地hosts中的服务提供者域名与Nginx的IP绑定信息来调用服务
  • Nginx用来对服务提供者提供的服务进行存活检查和负载均衡
  • 服务提供者提供服务给服务消费者访问,并通过Nginx来进行请求分发

这在内部系统比较少,访问量比较小的情况下,解决了服务的注册,发现与负载均衡等问题。但是,随着内部服务越来愈多,访问量越来越大的情况下,该架构的隐患逐渐暴露出来:

  • 最明显的问题是Nginx存在单点故障(SPOF),同时随着访问量的提升,会成为一个性能瓶颈
  • 随着内部服务的越来越多,不同的服务消费方需要配置不同的hosts,很容易在增加新的主机时忘记配置hosts导致服务不能调用问题,增加了运维负担
  • 服务的配置信息分散在各个主机hosts中,难以保持一致性,不便于服务的管理
  • 服务主机的发布和下线需要手工的修改Nginx upstream配置,修改的配置需要上线,不利于服务的快速部署

解决的思路如下:

  • 服务的注册信息应该统一保存,方便于服务的管理
  • 自动通过服务的名称去发现服务,而不必了解这个服务提供的end-point到底是哪台主机
  • 支持服务的负载均衡及fail-over
  • 增加或移除某个服务的end-point时,对于服务的消费者来说是透明的

目前方案

在此架构中分为服务消费者、服务提供者及服务注册中心三个角色。当然这种架构目前有挺多成熟实现,如阿里的Dubbo及Spring Cloud组件中的EurekaConsuletcd等。

服务提供者

服务提供者作为服务的提供方在启动时将自身的服务信息注册到服务注册中心中去,服务信息一般包括但不限于隶属于哪个系统、服务的IP及端口、服务请求的URL、服务的权重等等。

服务的消费者

  • 服务消费者在启动时从服务注册中心获取需要的服务注册信息
  • 将服务注册信息缓存在本地
  • 监听服务注册信息的变更,如接收到服务注册中心的服务变更通知,则在本地缓存中更新服务的注册信息
  • 根据本地缓存中的服务注册信息构建服务调用请求,并根据负载均衡策略(随机负载均衡,Round-Robin负载均衡等)来转发请求

服务注册中心

服务注册中心主要提供所有服务注册信息的中心存储,解决了早期nginx单点问题,实现服务名与服务实现端点的对应,同时负责将服务注册信息的更新通知实时的Push给服务消费者(主要是通过Zookeeper的Watcher机制来实现的)。

ZK客户端

zookeeper本身提供的API接口太底层,使用不方便。有对API进一步封装的ZK客户端或框架,目前开源之中用得比较多的为zkclientCurator。两者分别有其优缺点。

原生API

  • 连接的创建是异步的,需要开发人员自行编码实现等待
  • 连接没有自动的超时重连机制
  • zk本身没提供序列化机制,需要开发人员自行指定,从而实现数据的序列化和反序列化
  • Watcher注册一次只会生效一次,需要不断的重复注册
  • 不支持递归创建树形节点

zkclient

  • 对原生接口封装,简化开发API
  • 解决session会话超时重连
  • Watcher反复注册
  • 参考文档少,异常处理简化
  • 没有提供各种使用场景的实现

Curator

  • 除开zkclient提供的功能外,还提供了各种场景应用(Recipe,如共享锁服务、Master选举机制和分布式计算器等)的抽象封装
  • 学习门槛比zkclient高些

服务的定义

服务的定义是指将服务对外提供的功能接口以某种方式发布出来,在此例中打算定义一个用户的服务,通过服务消费者传入用户ID,来获取该用户的相关信息的功能,利用protobuff来进行服务接口的定义如下,相关语法上一节RPC使用中己讲到,不再详述。

syntax = "proto3";option java_multiple_files = true;
option java_package = "com.comba.zookeeper.rpc";
option java_outer_classname = "UserProto";package rpc;// 定义用户接口
service User {// 获取用户信息rpc GetUser (UserRequest) returns (UserReply) {}
}message UserRequest {int32 id = 1;
}message UserReply {int32 id = 1;string name = 2;int32 age = 3;
}

服务提供者实现

依赖引入

<!-- 引入zkclient -->
<dependency><groupId>com.101tec</groupId><artifactId>zkclient</artifactId><version>${zkclient.version}</version>
</dependency>
<!-- 引入grpc -->
<dependency><groupId>io.grpc</groupId><artifactId>grpc-all</artifactId><version>${grpc.version}</version>
</dependency><!-- 引入grpc的编译插件 -->
<build><extensions><extension><groupId>kr.motd.maven</groupId><artifactId>os-maven-plugin</artifactId><version>1.4.1.Final</version></extension></extensions><plugins><plugin><groupId>org.xolstice.maven.plugins</groupId><artifactId>protobuf-maven-plugin</artifactId><version>0.5.0</version><configuration><protocArtifact>com.google.protobuf:protoc:3.0.0:exe:${os.detected.classifier}</protocArtifact><pluginId>grpc-java</pluginId><pluginArtifact>io.grpc:protoc-gen-grpc-java:1.0.0:exe:${os.detected.classifier}</pluginArtifact></configuration><executions><execution><goals><goal>compile</goal><goal>compile-custom</goal></goals></execution></executions></plugin></plugins></build>

服务注册

服务提供者在启动时,需要向注册中心注册服务信息,在此实现服务注册的通用方法。

/*** 服务注册* @author doublerabbit* @date 2017年11月2日 上午9:32:30*/
public class ServiceRegister {private ZkClient zkClient;private String host;private String path;public ServiceRegister(String host){this.host = host;init();}public void init(){zkClient = new ZkClient(host, SESSION_TIMEOUT, CONNECTION_TIMEOUT);}public void addNode(String nodePath){// 验证节点路径的合法性if (!nodePath.startsWith("/")) {System.out.println("nodePath must be start with /");return;}// 如果不存在节点,就新创建一个if (!zkClient.exists(nodePath)) {zkClient.createPersistent(nodePath,true);path = nodePath;}else {System.out.println(nodePath + "is exists.");}}public void updateData(Object data){if (StringUtils.isNotBlank(path)) {updateData(path, data);}}public void updateData(String nodePath,Object data){// 验证节点路径的合法性if (!nodePath.startsWith("/")) {System.out.println("nodePath must be start with /");return;}if (zkClient.exists(nodePath)) {zkClient.writeData(nodePath, data);}else {System.out.println(nodePath + "is not exists.");}}public void deleteNode(String nodePath){// 验证节点路径的合法性if (!nodePath.startsWith("/")) {System.out.println("nodePath must be start with /");return;}if (zkClient.exists(nodePath)) {zkClient.deleteRecursive(nodePath);}else {System.out.println(nodePath + "is not exists.");}}
}

服务接口实现

服务接口实现利用GRPC方式。

public class ServiceProvider {private int port = 50051;private Server server;private String host = "10.10.107.104:2181,10.10.107.104:2182,10.10.107.104:2183";private String path = "/servers/user1";private ServiceRegister register;public ServiceProvider(){register = new ServiceRegister(host);register.addNode(path);}private void start() throws IOException {server = ServerBuilder.forPort(port).addService(new UserImpl()).build().start();System.out.println("service start...");Runtime.getRuntime().addShutdownHook(new Thread() {@Overridepublic void run() {System.err.println("*** shutting down gRPC server since JVM is shutting down");ServiceProvider.this.stop();System.err.println("*** server shut down");System.err.println("*** service deregister");register.deleteNode(path);}});}private void stop() {if (server != null) {server.shutdown();}}// block 一直到退出程序 private void blockUntilShutdown() throws InterruptedException {if (server != null) {server.awaitTermination();}}public ServiceRegister getRegister(){return this.register;}public static void main(String[] args) throws IOException, InterruptedException {final ServiceProvider server = new ServiceProvider();server.start();server.getRegister().updateData("127.0.0.1:50051");//server.blockUntilShutdown();Thread.sleep(2*60*1000L);}// 实现 定义一个实现服务接口的类 private class UserImpl extends UserGrpc.UserImplBase {public void getUser(UserRequest req, StreamObserver<UserReply> responseObserver) {System.out.println("service:"+req.getId());UserReply reply = UserReply.newBuilder().setId(req.getId()).setName("zhangsan-service1").setAge(20).build();responseObserver.onNext(reply);responseObserver.onCompleted();}}
}

服务消费者实现

依赖引入

消费者侧与服务提供者的依赖引入一致。

服务发现

服务发现是指消费者在己知服务名的前提下,从注册中将可用的服务信息拉取的本地缓存,待需要使用时从本地缓存中随机选取某个实例。

/*** 服务消费者进行服务发现,并包含负载均衡功能简略实现* @author doublerabbit* @date 2017年11月2日 下午3:07:13*/
public class ServiceDiscovery {private ZkClient zkClient;// 服务实例private Map<String, String> instanceMap;public ServiceDiscovery(){init();}public void init(){zkClient = new ZkClient(ZK_HOSTS, SESSION_TIMEOUT, CONNECTION_TIMEOUT);instanceMap = new ConcurrentHashMap<String, String>();}public void subDataChange(String path){zkClient.subscribeDataChanges(path, new IZkDataListener() {public void handleDataDeleted(String dataPath) throws Exception {System.out.println(dataPath + " delete noitfy====");zkClient.unsubscribeDataChanges(dataPath, this);}public void handleDataChange(String dataPath, Object data) throws Exception {System.out.println(dataPath + " notify,value=" + data);instanceMap.put(dataPath, (String)data);}});}public void subChildChange(){zkClient.subscribeChildChanges(ZNODE_PATH, new IZkChildListener() {public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {System.out.println("parentPath=" + parentPath);for (String str : currentChilds) {System.out.println("service path=" + str);String path = parentPath + "/" + str;Object data = zkClient.readData(path);if (data == null) {subDataChange(path);}if (!instanceMap.containsKey(path)) {System.out.println("path=" + path + ",data=" + (String)data);instanceMap.put(path, (String)data);System.out.println("map.size=" + instanceMap.size());subDataChange(path);}}}});}public Map<String, String> getServers(){return instanceMap;}/*** 随机获取一个可用的服务地址,作负载均衡使用* @return*/public Optional<String> getServer(){int size = instanceMap.size();if (size == 0) {System.err.println("does not have available server.");return Optional.ofNullable(null);}int rand = new Random().nextInt(size);System.out.println("size=" + size + ",rand=" + rand);;String server = (String)instanceMap.values().toArray()[rand];return Optional.ofNullable(server);}public static void main(String[] args) throws Exception {ServiceDiscovery consumer = new ServiceDiscovery();consumer.subChildChange();Thread.sleep(20*60*1000L);for (Map.Entry<String, String> entry : consumer.getServers().entrySet()) {System.out.println("key=" + entry.getKey() + ",value=" + entry.getValue());}}public static class DataListener implements IZkDataListener{public void handleDataChange(String dataPath, Object data) throws Exception {System.out.println(dataPath + "notify,value=" + data);}public void handleDataDeleted(String dataPath) throws Exception {System.out.println(dataPath + " delete noitfy====");}}
}

客户端负载均衡

客户端负载均衡是指客户端从本地缓存中选取服务实例的策略,本例中实现简单,采用随机数方式,不过该功能就类似于spring cloud组件中的Ribbon

public Optional<String> getServer(){int size = instanceMap.size();if (size == 0) {System.err.println("does not have available server.");return Optional.ofNullable(null);}int rand = new Random().nextInt(size);System.out.println("size=" + size + ",rand=" + rand);;String server = (String)instanceMap.values().toArray()[rand];return Optional.ofNullable(server);}

服务消费实现

服务消费实现从注册中心发现服务,并解析服务地址,从而达到调用服务的目的。

/*** 服务消费者* @author doublerabbit* @date 2017年11月3日 下午12:18:57*/
public class ServiceConsumer {private final ManagedChannel channel; private final UserGrpc.UserBlockingStub blockingStub; public ServiceConsumer(String host,int port){ channel = ManagedChannelBuilder.forAddress(host,port) .usePlaintext(true) .build();blockingStub = UserGrpc.newBlockingStub(channel); }public void shutdown() throws InterruptedException { channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); }public void greet(){ UserRequest request = UserRequest.newBuilder().setId(10).build(); UserReply  response = blockingStub.getUser(request);System.out.println("id="+response.getId()+",name="+response.getName()+",age="+response.getAge());}public static void main(String[] args) throws InterruptedException {ServiceDiscovery discovery = new ServiceDiscovery();discovery.subChildChange();Thread.sleep(30*1000L);for (int i = 0; i < 5; i++) {Optional<String> server = discovery.getServer();if (server.isPresent()) {String[] array = server.get().split(":");System.out.println("server host=" + array[0] + ",port=" + array[1]);ServiceConsumer client = new ServiceConsumer(array[0],Integer.parseInt(array[1]));client.greet();client.shutdown();}else {System.err.println("not find avaliable server====");}}}
}

测试结果

启动服务消费者与服务提供者,然后查看消费者与提供者的控制台输出如下。

// 消费者启动后控制台打印,可能看出消费者侧均感知到服务提供信息
parentPath=/servers
service path=user1
path=/servers/user1,data=null
/servers/user1 notify,value=127.0.0.1:50051
parentPath=/servers
service path=user1
service path=user2
path=/servers/user2,data=null
/servers/user2 notify,value=127.0.0.1:50052
parentPath=/servers
service path=user1
service path=user2
service path=user3
path=/servers/user3,data=null
/servers/user3 notify,value=127.0.0.1:50053// 消费者进行消费,以10次为例,可以看出在随机选择服务实例进行调用
size=3,rand=2
server host=127.0.0.1,port=50053
id=10,name=zhangsan-service3,age=20
size=3,rand=2
server host=127.0.0.1,port=50053
id=10,name=zhangsan-service3,age=20
size=3,rand=2
server host=127.0.0.1,port=50053
id=10,name=zhangsan-service3,age=20
size=3,rand=2
server host=127.0.0.1,port=50053
id=10,name=zhangsan-service3,age=20
size=3,rand=1
server host=127.0.0.1,port=50052
id=10,name=zhangsan-service2,age=20
size=3,rand=1
server host=127.0.0.1,port=50052
id=10,name=zhangsan-service2,age=20
size=3,rand=0
server host=127.0.0.1,port=50051
id=10,name=zhangsan-service1,age=20
size=3,rand=2
server host=127.0.0.1,port=50053
id=10,name=zhangsan-service3,age=20
size=3,rand=0
server host=127.0.0.1,port=50051
id=10,name=zhangsan-service1,age=20
size=3,rand=1
server host=127.0.0.1,port=50052
id=10,name=zhangsan-service2,age=20

问题

为什么Eureka比ZK更适合构建服备注册与发现

CAP原理,Eureka是基于AP原则构建,ZK是基于CP原则来构建,相对于一个服务访问来讲,在出现问题时能够访问是最重要的,可能访问的数据出现不一致现象,但总比访问不到报错更合理。

zookeeper与grpc集成实现服务注册与发现相关推荐

  1. Zookeeper服务注册与发现

    Zookeeper作为服务注册与发现的解决方案,它有如下优点: 1. 它提供的简单API 2. 已有互联网公司(例如:Pinterest,Airbnb)使用它来进行服务注册与发现 3. 支持多语言的客 ...

  2. 微服务框架 Go-Micro 集成 Nacos 实战之服务注册与发现

    作者 | 张斌斌 导读:本文主要介绍如何使用 Golang 生态中的微服务框架 Go-Micro(v2) 集成 Nacos 进行服务注册与发现.(Go-Micro 目前已经是 v3 版本,但由于某些原 ...

  3. 微服务框架Go-Micro集成Nacos实战之服务注册与发现

    简介:本文主要介绍如何使用 Golang 生态中的微服务框架 Go-Micro(v2) 集成 Nacos 进行服务注册与发现.(Go-Micro 目前已经是 v3 版本,但由于某些原因项目已经更名为 ...

  4. 基于 Consul 实现 MagicOnion(GRpc) 服务注册与发现

    0.简介 0.1 什么是 Consul Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置. 这里所谓的服务,不仅仅包括常用的 Api 这些服务,也包括软件开发过程 ...

  5. Web Api 基于Zookeeper的服务注册与发现

    差异 基于Nginx的服务提供和消费 基于zookeeper的服务注册和发现 zk的负载均衡是可以调控,nginx只是能调权重,其他需要可控的都需要自己写插件:但是nginx的吞吐量比zk大很多,可以 ...

  6. ASP.NET Core gRPC 使用 Consul 服务注册发现

    一. 前言 gRPC 在当前最常见的应用就是在微服务场景中,所以不可避免的会有服务注册与发现问题,我们使用gRPC实现的服务可以使用 Consul 或者 etcd 作为服务注册与发现中心,本文主要介绍 ...

  7. .net core grpc consul 实现服务注册 服务发现 负载均衡(二)

    在上一篇 .net core grpc 实现通信(一) 中,我们实现的grpc通信在.net core中的可行性,但要在微服务中真正使用,还缺少 服务注册,服务发现及负载均衡等,本篇我们将在 .net ...

  8. Nacos服务注册与发现源码(一)之gRPC协议的实例注册

    Nacos核心功能点 服务注册:Nacos Client会通过发送请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如ip地址.端口等信息.Nacos Server接收到注册请求 ...

  9. SpringBoot系列:9. 分布式系统,Dubbo,Zookeeper服务注册与发现

    前言 本章主要对分布式系统,RPC的实现方式和Zookeeper实现做一个详细的概述并通过实战代码加深对他们的了解. 1. 分布式 什么是分布式系统? :"分布式系统是若干独立计算机的集合, ...

最新文章

  1. 神器 jupyter,终于来了!
  2. 《BI那点儿事》三国人物智力分布状态分析
  3. C++易被忽略的知识点:移动语义 左值右值
  4. 02_Jquery_02_元素选择器
  5. form表单元素设置只读
  6. Python 实现校园卡目标检测与文字识别系统
  7. mysql数据库学习6_MySQL学习(六)
  8. OpenCV Show Image cvShowImage() 使用方法
  9. 进程线程之pid,tid
  10. java影院座位订票代码_基于jsp的影院订票-JavaEE实现影院订票 - java项目源码
  11. HTML5期末大作业:仿华为手机商城网站设计——仿华为手机电子商城 (1页) HTML+CSS+JavaScript html网页制作期末大作业成品_网页设计期末作业
  12. 用python函数画德国国旗代码_给我一面国旗 python帮你实现
  13. 通过修改window本地hosts文件修改域名指向
  14. ps抠头发丝详细教程,去除人像中的杂乱头发
  15. Tomcat双向SSL认证及CA数字证书安装和配置QQ即时通信协议窥探
  16. 揭秘Google排名的60个因素
  17. 南阳理工学院ACM语言入门题目1的思考
  18. 网盘防和谐姿势①:压缩包篇
  19. RD650 raid5 linux,联想RD650服务器Raid5配置图文教程.docx
  20. 很多人都有这个疑问:仰卧起坐真的能够减肚子么?

热门文章

  1. 【linux0.12】从open系统调用到柱面磁头扇区上篇-----原理讲解
  2. ubuntu 开机卡在logo界面
  3. 浅谈通过CMOS放电破解BIOS密码的原理
  4. silabs ZLL
  5. 如何调用调用系统邮件程序包括Gamil
  6. 分布式开发最全的解决方案
  7. 函数指针和函数指针类型
  8. 从远程服务器上复制文件到本地电脑报错:复制文件或文件夹时出错---未指定的错误
  9. 微信和QQ凌晨崩了 网民:该崩溃的是我(微信登不上没钱吃早饭)
  10. 让机器“看山是山”:脑启发的视觉计算|VALSE2018之五