From 4dd53cfbe15b24c7cfcbd5f964a5b87b6fa8ada2 Mon Sep 17 00:00:00 2001 From: luoxiang <2806718453@qq.com> Date: Sun, 26 May 2019 19:03:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- notes/Spark_Streaming整合Kafka.md | 323 +++++++++++ notes/Zookeeper_ACL权限控制.md | 20 +- notes/Zookeeper_Java客户端Curator.md | 646 +++++++++++----------- notes/Zookeeper常用Shell命令.md | 17 + notes/Zookeeper简介及核心概念.md | 30 +- pictures/spark-straming-kafka-console.png | Bin 0 -> 21340 bytes 7 files changed, 718 insertions(+), 320 deletions(-) create mode 100644 notes/Spark_Streaming整合Kafka.md create mode 100644 pictures/spark-straming-kafka-console.png diff --git a/README.md b/README.md index 03dc266..5b2743b 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ 1. [Spark Streaming简介](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Spark_Streaming与流处理.md) 2. DStream常用操作详解 3. [Spark Streaming 整合 Flume](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Spark_Streaming整合Flume.md) -4. Spark Streaming 整合 Kafka +4. [Spark Streaming 整合 Kafka](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Spark_Streaming整合Kafka.md) ## 四、Flink diff --git a/notes/Spark_Streaming整合Kafka.md b/notes/Spark_Streaming整合Kafka.md new file mode 100644 index 0000000..975a8d9 --- /dev/null +++ b/notes/Spark_Streaming整合Kafka.md @@ -0,0 +1,323 @@ +# Spark Streaming 整合 Kafka + + + + +## 一、版本说明 + +Spark针对Kafka的不同版本,提供了两套整合方案:`spark-streaming-kafka-0-8`和`spark-streaming-kafka-0-10`,其主要区别如下: + +| | [spark-streaming-kafka-0-8](https://spark.apache.org/docs/latest/streaming-kafka-0-8-integration.html) | [spark-streaming-kafka-0-10](https://spark.apache.org/docs/latest/streaming-kafka-0-10-integration.html) | +| :--------------------------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- | +| Kafka版本 | 0.8.2.1 or higher | 0.10.0 or higher | +| AP状态 | Deprecated
从Spark 2.3.0版本开始,Kafka 0.8支持已被弃用 | Stable | +| 语言支持 | Scala, Java, Python | Scala, Java | +| Receiver DStream | Yes | No | +| Direct DStream | Yes | Yes | +| SSL / TLS Support | No | Yes | +| Offset Commit API(偏移量提交) | No | Yes | +| Dynamic Topic Subscription(动态主题订阅) | No | Yes | + +本文使用的Kafka版本为`kafka_2.12-2.2.0`,故采用第二种方式进行整合。 + +## 二、项目依赖 + +项目采用Maven进行构建,主要依赖如下: + +```xml + + + + org.apache.spark + spark-streaming_${scala.version} + ${spark.version} + + + + org.apache.spark + spark-streaming-kafka-0-10_${scala.version} + 2.4.3 + + + + com.thoughtworks.paranamer + paranamer + 2.8 + + +``` + +> 完整源码见本仓库:https://github.com/heibaiying/BigData-Notes/tree/master/code/spark/spark-streaming-kafka + +## 三、整合Kafka + +通过调用`KafkaUtils`对象的`createDirectStream`方法来创建输入流,完整代码如下: + +```scala +import org.apache.kafka.common.serialization.StringDeserializer +import org.apache.spark.SparkConf +import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe +import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent +import org.apache.spark.streaming.kafka010._ +import org.apache.spark.streaming.{Seconds, StreamingContext} + +/** + * spark streaming 整合 kafka + */ +object KafkaDirectStream { + + def main(args: Array[String]): Unit = { + + val sparkConf = new SparkConf().setAppName("KafkaDirectStream").setMaster("local[2]") + val streamingContext = new StreamingContext(sparkConf, Seconds(5)) + + val kafkaParams = Map[String, Object]( + /* + * 指定broker的地址清单,清单里不需要包含所有的broker地址,生产者会从给定的broker里查找其他broker的信息。 + * 不过建议至少提供两个broker的信息作为容错。 + */ + "bootstrap.servers" -> "hadoop001:9092", + /*键的序列化器*/ + "key.deserializer" -> classOf[StringDeserializer], + /*值的序列化器*/ + "value.deserializer" -> classOf[StringDeserializer], + /*消费者所在分组的ID*/ + "group.id" -> "spark-streaming-group", + /* + * 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理: + * latest: 在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录) + * earliest: 在偏移量无效的情况下,消费者将从起始位置读取分区的记录 + */ + "auto.offset.reset" -> "latest", + /*是否自动提交*/ + "enable.auto.commit" -> (true: java.lang.Boolean) + ) + + /*可以同时订阅多个主题*/ + val topics = Array("spark-streaming-topic") + val stream = KafkaUtils.createDirectStream[String, String]( + streamingContext, + /*位置策略*/ + PreferConsistent, + /*订阅主题*/ + Subscribe[String, String](topics, kafkaParams) + ) + + /*打印输入流*/ + stream.map(record => (record.key, record.value)).print() + + streamingContext.start() + streamingContext.awaitTermination() + } +} +``` + +### 3.1 ConsumerRecord + +这里获得的输入流中每一个Record实际上是`ConsumerRecord `的实例,其包含了Record的所有可用信息,源码如下: + +```scala +public class ConsumerRecord { + + public static final long NO_TIMESTAMP = RecordBatch.NO_TIMESTAMP; + public static final int NULL_SIZE = -1; + public static final int NULL_CHECKSUM = -1; + + /*主题名称*/ + private final String topic; + /*分区编号*/ + private final int partition; + /*偏移量*/ + private final long offset; + /*时间戳*/ + private final long timestamp; + /*时间戳代表的含义*/ + private final TimestampType timestampType; + /*键序列化器*/ + private final int serializedKeySize; + /*值序列化器*/ + private final int serializedValueSize; + /*值序列化器*/ + private final Headers headers; + /*键*/ + private final K key; + /*值*/ + private final V value; + ..... +} +``` + +### 3.2 生产者属性 + +在示例代码中`kafkaParams`封装了Kafka消费者的属性,这些属性和Spark Streaming无关,是Kafka原生API中就有定义的。其中服务器地址、键序列化器和值序列化器是必选的,其他配置是可选的。其余可选的配置项如下: + +#### 1. fetch.min.byte + +消费者从服务器获取记录的最小字节数。如果可用的数据量小于设置值,broker会等待有足够的可用数据时才会把它返回给消费者。 + +#### 2. fetch.max.wait.ms + +broker返回给消费者数据的等待时间。 + +#### 3. max.partition.fetch.bytes + +分区返回给消费者的最大字节数。 + +#### 4. session.timeout.ms + +消费者在被认为死亡之前可以与服务器断开连接的时间。 + +#### 5. auto.offset.reset + +该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理: + +- latest(默认值) :在偏移量无效的情况下,消费者将从其启动之后生成的最新的记录开始读取数据; +- earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录。 + +#### 6. enable.auto.commit + +是否自动提交偏移量,默认值是true,为了避免出现重复数据和数据丢失,可以把它设置为false。 + +#### 7. client.id + +客户端id,服务器用来识别消息的来源。 + +#### 8. max.poll.records + +单次调用call()方法能够返回的记录数量。 + +#### 9. receive.buffer.bytes 和 send.buffer.byte + +这两个参数分别指定TCP socket 接收和发送数据包缓冲区的大小,-1代表使用操作系统的默认值。 + + + +### 3.3 位置策略 + +Spark Streaming中提供了如下三种位置策略,用于指定Kafka主题分区与Spark执行程序Executors之间的分配关系: + ++ **PreferConsistent** : 通用方式,它将在所有的Executors上均匀分配分区; + ++ **PreferBrokers** : 当Spark的Executors与Kafka brokers在同一机器上时可以选择该选项; ++ **PreferFixed** : 可以指定主题分区与特定主机的映射关系,显示地将分区分配到特定的主机,其构造器如下: + +```scala +@Experimental +def PreferFixed(hostMap: collection.Map[TopicPartition, String]): LocationStrategy = + new PreferFixed(new ju.HashMap[TopicPartition, String](hostMap.asJava)) + +@Experimental +def PreferFixed(hostMap: ju.Map[TopicPartition, String]): LocationStrategy = + new PreferFixed(hostMap) +``` + + + +### 3.4 订阅方式 + +Spark Streaming提供了两种主题订阅方式,分别为`Subscribe`和`SubscribePattern`。后者可以使用正则匹配订阅主题的名称。其构造器分别如下: + +```scala +/** + * @param 需要订阅的主题的集合 + * @param Kafka消费者参数 + * @param offsets(可选): 在初始启动时开始的偏移量。如果没有,则将使用保存的偏移量或auto.offset.reset属性的值 + */ +def Subscribe[K, V]( + topics: ju.Collection[jl.String], + kafkaParams: ju.Map[String, Object], + offsets: ju.Map[TopicPartition, jl.Long]): ConsumerStrategy[K, V] = { ... } + +/** + * @param 需要订阅的正则 + * @param Kafka消费者参数 + * @param offsets(可选): 在初始启动时开始的偏移量。如果没有,则将使用保存的偏移量或auto.offset.reset属性的值 + */ +def SubscribePattern[K, V]( + pattern: ju.regex.Pattern, + kafkaParams: collection.Map[String, Object], + offsets: collection.Map[TopicPartition, Long]): ConsumerStrategy[K, V] = { ... } +``` + +在示例代码中,我们实际上并没有指定第三个参数`offsets`,所以程序默认采用的是配置的`auto.offset.reset`属性的值latest,即在偏移量无效的情况下,消费者将从其启动之后生成的最新的记录开始读取数据。 + +### 3.5 提交偏移量 + +在示例代码中,我们将`enable.auto.commit`设置为true,代表自动提交。在某些情况下,你可能需要更高的可靠性,如在业务完全处理完成后再提交偏移量,这时候可以使用手动提交。目前Spark Streaming没有提供关于手动提交偏移量的方法,想要进行手动提交,只能调用Kafka原生的API : + ++ `commitSync`: 用于异步提交; ++ `commitAsync`:用于同步提交。 + +关于编程方面就介绍到这里,下面介绍如果测试整合结果。 + + + +## 四、启动测试 + +### 4.1 创建主题 + +#### 1. 启动Kakfa + +Kafka的运行依赖于zookeeper,需要预先启动,可以启动Kafka内置的zookeeper,也可以启动自己安装的: + +```shell +# zookeeper启动命令 +bin/zkServer.sh start + +# 内置zookeeper启动命令 +bin/zookeeper-server-start.sh config/zookeeper.properties +``` + +启动单节点kafka用于测试: + +```shell +# bin/kafka-server-start.sh config/server.properties +``` + +#### 2. 创建topic + +```shell +# 创建用于测试主题 +bin/kafka-topics.sh --create \ + --bootstrap-server hadoop001:9092 \ + --replication-factor 1 \ + --partitions 1 \ + --topic spark-streaming-topic + +# 查看所有主题 + bin/kafka-topics.sh --list --bootstrap-server hadoop001:9092 +``` + +#### 3. 创建生产者 + +这里创建一个Kafka生产者,用于发送测试数据: + +```shell +bin/kafka-console-producer.sh --broker-list hadoop001:9092 --topic spark-streaming-topic +``` + +### 4.2 本地模式测试 + +这里我直接使用本地模式启动Spark Streaming程序。启动后使用生产者发送数据,从控制台查看结果。 + +从控制台输出中可以看到数据流已经被成功接收,由于采用`kafka-console-producer.sh`发送的数据默认是没有key的,所以key值为null。同时从输出中也可以看到在程序中指定的groupId和程序自动分配的clientId。 + +
+ + + + + +## 参考资料 + +1. https://spark.apache.org/docs/latest/streaming-kafka-0-10-integration.html \ No newline at end of file diff --git a/notes/Zookeeper_ACL权限控制.md b/notes/Zookeeper_ACL权限控制.md index 866c76a..62c8275 100644 --- a/notes/Zookeeper_ACL权限控制.md +++ b/notes/Zookeeper_ACL权限控制.md @@ -1,5 +1,23 @@ # Zookeeper ACL + + + ## 一、前言 为了避免存储在Zookeeper上的数据被其他程序或者人为误修改,Zookeeper提供了ACL(Access Control Lists)进行权限控制。只有拥有对应权限的用户才可以对节点进行增删改查等操作。下文分别介绍使用原生的Shell命令和Apache Curator客户端进行权限设置。 @@ -127,7 +145,7 @@ Authentication is not valid : /hive # 当前主机已经不能访问 "-Dzookeeper.DigestAuthenticationProvider.superDigest=heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s=" ``` -![zookeeper-super](D:\BigData-Notes\pictures\zookeeper-super.png) +
修改完成后需要使用`zkServer.sh restart`重启服务,此时再次访问限制IP的节点: diff --git a/notes/Zookeeper_Java客户端Curator.md b/notes/Zookeeper_Java客户端Curator.md index cdf9fef..ccd9253 100644 --- a/notes/Zookeeper_Java客户端Curator.md +++ b/notes/Zookeeper_Java客户端Curator.md @@ -1,316 +1,334 @@ -# Zookeeper Java 客户端 ——Apache Curator +# Zookeeper Java 客户端 ——Apache Curator + -## 一、基本依赖 - -Curator是Netflix公司开源的一个Zookeeper客户端,目前由Apache进行维护。与Zookeeper原生客户端相比,Curator的抽象层次更高,功能也更加丰富,是Zookeeper使用范围最广的Java客户端。本片文章主要讲解其基本使用,以下项目采用Maven构建,以单元测试的方法进行讲解,相关依赖如下: - -```xml - - - org.apache.curator - curator-framework - 4.0.0 - - - org.apache.curator - curator-recipes - 4.0.0 - - - org.apache.zookeeper - zookeeper - 3.4.13 - - - - junit - junit - 4.12 - - -``` - -> 完整源码见本仓库: https://github.com/heibaiying/BigData-Notes/tree/master/code/Zookeeper/curator - - - -## 二、客户端相关操作 - -### 2.1 创建客户端实例 - -这里使用`@Before`在单元测试执行前创建客户端实例,并使用`@After`在单元测试后关闭客户端连接。 - -```java -public class BasicOperation { - - private CuratorFramework client = null; - private static final String zkServerPath = "192.168.0.226:2181"; - private static final String nodePath = "/hadoop/yarn"; - - @Before - public void prepare() { - // 重试策略 - RetryPolicy retryPolicy = new RetryNTimes(3, 5000); - client = CuratorFrameworkFactory.builder() - .connectString(zkServerPath) - .sessionTimeoutMs(10000).retryPolicy(retryPolicy) - .namespace("workspace").build(); //指定命名空间后,client的所有路径操作都会以/workspace开头 - client.start(); - } - - @After - public void destroy() { - if (client != null) { - client.close(); - } - } -} -``` - -### 2.2 重试策略 - -在连接Zookeeper服务时候,Curator提供了多种重试策略以满足各种需求,所有重试策略继承自`RetryPolicy`接口,如下图: - -![curator-retry-policy](D:\BigData-Notes\pictures\curator-retry-policy.png) - -而这些重试策略类又分为两大类别: - -+ RetryForever :代表一直重试,直到连接成功; -+ SleepingRetry : 基于一定间隔时间的重试,这里以其子类`ExponentialBackoffRetry`为例说明,其构造器如下: - -```java -/** - * @param baseSleepTimeMs 重试之间等待的初始时间 - * @param maxRetries 最大重试次数 - * @param maxSleepMs 每次重试间隔的最长睡眠时间(毫秒) - */ -ExponentialBackoffRetry(int baseSleepTimeMs, int maxRetries, int maxSleepMs) -``` -### 2.3 判断服务状态 - -```scala -@Test -public void getStatus() { - CuratorFrameworkState state = client.getState(); - System.out.println("服务是否已经启动:" + (state == CuratorFrameworkState.STARTED)); -} -``` - - - -## 三、节点增删改查 - -### 3.1 创建节点 - -```java -@Test -public void createNodes() throws Exception { - byte[] data = "abc".getBytes(); - client.create().creatingParentsIfNeeded() - .withMode(CreateMode.PERSISTENT) //节点类型 - .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE) - .forPath(nodePath, data); -} -``` - -创建时可以指定节点类型,这里的节点类型和Zookeeper原生的一致,全部定义在枚举类`CreateMode`中: - -```java -public enum CreateMode { - // 永久节点 - PERSISTENT (0, false, false), - //永久有序节点 - PERSISTENT_SEQUENTIAL (2, false, true), - // 临时节点 - EPHEMERAL (1, true, false), - // 临时有序节点 - EPHEMERAL_SEQUENTIAL (3, true, true); - .... -} -``` - -### 2.2 获取节点信息 - -```scala -@Test -public void getNode() throws Exception { - Stat stat = new Stat(); - byte[] data = client.getData().storingStatIn(stat).forPath(nodePath); - System.out.println("节点数据:" + new String(data)); - System.out.println("节点信息:" + stat.toString()); -} -``` - -如上所示,节点信息被封装在`Stat`类中,其主要属性如下: - -```java -public class Stat implements Record { - private long czxid; - private long mzxid; - private long ctime; - private long mtime; - private int version; - private int cversion; - private int aversion; - private long ephemeralOwner; - private int dataLength; - private int numChildren; - private long pzxid; - ... -} -``` - -每个属性的含义如下: - -| **状态属性** | **说明** | -| -------------- | ------------------------------------------------------------ | -| czxid | 数据节点创建时的事务ID | -| ctime | 数据节点创建时的时间 | -| mzxid | 数据节点最后一次更新时的事务ID | -| mtime | 数据节点最后一次更新时的时间 | -| pzxid | 数据节点的子节点最后一次被修改时的事务ID | -| cversion | 子节点的更改次数 | -| version | 节点数据的更改次数 | -| aversion | 节点的ACL的更改次数 | -| ephemeralOwner | 如果节点是临时节点,则表示创建该节点的会话的SessionID;如果节点是持久节点,则该属性值为0 | -| dataLength | 数据内容的长度 | -| numChildren | 数据节点当前的子节点个数 | - -### 2.3 获取子节点列表 - -```java -@Test -public void getChildrenNodes() throws Exception { - List childNodes = client.getChildren().forPath("/hadoop"); - for (String s : childNodes) { - System.out.println(s); - } -} -``` - -### 2.4 更新节点 - -更新时可以传入版本号也可以不传入,如果传入则类似于乐观锁机制,只有在版本号正确的时候才会被更新。 - -```scala -@Test -public void updateNode() throws Exception { - byte[] newData = "defg".getBytes(); - client.setData().withVersion(0) // 传入版本号,如果版本号错误则拒绝更新操作,并抛出BadVersion异常 - .forPath(nodePath, newData); -} -``` - -### 2.5 删除节点 - -```java -@Test -public void deleteNodes() throws Exception { - client.delete() - .guaranteed() // 如果删除失败,那么在会继续执行,直到成功 - .deletingChildrenIfNeeded() // 如果有子节点,则递归删除 - .withVersion(0) // 传入版本号,如果版本号错误则拒绝删除操作,并抛出BadVersion异常 - .forPath(nodePath); -} -``` - -### 2.6 判断节点是否存在 - -```java -@Test -public void existNode() throws Exception { - // 如果节点存在则返回其状态信息如果不存在则为null - Stat stat = client.checkExists().forPath(nodePath + "aa/bb/cc"); - System.out.println("节点是否存在:" + !(stat == null)); -} -``` - - - -## 三、监听事件 - -### 3.1 创建一次性监听 - -和Zookeeper原生监听一样,使用`usingWatcher`注册的监听是一次性的,即监听只会触发一次,触发后就销毁。示例如下: - -```java -@Test -public void DisposableWatch() throws Exception { - client.getData().usingWatcher(new CuratorWatcher() { - public void process(WatchedEvent event) { - System.out.println("节点" + event.getPath() + "发生了事件:" + event.getType()); - } - }).forPath(nodePath); - Thread.sleep(1000 * 1000); //休眠以观察测试效果 -} -``` - -### 3.2 创建永久监听 - -Curator还提供了创建永久监听的API,其使用方式如下: - -```java -@Test -public void permanentWatch() throws Exception { - // 使用NodeCache包装节点,对其注册的监听作用于节点,且是永久性的 - NodeCache nodeCache = new NodeCache(client, nodePath); - // 通常设置为true, 代表创建nodeCache时,就去获取对应节点的值并缓存 - nodeCache.start(true); - nodeCache.getListenable().addListener(new NodeCacheListener() { - public void nodeChanged() { - ChildData currentData = nodeCache.getCurrentData(); - if (currentData != null) { - System.out.println("节点路径:" + currentData.getPath() + - "数据:" + new String(currentData.getData())); - } - } - }); - Thread.sleep(1000 * 1000); //休眠以观察测试效果 -} -``` - -### 3.3 监听子节点 - -这里以监听`/hadoop`下所有子节点为例,实现方式如下: - -```scala -@Test -public void permanentChildrenNodesWatch() throws Exception { - - // 第三个参数代表除了节点状态外,是否还缓存节点内容 - PathChildrenCache childrenCache = new PathChildrenCache(client, "/hadoop", true); - /* - * StartMode代表初始化方式: - * NORMAL: 异步初始化 - * BUILD_INITIAL_CACHE: 同步初始化 - * POST_INITIALIZED_EVENT: 异步并通知,初始化之后会触发INITIALIZED事件 - */ - childrenCache.start(StartMode.POST_INITIALIZED_EVENT); - - List childDataList = childrenCache.getCurrentData(); - System.out.println("当前数据节点的子节点列表:"); - childDataList.forEach(x -> System.out.println(x.getPath())); - - childrenCache.getListenable().addListener(new PathChildrenCacheListener() { - public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) { - switch (event.getType()) { - case INITIALIZED: - System.out.println("childrenCache初始化完成"); - break; - case CHILD_ADDED: - // 需要注意的是: 即使是之前已经存在的子节点,也会触发该监听,因为会把该子节点加入childrenCache缓存中 - System.out.println("增加子节点:" + event.getData().getPath()); - break; - case CHILD_REMOVED: - System.out.println("删除子节点:" + event.getData().getPath()); - break; - case CHILD_UPDATED: - System.out.println("被修改的子节点的路径:" + event.getData().getPath()); - System.out.println("修改后的数据:" + new String(event.getData().getData())); - break; - } - } - }); - Thread.sleep(1000 * 1000); //休眠以观察测试效果 -} +## 一、基本依赖 + +Curator是Netflix公司开源的一个Zookeeper客户端,目前由Apache进行维护。与Zookeeper原生客户端相比,Curator的抽象层次更高,功能也更加丰富,是Zookeeper使用范围最广的Java客户端。本片文章主要讲解其基本使用,以下项目采用Maven构建,以单元测试的方法进行讲解,相关依赖如下: + +```xml + + + org.apache.curator + curator-framework + 4.0.0 + + + org.apache.curator + curator-recipes + 4.0.0 + + + org.apache.zookeeper + zookeeper + 3.4.13 + + + + junit + junit + 4.12 + + +``` + +> 完整源码见本仓库: https://github.com/heibaiying/BigData-Notes/tree/master/code/Zookeeper/curator + + + +## 二、客户端相关操作 + +### 2.1 创建客户端实例 + +这里使用`@Before`在单元测试执行前创建客户端实例,并使用`@After`在单元测试后关闭客户端连接。 + +```java +public class BasicOperation { + + private CuratorFramework client = null; + private static final String zkServerPath = "192.168.0.226:2181"; + private static final String nodePath = "/hadoop/yarn"; + + @Before + public void prepare() { + // 重试策略 + RetryPolicy retryPolicy = new RetryNTimes(3, 5000); + client = CuratorFrameworkFactory.builder() + .connectString(zkServerPath) + .sessionTimeoutMs(10000).retryPolicy(retryPolicy) + .namespace("workspace").build(); //指定命名空间后,client的所有路径操作都会以/workspace开头 + client.start(); + } + + @After + public void destroy() { + if (client != null) { + client.close(); + } + } +} +``` + +### 2.2 重试策略 + +在连接Zookeeper服务时候,Curator提供了多种重试策略以满足各种需求,所有重试策略继承自`RetryPolicy`接口,如下图: + +
+ +而这些重试策略类又分为两大类别: + ++ RetryForever :代表一直重试,直到连接成功; ++ SleepingRetry : 基于一定间隔时间的重试,这里以其子类`ExponentialBackoffRetry`为例说明,其构造器如下: + +```java +/** + * @param baseSleepTimeMs 重试之间等待的初始时间 + * @param maxRetries 最大重试次数 + * @param maxSleepMs 每次重试间隔的最长睡眠时间(毫秒) + */ +ExponentialBackoffRetry(int baseSleepTimeMs, int maxRetries, int maxSleepMs) +``` +### 2.3 判断服务状态 + +```scala +@Test +public void getStatus() { + CuratorFrameworkState state = client.getState(); + System.out.println("服务是否已经启动:" + (state == CuratorFrameworkState.STARTED)); +} +``` + + + +## 三、节点增删改查 + +### 3.1 创建节点 + +```java +@Test +public void createNodes() throws Exception { + byte[] data = "abc".getBytes(); + client.create().creatingParentsIfNeeded() + .withMode(CreateMode.PERSISTENT) //节点类型 + .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE) + .forPath(nodePath, data); +} +``` + +创建时可以指定节点类型,这里的节点类型和Zookeeper原生的一致,全部定义在枚举类`CreateMode`中: + +```java +public enum CreateMode { + // 永久节点 + PERSISTENT (0, false, false), + //永久有序节点 + PERSISTENT_SEQUENTIAL (2, false, true), + // 临时节点 + EPHEMERAL (1, true, false), + // 临时有序节点 + EPHEMERAL_SEQUENTIAL (3, true, true); + .... +} +``` + +### 2.2 获取节点信息 + +```scala +@Test +public void getNode() throws Exception { + Stat stat = new Stat(); + byte[] data = client.getData().storingStatIn(stat).forPath(nodePath); + System.out.println("节点数据:" + new String(data)); + System.out.println("节点信息:" + stat.toString()); +} +``` + +如上所示,节点信息被封装在`Stat`类中,其主要属性如下: + +```java +public class Stat implements Record { + private long czxid; + private long mzxid; + private long ctime; + private long mtime; + private int version; + private int cversion; + private int aversion; + private long ephemeralOwner; + private int dataLength; + private int numChildren; + private long pzxid; + ... +} +``` + +每个属性的含义如下: + +| **状态属性** | **说明** | +| -------------- | ------------------------------------------------------------ | +| czxid | 数据节点创建时的事务ID | +| ctime | 数据节点创建时的时间 | +| mzxid | 数据节点最后一次更新时的事务ID | +| mtime | 数据节点最后一次更新时的时间 | +| pzxid | 数据节点的子节点最后一次被修改时的事务ID | +| cversion | 子节点的更改次数 | +| version | 节点数据的更改次数 | +| aversion | 节点的ACL的更改次数 | +| ephemeralOwner | 如果节点是临时节点,则表示创建该节点的会话的SessionID;如果节点是持久节点,则该属性值为0 | +| dataLength | 数据内容的长度 | +| numChildren | 数据节点当前的子节点个数 | + +### 2.3 获取子节点列表 + +```java +@Test +public void getChildrenNodes() throws Exception { + List childNodes = client.getChildren().forPath("/hadoop"); + for (String s : childNodes) { + System.out.println(s); + } +} +``` + +### 2.4 更新节点 + +更新时可以传入版本号也可以不传入,如果传入则类似于乐观锁机制,只有在版本号正确的时候才会被更新。 + +```scala +@Test +public void updateNode() throws Exception { + byte[] newData = "defg".getBytes(); + client.setData().withVersion(0) // 传入版本号,如果版本号错误则拒绝更新操作,并抛出BadVersion异常 + .forPath(nodePath, newData); +} +``` + +### 2.5 删除节点 + +```java +@Test +public void deleteNodes() throws Exception { + client.delete() + .guaranteed() // 如果删除失败,那么在会继续执行,直到成功 + .deletingChildrenIfNeeded() // 如果有子节点,则递归删除 + .withVersion(0) // 传入版本号,如果版本号错误则拒绝删除操作,并抛出BadVersion异常 + .forPath(nodePath); +} +``` + +### 2.6 判断节点是否存在 + +```java +@Test +public void existNode() throws Exception { + // 如果节点存在则返回其状态信息如果不存在则为null + Stat stat = client.checkExists().forPath(nodePath + "aa/bb/cc"); + System.out.println("节点是否存在:" + !(stat == null)); +} +``` + + + +## 三、监听事件 + +### 3.1 创建一次性监听 + +和Zookeeper原生监听一样,使用`usingWatcher`注册的监听是一次性的,即监听只会触发一次,触发后就销毁。示例如下: + +```java +@Test +public void DisposableWatch() throws Exception { + client.getData().usingWatcher(new CuratorWatcher() { + public void process(WatchedEvent event) { + System.out.println("节点" + event.getPath() + "发生了事件:" + event.getType()); + } + }).forPath(nodePath); + Thread.sleep(1000 * 1000); //休眠以观察测试效果 +} +``` + +### 3.2 创建永久监听 + +Curator还提供了创建永久监听的API,其使用方式如下: + +```java +@Test +public void permanentWatch() throws Exception { + // 使用NodeCache包装节点,对其注册的监听作用于节点,且是永久性的 + NodeCache nodeCache = new NodeCache(client, nodePath); + // 通常设置为true, 代表创建nodeCache时,就去获取对应节点的值并缓存 + nodeCache.start(true); + nodeCache.getListenable().addListener(new NodeCacheListener() { + public void nodeChanged() { + ChildData currentData = nodeCache.getCurrentData(); + if (currentData != null) { + System.out.println("节点路径:" + currentData.getPath() + + "数据:" + new String(currentData.getData())); + } + } + }); + Thread.sleep(1000 * 1000); //休眠以观察测试效果 +} +``` + +### 3.3 监听子节点 + +这里以监听`/hadoop`下所有子节点为例,实现方式如下: + +```scala +@Test +public void permanentChildrenNodesWatch() throws Exception { + + // 第三个参数代表除了节点状态外,是否还缓存节点内容 + PathChildrenCache childrenCache = new PathChildrenCache(client, "/hadoop", true); + /* + * StartMode代表初始化方式: + * NORMAL: 异步初始化 + * BUILD_INITIAL_CACHE: 同步初始化 + * POST_INITIALIZED_EVENT: 异步并通知,初始化之后会触发INITIALIZED事件 + */ + childrenCache.start(StartMode.POST_INITIALIZED_EVENT); + + List childDataList = childrenCache.getCurrentData(); + System.out.println("当前数据节点的子节点列表:"); + childDataList.forEach(x -> System.out.println(x.getPath())); + + childrenCache.getListenable().addListener(new PathChildrenCacheListener() { + public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) { + switch (event.getType()) { + case INITIALIZED: + System.out.println("childrenCache初始化完成"); + break; + case CHILD_ADDED: + // 需要注意的是: 即使是之前已经存在的子节点,也会触发该监听,因为会把该子节点加入childrenCache缓存中 + System.out.println("增加子节点:" + event.getData().getPath()); + break; + case CHILD_REMOVED: + System.out.println("删除子节点:" + event.getData().getPath()); + break; + case CHILD_UPDATED: + System.out.println("被修改的子节点的路径:" + event.getData().getPath()); + System.out.println("修改后的数据:" + new String(event.getData().getData())); + break; + } + } + }); + Thread.sleep(1000 * 1000); //休眠以观察测试效果 +} ``` \ No newline at end of file diff --git a/notes/Zookeeper常用Shell命令.md b/notes/Zookeeper常用Shell命令.md index 48e7644..077dbab 100644 --- a/notes/Zookeeper常用Shell命令.md +++ b/notes/Zookeeper常用Shell命令.md @@ -1,5 +1,22 @@ # Zookeeper常用Shell命令 + + + ## 一、节点增删改查 ### 1.1 启动服务和连接服务 diff --git a/notes/Zookeeper简介及核心概念.md b/notes/Zookeeper简介及核心概念.md index 2c2c457..57ad26f 100644 --- a/notes/Zookeeper简介及核心概念.md +++ b/notes/Zookeeper简介及核心概念.md @@ -1,5 +1,27 @@ # Zookeeper简介及核心概念 + + + ## 一、Zookeeper简介 Zookeeper是一个开源的分布式协调服务,由雅虎创建,是Google Chubby的开源实现。Zookeeper可以用于实现分布式系统中常见的发布/订阅、负载均衡、命令服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等功能。Zookeeeper可以保证如下的分布式一致性特性: @@ -20,13 +42,13 @@ Zookeeper致力于为那些高吞吐的大型分布式系统提供一个高性 Zookeeper通过树形结构来存储数据,它由一系列被称为ZNode的数据节点组成,类似于常见的文件系统。不过和常见的文件系统不同,Zookeeper将数据全量存储在内存中,以此来实现高吞吐,减少访问延迟。 -![zookeeper-zknamespace](D:\BigData-Notes\pictures\zookeeper-zknamespace.jpg) +
### 2.2 目标二:构建集群 可以由一组Zookeeper服务构成Zookeeper服务集群,集群中每台机器都会单独在内存中维护自身的状态,并且每台机器之间都保持着通讯,只要集群中有半数机器能够正常工作,那么整个集群就可以正常提供服务。 -![zookeeper-zkservice](D:\BigData-Notes\pictures\zookeeper-zkservice.jpg) +
### 2.3 目标三:顺序访问 @@ -105,7 +127,7 @@ ZAB协议是Zookeeper专门设计的一种支持崩溃恢复的原子广播协 Zookeeper使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用原子广播协议将数据的状态变更以事务Proposal的形式广播到所有的副本进程上去。如下图: -![zookeeper-zkcomponents](D:\BigData-Notes\pictures\zookeeper-zkcomponents.jpg) +
具体流程如下: @@ -125,7 +147,7 @@ ZAB协议的消息广播过程使用的是原子广播协议。在整个消息 Leader服务会为每一个Follower服务器分配一个单独的队列,然后将事务Proposal依次放入队列中,并根据FIFO(先进先出)的策略进行消息发送。Follower服务在接收到Proposal后,会将其以事务日志的形式写入本地磁盘中,并在写入成功后反馈给Leader一个Ack响应。当Leader接收到超过半数Follower的Ack响应后,就会广播一个Commit消息给所有的Follower以通知其进行事务提交,之后Leader自身也会完成对事务的提交。而每一个Follower则在接收到Commit消息后,完成事务的提交。 -![zookeeper-brocast](D:\BigData-Notes\pictures\zookeeper-brocast.jpg) +
diff --git a/pictures/spark-straming-kafka-console.png b/pictures/spark-straming-kafka-console.png new file mode 100644 index 0000000000000000000000000000000000000000..a277a96cfee0504ecc4cbb0e6dc4c96a572fe603 GIT binary patch literal 21340 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