美文网首页
dapeng-soa zk watch失效分析

dapeng-soa zk watch失效分析

作者: Ever_00 | 来源:发表于2018-11-14 00:54 被阅读0次
dapeng-soa

1. 背景

系统上线后,一直运行的很稳。 但随着时间的推移,逐渐暴露出一些问题:
1.1. 服务灰度上线后,zk上的服务路由没有同步到服务节点,导致灰度节点收到很多计划外的请求
1.2. 服务升级的时候,明明某服务节点已经从zk上摘除了, 可还是有些请求来到该节点,引发异常。

种种矛头指向了zookeeper的watch。莫非watch失效了?

2. 相关概念

zk session

每个zk 客户端都有个sessionId,用以表明该客户端在zkServer的唯一标识符。
zk客户端跟服务端断开的时候, 会带着这个sessionId去重连zk服务端。
在session 没有timeout之前连上的话, 那么SyncConnected事件会在客户端触发, 且该客户端所有的watcher都会自动重新注册上。
如果session timeout了,那么一方面服务端会清理该session注册的临时节点以及所有的watcher,且如果这时候,客户端带着已经超时的sessionId连接上来的话, 服务端会引发客户端一个Expired事件,然后该客户端的两个线程(io线程跟事件线程)会终止。客户端能做的就是关闭当前客户端,然后重新创建一个客户端再去连接zk。

session是否超时是有服务端判断的。

zk的线程

主要有两个,

  • IO线程(SendThread),负责跟zk服务端的通讯,包括数据收发以及心跳。
  • 事件线程(EventThread),事件产生的时候负责通知用户并回调用户的事件处理逻辑

zk的watcher

watcher有两种,

连接状态监控watcher

这种watcher是创建客户端的时候作为参数的,它负责管理zk客户端跟服务端的连接状态,一般的状态有synconnected, disconnected,expired,AuthFailed

zk数据监控watcher

监控zk上的目录或者节点变化

3. dapeng-soa zk 结构

dapeng-soa 使用zookeeper作为注册中心跟配置中心的核心组件。它包括如下几种节点:

3.1. 服务运行时信息

存放了服务的运行时信息,包括服务名,ip,端口, 版本号以及序号
其中,固定前缀+服务全限定名作为目录:

/soa/runtime/services/com.github.dapeng.service.DemoService

目录下存放具体的节点以及运行时信息:

192.168.10.126:9099:1.0.0:0000000300
192.168.20.133:9099:1.0.0:0000000304
192.168.10.130:9099:1.0.0:0000000305
192.168.20.102:9099:1.0.0:0000000303
192.168.10.131:9099:1.0.0:0000000302

服务运行时信息是zk临时节点,一旦某节点下线,则该节点信息从服务目录中消失。
序号作为服务节点选举master的依据。最早注册的服务节点序号最小,同时也是master节点

3.2. 服务配置信息

服务配置信息存放了dapeng-soa所需要的一些配置项,可通过命令行工具或者配置中心进行管理。
配置信息包括服务路由信息,服务限流配置信息以及其它配置信息(例如负载均衡策略、权重以及超时等)

3.2.1. 服务路由信息

目录路径:

/soa/config/routes

目录中存放各服务节点:

com.github.dapeng.service.DemoService
com.github.dapeng.service.OrderService

而路由信息作为data直接写在服务节点上。例如下面是订单服务的路由信息:

method match r"create.*" => ip"192.168.10.0/24"
cookie_storeId match %"10n+1..6" => ip"192.168.20.128"

详见dapeng-soa路由文档

3.2.2. 限流配置

目录路径:

/soa/config/freq

目录中存放各服务节点:

com.github.dapeng.service.DemoService
com.github.dapeng.service.OrderService

同样,限流信息作为data直接写在服务节点上。例如下面是订单服务的限流信息:

[rule1]
match_app = listOrder # 针对具体方法限流
rule_type = callerIp # 对每个请求端IP
min_interval = 60,5  # 每分钟请求数不超过5次
mid_interval = 3600,100 # 每小时请求数不超过100次
max_interval = 86400,200 # 每天请求数不超过200次

[rule2]
match_app = * # 针对服务限流
rule_type = callerIp # 对每个请求端IP
min_interval = 60,600  # 每分钟请求数不超过600
mid_interval = 3600,10000 # 每小时请求数不超过1万
max_interval = 86400,80000 # 每天请求数不超过8万

详见dapeng-soa限流文档

3.2.3. 服务白名单

