阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

ZooKeeper学习总结

530次阅读
没有评论

共计 23502 个字符,预计需要花费 59 分钟才能阅读完成。

1. 概述

Zookeeper 是 Hadoop 的一个子项目,它是分布式系统中的协调系统,可提供的服务主要有:配置服务、名字服务、分布式同步、组服务等。

它有如下的一些特点:

  • 简单

Zookeeper 的核心是一个精简的文件系统,它支持一些简单的操作和一些抽象操作,例如,排序和通知。

  • 丰富

        Zookeeper 的原语操作是很丰富的,可实现一些协调数据结构和协议。例如,分布式队列、分布式锁和一组同级别节点中的“领导者选举”。

  • 高可靠

Zookeeper 支持集群模式,可以很容易的解决单点故障问题。

  • 松耦合交互

不同进程间的交互不需要了解彼此,甚至可以不必同时存在,某进程在 zookeeper 中留下消息后,该进程结束后其它进程还可以读这条消息。

  • 资源库

        Zookeeper 实现了一个关于通用协调模式的开源共享存储库,能使开发者免于编写这类通用协议。

 

2. ZooKeeper 的安装

  • 独立模式安装

Zookeeper 的运行环境是需要 Java 的,建议安装 Oracle 的 java6.

可去官网下载一个稳定的版本,然后进行安装:http://zookeeper.apache.org/

解压后在 zookeeper 的 conf 目录下创建配置文件 zoo.cfg,里面的配置信息可参考统计目录下的 zoo_sample.cfg 文件,我们这里配置为:

tickTime=2000
initLimit=10
syncLimit=5
dataDir=/opt/zookeeper-data/
clientPort=2181

tickTime指定了 ZooKeeper 的基本时间单位(以毫秒为单位);

initLimit指定了启动 zookeeper 时,zookeeper 实例中的随从实例同步到领导实例的初始化连接时间限制,超出时间限制则连接失败(以 tickTime 为时间单位);

syncLimit指定了 zookeeper 正常运行时,主从节点之间同步数据的时间限制,若超过这个时间限制,那么随从实例将会被丢弃;

dataDirzookeeper 存放数据的目录;

clientPort用于连接客户端的端口。

  • 启动一个本地的 ZooKeeper实例
% zkServer.sh start

检查 ZooKeeper 是否正在运行

echo ruok | nc localhost 2181

若是正常运行的话会打印“imok”。

3. ZooKeeper 监控

  • 远程 JMX配置

默认情况下,zookeeper 是支持本地的 jmx 监控的。若需要远程监控 zookeeper,则需要进行进行如下配置。

默认的配置有这么一行:

ZOOMAIN="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=$JMXLOCALONLY org.apache.zookeeper.server.quorum.QuorumPeerMain"

咱们在 $JMXLOCALONLY 后边添加 jmx 的相关参数配置:

ZOOMAIN="-Dcom.sun.management.jmxremote
        -Dcom.sun.management.jmxremote.local.only=$JMXLOCALONLY
                -Djava.rmi.server.hostname=192.168.1.8
                -Dcom.sun.management.jmxremote.port=1911
                -Dcom.sun.management.jmxremote.ssl=false
                -Dcom.sun.management.jmxremote.authenticate=false
                 org.apache.zookeeper.server.quorum.QuorumPeerMain"

这样就可以远程监控了,可以用 jconsole.exe 或 jvisualvm.exe 等工具对其进行监控。

  • 身份验证

这里没有配置验证信息,如果需要请参见我的博文 jvisualvm 远程监控 tomcat:http://www.cnblogs.com/leocook/p/jvisualvmandtomcat.html

4. Zookeeper 的存储模型

Zookeeper 的数据存储采用的是结构化存储,结构化存储是没有文件和目录的概念,里边的目录和文件被抽象成了节点(node),zookeeper 里可以称为 znode。Znode 的层次结构如下图:

ZooKeeper 学习总结

最上边的是根目录,下边分别是不同级别的子目录。

5. Zookeeper 客户端的使用

  • zkCli.sh

可使用 ./zkCli.sh -server localhost 来连接到 Zookeeper 服务上。

使用 ls / 可查看根节点下有哪些子节点,可以双击 Tab 键查看更多命令。

  • Java客户端

可创建 org.apache.zookeeper.ZooKeeper 对象来作为 zk 的客户端,注意,java api 里创建 zk 客户端是异步的,为防止在客户端还未完成创建就被使用的情况,这里可以使用同步计时器,确保 zk 对象创建完成再被使用。

  • C客户端

可以使用 zhandle_t 指针来表示 zk 客户端,可用 zookeeper_init 方法来创建。可在 ZK_HOME\src\c\src\ cli.c 查看部分示例代码。

Ubuntu 14.04 安装分布式存储 Sheepdog+ZooKeeper  http://www.linuxidc.com/Linux/2014-12/110352.htm

CentOS 6 安装 sheepdog 虚拟机分布式储存  http://www.linuxidc.com/Linux/2013-08/89109.htm

ZooKeeper 集群配置 http://www.linuxidc.com/Linux/2013-06/86348.htm

使用 ZooKeeper 实现分布式共享锁 http://www.linuxidc.com/Linux/2013-06/85550.htm

分布式服务框架 ZooKeeper — 管理分布式环境中的数据 http://www.linuxidc.com/Linux/2013-06/85549.htm

ZooKeeper 集群环境搭建实践 http://www.linuxidc.com/Linux/2013-04/83562.htm

ZooKeeper 服务器集群环境配置实测 http://www.linuxidc.com/Linux/2013-04/83559.htm

ZooKeeper 集群安装 http://www.linuxidc.com/Linux/2012-10/72906.htm

Zookeeper3.4.6 的安装 http://www.linuxidc.com/Linux/2015-05/117697.htm

6. Zookeeper 创建 Znode

Znode 有两种类型:短暂的和持久的。短暂的 znode 在创建的客户端与服务器端断开(无论是明确的断开还是故障断开)连接时,该 znode 都会被删除;相反,持久的 znode 则不会。

public class CreateGroup implements Watcher{private static final int SESSION_TIMEOUT = 1000;//会话延时

