ZooKeeper面试题


序号内容链接地址
1Java面试题https://blog.csdn.net/golove666/article/details/137360180
2JVM面试题 https://blog.csdn.net/golove666/article/details/137245795
3Servlet面试题 https://blog.csdn.net/golove666/article/details/137395779
4Maven面试题 https://blog.csdn.net/golove666/article/details/137365977
5Git面试题https://blog.csdn.net/golove666/article/details/137368870
6Gradle面试题https://blog.csdn.net/golove666/article/details/137368172
7Jenkins 面试题 https://blog.csdn.net/golove666/article/details/137365214
8Tomcat面试题 https://blog.csdn.net/golove666/article/details/137364935
9Docker面试题 https://blog.csdn.net/golove666/article/details/137364760
10多线程面试题 https://blog.csdn.net/golove666/article/details/137357477
11Mybatis面试题 https://blog.csdn.net/golove666/article/details/137351745
12Nginx面试题 https://blog.csdn.net/golove666/article/details/137349465
13Spring面试题 https://blog.csdn.net/golove666/article/details/137334729
14Netty面试题https://blog.csdn.net/golove666/article/details/137263541
15SpringBoot面试题https://blog.csdn.net/golove666/article/details/137192312
16SpringBoot面试题1 https://blog.csdn.net/golove666/article/details/137383473
17Mysql面试题 https://blog.csdn.net/golove666/article/details/137261529
18Redis面试题 https://blog.csdn.net/golove666/article/details/137267922
19PostgreSQL面试题 https://blog.csdn.net/golove666/article/details/137385174
20Memcached面试题 https://blog.csdn.net/golove666/article/details/137384317
21Linux面试题https://blog.csdn.net/golove666/article/details/137384729
22HTML面试题 https://blog.csdn.net/golove666/article/details/137386352
23JavaScript面试题 https://blog.csdn.net/golove666/article/details/137385994
24Vue面试题https://blog.csdn.net/golove666/article/details/137341572
25Ajax面试题https://blog.csdn.net/golove666/article/details/137421929
26Python面试题 https://blog.csdn.net/golove666/article/details/137385635
27Spring Cloud Alibaba面试题 https://blog.csdn.net/golove666/article/details/137372112
28SpringCloud面试题 https://blog.csdn.net/golove666/article/details/137345465
29RabbitMQ面试题 https://blog.csdn.net/golove666/article/details/137344188
30Dubbo面试题 https://blog.csdn.net/golove666/article/details/137346834
31Elasticsearch面试题https://blog.csdn.net/golove666/article/details/137348184
32Oracle面试题https://blog.csdn.net/golove666/article/details/137350452
33Android面试题https://blog.csdn.net/golove666/article/details/137358253
34Kafka面试题 https://blog.csdn.net/golove666/article/details/137358607
35ZooKeeper面试题 https://blog.csdn.net/golove666/article/details/137359255
36Kubernetes面试题 https://blog.csdn.net/golove666/article/details/137365540
37Flink面试题 https://blog.csdn.net/golove666/article/details/137369555
38Hadoop面试题https://blog.csdn.net/golove666/article/details/137370194
39Hive面试题https://blog.csdn.net/golove666/article/details/137371835
40Hbase面试题 https://blog.csdn.net/golove666/article/details/137381853
41Spark面试题https://blog.csdn.net/golove666/article/details/137382815
42Golang面试题 https://blog.csdn.net/golove666/article/details/137395486
43Solr面试题 https://blog.csdn.net/golove666/article/details/137420799

1 ZooKeeper 基础概念

1.1 什么是 ZooKeeper?

ZooKeeper 是一个由 Apache 软件基金会开发的开源分布式协调和配置服务。它主要用于大型分布式系统中,用来解决诸如配置管理、服务发现、同步服务等分布式系统常见问题。ZooKeeper 允许分布式进程通过共享的层次名字空间,这是一组以目录树方式组织的数据注册引用,共同协调信息。

以下是 ZooKeeper 的一些核心功能:

  1. 一致性保证
    ZooKeeper 提供一致的服务,无论客户端连接到哪个服务器都会看到相同的视图。

  2. 分布式锁服务
    在分布式环境中实现分布式锁机制,用于控制资源的并发访问。

  3. 配置管理
    对大型分布式系统中的配置信息进行统一管理和分发。

  4. 集群管理
    实现如 Leader 选举,以及集群成员间状态的同步。

  5. 服务注册和发现
    分布式系统中的服务可以注册自己的服务地址以及元信息供其他服务发现和访问。

  6. 命名服务
    为分布式资源提供统一的命名服务。

ZooKeeper 为分布式应用提供了一组简单的原语,开发者可以利用这些原语搭建出更复杂的同步策略、组成员发现等服务。ZooKeeper 被设计成易于使用,其通过数据模型自身就表达了分布式环境下的同步问题,客户端可以在其上直接构建自己的服务逻辑。

由于 ZooKeeper 的设计目标是专注于提供原子性操作,并确保顺序性和速度,它非常适合那些对一致性要求比较高的大规模分布式应用。ZooKeeper 在多个副本之间使用简单的领导者选举算法来维护高可用性和强一致性。它通常与大数据技术堆栈(如 Hadoop 和 HBase)、分布式消息传递系统(如 Kafka)以及许多其他分布式应用一起使用。

1.2 ZooKeeper 的核心功能有哪些?

Apache ZooKeeper 是一个开源的分布式协调服务,用于维护配置信息、命名、提供分布式同步和提供组服务。以下是 ZooKeeper 的核心功能:

1. 配置管理

ZooKeeper 允许分布式应用程序从中央存储库读取应用程序设置和配置信息,当配置信息更新时可以即时通知到各个应用实例。

2. 名称服务

ZooKeeper 提供了一个全局唯一的命名空间,用于解决名字到系统资源或服务的映射问题。

3. 分布式锁

ZooKeeper 可以提供跨多个节点的分布式锁服务,确保在分布式环境中只有一个进程能执行关键代码段。

4. 选举机制

对于需要选出一个主节点(Leader)的分布式系统,ZooKeeper 是一个理想的解决方案,它可以协助进行 Leader 选举。

5. 队列管理

借助 ZooKeeper,你可以实现分布式队列,确保任务的安全执行并保证任务不会被重复执行。

6. 节点和数据模型