只有在白名单中的服务,才对外(三方合作方)开放访问权限。一般应用在服务网关中(dapengMesh)。
目录路径:

/soa/whitelist/services/

目录下存放具体的白名单:

com.github.dapeng.service.DemoService
com.github.dapeng.service.OrderService

4. dapeng-soa zk 变迁史

我们先来了解一下dapeng-soa zk的几个重大重构。

4.1 阶段一 粗放型

新建某服务的客户端的时候,同步该服务在zk上的运行时信息到客户端,并对zk上的运行时目录以及服务配置目录保持同步(新建一个watch,通过watch来监听服务信息的变化)。由于zk的一次性监听机制,处理同步的watch在消费完zk事件后需再次注册。

该方案简单清晰, 但由于我们的服务客户端很轻,且我们鼓励无状态的、对GC友好的编码方式,我们建议应用在调用服务的时候新建客户端,用完丢弃,而不是预先创建客户端缓存在应用中并多次重用。
那么在客户端空闲甚至被gc后,应用无需再继续保持对服务的监听。尤其是集群内部服务很多的情况下,海量的无用的watch监听会让zk不堪重负。

4.2 阶段二 精细型

由于阶段一的粗放型方式会导致无用的zk watch,我们通过java的虚引用机制,让客户端被gc后,通知清理线程取消对zk的watch,从而有效减少了不必要的同步。

4.3 阶段三 watch重用

阶段二的想法是服务客户端被gc后,取消对zk的watch。然而在实际应用中,取消watch并没有直接的api,只能等监听的事件触发消费后,watch才会真正消失。 经过一段时间的观察,我们发现线上watch的数量越来越多,不加节制的创建watch以及watch难以快速真正取消,甚至造成了几次内存溢出。
其实watch本质上也是无状态的,只是对zk 事件的简单同步处理而已。理解了这点后,我们果断以服务为单位缓存watch,一个服务有且仅有一个watch。
经过这个处理之后,watch的数量得到了真正有效的控制, 再也没有出现内存溢出的情况了。

自从进入阶段三以来,原本以为系统进入了坚如磐石的阶段,直到出现本文开头背景介绍中的故障

5. zk watch失效分析

首先查了一下最后一次watch生效之后的日志,发现zk客户端跟服务端的连接短暂的断开了(网络抖动)。
我们对zk连接事件的处理如下:

    /**
     * 连接zookeeper
     */
    private void connect() {
        try {
            CountDownLatch semaphore = new CountDownLatch(1);

            // default watch
            zk = new ZooKeeper(zkHost, 30000, e -> {
                LOGGER.info("ClientZk::connect zkEvent:" + e);
                switch (e.getState()) {
                    case Expired:
                        LOGGER.info("Client's host: {} 到zookeeper Server的session过期,重连", zkHost);
                        zk.close();
                        zk = null;
                        connect();
                        break;
                    case SyncConnected:
                        semaphore.countDown();
                        LOGGER.info("Client's host: {}  已连接 zookeeper Server", zkHost);
                        config.clear();
                        break;
                    case Disconnected:
                        LOGGER.error("Client's host: {} 到zookeeper的连接被断开, do nothing.", zkHost);
                        zk.close();
                        zk = null;
                        connect();
                        break;
                    case AuthFailed:
                        LOGGER.error("Zookeeper connection auth failed ...");
                        zk.close();
                        zk = null;
                        break;
                    default:
                        break;
                }
            });
            semaphore.await(10000, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            LOGGER.info(e.getMessage(), e);
        }
    }

当收到zk连接给断开的事件后,首先会调用close方法主动断开跟zk的连接,然后置空zk客户端,最后重新创建zk客户端。
实际上这个Disconnected事件的处理逻辑是画蛇添足的,收到该事件后,zk客户端会自动重连zk并重新注册所有的watch。 而现在我们是把zk客户端关闭了,那么也就不会有自动重连并注册watch这个过程了。 然后这里做了主动重连。
需要注意的是,主动重连zk后,产生了一个新的zk客户端。 而我们的watch,是注册在老的zk客户端上的。 随着老zk客户端的消亡,这些watch也就失效了。

解决方案很简单, 不要处理Disconnected,同时在处理Expired事件的时候清理所有已存在的watch,关闭zk连接并重新创建zk客户端。

相关文章

网友评论

      本文标题:dapeng-soa zk watch失效分析

      本文链接:https://www.haomeiwen.com/subject/zgvrfqtx.html