zookeeper 实现分布式锁安全用法

阅读数:40 2019 年 11 月 6 日 18:25

zookeeper 实现分布式锁安全用法

分布式锁现在用的越来越多,通常用来协调多个并发任务。在一般的应用场景中存在一定的不安全用法,不安全用法会带来多个 master 在并行执行,业务或数据可能存在重复计算带来的副作用,在没有拿到 lock 的情况下扮演者 master 等诸如此类。

要想准确的拿到分布式锁,并且准确的捕获在分布式情况下锁的动态转移状态,需要处理网络变化带来的连锁反应。比如常见的 session expire、connectionLoss,在设置 lock 状态的时候我们如何保证准确拿到 lock。

在设计任务的时候我们需要具有 stop point 的策略,这个策略是用来在感知到 lock 丢失后能够交付执行权的机制。但是是否需要这么严肃的处理这个问题还取决于业务场景,比如下游的任务已经做好幂等也无所谓重复计算。 但是在有些情况下确实需要严肃精准控制。

ConnectionLoss 链接丢失

先说第一个场景,connectionLoss 事件,此事件表示提交的 commit 有可能执行成功也有可能执行失败,成功是指在 zookeeper broker 中执行成功但是返回的时候 tcp 断开了,导致未能拿到返回的状态。失败是指根本就没有提交到 zookeper broker 中链接就断开了。

所以在我们获取 lock 的时候需要做 connectionLoss 事件处理,我们看个例子。

复制代码
protected void runForMaster() {
logger.info("master:run for master.");
AsyncCallback.StringCallback createCallback =
(rc, path, ctx, name) -> {
switch (KeeperException.Code.get(rc)) {
case CONNECTIONLOSS:
checkMaster();// 链接失效检查 znode 设置是否成功
return;
case OK:
isLeader = true;
logger.info("master:I'm the leader serverId:" + serverId);
addMasterWatcher();// 监控 master znode
this.takeLeadership();// 执行 leader 权利
break;
case NODEEXISTS:
isLeader = false;
String serverId = this.getMasterServerId();
this.takeBackup(serverId);
break;
}
};
zk.create(rootPath + "/master", serverId.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL, createCallback, null);// 创建 master 节点
}
/**
* check master 循环检查
*/
private void checkMaster() {
AsyncCallback.DataCallback masterCheckCallback =
(rc, path, ctx, data[], stat) -> {
switch (KeeperException.Code.get(rc)) {
case CONNECTIONLOSS:
checkMaster();
return;
case NONODE:
runForMaster();
return;
default: {
String serverId = this.getMasterServerId();
isLeader = serverId.equals(this.serverId);
if (BooleanUtils.isNotTrue(isLeader)) {
this.takeBackup(serverId);
} else {
this.takeLeadership();
}
}
return;
}
};
zk.getData(masterZnode, false, masterCheckCallback, null);
}

这里的 master 表示具有执行权,只有成功拿到 master 角色才能履行 master 权利。

runForMaster 方法一旦发现有 connectionLoss 就发起 checkMaster 进行检查,同时 checkMaster 方法中也进行 connectinLoss 检查,直到拿到明确的状态为止。在此时有可能有另外的节点获取到了 master 角色,那么当前节点就做好 backup 等待机会。

我们需要捕获 zookeeper 所有的状态变化,要知道 master 什么时候失效做好申请准备,当自己是 master 时候会话失效需要释放 master 权利。

复制代码
/**
* 监控 master znode 做 master/slave 切换
*/
private void addMasterWatcher() {
AsyncCallback.StatCallback addMasterWatcher = (rc, path, ctx, stat) -> {
switch (KeeperException.Code.get(rc)) {
case CONNECTIONLOSS:
addMasterWatcher();
break;
case OK:
if (stat == null) {
runForMaster();//master 已经不存在
} else {
logger.info("master:watcher master znode ok.");
}
break;
case NONODE:
logger.info("master:master znode delete.");
runForMaster();
break;
}
};
zk.exists(masterZnode, MasterExistsWatcher, addMasterWatcher, null);
}

通过 zookeeper watcher 机制来进行状态监听,保持与网络、zookeeper 状态变化联动。

SessionExpired 会话过期

我们在来看第二个问题,第一个问题是获取 lock 的时候如何保证一定可以准确拿到状态,这里状态是指 master 角色或者 backup 角色。

当我们成功与 zookeeper broker 建立链接,成功获取到 master 角色并且正在履行 master 义务时突然 zookeeper 通知 session 过期,SessionExpired 事件表示 zookeeper 将会删除所有当前会话创建的临时 znode,也就意味这 master znode 将会被其他会话创建。

此时我们需要将自己的 master 权利交出去,也就是我们必须放下目前手上执行的任务,这个停止的状态必须能够反应到全局。此时最容易出现到问题就是,我们已经不是 master 了但是还在偷偷到执行 master 权利,通过 dashboard 会看到很奇怪的问题,不是 master 的服务器还在执行。

复制代码
case SESSIONEXPIRED:
// 执行 stop point 通知
this.stopPoint();
break;

所以这里需要我们在设计任务时有 stop point 策略,类似 jvm 的 safe point,随时响应全局停止。

绕开 zookeeper broker 进行状态通知