ZooKeeper 的架构是树形结构,每个节点称为 Znode,既可以用于存储数据,也可以用于监控子节点的变化。

7. 通知机制

ZooKeeper 提供了一个基于推送的通知机制,客户端可以在 znode 发生变化时获得通知,从而更新本地数据或执行相关操作。

8. 可靠性和顺序性

ZooKeeper 保证了一个客户端请求的串行执行,并为服务中的每个更新都分配一个全局唯一的递增编号,保证了顺序性和一致性。

9. 多客户端协调

支持从多个客户端并发访问,而且保证客户端间的逻辑一致性。

10. 持久性和临时性节点

ZooKeeper 中的节点可以是持久化的也可以是临时的。持久化节点在创建后会一直存在,直到显式删除;临时节点在创建它的客户端会话结束时自动删除。

ZooKeeper 的这些核心功能是许多分布式系统和大型云应用的基础组件,如 Apache Kafka、Hadoop 和 HBase 等。通过提供这些服务,ZooKeeper 有效地解决了在分布式环境中常见的一些最棘手的问题。

1.3 解释 ZooKeeper 中的 znode。

Apache ZooKeeper 是一个分布式协调服务,用于提供高度可靠的分布式同步。一个 ZooKeeper 服务(集群)由一组服务器(节点)组成,为分布式应用程序提供基本的协调和状态同步功能。

在 ZooKeeper 中,Znode 是 ZooKeeper 数据模型的核心概念和构建块,它表示 ZooKeeper 中存储数据的节点。Znode 的概念类似于文件系统中的文件和目录,但它们用于存储数据以及维护分布式系统状态。以下是 Znode 的关键特点和类型:

关键特点

  • 每个 Znode 都有一个全局唯一的路径,例如 /app1/server1
  • Znode 可以用来存储小量数据(如配置信息)及其状态信息(元数据,如版本号)。
  • Znode 路径遵循类似文件系统的层次结构。
  • Znode 可以具有子 Znode,从而形成一棵树。

类型

Znode 的具体类型根据它们的特性,可以分为多种:

  1. 持久 Znode(Persistent Znode):

    • 它们在创建后会一直存在于 ZooKeeper 中,直到明确删除。
    • 持久 Znode 可以有子节点,子节点也可以是持久的。
  2. 临时 Znode(Ephemeral Znode):

    • 它们的生命周期绑定到创建 Znode 的客户端会话。当会话结束时,临时 Znode 会被自动删除。
    • 临时 Znode 不可以有子节点。
  3. 顺序 Znode(Sequential Znode):

    • 在创建时,ZooKeeper 会自动在 Znode 名称后附加一个单调递增的计数器作为后缀,这保证了节点名称的唯一性。
    • 临时和持久节点都可以是顺序的。

典型用途

  • 配置管理:持久 Znode 用于存储配置信息,当配置发生变化时,可以通知相关的客户端。
  • 服务注册与发现:服务实例在 ZooKeeper 中创建临时 Znode 进行注册,服务消费者可以通过监听这些 Znode 来发现服务实例的变化。
  • 集群管理:跟踪集群中的节点以及它们的状态,例如,通过临时 Znode 来监测节点的存活情况。
  • 分布式锁:使用 Znode 的顺序性和临时性来实现锁和同步原语。

操作

  • 创建 Znode:客户端可以向 ZooKeeper 请求创建 Znode。
  • 读取和修改:可以读取 Znode 存储的数据和元数据,或者更新存储的数据。
  • 监听:客户端可以设置监听器监控特定 Znode 的变化。
  • 删除:除临时 Znode 自动删除外,也可以请求删除特定的永久 Znode。

ZooKeeper 提供的原语丰富而强大,Znode 在其中扮演了实现这些原语的关键部分。正确使用这些 Znode,可以为分布式应用或系统建立稳健的协调机制。在设计基于 ZooKeeper 的系统时,需要考虑数据结构如何反映分布式的状态,以及如何使用 Znode 来实现所需的协调功能。

2 ZooKeeper 数据模型

2.1 描述 ZooKeeper 的数据模型。

Apache ZooKeeper 的数据模型非常类似于文件系统。它组织数据结构为层次化的命名空间,类似于一棵树。每个节点在ZooKeeper的树状结构中被称为一个znode,并且每个节点都可以拥有子节点。

znode

ZooKeeper中的每个节点(znode)既可以存储数据,也可以拥有子节点。具体地,znode具有以下的特性:

  1. 路径:每个znode都由一个路径标识,这个路径是绝对路径,类似于文件系统中的路径,以斜杠(/)为开始。

  2. 序列化节点:znode可以被设置为顺序性的,即当它们被创建时,ZooKeeper会自动在名称后附加一个递增的计数器。这对于实现一些复杂的协调机制,如分布式队列、锁等非常有用。

  3. 数据和版本号:每个znode保存的数据可以有大小限制,并可以更改。每次数据被更改,ZooKeeper都会增加该节点的数据版本号。

  4. 子节点:znode可以有子节点。与文件系统不同,znode存储数据的同时也可能拥有子节点。

  5. 临时节点:znode可以是临时的,这种znode的生命周期与创建它的会话绑定。如果会话关闭或会话超时,这些临时节点将被自动删除。

  6. 监视器(Watches):客户端可以对特定的znode设置监视器。这些监视器会在znode发生特定变化时通知客户端,例如,znode本身的创建、删除或znode数据的更改。

树状结构举例

下面是一个简单的ZooKeeper的树状结构例子,显示了几个znode和它们路径的模样:

/
├── zookeeper // ZooKeeper的内部数据,比如选举信息等
├── app1
│   ├── config // 应用的配置信息
│   ├── workers // 工作节点的注册
│   │   ├── worker1 // 特定的工作节点
│   │   └── worker2
│   └── tasks // 分布式应用分配的任务
│       ├── task-1
│       └── task-2
└── app2
    ├── sessions
    └── settings

每个节点(如/app1, /app1/config, /app1/workers/worker1等)都可以存储数据,并可能拥有子节点。

使用场景

ZooKeeper的强大之处在于,虽然它提供的是一个简单的数据模型,但是通过该模型可以实现一系列分布式协同的开发模式,包含但不限于:

  • 配置管理
  • 名称服务(Name Service)
  • 分布式锁
  • 队列管理
  • 集群管理及领导选举