    private ZooKeeper zk = null;
    private CountDownLatch countDownLatch = new CountDownLatch(1);//同步计数器

    public void process(WatchedEvent event) {if(event.getState() == KeeperState.SyncConnected){countDownLatch.countDown();//计数器减一
        }
    }

    /**
     * 创建 zk 对象
     * 当客户端连接上 zookeeper 时会执行 process(event)里的 countDownLatch.countDown(),计数器的值变为 0,则 countDownLatch.await()方法返回。* @param hosts
     * @throws IOException
     * @throws InterruptedException
     */
    public void connect(String hosts) throws IOException, InterruptedException {zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this);
        countDownLatch.await();//阻塞程序继续执行
    }
    
    /**
     * 创建 group
     * 
     * @param groupName 组名
     * @throws KeeperException
     * @throws InterruptedException
     */
    public void create(String groupName) throws KeeperException, InterruptedException {String path = "/" + groupName;
        String createPath = zk.create(path, null, Ids.OPEN_ACL_UNSAFE/*允许任何客户端对该 znode 进行读写 */, CreateMode.PERSISTENT/* 持久化的 znode*/);
        System.out.println("Created" + createPath);
    }
    
    /**
     * 关闭 zk
     * @throws InterruptedException
     */
    public void close() throws InterruptedException {if(zk != null){try {zk.close();
            } catch (InterruptedException e) {throw e;
            }finally{zk = null;
                System.gc();}
        }
    }
}

这里我们使用了同步计数器 CountDownLatch,在 connect 方法中创建执行了 zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this); 之后,下边接着调用了 CountDownLatch 对象的 await 方法阻塞,因为这是 zk 客户端不一定已经完成了与服务端的连接,在客户端连接到服务端时会触发观察者调用 process()方法,我们在方法里边判断一下触发事件的类型,完成连接后计数器减一,connect 方法中解除阻塞。

还有两个地方需要注意:这里创建的 znode 的访问权限是 open 的,且该 znode 是持久化存储的。

测试类如下:

public class CreateGroupTest {private static String hosts = "192.168.1.8";
    private static String groupName = "zoo";
    
    private CreateGroup createGroup = null;
    
    /**
     * init
     * @throws InterruptedException 
     * @throws KeeperException 
     * @throws IOException 
     */
    @Before
    public void init() throws KeeperException, InterruptedException, IOException {createGroup = new CreateGroup();
        createGroup.connect(hosts);
    }
    
    @Test
    public void testCreateGroup() throws KeeperException, InterruptedException {createGroup.create(groupName);
    }
    
    /**
     * 销毁资源
     */
    @After
    public void destroy() {try {createGroup.close();
            createGroup = null;
            System.gc();} catch (InterruptedException e) {e.printStackTrace();
        }
    }
}

由于 zk 对象的创建和销毁代码是可以复用的,所以这里我们把它分装成了接口:

/**
 * 连接的观察者,封装了 zk 的创建等
 * @author leo
 *
 */
public class ConnectionWatcher implements Watcher {private static final int SESSION_TIMEOUT = 5000;

    protected ZooKeeper zk = null;
    private CountDownLatch countDownLatch = new CountDownLatch(1);

    public void process(WatchedEvent event) {KeeperState state = event.getState();
        
        if(state == KeeperState.SyncConnected){countDownLatch.countDown();
        }
    }
    
    /**
     * 连接资源
     * @param hosts
     * @throws IOException
     * @throws InterruptedException
     */
    public void connection(String hosts) throws IOException, InterruptedException {zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this);
        countDownLatch.await();}
    
    /**
     * 释放资源
     * @throws InterruptedException
     */
    public void close() throws InterruptedException {if (null != zk) {try {zk.close();
            } catch (InterruptedException e) {throw e;
            }finally{zk = null;
                System.gc();}
        }
    }
}

7. Zookeeper 删除 Znode

/**
 * 删除分组
 * @author leo
 *
 */
public class DeleteGroup extends ConnectionWatcher {public void delete(String groupName) {String path = "/" + groupName;
        
        try {List<String> children = zk.getChildren(path, false);
            
            for(String child : children){zk.delete(path + "/" + child, -1);
            }
            zk.delete(path, -1);//版本号为 -1,
        } catch (KeeperException e) {e.printStackTrace();
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }
}

zk.delete(path,version)方法的第二个参数是 znode 版本号,如果提供的版本号和 znode 版本号一致才会删除这个 znode,这样可以检测出对 znode 的修改冲突。通过将版本号设置为 -1,可以绕过这个版本检测机制,无论 znode 的版本号是什么,都会直接将其删除。

测试类:

 
public class DeleteGroupTest {private static final String HOSTS = "192.168.1.137";
    private static final String groupName = "zoo";
    
    private DeleteGroup deleteGroup = null;
    
    @Before
    public void init() throws IOException, InterruptedException {deleteGroup = new DeleteGroup();
        deleteGroup.connection(HOSTS);
    }
    
    @Test
    public void testDelete() throws IOException, InterruptedException, KeeperException {deleteGroup.delete(groupName);
    }
    
    @After
    public void destroy() throws InterruptedException {if(null != deleteGroup){try {deleteGroup.close();
            } catch (InterruptedException e) {throw e;
            }finally{deleteGroup = null;
                System.gc();}
        }
    }
}

8. Zookeeper 的相关操作

ZooKeeper 中共有 9 中操作:

create:创建一个 znode

delete:删除一个 znode

exists:测试一个 znode

getACL,setACL:获取 / 设置一个 znode 的 ACL(权限控制)

getChildren:获取一个 znode 的子节点

getData,setData:获取 / 设置一个 znode 所保存的数据

sync:将客户端的 znode 视图与 ZooKeeper 同步

这里更新数据是必须要提供 znode 的版本号(也可以使用 - 1 强制更新,这里可以执行前通过 exists 方法拿到 znode 的元数据 Stat 对象,然后从 Stat 对象中拿到对应的版本号信息),如果版本号不匹配,则更新会失败。因此一个更新失败的客户端可以尝试是否重试或执行其它操作。

9. ZooKeeper 的 API

ZooKeeper 的 api 支持多种语言,在操作时可以选择使用同步 api 还是异步 api。同步 api 一般是直接返回结果,异步 api 一般是通过回调来传送执行结果的,一般方法中有某参数是类 AsyncCallback 的内部接口,那么该方法应该就是异步调用,回调方法名为 processResult。

10. 观察触发器

可以对客户端和服务器端之间的连接设置观察触发器(后边称之为 zookeeper 的状态观察触发器),也可以对 znode 设置观察触发器。

