连接假死

什么是连接假死呢?

如果底层的TCP连接已经断开,但是服务器端并没有正常地关闭套接字,服务器端认为这条TCP连接仍然是存在。

连接假死的具体表现如下:

(1)在服务器端,会有一些处于TCP_ESTABLISHED状态的“正常”连接。

(2)但在客户端,TCP客户端已经显示连接已经断开。

(3)客户端此时虽然可以进行断线重连操作,但是上一次的连接状态依然被服务器端认为有效,并且服务器端的资源得不到正确释放,包括套接字上下文以及接收/发送缓冲区。

连接假死通常是由以下多个原因造成的,例如:

(1)应用程序出现线程堵塞,无法进行数据的读写。

(2)网络相关的设备出现故障,例如网卡、机房故障。

(3)网络丢包。公网环境非常容易出现丢包和网络抖动等"

解决假死的有效手段是:客户端定时进行心跳检测,服务器端定时进行空闲检测。

  • 空闲检测:就是每隔一段时间,检测子通道是否有数据读写,如果有,则子通道是正常的;如果没有,则子通道被判定为假死,关掉子通道。
  • 使用Netty自带的IdleStateHandler空闲状态处理器就可以实现空闲检测功能。HeartBeatServerHandler实现的主要功能是空闲检测,需要客户端定时发送心跳数据包(或报文、消息)进行配合。而且客户端发送心跳数据包的时间间隔需要远远小于服务器端的空闲检测时间间隔;与服务器端的空闲检测相配合,客户端需要定期发送数据包到服务器端,通常这个数据包称为心跳数据包。

Zookeeper:

ZooKeeper节点数有以下要求:

(1)ZooKeeper集群节点数必须是奇数。需要一个主节点,也称为Leader节点。主节点是集群通过选举的规则从所有节点中选举出来的。在选举的规则中很重要的一条是:要求可用节点数量>总节点数量/2。如果是偶数个节点,则可能会出现不满足这个规则的情况。

(2)ZooKeeper集群至少是3个。安装集群的第一步是在安装目录下提前为每一个伪节点创建好两个目录:日志目录和数据目录。

安装ZooKeeper集群:

  • 第一步是在安装目录下提前为每一个伪节点创建好两个目录:日志目录和数据目录。
  • 第二步,为创建一个文件名为myid的id文件。myid文件的特点如下:
  1. ·myid文件的唯一作用是记录(伪)节点的编号。
  2. ·myid文件是一个文本文件,文件名称为myid。
  3. ·myid文件内容为一个数字,表示节点的编号。
  4. ·myid文件中只能有一个数字,不能有其他的内容。
  5. ·myid文件的存放位置,默认处于数据目录下。
  • 第三步,为每个节点创建配置文件。仿照zoo_sample.cfg文件配置。配置节点的时候,需要注意四点:
  1. 不能有相同id的节点,需要确保每个节点的myid文件中的id值不同;
  2. 每一行“server.id=host:port:port”中的id值,需要与所对应节点的数据目录下的myid文件中的id值保持一致;
  3. 每一个配置文件都需要配置全部的节点信息。不仅仅是配置自己的那份,还需要配置所有节点的id、ip、端口。
  4. 在每一行“server.id=host:port:port”中,需要配置两个端口。前一个端口用于节点之间的通信,后一个端口用于选举主节点。

验证集群的启动是否成功的两个方法:

  • 方法一,可以通过执行jps命令,我们可以看到QuorumPeerMain进程的数量。
  • 方法二,启动ZooKeeper客户端,运行并查看一下是否能连接集群。 如果最后显示出“CONNECTED”连接状态,则表示已经成功连接。

Windows下,ZooKeeper是通过.cmd命令文件中的批处理命令来运行的,默认没有提供Windows后台服务方案。为了避免每次关闭后,再一次启动ZooKeeper时还需要使用cmd带来的不便,可以通过prunsrv第三方工具来将ZooKeeper做成Windows服务,将ZooKeeper变成后台服务来运行。

ZooKeeper的存储模型非常简单,它和Linux的文件系统非常的类似。简单地说,ZooKeeper的存储模型是一棵以"/"为根节点的树。ZooKeeper的存储模型中的每一个节点,叫作ZNode(ZooKeeper Node)节点。通过ZNode树,ZooKeeper提供了一个多层级的树形命名空间。与文件的目录系统中的目录有所不同的是,这些ZNode节点可以保存二进制有效负载数据(Payload)。而文件系统目录树中的目录,只能存放路径信息,而不能存放负载数据。ZooKeeper为了保证高吞吐和低延迟,整个树形的目录结构全部都放在内存中。ZooKeeper官方的要求是,每个节点存放的有效负载数据(Payload)的上限仅为1MB。