ZooKeeper的设计尽可能简单和通用,因此它也可以作为其他分布式系统组件或服务的基础设施,例如Kafka的负载协调和配置管理。

2.2 讲述 ZooKeeper 中持久节点和临时节点的区别。

Apache ZooKeeper 是一个分布式协调服务,它提供了一种中心化的架构来维护配置信息,命名,提供分布式同步,并提供组服务。在 ZooKeeper 中,数据存储在层次化的名称空间中,类似于文件系统中的路径结构,这些路径位置被称为节点(ZNode)。ZooKeeper 中的节点可以是持久(Persistent)节点或者临时(Ephemeral)节点,它们有以下区别:

持久节点(Persistent ZNodes):

  • 持久节点一旦被创建,就会一直存储在 ZooKeeper 中,直到它被显式地删除。
  • 即使创建该节点的客户端会话结束,节点依然存在。
  • 持久节点可以有子节点。
  • 它们常用来存储那些即使在客户端断开连接后也需要保留的长期配置信息。

临时节点(Ephemeral ZNodes):

  • 临时节点的生命周期绑定于创建它们的客户端会话。当会话结束或客户端与服务端断开连接时(包括心跳超时),临时节点自动被删除。
  • 临时节点在客户端意外断开时提供了快速的故障通知,因此它们常用于实现锁服务、领导者选举等机制,以及管理分布式系统中的临时配置。
  • 临时节点不能有子节点。
  • 当节点被删除时,ZooKeeper 可以为相关节点设定观察者(watcher),这样在节点消失时,可以通知那些对此事件感兴趣的客户端。

临时顺序节点(Ephemeral Sequential ZNodes):

除了普通的临时节点,ZooKeeper还提供了临时顺序节点,它们结合了临时节点和顺序节点的特性:

  • 临时顺序节点是创建时自动在其名称后面添加一个自增序列号的临时节点。
  • 这对于实现如领导者选举和队列等分布式数据结构非常有用。

每个节点的类型在创建时决定,并在节点的生命周期内保持不变。选择持久节点还是临时节点取决于应用和数据的具体需求。在实际的分布式系统中,持久节点和临时节点经常一起使用,以满足不同场景的要求。

设计系统时,需要特别注意的是,临时节点在客户端断开连接时会自动被删除,这可能会引发复杂的故障恢复场景,并影响分布式锁和其他协调原语的行为。因此,使用 ZooKeeper 时,必须谨慎处理网络问题、会话超时以及客户端重连逻辑。

2.3 解释 ZooKeeper 的 Watcher 机制。

Apache ZooKeeper 是一个分布式协调服务,它提供了一系列简单的操作,用于管理分布式应用中的共享数据并维护强一致性。ZooKeeper 中的 “Watcher 机制” 是其核心特性之一,它允许客户端对 ZooKeeper 的数据节点(znodes)设置监听器。

Watcher 机制的工作原理

1. 设置 Watcher

客户端在读取节点数据或获取节点子列表的过程中可以向这些节点注册 Watcher。例如,使用 getData()getChildren() API 方法时设置一个 watch 标志。

zooKeeper.getData("/node-path", true, stat); // `true` 表示设置了 Watcher

2. Watcher 事件触发

当一个 Watcher 被注册后,如果被监听的节点发生了特定的事件变化,比如节点数据的更改、节点创建或删除,ZooKeeper 将向所有注册了相应节点 Watcher 的客户端发送通知。

public class WatcherExample implements Watcher {
    @Override
    public void process(WatchedEvent event) {
        String path = event.getPath();
        EventType type = event.getType();
        if (type == EventType.NodeDataChanged) {
            // 节点数据发生变化
        }
    }
}

3. Watcher 的一次性

在 ZooKeeper 中,Watchers 是一次性的,这意味着一旦触发,它们将失效。如果客户端需要持续监听节点变化,需要在获取事件通知后重新注册 Watcher。

@Override
public void process(WatchedEvent event) {
    if (event.getType() == Event.EventType.NodeDataChanged) {
        // 节点数据发生变化
        // 重新注册 Watcher,以继续监听后续变化
        zooKeeper.getData("/node-path", true, null);
    }
}

Watcher 事件类型

ZooKeeper 在节点上支持以下 Watcher 事件类型:

  • NodeCreated:指定节点被创建时触发。
  • NodeDeleted:指定节点被删除时触发。
  • NodeDataChanged:指定节点数据变化时触发。
  • NodeChildrenChanged:指定节点的子节点列表变化时触发。

Watcher 使用场景

  • 配置管理:监听配置节点的变化,动态更新分布式应用中的配置。
  • 服务发现:监听表示服务地址的节点,当服务实例变化时进行响应。
  • 集群管理:检测节点添加或移除,进行分布式锁和领导选举等操作。
  • 同步原语:实现分布式锁和队列等同步原语。

注意事项

  • 慎重使用 Watcher 以避免大量并发的触发可能导致的性能问题。
  • Watcher 应该只用于对于应用很关键并需及时响应的变化。对于不太敏感的更改,可以考虑使用轮询等其他机制。
  • 由于 Watchers 是一次性的,为了持续监听,需要在事件处理函数中重新注册 Watcher,因此要设计好事件处理的逻辑,以免错过事件通知。

ZooKeeper 的 Watcher 机制是一个强大的特性,对于需要实时响应系统变化的分布式应用至关重要。它的合理使用是构建稳定和响应式系统的关键。

3 ZooKeeper 使用场景

3.1 如何使用 ZooKeeper 实现分布式锁?

使用 ZooKeeper 实现分布式锁通常涉及以下几个步骤:

  1. 连接到 ZooKeeper 集群
    首先,你需要创建一个客户端实例来连接到 ZooKeeper 集群。

  2. 创建锁节点
    在构建分布式锁时,你需要选定一个特定的路径,作为锁的节点(例如,/locks/my_lock)。所有想要获取锁的客户端都会在这个节点下创建子节点。

  3. 尝试获取锁
    客户端尝试通过在锁节点下创建一个临时顺序节点(例如,/locks/my_lock/lock_)来获取锁。ZooKeeper 会自动在该路径下追加一个唯一的递增序号,形成如/locks/my_lock/lock_00000001的节点路径。

  4. 锁排序
    客户端获取/locks/my_lock/下所有的子节点,并对这些子节点按照节点名(包含的序号)进行排序。

  5. 节点检查
    如果一个客户端创建的临时顺序节点在排序顺序中是最小的,那么它将获得锁并继续执行关键区域(critical section)的操作。如果不是最小的,客户端找到排在它之前的那个节点,并在那个节点上注册一个 Watcher 监听节点的消失事件。

  6. 锁的释放
    当持有锁的客户端完成了关键区域的操作,它将删除自己创建的临时顺序节点,释放锁。ZooKeeper 删除节点后将触发 Watcher 事件。

  7. 等待并尝试重新获取锁
    其他客户端通过 Watcher 被通知前序节点已被删除,这时重新走到第4步进行判断自己是否是最小节点,从而尝试获取锁。