  • 状态观察器

zk 的整个生命周期如下:

ZooKeeper 学习总结

可在创建 zk 对象时传入一个观察器,在完成 CONNECTING 状态到 CONNECTED 状态时,观察器会触发一个事件,该触发的事件类型为 NONE,通过 event.getState()方法拿到事件状态为 SyncConnected。有一点需要注意的就是,在 zk 调用 close 方法时不会触发任何事件,因为这类的显示调用是开发者主动执行的,属于可控的,不用使用事件通知来告知程序。这一块在下篇博文还会详细解说。

  • 设置 znode的观察器

可以在读操作 exists、getChildren 和 getData 上设置观察,在执行写操作 create、delete 和 setData 将会触发观察事件,当然,在执行写的操作时,也可以选择是否触发 znode 上设置的观察器,具体可查看相关的 api。

当观察的 znode被创建、删除或其数据被更新时,设置在 exists 上的观察将会被触发;

当观察的 znode被删除或数据被更新时,设置在 getData 上的观察将会被触发;

当观察的 znode的子节点被创建、删除或 znode自身被删除时,设置在 getChildren 上的观察将会被触发,可通过观察事件的类型来判断被删除的是 znode 还是它的子节点。

ZooKeeper 学习总结

对于 NodeCreatedNodeDeleted根据路径就能发现是哪个 znode 被写;对于 NodeChildrenChanged 可根据 getChildren 来获取新的子节点列表。

注意:在收到收到触发事件到执行读操作之间,znode 的状态可能会发生状态,这点需要牢记。

至此,编写简单的 zookeeper 应该是可以的了,下篇博文咱们来深入探讨 zookeeper 的相关知识。

参考地址:http://zookeeper.apache.org/doc/r3.4.6/

参考书籍:《Hadoop 权威指南》

Hadoop 权威指南(第 3 版) 修订版(带目录书签) 中文 PDF 高清晰 下载见 http://www.linuxidc.com/Linux/2016-03/129542.htm

更多详情见请继续阅读下一页的精彩内容:http://www.linuxidc.com/Linux/2016-07/133179p2.htm

其实 zookeeper 系列的学习总结很早就写完了,这段时间在准备找工作的事情,就一直没有更新了。下边给大家送上,文中如有不恰当的地方,欢迎给予指证,不胜感谢!。

1. 数据模型

1.1. 只适合存储小数据

Zk 维护着一个逻辑上的树形层次结构,树中的节点称为 znode,个 znode 都有一个 ACL(权限控制)。Zookeeper 是被设计用来协调服务的,因此 znode 里存储的都是小数据,而不是大容量的数据,数据容量一般在 1MB 范围内。

1.2. 操作的原子性

Znode 的数据读写是原子的,要么读或写了完整的数据,要么就失败,不会出现只读或写了部分数据。

1.3. Znode 的路径

和 Unix 中的文件系统路径格式很想,但是只支持绝对路径,不支持相对路径,也不支持点号(”.”和”..”)。

1.4. 短暂的 znode 和持久的 znode

Znode 有两种类型:短暂的和持久的。短暂的 znode 生命周期仅限创建它的客户端与服务器端之间的连接没有断开,客户端断开连接后,znode 将会被删除。

1.5. 顺序 znode

名称中包含 Zookeeper 指定顺序号的 znode。若在创建 znode 时设置了顺序标识,那么该 znode 被创建后,名字的后边将会附加一串数字,该数字是由一个单调递增的计数器来生成的。例如,创建节点时传入的 path 是”/aa/bb”,创建后的则可能是”/aa/bb0002”,再次创建后是”/aa/bb0003”。

Znode 的创建模式 CreateMode 有四种,分别是:EPHEMERAL(短暂的 znode)、EPHEMERAL_SEQUENTIAL(短暂的顺序 znode)、PERSISTENT(持久的 znode)和PERSISTENT_SEQUENTIAL(持久的顺序 znode)。如果您已经看过了上篇博文,那么这里的 api 调用应该是很好理解的,见:http://www.cnblogs.com/leocook/p/zk_0.html。

1.6. 观察

这部分在上篇博文中已经做了详细的说明,包括连接的观察和 znode 的观察, 这部分在构建一个稳定的 zookeeper 应用中有着很重要的作用,具体会在下边说到。

2.  ACL

即:Access Control List(访问控制列表)。Znode 被创建时带有一个 ACL 列表,zk 提供了下边 三种身份验证模式

  • digest

用户名 + 密码验证。

  • host

客户端主机名 hostname 验证。

  • ip

客户端的 IP 验证。

  • auth

使用 sessionID 验证

  • world

无验证,默认是无任何权限。该模式较为特殊,在 zk连接添加 ACL中会说到

ACL 权限对应如下表:

ZooKeeper 学习总结

在设置 ACL 时,可以给 zk 客户端和服务器端的连接设置 ACL,也可以在创建 znode 时,给 znode 设置 ACL,在创建了 znode 后,如果有 zk 客户端来操作 znode,只有满足权限要求时,才能完成相对应的操作:

2.1. 给 ZK 连接添加 ACL

可使用 zk 对象的 addAuthInfo()方法来添加验证模式,如使用 digest 模式进行身份验证:zk.addAuthInfo(“digest”,”username:passwd”.getBytes());

在 zookeeper 对象被创建时,初始化会被添加 world 验证模式。world 身份验证模式的验证 id 是”anyone”。

若该连接创建了 znode,那么他将会被添加 auth 身份验证模式的验证 id 是””,即空字符串,这里将使用 sessionID 进行验证。

2.2. 给 znode 设置 ACL