还有一种常见的使用方式是绕开 zookeeper 来做状态通知。

我们都知道 zookeeper cluster 是由多台实例组成,每个实例都在全国甚至全球的不同地方,leader 到这些节点之间都有很大的同步延迟差异,zookeeper 内部采用法定人数的两阶段提交的方式来完成一次 commit。

比如有 7 个实例构成一套 zookeeper cluster ,当一次 client 写入 commit 只需要集群中有超过半数完成写入就算这次 commit 提交成功了。但是 cleint 得到这个提交成功的响应之后立马执行接下来的任务,这个任务可能是读取某个 znode 下的所有状态数据,此时有可能无法读取到这个状态。

如果是分布式锁的话很有可能是锁在 zk 集群中的转移无法和 client 集群保持一直。所以只要是基于 zookeeper 做集群调度就要完全原来 zookeeper 来做状态通知,不可以绕开 zookeeper 来自行调度。

leader 选举与 zkNode 断开

zookeeper leader 是所有状态变更的串行化器,add、update、delete 都需要 leader 来处理,然后传播给所有 follower、observer 节点。

所有的 session 是保存在 leader 中的,所有的 watcher 是保存在 client 链接的 zookeper node 中的,这里两个场景都会导致状态迁移的通知不准时。

如果 zookeeper 是由多数据中心构成的一套集群,存在异地同步延迟的问题,leader 是肯定会放在写入的数据中心中,同时 zid 应该是最大的,甚至是一组高 zid 的机器都在写入的数据中心中,这样保证 leader 宕机也不会轻易导致 leader 选举到其他数据中心。

但是 follower、observer 都会有 client 在使用,也会有在这些节点进行协调的分布式集群。

先说 leader 选举导致异地节点延迟感知问题,比如当前 zookeeper cluster 有 7 台机器构成:

复制代码
dataCenter shanghai:zid=100、zid=80、zid=50
dataCenter beijing: zid=10、zid=20
dataCenter shenzhen:zid=30、zid=40

由于网络问题集群发生 leader 选举,zid=100 暂时脱离集群,zid=80 成为 leader,这里不考虑日志新旧问题,优先使用 zid 进行选举。

由于集群中所有的 session 是保存在原来 zid=100 的机器中的,新 leader 没有任何 session 信息,所以将导致所有 session 丢失。

session 的保持时间是取决于我们设置的 sessinoTimeout 时间来的,client 通过 ping 来将心跳传播到所链接的 zkNode,这个 zkNode 可能是任意角色的 node,然后 zkNode 在与 zkleaderNode 进行心跳来保持会话,同时 zkNode 也会通过 ping 来保持会话超时时间。

此时当原有当 client 在重新链接上 zkNode 时会被告知 sessionExpired。sessionExpired 是由 zkNode 通知出来的,当会话丢失或者过期,client 在去尝试链接 zkNode 时候会被 zkNode 告知会话过期。

如果 client 只捕获了 sessionExpired 显然会出现多个 master 运行情况,因为当你与 zkNode 断开到时候,当时还没有收到 sessionExpired 事件时,已经有另外 client 成功创建 master 拿到权利。

这种情况在 zkNode 出现脱离集群当时候也会出现,当 zkNode 断开之后也会出现 sessionExpired 延迟通知问题。所有的 watcher 都是需要在新的 zkNode 上创建才会收到新的事件。

静态扩容、动态扩容

在极端情况下静态扩容可能会导致 zookeeper 集群出现严重的数据不一致问题,比如现有集群:A、B、C,现在需要进行静态扩容,停止 ABC 实例,拉入 DE 实例,此时如果 C 实例是 ABC 中最滞后的实例,如果 AB 启动的速度没有 C 快就会导致 CDE 组成新的集群,新的纪元号会覆盖原来的 AB 日志。当然现在基本上不会接受静态扩容,基本上都是动态扩容。

动态扩容在极端情况下也会出现类似问题,比如现在有三个机房,1、2、3,1 机房方 leader zid=200、100,2 机房 zid=80、50,3 机房 zid=40,假设上次的 commit 是在 zid=200、100、50 之间提交的,此时机房 1 出现断网,2 机房 zid=80、50 与 3 机房 zid=40 开始组成新的集群,新的纪元在 zid=50 上产生。

做好幂等

在使用 zookeeper 来实现分布式锁或者集群调度的时候会出现很多分布式下的问题,为了保证这些问题的出现不会带来业务系统或者业务数据的不一致,我们还是在这些任务上做好幂等性考虑。

比如进行数据的计算,做个时间检查,版本检查之类的。如果本身是基于 zookeeper 实现的一套独立的分布式系统需要的工作会更多点。

作者介绍:
王清培,腾讯云 TVP,沪江资深应用架构师 、微软全球最有价值专家、畅销书《.NET 框架设计 - 模式、配置、工具》作者、图灵社区专家顾问团专家、51CTO 特邀讲师、云栖社区技术专家。 先后效力 美国新蛋网、携程、找钢网, 十年应用系统开发架构经验,在电商 、交易系统、营销平台有一定的积累。

本文转载自公众号云加社区(ID:QcloudCommunity)。

原文链接:

https://mp.weixin.qq.com/s/tly6H8XwXPJwvcQ3evV_tQ

评论

发布