以下是一个使用伪代码实现的基本分布式锁示例:

public class DistributedLock {
    private String zkHostPort;
    private String lockName;
    private ZooKeeper zk;
    private String myNode = null;
    private String watchNode = null;

    public DistributedLock(String lockName) {
        // 初始化 ZooKeeper 实例等...
    }

    public boolean lock() {
        myNode = zk.create("/locks/my_lock/lock_", new byte[0], 
                           ZooDefs.Ids.OPEN_ACL_UNSAFE, 
                           CreateMode.EPHEMERAL_SEQUENTIAL);
        
        List<String> subNodes = zk.getChildren("/locks/my_lock", false);
        Collections.sort(subNodes);

        if (myNode.equals("/locks/my_lock/" + subNodes.get(0))) {
            // 拿到了锁
            return true;
        }
        
        int prevNodeIndex = Collections.binarySearch(subNodes, myNode.substring(myNode.lastIndexOf("/") + 1)) - 1;
        watchNode = subNodes.get(prevNodeIndex);

        // 在 watchNode 上注册监听器,等待其释放锁
        zk.exists("/locks/my_lock/" + watchNode, watchedEvent -> {
            if (watchedEvent.getType() == Event.EventType.NodeDeleted) {
                lock();
            }
        });
        
        return false; // 还没有得到锁
    }
    
    public void unlock() {
        if (myNode != null) {
            zk.delete(myNode, -1);
            myNode = null;
        }
    }
}

使用 ZooKeeper 实现分布式锁需要确保客户端在异常情况下能够释放锁,例如在客户端崩溃时,ZooKeeper 的临时节点能够被自动移除。同时还要注意处理好网络问题导致的重试操作及其它并发问题。

3.2 描述 ZooKeeper 在配置管理中的应用。

Apache ZooKeeper 在配置管理中通常被用作中央配置服务,它允许系统中的各个部分可以存储并检索配置信息,并且当配置信息发生变动时可以及时通知到各个应用实例。以下是 ZooKeeper 在配置管理中的主要应用:

集中式配置存储

ZooKeeper 可以作为一个中央集中式存储服务来保存系统的所有配置信息。这些配置信息可以是数据库的连接字符串、系统参数、功能开关等。ZooKeeper 的 znode(数据节点)结构用于存储这些配置信息,而客户端可以通过 znode 的路径来访问配置数据。

配置更新的通知

一个系统中的配置信息可能需要经常更新。ZooKeeper 提供了一种机制,当配置信息发生变化时,它会通知所有订阅这些信息的客户端。这通过 ZooKeeper 的监视器(watcher)功能来实现,使得客户端在配置更新时立即收到回调并采取行动,例如重新加载配置。

读取配置

应用启动或运行时,可以从 ZooKeeper 读取配置信息。这些配置信息单独存储在 ZooKeeper 的节点上,应用可以通过节点的路径来查询配置数据。

版本化控制

ZooKeeper 允许对配置信息的变化进行版本控制,每一次的更改都对应一个版本号。客户端在更新和读取配置信息时可以利用版本号来检测和解决可能的冲突。

实现配置管理的大致步骤:

  1. 在 ZooKeeper 中创建一个或多个 znode 用于存储配置信息。
  2. 应用在启动时,从对应 znode 读取配置数据,并设置监视器(watcher)来监听配置变更事件。
  3. 当配置信息变更时,操作者更新对应的 znode。
  4. 更新后,ZooKeeper 通知所有设置了监视器的客户端。
  5. 客户端收到通知后,重新从 znode 读取配置信息。

通过以上方式,ZooKeeper 关于配置管理的使用,尤其适用与大型分布式系统,它们因为组件众多、配置项繁杂,需要牢靠且灵活的配置管理机制。

3.3 ZooKeeper 在服务发现中扮演什么角色?

在分布式系统中,服务发现是一个核心组件,用于自动检测和定位可以供消费者使用的服务实例。Apache ZooKeeper 在服务发现中扮演着极其重要的角色,它通常作为一个可信赖的中心化的服务注册表,让服务提供者(Provider)可以注册自己的存在,并让服务消费者(Consumer)能够发现这些提供者并进行通信。

ZooKeeper 在服务发现中的角色及其机制:

1. 注册中心

ZooKeeper 充当注册中心,服务实例在 ZooKeeper 中创建对应的Znode用于注册。Znode保存有关服务的元数据,包括服务的地址、端口号和其他配置信息。

2. 动态服务注册

当服务实例启动时,它会在 ZooKeeper 中创建一个临时Znode(Ephemeral),通常还是个顺序Znode(Sequential)。如果服务实例停止或连接断开,对应的临时Znode将会自动从 ZooKeeper 中移除,从而实现服务实例的动态注册与注销。

3. 服务发现

服务消费者使用 ZooKeeper 客户端来查询服务实例的信息。消费者可以监听特定服务的 Znode 目录,从而发现新注册的服务实例或已经下线的实例。

4. 负载均衡

服务消费者可以实现简单的负载均衡策略,通过选择一组服务实例中的任意一个进行通信。更复杂的负载均衡策略可能根据注册的元数据(如服务实例的负载、地理位置)来选择最合适的实例。

5. 故障检测和自我修复

由于服务实例的注册信息是通过临时Znode维护的,一旦服务实例失败,与其相关的Znode将会消失,这一点可以被其他服务实例或消费者监听到,启动故障恢复和重新选举流程(如果适用)。

6. 集群管理

在一些集群中,ZooKeeper 也用于管理服务实例的配置,以及选举主服务实例(如 Master election)。

使用示例:

当一个服务启动时,它会在 ZooKeeper 中的特定路径下创建一个 Znode,并在持久节点(Persistent Znode)或临时节点中存储服务的地址和端口等信息。例如,一个名为 “order-service”的服务可能在 /services/order-service/00000001 的路径下注册。

服务消费者在 /services/order-service 路径下监听子节点变化事件,一旦有新服务实例注册或旧服务实例消失,消费者会从监听事件中得到通知,并据此更新本地的服务实例列表。

ZooKeeper 能够为分布式系统中的服务发现提供一种稳定、可靠、可扩展的解决方案。它的设计为各种分布式场景提供了必要的组件和协议支持,使服务的动态发现和负载均衡变得可行且高效。

4 ZooKeeper 集群管理

4.1 描述 ZooKeeper 集群的工作原理。

Apache ZooKeeper是一个用于分布式系统协调和配置管理的开源服务。它提供了一种可靠的方式来维持分布式应用程序中的全局数据(比如配置信息)同步。ZooKeeper通过一个称为“集群”(ensemble)的服务器组来实现高可用和容错。

一个ZooKeeper集群由一组相互协作的ZooKeeper服务器组成,这些服务器被称为“节点”(peers)。以下是ZooKeeper集群的工作原理的描述:

选举和领导者(Leader Election)

当ZooKeeper集群启动时,它会执行“领导者选举”(leader election)过程。在一个集群中,一台服务器作为领导者,其余服务器作为跟随者(followers)或观察者(observers):

  • 领导者(Leader):负责处理所有的客户端修改请求(创建、删除、更新znodes),并负责将这些更改广播到其它服务器。还负责进行心跳检查并协调选举。
  • 跟随者(Followers):可以处理来自客户端的读请求,并将客户端的写请求转发给Leader,同时参与事务日志持久化和领导者选举。
  • 观察者(Observers):观察者与跟随者类似,但不参与领导者选举,也不在事务日志中投票,通常用于在不影响写吞吐的情况下增加读取速度。

数据的复制和一致性

一旦选举出领导者,领导者就会开始处理事务请求。为了保持数据的一致性,ZooKeeper使用一种原子广播协议(Zab)来同步所有节点上的更新。当领导者收到一个修改请求:

  1. 领导者分配一个单调递增的事务ID,并将该请求作为提案发送给所有的跟随者。
  2. 跟随者将提案记录到磁盘(事务日志),然后向领导者发送“接受”(ack)响应。
  3. 当领导者接收到超过半数服务器的响应时,就会提交该提案,并通知跟随者此提案已被提交。

这种协议确保了集群的每个节点最终都将拥有相同的数据拷贝。

容错机制

如果领导者服务器发生故障,剩余的节点会重新进行领导者选举。通过该机制,ZooKeeper集群能够在部分节点失效的情况下继续有效运行。对于集群中跟随者节点的绝大多数失败(例如,由于网络分区),领导者将丢弃所有更改直到超过半数的节点恢复正常,以确保集群数据的一致性不会遭到破坏。

观察和通知

客户端可以向ZooKeeper注册对特定znode的观察(watch),如果注册的znode发生更改(数据更改、节点创建或删除),ZooKeeper将通知所有对此znode有观察的客户端,允许客户端对变化作出响应。

ZooKeeper通过上述机制提供了高可靠和高可用的、一致性的服务,这使得它在很多大型分布式系统(如HDFS, HBase, Kafka)中都成为了协调服务的基础设施组件。

4.2 如何设置和维护 ZooKeeper 集群?

建立和维护 Apache ZooKeeper 集群包括几个关键步骤。这些步骤确保了集群的高可用性和一致性,对于那些依赖 ZooKeeper 作为关键数据存储和服务协调的系统来说至关重要。以下是设置和维护 ZooKeeper 集群的指导:

设置集群:

  1. 准备机器
    准备多台机器(推荐奇数台,如 3、5、7 台),使集群能够在少数节点失效的情况下依然保持正确的多数节点决策。

  2. 安装 ZooKeeper
    在每台预计作为集群节点的机器上安装 ZooKeeper。确保安装相同版本的 ZooKeeper,以避免版本不致带来的问题。

  3. 配置 ZooKeeper
    修改 conf/zoo.cfg 配置文件,配置以下参数:

    • clientPort:客户端连接的端口(默认 2181)。
    • dataDir:ZooKeeper 数据存储目录,需要在其中创建 myid 文件。
    • server.X:定义整个集群的机器和它们的 Quorum 端口(通常是 2888)和 Leader 选举端口(通常是 3888),其中 X 是每台机器的唯一标识符。

    例如:

    server.1=zk1:2888:3888
    server.2=zk2:2888:3888
    server.3=zk3:2888:3888
    
  4. 配置 myid 文件
    在每台机器的 dataDir 目录下创建一个名为 myid 的文件,其内容是该机器在 server.X 配置中对应的 X 值。

  5. 启动 ZooKeeper 服务
    在每台机器上启动 ZooKeeper 服务。

  6. 验证集群状态
    使用 zkServer.sh status 命令查看服务器状态,确保有一个 Leader 和其他的 Follower。

维护集群:

  1. 定期备份数据
    定期备份 ZooKeeper 的数据目录,尤其是一致性日志。

  2. 监控集群状态
    监控集群的性能和可用性,比如通过使用 JMX 或 ZooKeeper 自带的 mntr 命令。

  3. 调整配置和容量规划
    根据需求和性能监控结果,适时调整配置(如 JVM 参数),并进行必要的容量规划。

  4. 管理 ZooKeeper 版本
    保持集群中所有机器上的 ZooKeeper 版本一致,并且跟随官方推荐的稳定版本。

  5. 处理网络分区
    在发生网络分区时,ZooKeeper 的 Zab 协议可以保证集群仍然维持一致性,但可能会丧失可用性。设计网络设施来尽量减小网络分区的影响。

  6. 操作规范和故障恢复计划
    编写标准操作步骤和故障恢复计划,定期进行模拟练习。

  7. 升级策略
    跟随 ZooKeeper 官方发布的升级指导,通常按滚动升级的方式逐个升级集群节点,以避免服务中断。

  8. 日志管理
    管理 ZooKeeper 服务的日志,确保它们被适当地归档和分析。日志可以在调试问题时提供关键信息。

  9. 集群扩展
    当需要扩展 ZooKeeper 集群以适应更高的负载时,需要按照推荐的方式逐步添加新节点,并确保集群正常同步和运作。