  • 自己创建 ACL

创建 ACL 对象时,可用 ACL 类的构造方法 ACL(int perms, Id id)

其中 参数 perms 表示权限,在接口 org.apache.zookeeper.ZooDefs.Perms 中有相关的常量:READ、WRITE、CREATE、DELETE、ALL 和 ADMIN,它们值如下表:

ZooKeeper 学习总结

Id参数是验证模式 ,可用构造方法 Id(String scheme, String id) 来创建。参数 scheme 是验证模式,digest、host 或 ip,id 是对应的验证,digest 对应用户名和密码对,如“user:passwd”;host 对应主机名,如”localhost”;ip 对应 ip 地址,如”192.168.1.120”。

  • 使用 api中预设的 ACL

在创建 znode 时可以设置该 znode 的 ACL 列表。接口 org.apache.zookeeper.ZooDefs.Ids 中有一些已经设置好的权限常量,例如:

(1)、OPEN_ACL_UNSAFE:完全开放。事实上这里是采用了 world 验证模式,由于每个 zk 连接都有 world 验证模式,所以 znode 在设置了 OPEN_ACL_UNSAFE 时,是对所有的连接开放。

(2)、CREATOR_ALL_ACL:给创建该 znode 连接所有权限。事实上这里是采用了 auth 验证模式,使用 sessionID 做验证。所以设置了 CREATOR_ALL_ACL 时,创建该 znode 的连接可以对该 znode 做任何修改。

(3)、READ_ACL_UNSAFE:所有的客户端都可读。事实上这里是采用了 world 验证模式,由于每个 zk 连接都有 world 验证模式,所以 znode 在设置了 READ_ACL_UNSAFE 时,所有的连接都可以读该 znode。

注:红色部分是本人阅读源码的一些研究,auth 和 world 的相关描述经供参考。

3. 运行模式

Zookeeper 有两种运行模式:独立模式(standalone mode)和复制模式(replicated mode)。

3.1. 独立模式

只有一个 zookeeper 服务实例,不可保证高可靠性和恢复性,可在测试环境中使用,生产环境不建议使用。

3.2. 复制模式

复制模式也就是集群模式,有多个 zookeeper 实例在运行,建议多个 zk 实例是在不同的服务器上。集群中不同 zookeeper 实例之间数据不停的同步。有半数以上的实例保持正常运行,zk 服务就能正常运行,例如:有 5 个 zk 实例,挂了 2 个,还剩 3 个,依然可以正常工作;如有 6 个 zk 实例,挂了 3 个,则不能正常工作。

每个 znode 的修改都会被复制到超过半数的机器上,这样就会保证至少有一台机器会保存最新的状态,其余的副本最终都会跟新到这个状态。Zookeeper 为实现这个功能,使用了 Zab 协议,该协议有两个可以无限重复的阶段:

  • 选举领导

集群中所有的 zk 实例会选举出来一个“领导实例”(leader),其它实例称之为“随从实例”(follower)。如果 leader 出现故障,其余的实例会选出一台 leader,并一起提供服务,若之前的 leader 恢复正常,便成为 follower。选举 follower 是一个很快的过程,性能影响不明显。

Leader 主要功能是协调所有实例实现写操作的原子性,即:所有的写操作都会转发给 leader,然后 leader 会将更新广播给所有的 follower,当半数以上的实例都完成写操作后,leader 才会提交这个写操作,随后客户端会收到写操作执行成功的响应。

  • 原子广播

上边已经说到:所有的写操作都会转发给 leader,然后 leader 会将更新广播给所有的 follower,当半数以上的实例都完成写操作后,leader 才会提交这个写操作,随后客户端会收到写操作执行成功的响应。这么来的话,就实现了客户端的写操作的原子性,每个写操作要么成功要么失败。逻辑和数据库的两阶段提交协议很像。

3.3. 复制模式下的数据一致性

Znode 的每次写操作都相当于数据库里的一次事务提交,每个写操作都有个全局唯一的 ID,称为:zxid(ZooKeeper Transaction)。ZooKeeper 会根据写操作的 zxid 大小来对操作进行排序,zxid 小的操作会先执行。zk 下边的这些特性保证了它的数据一致性:

  • 顺序一致性

任意客户端的写操作都会按其发送的顺序被提交。如果一个客户端把某 znode 的值改为 a,然后又把值改为 b(后面没有其它任何修改),那么任何客户端在读到值为 b 之后都不会再读到 a。

  • 原子性

这一点再前面已经说了,写操作只有成功和失败两种状态,不存在只写了百分之多少这么一说。

  • 单一系统映像

客户端只会连接 host 列表中状态最新的那些实例。如果正在连接到的实例挂了,客户端会尝试重新连接到集群中的其他实例,那么此时滞后于故障实例的其它实例都不会接收该连接请求,只有和故障实例版本相同或更新的实例才接收该连接请求。

  • 持久性

写操作完成之后将会被持久化存储,不受服务器故障影响。

  • 及时性

在对某个 znode 进行读操作时,应该先执行 sync 方法,使得读操作的连接所连的 zk 实例能与 leader 进行同步,从而能读到最新的类容。

注意:sync调用是异步的,无需等待调用的返回,zk服务器会保证所有后续的操作会在 sync 操作完成之后才执行,哪怕这些操作是在执行 sync 之前被提交的。

4. 提高 ZooKeeper 应用的容错

分布式环境是很复杂的,网络的不可靠、单点故障等问题都是经常发生的。那么在构建一个分布式应用程序时,这些问题都是需要慎重考虑的。因此,如何构建一个可复原的分布式应用将成为一个值得讨论的话题。Java api 中每个异常都对应一类故障模式,下边我们将会以 Java api 中的异常为例来讨论 ZooKeeper 应用程序中可能会出现的一些故障。

4.1. Java API 中的一些常见异常

  • InterruptedException异常

若客户端的某操作被中断,则会抛出 InterruptedException 异常。抛出该异常时,不一定是出现故��,只能表明某个 zookeeper 操作被中断而已。

  • KeeperException异常

服务器发出错误信号或是服务器存在通信故障。该类现在共有 21 个子类,分为 3 大类:

 (1)、状态异常

当一个客户端对 zk 的某操作失败时,就会出现状态异常。例如:更新数据时所指定的版本号不正确就会抛出异常 BadVersionException、若在短暂的 znode 下创建子节点则会抛出异常 NoChildrenForEphemeralsException。

(2)、可恢复的异常

那些在 zk 会话中可以恢复的异常叫可恢复的异常。当丢失 zk 连接时就会抛出异常 ConnectionLossException,这时 zk 会自动尝试重新连接,以确保会话的完整性。Zk 无法判断 ConnectionLossException 异常相关的操作是否成功执行,有可能出现只完成部分,那么是否重新执行刚才的操作就得知道该操作是否是等幂的。

等幂操作是指一次或多次执行都会产生相同结果的操作;非等幂操作是指一次或多次执行会产生不相同结果的操作。非等幂操作就不能盲目操作了。

写操作里有创建、删除、修改。在一个分布式环境中,删除 zk 里的 znode 或是修改 znode 的数据是等幂的,只有创建 znode 可能不是等幂的,创建顺序 znode 就是一个非等幂的操作。

那么怎么样才能避免创建顺序 znode 不会出现重复创建呢?下面我来展开讨论:

假设的场景:

客户端:客户端任务是在连接到 zk 服务端时会只创建一个顺序 znode;

ConnectionLossException抛出 ConnectionLossException 异常重新连接后会话没有失效,但是 zk 无法判断创建 znode 的操作是否成功。

我们知道顺序 znode 的节点名称格式是形如”znodeName<sequentialNumber>”,zk 客户端和服务器端的会话有个全局唯一的 sessionID,我们可以把 sessionID 加入 znode 的名称中,形如:” znodeName<sessionID><sequentialNumber>”,sequentialNumber 是相对于父 znode 唯一的。这样我们在创建某个 znode 之前先判断一下父 znode 下有无名称形如” znodeName<sessionID>“这样字符开头的子 znode,就能确保每个客户端连接只创建一个 znode。

这种场景会在什么时候会遇到呢?在我们要实现一个分布式锁的时候,核心思想之一就在这里。那么问题来了,什么是分布式锁呢?后面会有独立的博文来讲解关于它的代码实现。

(3)、不可恢复的异常

不可恢复的异常发生时,所有的短暂 znode 都将会丢失,只有程序中显示的重建 zk 连接,并重建 znode 的状态。例如:会话过期会抛出异常 SessionExpiredException,身份验证失败会抛出异常 AuthFailedException。

(4)、异常捕捉处理

每个子类对应一种异常状态,且每个子类都对应一个关于错误类型的信息代码,可以通过 code 方法拿到。处理该种异常有两种办法:

1、通过检测错误代码(可调用 code 方法老获取)来确定是哪种异常,再决定应该采取何种补救措施;

2、通过追捕等价的 KeeperException 异常,然后再每段捕捉代码中执行相应的操作。

4.2. 构建可靠的 zookeeper 应用

上面说到了 zk 服务器端可能出现一些网络故障或单点故障登,那么怎么编写一个可靠的 zk 客户端程序来应对可能不稳定的 zk 实例呢?这里我们向一个 znode 写数据为例,来实现它:

/**
 * 显示配置
 * @throws KeeperException 服务器发出错误信号或是服务器存在通信故障。该类现在共有 21 个子类,* 分为 3 大类:<br/>
 * 1、状态异常(如:BadVersionException、NoChildrenForEphemeralsException);* 2、可恢复的异常(如:ConnectionLossException);* 3、不可恢复的异常(如:SessionExpiredException、AuthFailedException)。* 每个子类对应一种异常状态,且每个子类都对应一个关于错误类型的信息代码,可以通过 code 方法拿到。* 处理该种异常有两种办法:<br/>
 * 1、通过 <b> 检测错误代码 </b> 来决定应该采取何种补救措施;<br/>
 * 2、通过 <b> 追捕等价的 KeeperException 异常 </b>,然后再每段捕捉代码中执行相应的操作。* @throws InterruptedException zookeeper 操作被中断。<b> 并不一定就是出现故障,只能表明相对应的操作被取消 </b>。*/
public static void write(String path, String value) throws KeeperException, InterruptedException {int retries = 0;
    
    while (true) {try {Stat stat = zk.exists(path, false);
            if(stat == null){zk.create(path, value.getBytes(CHARSET), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }else {zk.setData(path, value.getBytes(CHARSET), -1);
            }
            break;
        } catch(KeeperException.SessionExpiredException e){//TODO 此处会话过期,抛出异常,由上层调用来重新创建 zookeeper 对象
            throw e;
        }catch(KeeperException.AuthFailedException e){//TODO 此处身份验证时,抛出异常,由上层来终止程序运行
            throw e;
        }catch (KeeperException e) {//检查有没有超出尝试的次数
            if(retries == MAXRETRIES){throw e;
            }
            retries++;
            TimeUnit.SECONDS.sleep(RETRY_PERIOD_SECONDS);
        }
    }
}

如果您是一名 Java 开发人员,那么我觉得上面的这些代码没什么好解释的了。下边看上层调用是怎么处理的:

int flag = 0;

while (true) {try {write(path, value);
        break;
    } catch (KeeperException.SessionExpiredException e) {// TODO: 重新创建、开始一个新的会话
        e.printStackTrace();
        zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this);
    } catch (KeeperException e) {// TODO 尝试了多次,还是出错,只有退出了
        e.printStackTrace();
        flag = 1;
        break;
    }catch(KeeperException.AuthFailedException e){//TODO 此处身份验证时,终止程序运行
        e.printStackTrace();
        flag = 1;
        break;
    } catch (IOException e) {// TODO 创建 zookeeper 对象失败,无法连接到 zk 集群
        e.printStackTrace();
        flag = 1;
        break;
    } 
}

System.exit(flag);

关于编写一个可恢复的 zookeeper 应用,这一块理解了,其它地方应该就是触类旁通了。

本文永久更新链接地址:http://www.linuxidc.com/Linux/2016-07/133179.htm

1. 概述

Zookeeper 是 Hadoop 的一个子项目,它是分布式系统中的协调系统,可提供的服务主要有:配置服务、名字服务、分布式同步、组服务等。

它有如下的一些特点:

  • 简单

Zookeeper 的核心是一个精简的文件系统,它支持一些简单的操作和一些抽象操作,例如,排序和通知。

  • 丰富

