赞
踩
ZooKeeper 是⼀种分布式协调组件
,用于管理大型主机。在分布式环境中协调和管理服务是一个复杂的过程。ZooKeeper 通过其简单的架构和 API 解决了这个问题。ZooKeeper 允许开发人员专注于核心应用程序逻辑,而不必担心应用程序的分布式特性。
在分布式系统中,需要有zookeeper作为分布式协调组件,协调分布式系统中的状态。
zk在实现分布式锁上,可以做到强⼀致性,分布式锁相关的知识,在后续的ZAB协议中介绍。
zookeeper可以保存登录信息,保证分布式登录系统多个系统无需重复登录。
zk由Java编写,安装依赖Java环境
上传zookeeper压缩包
解压缩
tar -zxvf apache-zookeeper-3.7.1-bin.tar.gz
zk目录结构:
zoo.cfg
文件zoo_sample.cfg配置文件说明:
# zookeeper时间配置中的基本单位 (毫秒)
tickTime=2000
# 允许follower初始化连接到leader最⼤时⻓,它表示tickTime时间倍数即:initLimit*tickTime
initLimit=10
# 允许follower与leader数据同步最⼤时⻓,它表示tickTime时间倍数
syncLimit=5
#zookeper 数据存储⽬录及⽇志保存⽬录(如果没有指明dataLogDir,则⽇志也保存在这个⽂件中)
dataDir=/tmp/zookeeper
#对客户端提供的端⼝号
clientPort=2181
#单个客户端与zookeeper最⼤并发连接数
maxClientCnxns=60
# 保存的数据快照数量,之外的将会被清除
autopurge.snapRetainCount=3
#⾃动触发清除任务时间间隔,⼩时为单位。默认为0,表示不⾃动清除。
autopurge.purgeInterval=1
稍作修改,然后重命名为zoo.cfg
mv zoo_sample.cfg zoo.cfg
./zkServer.sh start ../conf/zoo.cfg
./zkServer.sh restart ../conf/zoo.cfg
./zkServer.sh status ../conf/zoo.cfg
./zkServer.sh stop ../conf/zoo.cfg
zk中的数据是保存在节点上的,节点就是znode
,多个znode之间构成⼀颗树的⽬录结构
(类似Linux目录结构)。
顶层目录为/
树是由节点所组成,Zookeeper的数据存储也同样是基于节点,这种节点叫做 Znode
不同于树的节点,Znode 的引用方式是路径引用,类似于⽂件路径:
/service/org-service
这样的层级结构,让每⼀个 Znode 节点拥有唯⼀的路径,就像命名空间⼀样对不同信息作出清晰的隔离。
zk中的znode,包含了四个部分:
创建节点命令:(不同参数代表不同类型znode)
持久节点
: 创建出的节点,在会话结束后依然存在。保存数据(默认)持久顺序节点
: 创建出的节点,根据先后顺序,会在节点之后带上⼀个数值,越后执行数值越⼤,适用于分布式锁的应用场景- 单调递增(-s)临时节点
: 临时节点是在会话结束后,自动被删除的,通过这个特性,zk可以实现服务注册与发现的效果。(-e)如何维持心跳?
临时顺序节点
:跟持久序号节点相同,适⽤于临时的分布式锁。(-es)Container节点
(3.5.3版本新增):Container容器节点,当容器中没有任何子节点,该容器节点会被zk定期删除(60s)。(-c)TTL节点
:可以指定节点的到期时间,到期后被zk定时删除。只能通过系统配置 zookeeper.extendedTypesEnabled=true 开启(-t)【不稳定,了解即可】zk的数据是运行在内存中,zk提供了两种持久化机制:
zk把执行的命令以日志形式保存在dataLogDir指定的路径中的文件中
(如果没有指定 dataLogDir,则按dataDir指定的路径)。
zk会在⼀定的时间间隔内做⼀次内存数据的快照,把该时刻的内存数据保存在快照⽂件中
。
【默认两种都是开启的】
zk通过两种形式的持久化,在恢复时先恢复快照文件中的数据到内存中,再用日志文件中的数据做增量恢复,这样的恢复速度更快。
启动客户端:./zkCli.sh -server ip:port(如果连接本地Zookeeper,ip:port可省略)
./zkCli.sh
退出客户端
quit
ls / (查看znode目录结构)
help (帮助命令)
创建Znode(不同参数对应不同类型节点)【不加参数,默认持久节点】
存取设置数据
普通查询
get /test
ls /
ls /test (-R参数 递归查询)
查看节点详细信息
get -s /test (-s参数)
删除节点
1、普通删除
节点不为空时,直接用delete无法删除,此时可以用deleteall进行删除
2、乐观锁删除 (-v 参数)
需要与节点详细信息dataVersion对应,否则无法删除
权限设置
addauth digest xiaowang:123456
create /test-node abcd auth:xiaowang:123456:cdwra
在另⼀个会话中必须先使用账号密码(步骤一),才能拥有操作该节点的权限
Curator是Netflix公司开源的⼀套zookeeper客户端框架,Curator是对Zookeeper支持最好的客户端框架。Curator封装了⼤部分Zookeeper的功能,比如Leader选举、分布式锁等,减少了技术人员在使用Zookeeper时的底层细节开发⼯作。
<!--Curator-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
<!--Zookeeper-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.7.14</version>
</dependency>
curator:
retryCount: 5
elapsedTimeMs: 5000
connectString: 124.222.253.33:2181
sessionTimeoutMs: 60000
connectionTimeoutM: 5000
@Data
@Component
@ConfigurationProperties(prefix = "curator")
public class WrapperZK {
private int retryCount;
private int elapsedTimeMs;
private String connectString;
private int sessionTimeoutMs;
private int connectionTimeoutMs;
}
@Configuration
public class CuratorConfig {
@Autowired
WrapperZK wrapperZk;
@Bean(initMethod = "start")
public CuratorFramework curatorFramework() {
return CuratorFrameworkFactory.newClient(
wrapperZk.getConnectString(),
wrapperZk.getSessionTimeoutMs(),
wrapperZk.getConnectionTimeoutMs(),
new RetryNTimes(wrapperZk.getRetryCount(), wrapperZk.getElapsedTimeMs()));
}
}
@Autowired
CuratorFramework curatorFramework;
@Test
void createNode() throws Exception {
// 添加持久节点
String path = curatorFramework.create().forPath("/curator-node");
// 添加临时序号节点
String path1 = curatorFramework.create().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath("/curator-node", "some-data".getBytes());
System.out.printf("curator create node :%s successfully.%n", path);
System.in.read();
}
@Test
public void testGetData() throws Exception {
byte[] bytes = curatorFramework.getData().forPath("/curator-node");
System.out.println(new String(bytes));
}
@Test
public void testSetData() throws Exception {
curatorFramework.setData().forPath("/curator-node", "changed!".getBytes());
byte[] bytes = curatorFramework.getData().forPath("/curator-node");
System.out.println(new String(bytes));
}
@Test
public void testCreateWithParent() throws Exception {
String pathWithParent = "/node-parent/sub-node-1";
String path = curatorFramework.create().creatingParentsIfNeeded().forPath(pathWithParent);
System.out.printf("curator create node :%s successfully.%n", path);
}
@Test
public void testDelete() throws Exception {
String pathWithParent = "/node-parent";
curatorFramework.delete().guaranteed().deletingChildrenIfNeeded().forPath(pathWithParent);
}
读锁共享
】写锁独占
】如果用上述的上锁方式,只要有节点发⽣变化,就会触发其他节点的监听事件,这样的话对 zk的压力⾮常⼤,——羊群效应。可以调整成链式监听
。解决这个问题。
监听当前节点的上一个节点即可。
1)获取读锁
@Autowired
private CuratorFramework client;
@Test
void testGetReadLock() throws Exception {
// 读写锁
InterProcessReadWriteLock interProcessReadWriteLock = new InterProcessReadWriteLock(client, "/lock1");
// 获取读锁对象
InterProcessLock interProcessLock = interProcessReadWriteLock.readLock();
System.out.println("等待获取读锁对象!");
// 获取锁
interProcessLock.acquire();
for (int i = 1; i <= 100; i++) {
Thread.sleep(3000);
System.out.println(i);
}
// 释放锁
interProcessLock.release();
System.out.println("等待释放锁!");
}
2)获取写锁
@Test
void testGetWriteLock() throws Exception {
// 读写锁
InterProcessReadWriteLock interProcessReadWriteLock = new InterProcessReadWriteLock(client, "/lock1");
// 获取写锁对象
InterProcessLock interProcessLock = interProcessReadWriteLock.writeLock();
System.out.println("等待获取写锁对象!");
// 获取锁
interProcessLock.acquire();
for (int i = 1; i <= 100; i++) {
Thread.sleep(3000);
System.out.println(i);
}
// 释放锁
interProcessLock.release();
System.out.println("等待释放锁!");
}
可以把 Watch 理解成是注册在特定 Znode 上的触发器。当这个 Znode 发⽣改变,也就是调用了 create ,delete ,setData 方法的时候,将会触发 Znode 上注册的对应事件, 请求 Watch 的客户端会接收到异步通知。
客户端使用了NIO通信模式监听服务端的调用。
create /test xxx
get -w /test ⼀次性监听节点
ls -w /test 监听目录,创建和删除子节点会收到通知。子节点中新增节点不会收到通知
ls -R -w /test 对于子节点中子节点的变化,但内容的变化不会收到通知
get
-w
/test 监听节点 监听是一次性的(监听当前节点内容变化,无法监听子节点),每次获取数据时都带-w参数可以实现持续监听节点ls
/w
/test 监听节点目录变化,可以监听子节点(一级子节点)加上-R
参数可以递归监听所有子节点
@Test
public void addNodeListener() throws Exception {
NodeCache nodeCache = new NodeCache(curatorFramework, "/curator-node");
nodeCache.getListenable().addListener(new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
log.info("{} path nodeChanged: ", "/curator-node");
printNodeData();
}
});
nodeCache.start();
System.in.read();
}
public void printNodeData() throws Exception {
byte[] bytes = curatorFramework.getData().forPath("/curator-node");
log.info("data: {}", new String(bytes));
}
zookeeper集群中的节点有三种角色
搭建4个节点,其中⼀个节点为Observer
1)创建4个节点的myid,并设值
在/usr/local/zookeeper中创建以下四个文件
/usr/local/zookeeper/data/zk1# echo 1 > myid
/usr/local/zookeeper/data/zk2# echo 2 > myid
/usr/local/zookeeper/data/zk3# echo 3 > myid
/usr/local/zookeeper/data/zk4# echo 4 > myid
2)编写4个zoo.cfg
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# 修改对应的zk1 zk2 zk3 zk4
dataDir=/usr/local/zookeeper/data/zk1
# 修改对应的端⼝ 2181 2182 2183 2184
clientPort=2181
# 2001为集群通信端⼝,3001为集群选举端⼝,observer表示不参与集群选举
server.1=124.222.253.33:2001:3001
server.2=124.222.253.33:2002:3002
server.3=124.222.253.33:2003:3003
server.4=124.222.253.33:2004:3004:observer
# 上述配置,启动zk时,无法监听端口,可以加下面一行配置解决 (监听所有网卡)
quorumListenOnAllIPs=true
3)启动4台Zookeeper
./bin/zkServer.sh start ../conf/zoo1.cfg
./bin/zkServer.sh start ../conf/zoo2.cfg
./bin/zkServer.sh start ../conf/zoo3.cfg
./bin/zkServer.sh start ../conf/zoo4.cfg
查看集群状态
./bin/zkServer.sh status ../conf/zoo1.cfg
./bin/zkServer.sh status ../conf/zoo2.cfg
./bin/zkServer.sh status ../conf/zoo3.cfg
./bin/zkServer.sh status ../conf/zoo4.cfg
./bin/zkCli.sh -server 124.222.253.33:2181,124.222.253.33:2182,124.222.253.33:2183,124.222.253.33:2184
zookeeper作为非常重要的分布式协调组件,需要进行集群部署,集群中会以⼀主多从的形式进行部署。zookeeper为了保证数据的⼀致性,使用了ZAB(Zookeeper Atomic Broadcast 原子消息广播)协议,这个协议解决了Zookeeper的崩溃恢复和主从数据同步
的问题。
Zookeeper集群中的节点在上线时,将会进⼊到Looking状态,也就是选举Leader的状态,具体过程如下:
选票格式
myid | zXid
myid:myid文件配置数字
zXid:事务id,记录节点数据变化次数(增删改操作)
Leader建立完后,Leader周期性地不断向Follower发送心跳(ping命令,没有内容的 socket)。当Leader崩溃后,Follower发现socket通道已关闭,于是Follower开始进⼊到 Looking状态,重新回到上⼀节中的Leader选举过程,Leader选举期间集群不能对外提供服务。
一定是主节点写数据,client连接到从节点也是发给主节点写数据
半数以上
【相对于集群数量】(说明集群网络是没问题的)为了提高集群写数据的性能
NIO
BIO
2000 年 7月,加州大学伯克利分校的 Eric Brewer 教授在 ACM PODC 会议上提出 CAP 猜想。2年后,麻省理⼯学院的 Seth Gilbert 和 Nancy Lynch 从理论上证明了 CAP。之后, CAP理论正式成为分布式计算领域的公认定理。
CAP 理论为:⼀个分布式系统最多只能同时满足一致性(Consistency)、可⽤性 (Availability)和分区容错性(Partition tolerance)这三项中的两项
。
⼀致性指 “all nodes see the same data at the same time”,即更新操作成功并返回客户端完成后,所有节点在同⼀时间的数据完全⼀致。
可用性指“Reads and writes always succeed”,即服务⼀直可用,而且是正常响应时间。
分区容错性指“the system continues to operate despite arbitrary message loss or failure of part of the system”,即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足⼀致性或可⽤性的服务。——避免单点故障,就要进行冗余部署,冗余部署相当于是服务的分区,这样的分区就具备了容错性。
通过 CAP 理论,我们知道⽆法同时满足⼀致性、可用性和分区容错性这三个特性,那要舍弃哪个呢?
对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到 N 个 9,即保证 P 和 A,舍弃 C(退而求其次保证最终⼀致性)。虽然某些地方会影响客户体验,但没达到造成用户流失的严重程度。
对于涉及到钱财这样不能有⼀丝让步的场景,C 必须保证。网络发生故障宁可停止服务,这是保证 CA,舍弃 P。还有⼀种是保证 CP,舍弃 A。例如网络故障是只读不写。
孰优孰略,没有定论,只能根据场景定夺,适合的才是最好的!
eBay 的架构师 Dan Pritchett 源于对大规模分布式系统的实践总结,在 ACM 上发表⽂章提出 BASE 理论,BASE 理论是对 CAP 理论的延伸,核心思想是即使无法做到强⼀致性(Strong Consistency,CAP 的⼀致性就是强⼀致性),但应用可以采用适合的方式达到最终⼀致性 (Eventual Consitency)。
BA
基本可用指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。
S
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可⽤性。分布式存储中⼀般⼀份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication 的异步复制也是⼀种体现。
E
最终⼀致性是指系统中的所有数据副本经过⼀定时间后,最终能够达到⼀致的状态。弱⼀致性和强⼀致性相反,最终⼀致性是弱⼀致性的⼀种特殊情况。
Zookeeper在数据同步时,追求的并不是强⼀致性,而是顺序⼀致性(事务id的单调递增)。
zk追求CP原则,但也不是强一致性(集群半数以上ACK),而是顺序一致性。
Curator客户端相关代码:https://github.com/lkl778/zk.git
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。