确保 ZooKeeper 集群的健康运行需要持续的操作和监控。集群的维护涉及监视系统负载、资源使用、日志记录、备份和灾难恢复策略。通过执行上述步骤,你可以建立并维护一个健壮且可信赖的 ZooKeeper 集群。

4.3 解释 Leader 选举过程。

在分布式系统中,leader 选举是一种协调机制,它确保在一组节点中只有一个节点(leader)可以对某些操作进行协调和决策。leader 选举对于实现分布式系统中的一致性和协调性是非常重要的,特别是在处理分布式锁、发送可靠的消息、进行状态复制、事务处理等方面。

Leader 选举通常包括以下步骤:

初始化

  • 首先,所有参与者(节点)处于等待状态,准备进入选举过程。

选举触发

  • 选举可以由节点故障、网络分区或系统启动触发。
  • 当一个节点认为当前的 leader 无法正常工作时,它可能会启动新的选举。

竞选

  • 各个节点开始参与选举过程。根据不同的选举算法,节点可能需要投票决定新的 leader。
  • 竞选过程可以基于节点的ID、先到时间、日志顺序或特定属性。

参与投票

  • 在投票过程中,每个节点都会考虑候选者的资格,并给出自己的投票。通常,每个节点只能投一票,并且可能会把票投给具有最高日志顺序的节点。

选举结果

  • 一旦候选者获得了大多数的票数,他就会被选为新的 leader。
  • 新的 leader 需要得到集群中大多数节点的认可,实现“多数派”共识。

确认和通告

  • 被选为 leader 的节点会发送消息给集群中的其他节点,通告自己的胜利。
  • 一旦其他节点收到通告,它们会认可新 leader 的地位,并将自己的状态更新为 follower。

集群恢复

  • 在 leader 成功选举并被集群认可后,集群返回到稳定的状态,leader 开始处理请求和协调操作。

不同的分布式系统实现可能使用不同的算法来进行 leader 选举,例如:

  • Raft:在这个算法中,节点被选为候选者,并开始请求来自其他节点的投票,获得大多数投票后成为 leader。
  • Paxos:一个更复杂的共识算法,涉及多个阶段,以保证即使在最糟糕的网络条件下也能取得一致。
  • ZooKeeper的Zab协议:跟随者投票给有最新系统状态的候选者。
  • Bully算法:具有最高进程ID的活跃节点会成为 leader。

注意事项

时刻准备处理参与者宕机、网络延迟及分区,并在设计中考虑到重新选举和故障恢复的情况。需要提供充足的监控和日志记录,以追踪选举过程并在出现问题时进行调试。在实践中,leader 选举并不常见,因为复杂性和潜在的风险,通常会倾向于使用成熟的分布式系统算法和框架来管理此过程。

5 ZooKeeper 客户端

5.1 讲述客户端与 ZooKeeper 服务器之间的交互流程。

客户端与 ZooKeeper 服务器之间的交互主要包含以下几个步骤:

1. 建立连接

  • 客户端启动时,会通过 ZooKeeper 类(或其他相应客户端库)创建一个连接到 ZooKeeper 服务器的会话。
  • 在这个过程中,客户端会提供一个服务端列表(可以是单节点或集群),以及一个会话超时值。

2. 会话的创建和确认

  • 服务器收到客户端的连接请求后,将创建一个新的会话,并生成一个唯一的会话 ID。
  • 服务器返回一个响应,包含会话 ID 和已协商会话超时的值。
  • 客户端在本地缓存会话 ID,并根据超时值来判断何时认为该会话失效。

3. 请求处理

  • 客户端可以发送请求来读取、修改数据或更改其状态。常见操作包括创建、删除 znode,或设置对 znode 的监听(Watch)。
  • 所有更改状态的操作(如创建、删除、更新 znode)都由选定的服务器(Leader)来协调,并确保这些更改在集群的所有服务器中复制和一致。

4. 响应和回调

  • 对于读操作(如 getData),服务器将直接响应客户端的请求。
  • 修改状态的操作将首先全局协调一致性,完成后再响应客户端。
  • 如果客户端对某个 znode 设置了监听(Watch),那么在该 znode 发生改变时,服务器会向客户端发送一个通知。

5. 会话维持

  • 客户端与服务器之间会定期交换心跳,以维持会话的活跃。
  • 会话超时是基于心跳的。如果超过了会话超时时间,客户端没有收到服务器的响应,它可能会尝试重新建立连接。

6. 断开连接

  • 客户端完成操作后,可以显式关闭会话,或者由于超时而使会话结束。
  • 关闭会话时,所有由该会话创建的临时节点都会被自动删除。

需要注意的是,ZooKeeper 使用一个单线程的模型来处理客户端的桩胁,这意味着来自同一客户端的所有操作请求都是顺序处理的。这有助于避免并发问题,并确保操作的连贯性。此外,ZooKeeper 提供了强一致性保证;无论客户端连接到集群中的哪个 ZooKeeper 服务器节点,它都看到相同的数据视图。

5.2 如何使用 ZooKeeper API 进行编程?

ZooKeeper Java API 提供了一系列的方法来与 ZooKeeper 集群进行交互,包括创建和删除节点、获取节点数据、设置节点数据等。以下是如何使用 ZooKeeper API 进行编程的基本步骤:

1. 添加 Maven 依赖

首先,在 Maven 项目的 pom.xml 文件中添加 ZooKeeper 的依赖:

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>Your-ZooKeeper-Version</version>
</dependency>

2. 创建 ZooKeeper 实例

使用 ZooKeeper 类的构造函数建立与服务器的连接。该构造函数接收三个参数:连接字符串(服务器列表)、会话超时时间和一个用于接收 ZooKeeper 事件的 Watcher 对象:

ZooKeeper zoo = new ZooKeeper("localhost:2181", 
                                3000, 
                                new Watcher() { 
                                  public void process(WatchedEvent we) {
                                      if (we.getState() == KeeperState.SyncConnected) {
                                          // 处理连接后的逻辑
                                      }
                                  }
                                });

3. 创建节点(znode)

使用 create 方法创建新的节点,可以指定节点的 ACL(访问控制列表)和类型(如持久、临时等):

zoo.create("/path/to/znode", 
            data.getBytes(), 
            ZooDefs.Ids.OPEN_ACL_UNSAFE, 
            CreateMode.PERSISTENT);