        Zookeeper 的原语操作是很丰富的,可实现一些协调数据结构和协议。例如,分布式队列、分布式锁和一组同级别节点中的“领导者选举”。

  • 高可靠

Zookeeper 支持集群模式,可以很容易的解决单点故障问题。

  • 松耦合交互

不同进程间的交互不需要了解彼此,甚至可以不必同时存在,某进程在 zookeeper 中留下消息后,该进程结束后其它进程还可以读这条消息。

  • 资源库

        Zookeeper 实现了一个关于通用协调模式的开源共享存储库,能使开发者免于编写这类通用协议。

 

2. ZooKeeper 的安装

  • 独立模式安装

Zookeeper 的运行环境是需要 Java 的,建议安装 Oracle 的 java6.

可去官网下载一个稳定的版本,然后进行安装:http://zookeeper.apache.org/

解压后在 zookeeper 的 conf 目录下创建配置文件 zoo.cfg,里面的配置信息可参考统计目录下的 zoo_sample.cfg 文件,我们这里配置为:

tickTime=2000
initLimit=10
syncLimit=5
dataDir=/opt/zookeeper-data/
clientPort=2181

tickTime指定了 ZooKeeper 的基本时间单位(以毫秒为单位);

initLimit指定了启动 zookeeper 时,zookeeper 实例中的随从实例同步到领导实例的初始化连接时间限制,超出时间限制则连接失败(以 tickTime 为时间单位);

syncLimit指定了 zookeeper 正常运行时,主从节点之间同步数据的时间限制,若超过这个时间限制,那么随从实例将会被丢弃;

dataDirzookeeper 存放数据的目录;

clientPort用于连接客户端的端口。

  • 启动一个本地的 ZooKeeper实例
% zkServer.sh start

检查 ZooKeeper 是否正在运行

echo ruok | nc localhost 2181

若是正常运行的话会打印“imok”。

3. ZooKeeper 监控

  • 远程 JMX配置

默认情况下,zookeeper 是支持本地的 jmx 监控的。若需要远程监控 zookeeper,则需要进行进行如下配置。

默认的配置有这么一行:

ZOOMAIN="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=$JMXLOCALONLY org.apache.zookeeper.server.quorum.QuorumPeerMain"

咱们在 $JMXLOCALONLY 后边添加 jmx 的相关参数配置:

ZOOMAIN="-Dcom.sun.management.jmxremote
        -Dcom.sun.management.jmxremote.local.only=$JMXLOCALONLY
                -Djava.rmi.server.hostname=192.168.1.8
                -Dcom.sun.management.jmxremote.port=1911
                -Dcom.sun.management.jmxremote.ssl=false
                -Dcom.sun.management.jmxremote.authenticate=false
                 org.apache.zookeeper.server.quorum.QuorumPeerMain"

这样就可以远程监控了,可以用 jconsole.exe 或 jvisualvm.exe 等工具对其进行监控。

  • 身份验证

这里没有配置验证信息,如果需要请参见我的博文 jvisualvm 远程监控 tomcat:http://www.cnblogs.com/leocook/p/jvisualvmandtomcat.html

4. Zookeeper 的存储模型

Zookeeper 的数据存储采用的是结构化存储,结构化存储是没有文件和目录的概念,里边的目录和文件被抽象成了节点(node),zookeeper 里可以称为 znode。Znode 的层次结构如下图:

ZooKeeper 学习总结

最上边的是根目录,下边分别是不同级别的子目录。

5. Zookeeper 客户端的使用

  • zkCli.sh

可使用 ./zkCli.sh -server localhost 来连接到 Zookeeper 服务上。

使用 ls / 可查看根节点下有哪些子节点,可以双击 Tab 键查看更多命令。

  • Java客户端

可创建 org.apache.zookeeper.ZooKeeper 对象来作为 zk 的客户端,注意,java api 里创建 zk 客户端是异步的,为防止在客户端还未完成创建就被使用的情况,这里可以使用同步计时器,确保 zk 对象创建完成再被使用。

  • C客户端

可以使用 zhandle_t 指针来表示 zk 客户端,可用 zookeeper_init 方法来创建。可在 ZK_HOME\src\c\src\ cli.c 查看部分示例代码。

Ubuntu 14.04 安装分布式存储 Sheepdog+ZooKeeper  http://www.linuxidc.com/Linux/2014-12/110352.htm

CentOS 6 安装 sheepdog 虚拟机分布式储存  http://www.linuxidc.com/Linux/2013-08/89109.htm

ZooKeeper 集群配置 http://www.linuxidc.com/Linux/2013-06/86348.htm

使用 ZooKeeper 实现分布式共享锁 http://www.linuxidc.com/Linux/2013-06/85550.htm

分布式服务框架 ZooKeeper — 管理分布式环境中的数据 http://www.linuxidc.com/Linux/2013-06/85549.htm

ZooKeeper 集群环境搭建实践 http://www.linuxidc.com/Linux/2013-04/83562.htm

ZooKeeper 服务器集群环境配置实测 http://www.linuxidc.com/Linux/2013-04/83559.htm

ZooKeeper 集群安装 http://www.linuxidc.com/Linux/2012-10/72906.htm

Zookeeper3.4.6 的安装 http://www.linuxidc.com/Linux/2015-05/117697.htm

6. Zookeeper 创建 Znode

Znode 有两种类型:短暂的和持久的。短暂的 znode 在创建的客户端与服务器端断开(无论是明确的断开还是故障断开)连接时,该 znode 都会被删除;相反,持久的 znode 则不会。

public class CreateGroup implements Watcher{private static final int SESSION_TIMEOUT = 1000;//会话延时

    private ZooKeeper zk = null;
    private CountDownLatch countDownLatch = new CountDownLatch(1);//同步计数器

    public void process(WatchedEvent event) {if(event.getState() == KeeperState.SyncConnected){countDownLatch.countDown();//计数器减一
        }
    }

    /**
     * 创建 zk 对象
     * 当客户端连接上 zookeeper 时会执行 process(event)里的 countDownLatch.countDown(),计数器的值变为 0,则 countDownLatch.await()方法返回。* @param hosts
     * @throws IOException
     * @throws InterruptedException
     */
    public void connect(String hosts) throws IOException, InterruptedException {zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this);
        countDownLatch.await();//阻塞程序继续执行
    }
    