ZooKeeper API:

  • ZooKeeper官方API有一些不足之处:
  1. ·ZooKeeper的Watcher监测是一次性的,每次触发之后都需要重新进行注册。
  2. ·会话超时之后没有实现重连机制。
  3. ·异常处理烦琐,ZooKeeper提供了很多异常,对于开发人员来说可能根本不知道应该如何处理这些抛出的异常。
  4. ·仅提供了简单的byte[]数组类型的接口,没有提供Java POJO级别的序列化数据处理接口。
  5. ·创建节点时如果抛出异常,需要自行检查节点是否存在。
  6. ·无法实现级联删除。                                              
  • ZkClient对原生API进行了封装,但也有它自身的不足之处:
  1. ·ZkClient社区不活跃,文档不够完善,几乎没有参考文档。
  2. ·异常处理简化(抛出RuntimeException)。
  3. ·重试机制比较难用。
  4. ·没有提供各种使用场景的参考实现。
  • curator-framework:
  1. curator-framework是对ZooKeeper的底层API的一些封装。
  2. ·curator-client提供了一些客户端的操作,例如重试策略等。
  3. ·curator-recipes封装了一些高级特性,如:Cache事件监听、选举、分布式锁、分布式计数器、分布式Barrier等。

curator-framework

在使用curator-framework包操作ZooKeeper前,首先要创建一个客户端实例——这是一个CuratorFramework类型的对象,有两种方法:

  • ·使用工厂类CuratorFrameworkFactory的静态newClient()方法。
  • ·使用工厂类CuratorFrameworkFactory的静态builder构造者方法。

创建客户端实例:

public class ClientFactory {
    /**
     * @param connectionString zk的连接地址
     * @return CuratorFramework 实例
     */
    public static CuratorFramework createSimple(String connectionString) {
        // 重试策略:第一次重试等待1s,第二次重试等待2s,第三次重试等待4s
        // 第一个参数:等待时间的基础单位,单位为毫秒
        // 第二个参数:最大重试次数
        ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
        // 获取 CuratorFramework 实例的最简单的方式
        // 第一个参数:zk的连接地址
        // 第二个参数:重试策略
  return CuratorFrameworkFactory.newClient(connectionString, retryPolicy);
    }
    /**
     * @param connectionString    zk的连接地址
     * @param retryPolicy         重试策略
     * @param connectionTimeoutMs 连接
     * @param sessionTimeoutMs
     * @return CuratorFramework 实例
     */
    public static CuratorFramework createWithOptions(String connectionString, RetryPolicy retryPolicy,
            int connectionTimeoutMs, int sessionTimeoutMs) {
        // builder 模式创建 CuratorFramework 实例
        return CuratorFrameworkFactory.builder()
                .connectString(connectionString)
                .retryPolicy(retryPolicy)
                .connectionTimeoutMs(connectionTimeoutMs)
                .sessionTimeoutMs(sessionTimeoutMs)
                // 其他的创建选项
                .build();
    }
}

通过Curator创建ZNode节点可以使用create()方法。create()方法不需要传入ZNode的节点路径,所以并不会立即创建节点,仅仅返回一个CreateBuilder构造者实例。通过该CreateBuilder构造者实例,可以设置创建节点时的一些行为参数,最后再通过构造者实例的forPath(String znodePath,byte[] payload)方法来完成真正的节点创建。

ZooKeeper节点有4种类型:

(1)PERSISTENT——持久化节点

(2)PERSISTENT_SEQUENTIAL——持久化顺序节点

(3)PHEMERAL——临时节

(4)EPHEMERAL_SEQUENTIAL——临时顺序节点

