从架构的发展谈起
- 早期, 我们使用单一的应用架构, 但随着发展, 后端的架构通过垂直伸缩的方式很难达到我们期望的性能要求, 产出比也很大, 所以水平伸缩成为主流.
- 分布式架构下, 当服务越来越多, 规模越来越大时, 对应的机器数量也越来越多, 单靠人工来管理和维护服务及地址的配置地址信息会越来越困难. 同时, 单点故障问题也凸显出来(一旦服务路由或负载均衡服务器宕机, 依赖他们的所有服务均失效)
- 这时, 需要一个能动态注册和获取服务信息的地方, 来统一管理服务名称和其对应的服务器列表信息, 称之为服务配置中心.
- 服务提供者在启动时, 将其提供的服务名称, 服务器地址注册到服务配置中心.
- 服务消费者通过服务配置中心来获取需要调用的服务的机器列表, 通过负载均衡算法, 选取其中一台服务器进行调用.
- 当服务器宕机或下线, 相应的机器需要能够动态地从服务配置中心移除, 并通知相应的服务消费者. 否则服务消费者就有可能因为调用到已经失效服务而发生错误.
- 在这个过程中, 服务消费者只有在第一次调用服务时需要查询服务配置中心, 然后将查询到的信息缓存到本地, 后面的调用直接使用本地缓存的服务地址列表信息, 不需要重新发起请求道服务配置中心去获取相应的服务地址列表. 直到服务的地址列表有变更.
- 这种去中心化的结构解决了之前负载均衡设备所导致的单点故障问题, 并且大大减轻了服务配置中心的压力.
Zookeeper简介
- Zookeeper是什么
- 它是一个典型的分布式数据一致性的解决方案, 分布式应用可以基于它实现数据的发布/订阅, 负载均衡, 服务命名, 分布式协调, 集群管理, Master选举, 分布式锁和分布式队列等功能.
- Zookeeper可以保证分布式的顺序一致性, 原子性, 单一视图, 可靠性, 实时性.
- 顺序一致性: 同意客户端发出的事务请求, 会严格按照发起顺序被应用到Zookeeper中.
- 原子性: 要么整个集群所有机器都应用了某事物, 要么都没应用.
- 单一视图: 无论客户端连接哪个Zookeeper服务器, 看到的服务端数据模型都一致.
- 可靠性: 一旦服务端成功应用了一个事务, 并完成了客户端的响应, 则该事务所引起的服务端状态将保留, 除非另一个事务又对其修改.
- 实时性: 这里Zookeeper只保证客户端最终一定能从服务端读取到最新的数据状态.
- Zookeeper的设计目标
- 致力于提供一个高性能, 高可用, 有严格的顺序访问控制能力的分布式协调服务. 下面看下其4个设计目标
- 简单的数据模型
- Zookeeper使得分布式程序能通过一个共享的,树型结构的名字空间进行协调.
- 可构建集群
- 组成Zookeeper集群的每台机器都会在内存中维护当前服务器状态, 并且每台机器都相互通信.
- 只要集群有一半以上的机器正常工作, 集群就能正常对外服务.
- Zookeeper的客户端程序会选择和集群中任意一台机器创建一个TCP连接, 而一旦客户端和某台服务器断开连接后, 会自动连另一台.
- 顺序访问
- 对于客户端的每个更新请求, Zookeeper都会分配一个全局唯一的递增编号, 这个编号反应所有事务操作的先后顺序.
- 高性能
- Zookeeper将全量数据存储在内存中, 并直接服务于客户端所有非事务请求, 因此尤其适用于以读操作为主的应用场景.
- 基本概念
- 集群角色
- 不使用Master/Slave模式, 而是Leader/Follower/Observer
- 所有机器通过选举选出Leader.
- Leader服务器为客户端提供读和写功能.
- Follower和Observer都能提供读功能. 唯一区别Observer不参与Leader选举.
- 会话(Session)
- Zookeeper默认对外服务端口2181, 客户端启动时, 首先和服务器建立一个TCP连接. 此时客户端会话生命周期开始了.
- 通过该连接, 客户端能与服务器保持有效会话, 能接收响应数据, 能收到来自服务器的Watch事件通知.
- Session的sessionTimeout用来设置客户端会话超时时间, 客户端连接断开后, 只要在超时时间之内能重连上集群中任意一台机器, 则之前创建的会话仍然有效.
- 数据节点(ZNode)
- Zookeeper数据模型是树, 由斜线分割的路径, 就是一个ZNode, 上面会保存数据内容和属性信息.
- 分为持久节点和临时节点, 其中一旦客户端会话失效, 客户端创建的临时节点被移除.
- 版本
- 每个ZNode, 都有一个Stat, 记录了ZNode的三个数据版本, 当前Znode版本, 其子节点版本, 其ACL版本.
- 事件监视器(Watcher)
- 在指定节点注册Watcher, 某些特定事件触发, Zookeeper服务端会把事件通知到感兴趣的客户端上.
- 权限控制ACL
- 5种权限
- CREATE: 创建子节点权限
- READ: 获取子节点数据/列表的权限
- WRITE: 更新
- DELETE: 删除
- ADMIN: 设置节点ACL权限
- 集群角色
Zookeeper的使用
部署与运行
- 集群模式
- 下载安装包zookeeper-3.4.14.tar.gz, 并解压.
- 初次使用时, 进入zookeeper-3.4.14/conf下, 把zoo_sample.cfg修改为zoo.cfg.
- 配置zoo.cfg
tickTime=2000 dataDir=/tmp/zookeeper/ clientPort=2181 initLimit=5 syncLimit=2 server.id=host:port:port server.id=host:port:port server.id=host:port:port
//id为ServerId, 用于标识某机器在集群中的序号. 同时, 需要在dataDir下创建myid文件 - 创建myid文件
- 在dataDir所配置的目录下, 创建一个名为myid的文件, 在第一行写上一个数字, 和zoo.cfg中当前机器的编号对上..
- 按相同步骤, 为其他机器都配置zoo.cfg和myid文件.
- 启动服务器
- 在zookeeper-3.4.14/bin目录下运行zkServer.sh脚本启动
sh zkServer.sh start
- 在zookeeper-3.4.14/bin目录下运行zkServer.sh脚本启动
- 单机模式
- 单机模式不过是一种特殊的集群模式, 即只有一台机器的集群.
- 对zoo.cfg稍作修改: 只需要一条: server.id=host:port:port
- 之后就可以启动服务器了.
- 伪集群模式
- 集群所有机器都在一个机子上, 但对端口号进行了修改, 如
server.1=IP1:2888:3888 server.2=IP1:2889:3889 server.3=IP1:2890:3898
- 集群所有机器都在一个机子上, 但对端口号进行了修改, 如
- 启动服务
- 在bin目录下有自带的脚本来启动, .sh和.cmd分别对应Linux和Windows系统.
- 可执行脚本说明
- sh zkServer.sh start 启动
- 停止服务
- sh zkServer.sh stop
- 查看ZK服务状态: zkServer.sh status
- 重启ZK服务: zkServer.sh restart
客户端脚本
- 上述过程搭建了zookeeper, 接下来使用客户端对Zookeeper进行操作.
- 连接本地服务器: sh zkCli.sh
- 连接指定服务器: sh zkCli.sh -server ip:port
//有如下显示, 则成功连接zookeeper服务器 WatchedEvent state:SyncConnected type:None path:null [zk: localhost:2181(CONNECTED) 0]
- 创建
- 使用create命令, 可创建一个节点: create [-s] [-e] path data [acl]
- -s或-e分别指定节点特性: 顺序或临时节点, 默认不添加则创建持久阶段.
create /zk-book 123 //创建叫/zk-book的节点, 数据内容是123 //acl是做权限控制的, 不加则不做任何权限控制
//创建了一个临时节点
create -e /zk-book/node-2 456
create -s /zk-book/node-1/node_1 789 //创建一个顺序节点
- -s或-e分别指定节点特性: 顺序或临时节点, 默认不添加则创建持久阶段.
- 读取
- ls命令
- 使用ls命令, 可列出Zookeeper指定节点下的所有子节点, 只能看指定节点下一级(斜杠分级)节点.
// ls path 如: ls /
- 使用ls命令, 可列出Zookeeper指定节点下的所有子节点, 只能看指定节点下一级(斜杠分级)节点.
- get命令
- 获取Zookeeper指定节点的数据内容和属性信息
// get path [zk: localhost:2181(CONNECTED) 2] get /zk-book 123 cZxid = 0x4 //创建该节点的事务ID ctime = Sun May 24 10:30:32 CST 2020 mZxid = 0x4 //最后一次更新该节点的事务ID mtime = Sun May 24 10:30:32 CST 2020 //最后更新该节点的时间 pZxid = 0x4 cversion = 0 //节点版本号 dataVersion = 0 //数据版本号 aclVersion = 0 //权限版本号 ephemeralOwner = 0x0 dataLength = 3 numChildren = 0
- 获取Zookeeper指定节点的数据内容和属性信息
- 更新
- 使用set命令, 可以更新指定节点的数据内容: set path data [version]
set /zk-book 88887 //这样数据内容就变成了88887 同时, dataVersion会加1,
- 如果我们在set的时候手动去指定了版本号, 就必须和上一次查询出来的结果一致, 否则 就会报错. 这个可以用于我们在修改节点数据的时候, 保证我们修改前数据没被别人修改过. 因为如果别人修改过了, 我们这次修改是不会成功的.
- 删除
- delete命令删除Zookeeper指定节点: delete path [version]
delete /zk-book //[version]与修改中用法一样.
- 注意: 无法删除一个包含子节点的节点.
Curator客户端
- Curator的maven依赖
<dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>4.2.0</version> <exclusions> <exclusion> <artifactId>zookeeper</artifactId> <groupId>org.apache.zookeeper</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.7</version> </dependency>
- 创建会话
- 使用CuratorFrameworkFactory这个工厂类的两个静态方法来创建一个客户端.
- CuratorFrameworkFactory.newClient(String connectString, RetryPolicy retryPolicy)
- CuratorFrameworkFactory.newClient(String connectString, int sessionTimeoutMs, int connectionTimeoutMs, RetryPolicy retryPolicy)
- connectString: 指Zookeeper服务器列表, 由逗号分隔的host:port
- sessionTimeoutMs: 会话超时时间, 默认60000ms
- connectionTimeoutMs: 连接创建超时时间, 默认15000ms
- retryPolicy: 重试策略(ExponentialBackoffRetry, RetryNTimes, RetryOneTime, RetryUntilElapsed)
- 通过RetryPolicy来让用户实现自定义的重试策略, 该接口只有一个方法
- boolean allowRetry(int retryCount, long elapsedTimeMs, RetrySleeper sleeper)
- ExponentialBackoffRetry
- 构造方法为: ExponentialBackoffRetry(int baseSleepTimesMs, int maxRetries, [int maxSleepMs])
- 重试策略: 给定一个初始sleep时间, 在这个基础上结合重试次数, 计算出当前需要sleep的时间= baseSleepTimeMs * Math.max(1, random.nextInt(1<<(retryCount+1))), 如果在maxSleepMs内, 就用它, 否则用maxSleepMs.
- 创建客户端实例后, 实际上没有完成会话的创建, 必须用start()来完成.
- 使用CuratorFramework中的start()方法启动会话.
public static void main(String[] args) throws InterruptedException { RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.47.132:2181", 5000, 3000, retryPolicy); client.start(); Thread.sleep(Integer.MAX_VALUE); }
- 使用Fluent风格
public static void main(String[] args) throws InterruptedException { RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); CuratorFramework client = CuratorFrameworkFactory.builder() .connectString("192.168.47.132:2181") .sessionTimeoutMs(5000) .retryPolicy(retryPolicy) .build(); client.start(); Thread.sleep(Integer.MAX_VALUE); }
- 创建含隔离命名空间的会话
- 为实现不同的Zookeeper业务间的隔离, 往往为每个业务分配一个独立的命名空间, 即指定Zookeeper根路径
CuratorFramework client = CuratorFrameworkFactory.builder() .connectString("192.168.47.132:2181") .sessionTimeoutMs(5000) .retryPolicy(retryPolicy) .namespace("base") .build();
- 为实现不同的Zookeeper业务间的隔离, 往往为每个业务分配一个独立的命名空间, 即指定Zookeeper根路径
- 创建节点
- Curator提供一系列Fluent风格的接口, 可自由组合使用完成节点的创建
- client.create().forPath(String path)
- 指定路径创建持久节点, 内容为空.
- client.create.forPath( path, "xxx".getBytes() )
- 创建一个节点, 附带初始内容(byte[]形式)
- client.create().withMode(CreateMode.EPHEMERAL).forPath(path)
- 创建一个临时节点, 内容为空.
- client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path)
- 创建一个临时节点, 并自动递归创建父节点.
- 若对不存在的父节点创建子节点, 会NoNodeException, 而使用creatingParentsIfNeeded会自动递归创建父节点.
- client.create().forPath(String path)
- 注意: Zookeeper中所有非叶子节点必须为持久节点.
public class CreateNode { static String path = "/zk-book/c1"; static CuratorFramework client = CuratorFrameworkFactory.builder() .connectString("192.168.47.132:2181") .sessionTimeoutMs(5000) .retryPolicy(new ExponentialBackoffRetry(1000, 3)) .build(); public static void main(String[] args) throws Exception { client.start(); client.create() .creatingParentsIfNeeded() .forPath(path, "HelloWorld".getBytes()); } }
- 删除节点
- Curator提供一系列Fluent风格的接口, 可自由组合使用完成节点的删除
- client.delete().forPath(path)
- 删除一个节点, 只能删除叶子节点.
- client.delete().deletingChildrenIfNeeded().forPath(path)
- 删除一个节点, 并递归删除其所有子节点.
- client.delete().withVersion(int version).forPath(path)
- 删除一个节点, 强制指定版本删除.
- client.delete().guranteed().forPath(path)
- 强制保证删除.
- guranteed(): 只要客户端会话有效, Curator在后台持续进行删除操作, 直到节点删除成功.
- client.delete().forPath(path)
-
public class DelNode { static String path = "/zk-book/c1"; static CuratorFramework client = CuratorFrameworkFactory.builder() .connectString("192.168.47.132:2181") .sessionTimeoutMs(5000) .retryPolicy(new ExponentialBackoffRetry(1000, 3)) .build(); public static void main(String[] args) throws Exception { client.start(); client.create() .creatingParentsIfNeeded() .withMode(CreateMode.EPHEMERAL) .forPath(path, "init".getBytes()); Stat stat = new Stat(); client.getData().storingStatIn(stat).forPath(path); client.delete().deletingChildrenIfNeeded() .withVersion(stat.getVersion()).forPath(path); } }
- 读取数据
- client.getData().forPath(path)
- 读取一个节点的数据内容, 返回值是byte[]
- client.getData().storingStatIn(Stat stat).forPath(path)
- 读取一个节点的数据, 同时获取其stat
-
public class GetNode { static String path = "/zk-book/c1"; static CuratorFramework client = CuratorFrameworkFactory.builder() .connectString("192.168.47.132:2181") .sessionTimeoutMs(5000) .retryPolicy(new ExponentialBackoffRetry(1000, 3)) .build(); public static void main(String[] args) throws Exception { client.start(); client.create() .creatingParentsIfNeeded() .withMode(CreateMode.EPHEMERAL) .forPath(path, "init".getBytes()); Stat stat = new Stat(); byte[] bytes = client.getData().storingStatIn(stat).forPath(path); System.out.println(new String(bytes)); } }
- client.getData().forPath(path)
- 更新数据
- client.setData().forPath(path, "xxx".getBytes())
- 更新一个节点的数据内容.
- client.setData().withVersion(version).forPath(path)
- 更新一个节点数据内容, 强制指定版本进行更新.
- 这里的version, 通常是从旧的stat中获取的.
-
public class SetNode { static String path = "/zk-book/c1"; static CuratorFramework client = CuratorFrameworkFactory.builder() .connectString("192.168.47.132:2181") .sessionTimeoutMs(5000) .retryPolicy(new ExponentialBackoffRetry(1000, 3)) .build(); public static void main(String[] args) throws Exception { client.start(); client.create() .creatingParentsIfNeeded() .withMode(CreateMode.EPHEMERAL) .forPath(path, "init".getBytes()); Stat stat = new Stat(); client.getData().storingStatIn(stat).forPath(path); client.setData().withVersion(stat.getVersion()).forPath(path, "INIT".getBytes()); } }
- client.setData().forPath(path, "xxx".getBytes())