    /**
     * 创建 group
     * 
     * @param groupName 组名
     * @throws KeeperException
     * @throws InterruptedException
     */
    public void create(String groupName) throws KeeperException, InterruptedException {String path = "/" + groupName;
        String createPath = zk.create(path, null, Ids.OPEN_ACL_UNSAFE/*允许任何客户端对该 znode 进行读写 */, CreateMode.PERSISTENT/* 持久化的 znode*/);
        System.out.println("Created" + createPath);
    }
    
    /**
     * 关闭 zk
     * @throws InterruptedException
     */
    public void close() throws InterruptedException {if(zk != null){try {zk.close();
            } catch (InterruptedException e) {throw e;
            }finally{zk = null;
                System.gc();}
        }
    }
}

这里我们使用了同步计数器 CountDownLatch,在 connect 方法中创建执行了 zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this); 之后,下边接着调用了 CountDownLatch 对象的 await 方法阻塞,因为这是 zk 客户端不一定已经完成了与服务端的连接,在客户端连接到服务端时会触发观察者调用 process()方法,我们在方法里边判断一下触发事件的类型,完成连接后计数器减一,connect 方法中解除阻塞。

还有两个地方需要注意:这里创建的 znode 的访问权限是 open 的,且该 znode 是持久化存储的。

测试类如下:

public class CreateGroupTest {private static String hosts = "192.168.1.8";
    private static String groupName = "zoo";
    
    private CreateGroup createGroup = null;
    
    /**
     * init
     * @throws InterruptedException 
     * @throws KeeperException 
     * @throws IOException 
     */
    @Before
    public void init() throws KeeperException, InterruptedException, IOException {createGroup = new CreateGroup();
        createGroup.connect(hosts);
    }
    
    @Test
    public void testCreateGroup() throws KeeperException, InterruptedException {createGroup.create(groupName);
    }
    
    /**
     * 销毁资源
     */
    @After
    public void destroy() {try {createGroup.close();
            createGroup = null;
            System.gc();} catch (InterruptedException e) {e.printStackTrace();
        }
    }
}

由于 zk 对象的创建和销毁代码是可以复用的,所以这里我们把它分装成了接口:

/**
 * 连接的观察者,封装了 zk 的创建等
 * @author leo
 *
 */
public class ConnectionWatcher implements Watcher {private static final int SESSION_TIMEOUT = 5000;

    protected ZooKeeper zk = null;
    private CountDownLatch countDownLatch = new CountDownLatch(1);

    public void process(WatchedEvent event) {KeeperState state = event.getState();
        
        if(state == KeeperState.SyncConnected){countDownLatch.countDown();
        }
    }
    
    /**
     * 连接资源
     * @param hosts
     * @throws IOException
     * @throws InterruptedException
     */
    public void connection(String hosts) throws IOException, InterruptedException {zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this);
        countDownLatch.await();}
    
    /**
     * 释放资源
     * @throws InterruptedException
     */
    public void close() throws InterruptedException {if (null != zk) {try {zk.close();
            } catch (InterruptedException e) {throw e;
            }finally{zk = null;
                System.gc();}
        }
    }
}

7. Zookeeper 删除 Znode

/**
 * 删除分组
 * @author leo
 *
 */
public class DeleteGroup extends ConnectionWatcher {public void delete(String groupName) {String path = "/" + groupName;
        
        try {List<String> children = zk.getChildren(path, false);
            
            for(String child : children){zk.delete(path + "/" + child, -1);
            }
            zk.delete(path, -1);//版本号为 -1,
        } catch (KeeperException e) {e.printStackTrace();
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }
}

zk.delete(path,version)方法的第二个参数是 znode 版本号,如果提供的版本号和 znode 版本号一致才会删除这个 znode,这样可以检测出对 znode 的修改冲突。通过将版本号设置为 -1,可以绕过这个版本检测机制,无论 znode 的版本号是什么,都会直接将其删除。

测试类:

 
public class DeleteGroupTest {private static final String HOSTS = "192.168.1.137";
    private static final String groupName = "zoo";
    
    private DeleteGroup deleteGroup = null;
    
    @Before
    public void init() throws IOException, InterruptedException {deleteGroup = new DeleteGroup();
        deleteGroup.connection(HOSTS);
    }
    
    @Test
    public void testDelete() throws IOException, InterruptedException, KeeperException {deleteGroup.delete(groupName);
    }
    
    @After
    public void destroy() throws InterruptedException {if(null != deleteGroup){try {deleteGroup.close();
            } catch (InterruptedException e) {throw e;
            }finally{deleteGroup = null;
                System.gc();}
        }
    }
}

8. Zookeeper 的相关操作

ZooKeeper 中共有 9 中操作:

create:创建一个 znode

delete:删除一个 znode

exists:测试一个 znode

getACL,setACL:获取 / 设置一个 znode 的 ACL(权限控制)

getChildren:获取一个 znode 的子节点

getData,setData:获取 / 设置一个 znode 所保存的数据

sync:将客户端的 znode 视图与 ZooKeeper 同步

这里更新数据是必须要提供 znode 的版本号(也可以使用 - 1 强制更新,这里可以执行前通过 exists 方法拿到 znode 的元数据 Stat 对象,然后从 Stat 对象中拿到对应的版本号信息),如果版本号不匹配,则更新会失败。因此一个更新失败的客户端可以尝试是否重试或执行其它操作。

9. ZooKeeper 的 API

ZooKeeper 的 api 支持多种语言,在操作时可以选择使用同步 api 还是异步 api。同步 api 一般是直接返回结果,异步 api 一般是通过回调来传送执行结果的,一般方法中有某参数是类 AsyncCallback 的内部接口,那么该方法应该就是异步调用,回调方法名为 processResult。

10. 观察触发器

可以对客户端和服务器端之间的连接设置观察触发器(后边称之为 zookeeper 的状态观察触发器),也可以对 znode 设置观察触发器。