4. 获取节点数据

通过 getData 方法来读取指定节点的数据。同时可以设置一个watcher来监视数据变化:

byte[] data = zoo.getData("/path/to/znode", 
                          new Watcher() { 
                              public void process(WatchedEvent we) {
                                  // 处理数据变更的逻辑
                              }
                          },
                          null);

5. 设置节点数据

通过 setData 方法来更新节点的数据,可用于配置管理或其他需求:

Stat stat = zoo.setData("/path/to/znode", 
                         newData.getBytes(), 
                         zoo.exists("/path/to/znode", true).getVersion());

6. 删除节点

使用 delete 方法删除节点,必须指定正确的版本号:

zoo.delete("/path/to/znode", 
            zoo.exists("/path/to/znode", true).getVersion());

7. 关闭连接

完成操作后,应关闭与 ZooKeeper 的连接以释放资源:

zoo.close();

8. 异常处理

ZooKeeper API 的方法可能会抛出异常。例如,KeeperException 表示服务器端发生错误,InterruptedException 表示等待服务器响应时被中断。合适的异常处理对于构建健壮的 ZooKeeper 客户端是必需的。

注意事项

  • 由于 ZooKeeper API 是异步的,很多操作会通过 watcher 和回调方法来通知应用程序。
  • 你提供的 watcher 需要处理不同类型的 WatchedEvent 事件。
  • 创建节点、设置数据、删除节点时要正确处理版本号,以支持并发控制。

通过这些 basic steps,可以构建一个 simple ZooKeeper 客户端来执行基本的分布式协调任务。ZooKeeper 提供了其他高级API来处理复杂的协同机制,如领导选举、分布式锁等。

5.3 讨论使用 ZooKeeper 时的最佳实践。

使用 ZooKeeper 时遵循最佳实践可以帮助保障系统的稳定性、可靠性和性能。以下是一些使用 ZooKeeper 时应考虑的最佳实践:

1. 管理连接

  • 创建最小所需数量的连接:避免为每个客户端或组件创建不必要的连接。尽量重用连接,并在必要时适当关闭它们。
  • 使用会话重连:在适当的会话超时参数配置下,确保处理网络分区和临时故障的时候,客户端能够重新连接到 ZooKeeper 服务。
  • 恰当的会话超时时间:根据应用的需求配置合适的会话超时时间。过短的超时可能导致频繁的会话过期,过长的超时会延迟故障检测。

2. 数据读写

  • 优先使用监听和回调:而不是频繁地主动检查数据变更,这可以减少不必要的网络负载和 ZooKeeper 服务器压力。
  • 控制数据大小:由于 ZooKeeper 不适合存储大数据,因此需要保持存储在节点中数据的大小尽可能小。
  • 合理使用临时节点:临时节点用于表示短暂状态,例如服务注册和选举。它们在会话结束时自动清除,因此不应用于长期数据存储。

3. Znode 结构设计

  • 树形结构规划:有组织地规划 Znode 结构,避免层级过深,以简化路径解析和监听设置。
  • 使用顺序节点进行锁和同步:用 Znode 的顺序性来生成唯一 ID。

4. 容错和监控

  • 监控 ZK 集群状态:监控 ZooKeeper 集群的节点状态、响应时间和系统指标,以便及时发现和解决问题。
  • 避免脑裂:合理设置minSessionTimeouttickTime等参数,以防止网络问题导致 ZooKeeper 集群的分割(脑裂现象)。

5. 性能优化

  • 批量读写:如果需要更新多个节点,尝试使用多个操作一次性提交,降低网络往返次数。
  • 避免热点节点:减少对单个热点节点的读写频率,防止成为性能瓶颈。

6. 安全措施

  • 访问权限控制: 使用 ACL(访问控制列表)来限制对 Znode 的访问。
  • 加密通信:启用 TLS/SSL 来保障客户端与服务器之间的通信安全。
  • 审计日志:记录关键操作的审计日志,以便追踪和安全分析。

7. 避免滥用

  • 限制应用场景:ZooKeeper 适用于协调服务,如服务发现、配置管理和领导者选举等,不适用于高吞吐量的数据存储。
  • 考虑服务压力:ZooKeeper 设计用于管理读多写少的负载环境。频繁写操作会给 ZooKeeper 集群带来较大压力。

8. 测试和容错

  • 灾难恢复计划:确保有明确的备份和灾难恢复流程。
  • 适当的测试:对配置和协调逻辑进行充分的测试,以确保在各种条件下都能可靠运行。

通过遵循这些最佳实践,可以确保你的 ZooKeeper 集群健壮且能够提供一致可靠的服务,同时为你的分布式系统和应用带来预期的效率和效益。

6 ZooKeeper 性能与优化

6.1 ZooKeeper 性能监控应该关注哪些指标?

监控Apache ZooKeeper性能涉及多个指标,它们提供了集群健康状况、服务质量和资源使用的重要信息。以下是一些关键的性能监控指标:

集群状态

  1. 节点角色
    监控每个节点是作为领导者(Leader)、跟随者(Follower)还是观察者(Observer)的角色运行。特别是要注意任何领导者角色的变化,因为这可能意味着发生了领导者选举。

  2. 领导者选举时间
    记录和监控领导者变更过程中的选举时间,确保选举能够迅速完成。

性能指标

  1. 请求延迟
    监控客户端请求的平均延时。高延迟可能是性能瓶颈的信号。

  2. 吞吐量
    监控ZooKeeper服务器处理的事务数,包括读写操作的次数。

资源使用

  1. JVM指标
    包括堆内存使用、垃圾回收(GC)频率和时间。由于ZooKeeper是用Java编写的,监控JVM性能对保障节点稳定性很关键。

  2. 文件描述符使用率
    ZooKeeper为每个客户端连接都维护一个文件描述符,应该确保它未达到系统限制。

  3. 磁盘空间使用
    确保每个ZooKeeper实例上的磁盘空间充足,因为所有的写操作都会记录到事务日志中。

数据及状态

  1. 快照的大小和频率
    ZooKeeper会定期存储内存中的数据快照到磁盘,监控快照大小和生成的频率可以帮助判断数据增长和系统负载问题。

  2. ZNode计数
    监控ZooKeeper中存储的ZNode数量以确保系统未超过其容量。

  3. 队列大小
    监控等待处理的读写请求的队列长度,以确保服务器能高效地处理请求。