public class ZKclient {
    private CuratorFramework client;
    //Zk集群地址
    private static final String ZK_ADDRESS = "127.0.0.1:2181";
    public static ZKclient instance = null;
    static {
        instance = new ZKclient();
        instance.init();
    }
    private ZKclient() {
    }
    public void init() {
        if (null != client) {
            return;
        }
        //创建客户端
        client = ClientFactory.createSimple(ZK_ADDRESS);
        //启动客户端实例,连接服务器
        client.start();
    }
    public void destroy() {
        CloseableUtils.closeQuietly(client);
    }
    /**
     * 创建节点
     */
    public void createNode(String zkPath, String data) {
        try {
            // 创建一个 ZNode 节点
            // 节点的数据为 payload
            byte[] payload = "to set content".getBytes("UTF-8");
            if (data != null) {
                payload = data.getBytes("UTF-8");
            }
            client.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath(zkPath, payload);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    /**
     * 删除节点
     */
    public void deleteNode(String zkPath) {
        try {
            if (!isNodeExist(zkPath)) {
                return;
            }
            client.delete().forPath(zkPath);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 检查节点
     */
    public boolean isNodeExist(String zkPath) {
        try {
            Stat stat = client.checkExists().forPath(zkPath);
            if (null == stat) {
                log.info("节点不存在:", zkPath);
                return false;
            } else {
                log.info("节点存在 stat is:", stat.toString());
                return true;
            }
 
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
    /**
     * 创建 临时 顺序 节点
     */
    public String createEphemeralSeqNode(String srcpath) {
        try {
            // 创建一个 ZNode 节点
            String path = client.create().creatingParentsIfNeeded()
                    .withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(srcpath);
            return path;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

Curator更新节点

节点的更新分为同步更新与异步更新。同步更新就是更新线程是阻塞的,一直阻塞到更新操作执行完成为止。通过SetDataBuilder构造者实例的inBackground(AsyncCallback callback)方法,设置一个AsyncCallback回调实例。简简单单的一个函数,就将更新数据的行为从同步执行变成了异步执行。异步执行完成之后,SetDataBuilder构造者实例会再执行AsyncCallback实例的processResult(…)方法中的回调逻辑,即可完成更新后的其他操作。

public class CRUD {
    private static final String ZK_ADDRESS = "127.0.0.1:2181";
    /**
     * 检查节点
     */
    @Test
public void checkNode() {
        CuratorFramework client = ClientFactory.createSimple(ZK_ADDRESS); //创建客户端
        try {
            client.start();//启动客户端实例,连接服务器
            // 创建一个 ZNode 节点,节点的数据为 payload
            String zkPath = "/test/CRUD/remoteNode-1";
            Stat stat = client.checkExists().forPath(zkPath);
            if (null == stat) {
                log.info("节点不存在:", zkPath);
            } else {
                log.info("节点存在 stat is:", stat.toString());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            CloseableUtils.closeQuietly(client);
        }
    }
    /**
     * 创建节点  持久化节点、临时节点、持久化顺序节点
     */
    @Test
    public void createNode() {
        CuratorFramework client = ClientFactory.createSimple(ZK_ADDRESS); //创建客户端
        try {
            client.start();//启动客户端实例,连接服务器
            // 创建一个 ZNode 节点,节点的数据为 payload
            String data = "hello";
            byte[] payload = data.getBytes("UTF-8");
            String zkPath = "/test/CRUD/remoteNode-1";
     client.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT)
.forPath(zkPath, payload);
// client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL)
.forPath(zkPath, payload);
// client.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT_SEQUENTIAL)
.forPath(zkPath, payload);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            CloseableUtils.closeQuietly(client);
        }
    }
  
    /**
     * 读取节点
     */
    @Test
    public void readNode() {
        //创建客户端
        CuratorFramework client = ClientFactory.createSimple(ZK_ADDRESS);
        try {
            //启动客户端实例,连接服务器
            client.start();
            String zkPath = "/test/CRUD/remoteNode-1";
            Stat stat = client.checkExists().forPath(zkPath);
            if (null != stat) {
                //读取节点的数据
                byte[] payload = client.getData().forPath(zkPath);
                String data = new String(payload, "UTF-8");
                log.info("read data:", data);
                String parentPath = "/test";
                List<String> children = client.getChildren().forPath(parentPath);
                for (String child : children) {
                    log.info("child:", child);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            CloseableUtils.closeQuietly(client);
        }
    }
    /**
     * 更新节点
     */
    @Test
    public void updateNode() {
        //创建客户端
        CuratorFramework client = ClientFactory.createSimple(ZK_ADDRESS);
        try {
            //启动客户端实例,连接服务器
            client.start();
            String data = "hello world";
            byte[] payload = data.getBytes("UTF-8");
            String zkPath = "/test/remoteNode-1";
            client.setData().forPath(zkPath, payload);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            CloseableUtils.closeQuietly(client);
        }
    }
    /**
     * 更新节点 - 异步模式
     */
    @Test
    public void updateNodeAsync() {
        //创建客户端
        CuratorFramework client = ClientFactory.createSimple(ZK_ADDRESS);
        try {
            //更新完成监听器
            AsyncCallback.StringCallback callback = new AsyncCallback.StringCallback() {
                @Override
                public void processResult(int i, String s, Object o, String s1) {
                    System.out.println(
                            "i = " + i + " | " +
                                    "s = " + s + " | " +
                                    "o = " + o + " | " +
                                    "s1 = " + s1
                    );
                }
            };
            //启动客户端实例,连接服务器
            client.start();
            String data = "hello ,every body! ";
            byte[] payload = data.getBytes("UTF-8");
            String zkPath = "/test/CRUD/remoteNode-1";
            client.setData().inBackground(callback).forPath(zkPath, payload);
            Thread.sleep(10000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            CloseableUtils.closeQuietly(client);
        }
    }
    /**
     * 删除节点
     */
    @Test
    public void deleteNode() {
        //创建客户端
        CuratorFramework client = ClientFactory.createSimple(ZK_ADDRESS);
        try {
            //启动客户端实例,连接服务器
            client.start();
            //删除节点
            String zkPath = "/test/CRUD/remoteNode-1";
            client.delete().forPath(zkPath);
            //删除后查看结果
            String parentPath = "/test";
            List<String> children = client.getChildren().forPath(parentPath);
            for (String child : children) {
                log.info("child:", child);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            CloseableUtils.closeQuietly(client);
        }
    }
}

在Curator的API中,事件监听有两种模式:

  • 一种是标准的观察者模式;通过Watcher监听器实现。
  • 另一种是缓存监听模式;通过引入了一种本地缓存视图Cache机制去实现的。

Cache机制提供了反复注册的能力,而观察模式的Watcher监听器只能监听一次。在类型上,Watcher监听器比较简单,只有一种。Cache事件监听的种类有3种:Path Cache,Node Cache和Tree Cache。

  • 接口类型Watcher包含KeeperStateEventType两个内部枚举类,分别代表了通知状态事件类型
  1. 通知事件WatchedEvent实例的类型,WatchedEvent包含了三个基本属性:◘通知状态(keeperState)◘事件类型(EventType) ◘节点路径(path)
  2. WatchedEvent并不是从ZooKeeper集群直接传递过来的事件实例,而是Curator封装过的事件实例。WatchedEvent类型没有实现序列化接口java.io.Serializable,因此不能用于网络传输。从ZooKeeper服务器端直接通过网络传输传递过来的事件实例其实是一个WatcherEvent类型的实例,WatchedEvent传输实例和Curator的WatchedEvent封装实例,在名称上基本上一样,只有一个字母之差,而且功能也是一样的,都表示的是同一个服务器端事件。
  3. Watcher监听器是一次性的,如果要反复使用,需要反复地通过构造者的usingWatcher方法去提前进行注册。所以,Watcher监听器不适用于节点的数据频繁变动或者节点频繁变动这样的业务场景,而是适用于一些特殊的、变动不频繁的场景,例如会话超时、授权失败等这样的特殊场景。
  • Cache机制对ZooKeeper事件监听进行了封装,能够自动处理反复注册监听。

(1)Node Cache节点缓存可用于ZNode节点的监听;如果在监听的时候NodeCache监听的节点为空(也就是说ZNode路径不存在),也是可以的。之后,如果创建了对应的节点,也是会触发事件从而回调nodeChanged方法。

(2)Path Cache子节点缓存可用于ZNode的子节点的监听;PathChildrenCache子节点缓存用于子节点的监听,监控当前节点的子节点被创建、更新或者删除。

只能监听子节点,监听不到当前节点。

不能递归监听,子节点下的子节点不能递归监控。

Path Cache的事件类型具体如下:

·CHILD_ADDED对应于子节点的增加。

·CHILD_UPDATED对应子于节点的修改。

·CHILD_REMOVED对应子于节点的删除。

(3)Tree Cache树缓存是Path Cache的增强,不光能监听子节点,还能监听ZNode节点自身。TreeCacheEvent的事件类型,具体为:

·NODE_ADDED对应于节点的增加。

·NODE_UPDATED对应于节点的修改。

·NODE_REMOVED对应于节点的删除。

Curator监听的原理:无论是PathChildrenCache,还是TreeCache,所谓的监听都是在进行Curator本地缓存视图和ZooKeeper服务器远程的数据节点的对比,并且在进行数据同步时会触发相应的事件。