  • 状态观察器

zk 的整个生命周期如下:

ZooKeeper 学习总结

可在创建 zk 对象时传入一个观察器,在完成 CONNECTING 状态到 CONNECTED 状态时,观察器会触发一个事件,该触发的事件类型为 NONE,通过 event.getState()方法拿到事件状态为 SyncConnected。有一点需要注意的就是,在 zk 调用 close 方法时不会触发任何事件,因为这类的显示调用是开发者主动执行的,属于可控的,不用使用事件通知来告知程序。这一块在下篇博文还会详细解说。

  • 设置 znode的观察器

可以在读操作 exists、getChildren 和 getData 上设置观察,在执行写操作 create、delete 和 setData 将会触发观察事件,当然,在执行写的操作时,也可以选择是否触发 znode 上设置的观察器,具体可查看相关的 api。

当观察的 znode被创建、删除或其数据被更新时,设置在 exists 上的观察将会被触发;

当观察的 znode被删除或数据被更新时,设置在 getData 上的观察将会被触发;

当观察的 znode的子节点被创建、删除或 znode自身被删除时,设置在 getChildren 上的观察将会被触发,可通过观察事件的类型来判断被删除的是 znode 还是它的子节点。

ZooKeeper 学习总结

对于 NodeCreatedNodeDeleted根据路径就能发现是哪个 znode 被写;对于 NodeChildrenChanged 可根据 getChildren 来获取新的子节点列表。

注意:在收到收到触发事件到执行读操作之间,znode 的状态可能会发生状态,这点需要牢记。

至此,编写简单的 zookeeper 应该是可以的了,下篇博文咱们来深入探讨 zookeeper 的相关知识。

参考地址:http://zookeeper.apache.org/doc/r3.4.6/

参考书籍:《Hadoop 权威指南》

Hadoop 权威指南(第 3 版) 修订版(带目录书签) 中文 PDF 高清晰 下载见 http://www.linuxidc.com/Linux/2016-03/129542.htm

更多详情见请继续阅读下一页的精彩内容:http://www.linuxidc.com/Linux/2016-07/133179p2.htm

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-21发表,共计23502字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中

星哥玩云

星哥玩云
星哥玩云
分享互联网知识
用户数
4
文章数
19350
评论数
4
阅读量
7959316
文章搜索
热门文章
星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛 NAS-6:抖音视频同步工具,视频下载自动下载保存 前言 各位玩 NAS 的朋友好,我是星哥!...
星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛 NAS-3:安装飞牛 NAS 后的很有必要的操作 前言 如果你已经有了飞牛 NAS 系统,之前...
我把用了20年的360安全卫士卸载了

我把用了20年的360安全卫士卸载了

我把用了 20 年的 360 安全卫士卸载了 是的,正如标题你看到的。 原因 偷摸安装自家的软件 莫名其妙安装...
再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见 zabbix!轻量级自建服务器监控神器在 Linux 的完整部署指南 在日常运维中,服务器监控是绕不开的...
飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛 NAS 中安装 Navidrome 音乐文件中文标签乱码问题解决、安装 FntermX 终端 问题背景 ...
阿里云CDN
阿里云CDN-提高用户访问的响应速度和成功率
随机文章
从“纸堆”到“电子化”文档:用这个开源系统打造你的智能文档管理系统

从“纸堆”到“电子化”文档:用这个开源系统打造你的智能文档管理系统

从“纸堆”到“电子化”文档:用这个开源系统打造你的智能文档管理系统 大家好,我是星哥。公司的项目文档存了一堆 ...
你的云服务器到底有多强?宝塔跑分告诉你

你的云服务器到底有多强?宝塔跑分告诉你

你的云服务器到底有多强?宝塔跑分告诉你 为什么要用宝塔跑分? 宝塔跑分其实就是对 CPU、内存、磁盘、IO 做...
再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见 zabbix!轻量级自建服务器监控神器在 Linux 的完整部署指南 在日常运维中,服务器监控是绕不开的...
每年0.99刀,拿下你的第一个顶级域名,详细注册使用

每年0.99刀,拿下你的第一个顶级域名,详细注册使用

每年 0.99 刀,拿下你的第一个顶级域名,详细注册使用 前言 作为长期折腾云服务、域名建站的老玩家,星哥一直...
星哥带你玩飞牛NAS-8:有了NAS你可以干什么?软件汇总篇

星哥带你玩飞牛NAS-8:有了NAS你可以干什么?软件汇总篇

星哥带你玩飞牛 NAS-8:有了 NAS 你可以干什么?软件汇总篇 前言 哈喽各位玩友!我是是星哥,不少朋友私...

免费图片视频管理工具让灵感库告别混乱

一言一句话
-「
手气不错
150元打造低成本NAS小钢炮,捡一块3865U工控板

150元打造低成本NAS小钢炮,捡一块3865U工控板

150 元打造低成本 NAS 小钢炮,捡一块 3865U 工控板 一块二手的熊猫 B3 工控板 3865U,搭...
星哥带你玩飞牛NAS-12:开源笔记的进化之路,效率玩家的新选择

星哥带你玩飞牛NAS-12:开源笔记的进化之路,效率玩家的新选择

星哥带你玩飞牛 NAS-12:开源笔记的进化之路,效率玩家的新选择 前言 如何高效管理知识与笔记,已经成为技术...
三大开源投屏神器横评:QtScrcpy、scrcpy、escrcpy 谁才是跨平台控制 Android 的最优解?

三大开源投屏神器横评:QtScrcpy、scrcpy、escrcpy 谁才是跨平台控制 Android 的最优解?

  三大开源投屏神器横评:QtScrcpy、scrcpy、escrcpy 谁才是跨平台控制 Andr...
星哥带你玩飞牛NAS-16:不再错过公众号更新,飞牛NAS搭建RSS

星哥带你玩飞牛NAS-16:不再错过公众号更新,飞牛NAS搭建RSS

  星哥带你玩飞牛 NAS-16:不再错过公众号更新,飞牛 NAS 搭建 RSS 对于经常关注多个微...
国产开源公众号AI知识库 Agent:突破未认证号限制,一键搞定自动回复,重构运营效率

国产开源公众号AI知识库 Agent:突破未认证号限制,一键搞定自动回复,重构运营效率

国产开源公众号 AI 知识库 Agent:突破未认证号限制,一键搞定自动回复,重构运营效率 大家好,我是星哥,...