网络

  1. 网络流量
    监视进出ZooKeeper服务器的网络流量,确保网络带宽不成为瓶颈。

  2. 跟随者与领导者之间的同步时间
    跟随者定期从领导者同步最新的状态信息,过长的同步时间可能表明问题。

操作

  1. 客户端连接数量
    活动客户端连接的数量,了解ZooKeeper服务的负载情况。

  2. 未完成的请求
    还未完成的请求数,可以反映ZooKeeper是否正在处理大量请求。

方便性工具

  • 四字命令(Four Letter Words):ZooKeeper提供一系列文本协议命令(如“stat”, “ruok”, “srvr”等)来获取服务器状态信息。
  • JMX(Java管理扩展):可以使用JMX监控和管理ZooKeeper实例。

监控这些指标有助于确保ZooKeeper集群的良好健康状况,并能够在问题发生前预警,帮助及时调优或采取行动。

6.2 ZooKeeper 性能优化的技巧有哪些?

优化 ZooKeeper 集群的性能主要包括应对读写负载的优化、网络通信的优化、硬件使用效率的提高以及合理的配置管理。以下是 ZooKeeper 性能优化的一些技巧:

1. 硬件优化

  • 使用 SSD:SSD 显著提高 I/O 性能,对于 ZooKeeper 来说,尤其是对于写操作(因为所有的写操作都需要同步到磁盘)很重要。

  • 足够的 RAM:确保有足够的内存来存储 ZooKeeper 的整个数据队列(DataTree),这样 ZooKeeper 就可以完全在内存中操作,从而提高读取速度。

2. 网络优化

  • 最小化网络延迟:部署 ZooKeeper 服务器时,应确保它们跨足够近的物理距离以减少网络延迟,例如在相同的数据中心或者区域内。

  • 使用高质量网络设备:高性能的网络交换机和网卡可以降低 ZooKeeper 集群的通信开销。

3. 细化配置

  • 客户端数量:限制连接到每个 ZooKeeper 服务器的客户端数量,过多的活跃连接会增加服务器的负担。

  • tickTimetickTime 对于集群的性能有直接影响,它决定了心跳的时间间隔和超时阈值,需要根据实际网络状况来调整。

  • 快照和事务日志:适量地调整 ZooKeeper 操作日志和快照的频率,保证不频繁地写入磁盘。

  • maxClientCnxns:限制每个客户端的最大连接数。

4. 避免不必要的写操作

  • 合并操作:尽量减少写操作,如果可能的话,可以合并多个较小的写操作为单个大的写操作。

  • 使用观察者 wisely:虽然 Watchers 是一个强大的特性,但过度使用 Watchers 会对性能产生负面影响,因为它们会引发大量的回调操作。

5. 应用设计

  • 读写分离:由于读取操作不会导致集群间的数据同步,因此可以让 Leader 节点处理所有的写请求,让 Follower 处理读请求。

  • 负载均衡:在客户端实施负载均衡,将请求均匀地分配给所有 ZooKeeper 服务器。

  • 缓存结果:客户端应缓存查询的结果,以减少对 ZooKeeper 服务器的查询频率。

6. 监控和日志

  • 监控:使用适当的监控工具监控性能指标,包括节点响应时间、处理队列长度等。

  • 日志级别:适当配置 ZooKeeper 的日志级别,避免过多的日志输出影响性能。

7. 节点数目

  • 奇数服务器:在集群中使用奇数个节点有助于更有效地处理脑裂问题,并在恢复时提供更好的性能。

8. 逆向优化

  • 去除非必需特性:比如正向 dns 查找(四字命令‘srvr’有这类信息),这可能引发问题和性能下降。

综合这些方法,可以在大多数情况下显著提升 ZooKeeper 集群的性能。不过,任何优化措施都应该小心进行,并通过测试保证改变后的系统依然稳定。此外,优化往往是相对的,并非所有情况下每条建议都适用,很多时候需要基于集群的特定负载和应用场景来调整。

6.3 描述 ZooKeeper 的负载均衡策略。

Apache ZooKeeper 本身不直接提供负载均衡功能,而是一个集中式服务,用于协调分布式应用中的各种服务。但是,ZooKeeper 可以作为实现负载均衡的基础设施之一,因为它提供了保存和检索服务注册信息的能力,还支持对服务实例的增删查改以及监听这些更改的事件通知。

在利用 ZooKeeper 实现负载均衡的策略中,ZooKeeper 通常承担以下角色:

服务注册和发现

在基于 ZooKeeper 实现的负载均衡模式中,服务的提供者在启动时会在 ZooKeeper 的特定路径(znodes)下注册自己的存在,包括其 IP 地址和端口号。这个路径充当了服务注册表。

动态更新和通知

服务的消费者订阅了注册表路径,并通过 Watcher 机制监听这个路径的变更事件(如服务实例的添加或移除)。因此,同样在 ZooKeeper 中,消费者也能实时地获悉可用的服务实例信息。

客户端负载均衡

基于 ZooKeeper 提供的服务注册和发现,客户端可以实现自己的负载均衡策略,如轮询、随机、加权轮询或最少连接等。当有一个新的请求时,客户端根据负载均衡策略选择一个合适的服务器实例,并将请求发送到该实例。

分布式锁

ZooKeeper 还能用来实现分布式锁,这对于确保在分布式环境中的负载均衡器能够同步地更新状态非常有用。

状态同步

ZooKeeper 可以存储关于各服务实例的元数据,如实例的权重、状态信息等,这些数据可以用来做智能的负载均衡决策。

在实际应用中,ZooKeeper 可能搭配其他组件一起使用以完成负载均衡,这些组件可能包括:

  • 内置于应用程序的负载均衡逻辑
  • 第三方负载均衡器(如 Nginx、HAProxy 或硬件负载均衡器)
  • 服务网格(如 Istio、Linkerd)

需要注意的是,ZooKeeper 本身的性能特性和部署大小会影响到它支持的服务注册和发现的可伸缩性。在设计系统时需要考虑到 ZooKeeper 集群的高可用性和性能。

请明确,尽管 ZooKeeper 可以作为实现负载均衡的支撑组件,但它不是一个专门的负载均衡器。负载均衡的逻辑还需要应用程序自身或专门的负载均衡组件来实现。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

御风行云天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值