kafka2.11入门
引言
在很多领域,如股市走向分析, 气象数据测控,网站用户行为分析等,由于数据产生快,实时性强,数据量大,所以很难统一采集并入库存储后再做处理,这便导致传统的数据处理架构不能满足需要。流计算的出现,就是为了更好地解决这类数据在处理过程中遇到的问题。与传统架构不同,流计算模型在数据流动的过程中实时地进行捕捉和处理,并根据业务需求对数据进行计算分析,最终把结果保存或者分发给需要的组件。本文将从实时数据产生和流向的各个环节出发,通过一个具有实际意义的案例,向读者介绍如何使用 Apache Kafka 和 Spark Streaming 模块构建一个实时的数据处理系统,当然本文只是抛砖引玉,因为构建一个良好健壮的实时数据处理系统并不是一篇文章可以说清楚的。在阅读本文前,假设您已经对 Apache Kafka 分布式消息系统有了基本的了解,并且可以使用 Spark Streaming API 进行简单的编程。接下来,就让我们一起看看如何构建一个简易的实时数据处理系统吧。
关于 Kafka
Kafka 是一个分布式的,高吞吐量,易于扩展地基于主题发布/订阅的消息系统,最早是由 Linkedin 开发,并于 2011 年开源并贡献给 Apache 软件基金会。一般来说,Kafka 有以下几个典型的应用场景:
- 作为消息队列。由于 Kafka 拥有高吞吐量,并且内置消息主题分区,备份,容错等特性,使得它更适合使用在大规模,高强度的消息数据处理的系统中。
- 流计算系统的数据源。流数据产生系统作为 Kafka 消息数据的生产者将数据流分发给 Kafka 消息主题,流数据计算系统 (Storm,Spark Streaming 等) 实时消费并计算数据。这也是本文将要介绍的应用场景。
- 系统用户行为数据源。这种场景下,系统将用户的行为数据,如访问页面,停留时间,搜索日志,感兴趣的话题等数据实时或者周期性的发布到 Kafka 消息主题,作为对接系统数据的来源。
- 日志聚集。Kafka 可以作为一个日志收集系统的替代解决方案,我们可以将系统日志数据按类别汇集到不同的 Kafka 消息主题中。
- 事件源。在基于事件驱动的系统中,我们可以将事件设计成合理的格式,作为 Kafka 消息数据存储起来,以便相应系统模块做实时或者定期处理。由于 Kafka 支持大数据量存储,并且有备份和容错机制,所以可以让事件驱动型系统更加健壮和高效。
当然 Kafka 还可以支持其他的应用场景,在这里我们就不一一罗列了。关于 Kafka 更详细的介绍,请读者参考Kafka 官网。需要指出的是,本文使用的 Kafka 版本是基于 Scala 2.10 版本构建的 0.8.2.1 版本。
常用命令
./bin/kafka-topics.sh --delete --topic test --zookeeper localhost:2181 # 删除主题
Kafka单机部署
官方文档:https://kafka.apache.org/quickstart
Kafka允许远程访问
0.8版本有两个配置参数需要修改:
advertised.host.name=192.168.1.13 #<==kafka开放到外网的IP(本地IP地址) advertised.port=9092 #<==9092映射到外网后的端口
修改配置文件config/server.properties,最新版本0.10.x broker配置弃用了advertised.host.name 和 advertised.port 这两个个配置项,就配置advertised.listeners就可以了:
advertised.listeners=PLAINTEST://192.168.1.13:9092 # 本地IP地址:端口
需要知晓这些参数的同学可以参考:kafka配置文件详解
Kafka 集群搭建步骤
准备环境
使用VMware安装3台Linux服务器
#关闭防火墙,每台机器都操作一遍 service iptables stop && chkconfig iptables off #配置/etc/hosts文件 192.168.140.128 node01 zk01 kafka01 192.168.140.129 node02 zk02 kafka02 192.168.140.130 node03 zk03 kafka03 # 拷贝到其他机器 scp /etc/hosts node02:/etc/ scp /etc/hosts node03:/etc/
免密登录
yum -y install openssh-clients ssh-keygen 四个回车 ssh-copy-id node1 ssh-copy-id node2 ssh-copy-id node3
zookeeper集群搭建
# 下载、解压、安装zookeeper wget http://219.238.7.73/files/703900000A354B91/apache.fayea.com/zookeeper/zookeeper-3.4.9/zookeeper-3.4.9.tar.gz tar -zxvf zookeeper-3.4.9.tar.gz mv zookeeper-3.4.9 /opt/zookeeper # 拷贝到其他机器 scp -r /opt/zookeeper node02:/opt/zookeeper scp -r /opt/zookeeper node03:/opt/zookeeper # 为每个zookeeper配置唯一ID touch /opt/data/zk/myid on node01、node02、node03 echo 1 > /opt/data/zk/myid on node01 echo 2 > /opt/data/zk/myid on node02 echo 3 > /opt/data/zk/myid on node03 # 配置zookeeper cd /opt/zookeeper/conf/ touch zoo.cfg vi zoo.cfg tickTime=2000 initLimit=10 syncLimit=5 dataDir=/export/data/zk dataLogDir=/export/logs/zk clientPort=2181 server.1=node01:2887:3887 server.2=node02:2887:3887 server.3=node03:2887:3887 # 把配置文件拷贝到其他机器 scp zoo.cfg node02:$PWD scp zoo.cfg node03:$PWD # 启动zookeeper zkServer.sh start on node01、node02、node03 zkServer.sh status on node01、node02、node03
详情: https://www.cnblogs.com/zhumengke/articles/10514770.html#cluster
JDK安装
# 解压JDK tar -zxvf jdk-8u141-linux-x64.tar.gz mv jdk-8u141-linux-x64 /opt/jdk8 # 配置环境变量 /etc/profile export JAVA_HOME=/opt/jdk8 export JRE_HOME=${JAVA_HOME}/jre export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib export PATH=${JAVA_HOME}/bin:$PATH #source /etc/profile # 拷贝到其他机器 scp -r jdk node02:/opt/jdk8 scp -r jdk node03:/opt/jdk8 scp /etc/profile node02:/etc/ scp /etc/profile node03:/etc/ #source /etc/profile on node01、node02、node03 java -version on node01 on node01、node02、node03
下载并安装 kafka_2.10-0.8.2.1
下载地址: https://kafka.apache.org/downloads.html
wget http://219.238.7.67/files/518200000AE89181/mirror.bit.edu.cn/apache/kafka/1.0.0/kafka_2.11-1.0.0.tgz tar -zxvf kafka_2.11-1.0.0.tgz mv kafka_2.11-1.0.0 /opt/kafka
配置Kafka(config/server.properties 文件)
#broker的全局唯一编号,不能重复 broker.id=0 #用来监听链接的端口,producer或consumer将在此端口建立连接 port=9092 #处理网络请求的线程数量 num.network.threads=3 #用来处理磁盘IO的现成数量 num.io.threads=8 #发送套接字的缓冲区大小 socket.send.buffer.bytes=102400 #接受套接字的缓冲区大小 socket.receive.buffer.bytes=102400 #请求套接字的缓冲区大小 socket.request.max.bytes=104857600 #kafka运行日志存放的路径 log.dirs=/export/servers/logs/kafka #topic在当前broker上的分片个数 num.partitions=2 #用来恢复和清理data下数据的线程数量 num.recovery.threads.per.data.dir=1 #segment文件保留的最长时间,超时将被删除 log.retention.hours=168 #滚动生成新的segment文件的最大时间 log.roll.hours=168 #日志文件中每个segment的大小,默认为1G log.segment.bytes=1073741824 #周期性检查文件大小的时间 log.retention.check.interval.ms=300000 #日志清理是否打开 log.cleaner.enable=true #broker需要使用zookeeper保存meta数据 zookeeper.connect=zk01:2181,zk02:2181,zk03:2181 #zookeeper链接超时时间 zookeeper.connection.timeout.ms=6000 #partion buffer中,消息的条数达到阈值,将触发flush到磁盘 log.flush.interval.messages=10000 #消息buffer的时间,达到阈值,将触发flush到磁盘 log.flush.interval.ms=3000 #删除topic需要server.properties中设置delete.topic.enable=true否则只是标记删除 delete.topic.enable=true #此处的host.name为本机IP(重要),如果不改,则客户端会抛出:Producer connection to localhost:9092 unsuccessful 错误! host.name=kafka01 advertised.host.name=192.168.239.128
Kafka Broker 配置项
- broker.id:Kafka broker 的唯一标识,集群中不能重复。
- port: Broker 的监听端口,用于监听 Producer 或者 Consumer 的连接。
- log.dirs:日志保存目录。
- zookeeper.contact: Broker 作为 zookeeper 的 client,可以连接的 zookeeper 的地址信息。
- host.name:当前 Broker 服务器的 IP 地址或者机器名。
- advertised.host.name=192.168.239.128 当前主机的ip
Kafka Producer 配置项(编辑 config/producer.properties 文件)
broker.list=192.168.1.1:9092,192.168.1.2:9092,192.168.1.3:9092 producer.type=async
这些配置项解释如下:
- broker.list:集群中 Broker 地址列表。
- producer.type: Producer 类型,async 异步生产者,sync 同步生产者。
Kafka Consumer 配置项(编辑 config/consumer.properties 文件)
zookeeper.contact=192.168.1.1:2181,192.168.1.2:2181,192.168.1.3:2181
配置项解释如下:
- zookeeper.contact: Consumer 可以连接的 zookeeper 服务器地址列表。
上传修改好的安装包到其他机器
至此,我们已经在 192.168.1.1 机器上修改好了所有需要的配置文件,那么接下来请用以下命令打包该 Kafka 安装包,并上传至 192.168.1.2 和 192.168.1.3 两台机器上。
# 免密拷贝 tar –cvf kafka_2.10-0.8.2.1.tar ./kafka_2.10-0.8.2. scp -r kafka node03:/opt/kafka/ scp -r kafka node03:/opt/kafka/ # 带用户名和密码拷贝 tar –cvf kafka_2.10-0.8.2.1.tar ./kafka_2.10-0.8.2.1 scp ./kafka_2.10-0.8.2.1.tar username@192.168.1.2:/opt/kafka/ scp ./kafka_2.10-0.8.2.1.tar username@192.168.1.3:/opt/kafka/
上传完成后,我们需要到 192.168.1.2 和 192.168.1.3 两台机器上解压刚才上传的 tar 包,命令如清单一。之后需要分别在两台机器上修改 config/server.properties 文件中的 broker.id 和 host.name. broker.id,可以分别复制 1 和 2,host.name 需要改成当前机器的 IP。
单个启动
nohup bin/kafka-server-start.sh config/server.properties & on node01、node02、node03
脚本启动
配置KAFKA_HOME
#set KAFKA_HOME export KAFKA_HOME=/export/app/kafka_2.11-1.0.0 export PATH=$PATH:$KAFKA_HOME/bin
创建一键启动脚本文件
mkdir -r /opt/app/onkey/kafka
创建三个脚本
vi slave node01 node02 node03 vi startkafka.sh cat /export/app/onkey/kafka/slave | while read line do { echo $line ssh $line "source /etc/profile;nohup kafka-server-start.sh /export/servers/kafka/config/server.properties >/dev/null 2>&1 &" }& wait done vi stopkafka.sh cat /export/app/onkey/kafka/slave | while read line do { echo $line ssh $line "source /etc/profile;jps |grep Kafka |cut -c 1-4 |xargs kill -s 9 " }& wait done
给予权限
chomd 777 startkafka.sh & stopkafka.sh
启动 zookeeper 和 Kafka 服务
分别在三台机器上运行下面命令启动 zookeeper 和 Kafka 服务。
启动 zookeeper 服务
nohup bin/zookeeper-server-start.sh config/zookeeper.properties &
启动 kafka 服务
nohup bin/kafka-server-start.sh config/server.properties &
验证安装
我们的验证步骤有两个。
第一步,分别在三台机器上使用下面命令查看是否有 Kafka 和 zookeeper 相关服务进程。
查看 Kafka 和 zookeeper 服务进程
ps –ef | grep kafka
第二步,创建消息主题,并通过 console producer 和 console consumer 验证消息可以被正常的生产和消费。
创建消息主题
bin/kafka-topics.sh --create \ --replication-factor 3 \ --partition 3 \ --topic user-behavior-topic \ --zookeeper 192.168.1.1:2181,192.168.1.2:2181,192.168.1.3:2181
运行下面命令打开打开 console producer。
启动 Console Producer
bin/kafka-console-producer.sh --broker-list 192.168.1.1:9092 --topic user-behavior-topic
在另一台机器打开 console consumer。
启动 Console Consumer
./kafka-console-consumer.sh --zookeeper 192.168.1.2:2181 --topic user-behavior-topic --from-beginning
然后如果在 producer console 输入一条消息,能从 consumer console 看到这条消息就代表安装是成功的。
List topic from another zookeeper
$ bin/kafka-topics.sh --list --zookeeper 10.0.0.5:2181 roytest
Describe topic
$ bin/kafka-topics.sh --zookeeper 10.0.0.7:2181,10.0.0.5:2181,10.0.0.6:2181 --describe --topic roytest Topic:roytest PartitionCount:2 ReplicationFactor:2 Configs: Topic: roytest Partition: 0 Leader: 1 Replicas: 1,2 Isr: 1,2 Topic: roytest Partition: 1 Leader: 2 Replicas: 2,3 Isr:
压力测试
$ bin/kafka-producer-perf-test.sh --num-records 10000000 --record-size 1024 --topic roytest --throughput 1000000 --producer-props bootstrap.servers=10.0.0.5:9092,10.0.0.6:9092,10.0.0.7:9092 batch.size=100000 209482 records sent, 41879.6 records/sec (40.90 MB/sec), 582.7 ms avg latency, 948.0 max latency. 220600 records sent, 44111.2 records/sec (43.08 MB/sec), 537.8 ms avg latency, 2951.0 max latency. 238177 records sent, 47635.4 records/sec (46.52 MB/sec), 720.9 ms avg latency, 2965.0 max latency. 258240 records sent, 51637.7 records/sec (50.43 MB/sec), 741.3 ms avg latency, 3048.0 max latency. 412224 records sent, 82428.3 records/sec (80.50 MB/sec), 395.2 ms avg latency, 681.0 max latency.
案例介绍与编程实现
1. 案例介绍
该案例中,我们假设某论坛需要根据用户对站内网页的点击量,停留时间,以及是否点赞,来近实时的计算网页热度,进而动态的更新网站的今日热点模块,把最热话题的链接显示其中。
2. 案例分析
对于某一个访问论坛的用户,我们需要对他的行为数据做一个抽象,以便于解释网页话题热度的计算过程。
首先,我们通过一个向量来定义用户对于某个网页的行为即点击的网页,停留时间,以及是否点赞,可以表示如下:
(page001.html, 1, 0.5, 1)
向量的第一项表示网页的 ID,第二项表示从进入网站到离开对该网页的点击次数,第三项表示停留时间,以分钟为单位,第四项是代表是否点赞,1 为赞,-1 表示踩,0 表示中立。
其次,我们再按照各个行为对计算网页话题热度的贡献,给其设定一个权重,在本文中,我们假设点击次数权重是 0.8,因为用户可能是由于没有其他更好的话题,所以再次浏览这个话题。停留时间权重是 0.8,因为用户可能同时打开多个 tab 页,但他真正关注的只是其中一个话题。是否点赞权重是 1,因为这一般表示用户对该网页的话题很有兴趣。
最后,我们定义用下列公式计算某条行为数据对于该网页热度的贡献值。
f(x,y,z)=0.8x+0.8y+z
那么对于上面的行为数据 (page001.html, 1, 0.5, 1),利用公式可得:
H(page001)=f(x,y,z)= 0.8x+0.8y+z=0.8*1+0.8*0.5+1*1=2.2
读者可以留意到,在这个过程中,我们忽略了用户本身,也就是说我们不关注用户是谁,而只关注它对于网页热度所做的贡献。
3. 生产行为数据消息
在本案例中我们将使用一段程序来模拟用户行为,该程序每隔 5 秒钟会随机的向 user-behavior-topic 主题推送 0 到 50 条行为数据消息,显然,这个程序扮演消息生产者的角色,在实际应用中,这个功能一般会由一个系统来提供。为了简化消息处理,我们定义消息的格式如下:
网页 ID|点击次数|停留时间 (分钟)|是否点赞
并假设该网站只有 100 个网页。以下是该类的 Scala 实现源码。
清单 14. UserBehaviorMsgProducer 类源码
import scala.util.Random import java.util.Properties import kafka.producer.KeyedMessage import kafka.producer.ProducerConfig import kafka.producer.Producer class UserBehaviorMsgProducer(brokers: String, topic: String) extends Runnable { private val brokerList = brokers private val targetTopic = topic private val props = new Properties() props.put("metadata.broker.list", this.brokerList) props.put("serializer.class", "kafka.serializer.StringEncoder") props.put("producer.type", "async") private val config = new ProducerConfig(this.props) private val producer = new Producer[String, String](this.config) private val PAGE_NUM = 100 private val MAX_MSG_NUM = 3 private val MAX_CLICK_TIME = 5 private val MAX_STAY_TIME = 10 //Like,1;Dislike -1;No Feeling 0 private val LIKE_OR_NOT = Array[Int](1, 0, -1) def run(): Unit = { val rand = new Random() while (true) { //how many user behavior messages will be produced val msgNum = rand.nextInt(MAX_MSG_NUM) + 1 try { //generate the message with format like page1|2|7.123|1 for (i <- 0 to msgNum) { var msg = new StringBuilder() msg.append("page" + (rand.nextInt(PAGE_NUM) + 1)) msg.append("|") msg.append(rand.nextInt(MAX_CLICK_TIME) + 1) msg.append("|") msg.append(rand.nextInt(MAX_CLICK_TIME) + rand.nextFloat()) msg.append("|") msg.append(LIKE_OR_NOT(rand.nextInt(3))) println(msg.toString()) //send the generated message to broker sendMessage(msg.toString()) } println("%d user behavior messages produced.".format(msgNum+1)) } catch { case e: Exception => println(e) } try { //sleep for 5 seconds after send a micro batch of message Thread.sleep(5000) } catch { case e: Exception => println(e) } } } def sendMessage(message: String) = { try { val data = new KeyedMessage[String, String](this.topic, message); producer.send(data); } catch { case e:Exception => println(e) } } } object UserBehaviorMsgProducerClient { def main(args: Array[String]) { if (args.length < 2) { println("Usage:UserBehaviorMsgProducerClient 192.168.1.1:9092 user-behavior-topic") System.exit(1) } //start the message producer thread new Thread(new UserBehaviorMsgProducer(args(0), args(1))).start() } }
4. 编写 Spark Streaming 程序消费消息
在弄清楚了要解决的问题之后,就可以开始编码实现了。对于本案例中的问题,在实现上的基本步骤如下:
- 构建 Spark 的 StreamingContext 实例,并且开启 checkpoint 功能。因为我们需要使用 updateStateByKey 原语去累计的更新网页话题的热度值。
- 利用 Spark 提供的 KafkaUtils.createStream 方法消费消息主题,这个方法会返回 ReceiverInputDStream 对象实例。
- 对于每一条消息,利用上文的公式计算网页话题的热度值。
- 定义一个匿名函数去把网页热度上一次的计算结果值和新计算的值相加,得到最新的热度值。
- 调用 updateStateByKey 原语并传入上面定义的匿名函数更新网页热度值。
- 最后得到最新结果后,需要对结果进行排序,最后打印热度值最高的 10 个网页。
源代码如下。
清单 15. WebPagePopularityValueCalculator 类源码
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
import org.apache.spark.SparkConfimport org.apache.spark.streaming.Secondsimport org.apache.spark.streaming.StreamingContextimport org.apache.spark.streaming.kafka.KafkaUtilsimport org.apache.spark.HashPartitionerimport org.apache.spark.streaming.Durationobject WebPagePopularityValueCalculator { private val checkpointDir = "popularity-data-checkpoint" private val msgConsumerGroup = "user-behavior-topic-message-consumer-group" def main(args: Array[String]) { if (args.length < 2) { println("Usage:WebPagePopularityValueCalculator zkserver1:2181, zkserver2:2181,zkserver3:2181 consumeMsgDataTimeInterval(secs)") System.exit(1) } val Array(zkServers,processingInterval) = args val conf = new SparkConf().setAppName("Web Page Popularity Value Calculator") val ssc = new StreamingContext(conf, Seconds(processingInterval.toInt)) //using updateStateByKey asks for enabling checkpoint ssc.checkpoint(checkpointDir) val kafkaStream = KafkaUtils.createStream( //Spark streaming context ssc, //zookeeper quorum. e.g zkserver1:2181,zkserver2:2181,... zkServers, //kafka message consumer group ID msgConsumerGroup, //Map of (topic_name -> numPartitions) to consume. Each partition is consumed in its own thread Map("user-behavior-topic" -> 3)) val msgDataRDD = kafkaStream.map(_._2) //for debug use only //println("Coming data in this interval...") //msgDataRDD.print() // e.g page37|5|1.5119122|-1 val popularityData = msgDataRDD.map { msgLine => { val dataArr: Array[String] = msgLine.split("\\|") val pageID = dataArr(0) //calculate the popularity value val popValue: Double = dataArr(1).toFloat * 0.8 + dataArr(2).toFloat * 0.8 + dataArr(3).toFloat * 1 (pageID, popValue) } } //sum the previous popularity value and current value val updatePopularityValue = (iterator: Iterator[(String, Seq[Double], Option[Double])]) => { iterator.flatMap(t => { val newValue:Double = t._2.sum val stateValue:Double = t._3.getOrElse(0); Some(newValue + stateValue) }.map(sumedValue => (t._1, sumedValue))) } val initialRDD = ssc.sparkContext.parallelize(List(("page1", 0.00))) val stateDstream = popularityData.updateStateByKey[Double](updatePopularityValue, new HashPartitioner(ssc.sparkContext.defaultParallelism), true, initialRDD) //set the checkpoint interval to avoid too frequently data checkpoint which may //may significantly reduce operation throughput stateDstream.checkpoint(Duration(8*processingInterval.toInt*1000)) //after calculation, we need to sort the result and only show the top 10 hot pages stateDstream.foreachRDD { rdd => { val sortedData = rdd.map{ case (k,v) => (v,k) }.sortByKey(false) val topKData = sortedData.take(10).map{ case (v,k) => (k,v) } topKData.foreach(x => { println(x) }) } } ssc.start() ssc.awaitTermination() }} |
部署和测试
读者可以参考以下步骤部署并测试本案例提供的示例程序。
第一步,启动行为消息生产者程序, 可以直接在 Scala IDE 中启动,不过需要添加启动参数,第一个是 Kafka Broker 地址,第二个是目标消息主题的名称。
图 1. UserBehaviorMsgProducer 类启动参数

启动后,可以看到控制台有行为消息数据生成。
图 2. 生成的行为消息数据预览

第二步,启动作为行为消息消费者的 Spark Streaming 程序,需要在 Spark 集群环境中启动,命令如下:
清单 16. WebPagePopularityValueCalculator 类启动命令
bin/spark-submit \ --jars $SPARK_HOME/lib/spark-streaming-kafka_2.10-1.3.1.jar, \ $SPARK_HOME/lib/spark-streaming-kafka-assembly_2.10-1.3.1.jar, \ $SPARK_HOME/lib/kafka_2.10-0.8.2.1.jar, \ $SPARK_HOME/lib/kafka-clients-0.8.2.1.jar \ --class com.ibm.spark.exercise.streaming.WebPagePopularityValueCalculator --master spark://<spark_master_ip>:7077 \ --num-executors 4 \ --driver-memory 4g \ --executor-memory 2g \ --executor-cores 2 \ /home/fams/sparkexercise.jar \ 192.168.1.1:2181,192.168.1.2:2181,192.168.1.3:2181 2
由于程序中我们要用到或者间接调用 Kafka 的 API,并且需要调用 Spark Streaming 集成 Kafka 的 API(KafkaUtils.createStream), 所以需要提前将启动命令中的 jar 包上传到 Spark 集群的每个机器上 (本例中我们将它们上传到 Spark 安装目录的 lib 目录下,即$SPARK_HOME/lib),并在启动命令中引用它们。
启动后,我们可以看到命令行 console 下面有消息打印出来,即计算的热度值最高的 10 个网页。
图 3. 网页话题热度当前排序预览

我们也可以到 Spark Web Console 上去查看当前 Spark 程序的运行状态, 默认地址为: http://spark_master_ip:8080。
图 4. 查看 Spark Streaming 程序的运行状态

注意事项
利用 Spark Streaming 构建一个高效健壮的流数据计算系统,我们还需要注意以下方面。
- 需要合理的设置数据处理的间隔,即需要保证每一批数据的处理时间必须小于处理间隔,保证在处理下一批数据的时候,前一批已经处理完毕。显然这需要由您的 Spark 集群的计算能力还有 input 数据的量决定。
- 需要尽可能的提升读取 input 数据的能力。在 Spark Streaming 与外部系统如 Kafka,Flume 等集成时,为了避免接收数据环节成为系统的瓶颈,我们可以启动多个 ReceiverInputDStream 对象实例。
- 虽然本文案例中,我们只是把 (近) 实时计算结果打印出来,但是实际上很多时候这些结果会被保存到数据库,HDFS, 或者发送回 Kafka, 以供其他系统利用这些数据做进一步的业务处理。
- 由于流计算对实时性要求很高,所以任何由于 JVM Full GC 引起的系统暂停都是不可接受的。除了在程序中合理使用内存,并且定期清理不需要的缓存数据外,CMS(Concurrent Mark and Sweep) GC 也是被 Spark 官方推荐的 GC 方式,它能有效的把由于 GC 引起的暂停维持在一个在很低的水平。我们可以在使用 spark-submit 命令时通过增加 --driver-java-options 选项来添加 CMS GC 相关的参数。
- 在 Spark 官方提供关于集成 Kafka 和 Spark Streaming 的指导文档中,提到了两种方式,第一种是 Receiver Based Approach,即通过在 Receiver 里实现 Kafka consumer 的功能来接收消息数据;第二种是 Direct Approach, 即不通过 Receiver,而是周期性的主动查询 Kafka 消息分区中的最新 offset 值,进而去定义在每个 batch 中需要处理的消息的 offset 范围。本文采用的是第一种方式,因为目前第二种方式还处于试验阶段。
- 如果采用 Receiver Based Approach 集成 Kafka 和 Spark Streaming,就需要考虑到由于 Driver 或者 Worker 节点宕机而造成的数据丢失的情况,在默认配置下,是有可能造成数据丢失的,除非我们开启 Write Ahead Log(WAL) 功能。在这种情况下,从 Kafka 接收到的消息数据会同步的被写入到 WAL 并保存到可靠的分布式文件系统上,如 HDFS。可以通过在 Spark 配置文件中 (conf/spark-defaults.conf) 把 spark.streaming.receiver.writeAheadLog.enable 配置项设置成 true 开启这个功能。当然在开启 WAL 的情况下,会造成单个 Receiver 吞吐量下降,这时候,我们可能需要并行的运行多个 Receiver 来改善这种情况。
- 由于 updateStateByKey 操作需要开启 checkpoint 功能,但是频繁的 checkpoint 会造成程序处理时间增长,也会造成吞吐量下降。默认情况下,checkpoint 时间间隔会取 steaming 程序数据处理间隔或者 10 秒两者中较大的那个。官方推荐的间隔是 streaming 程序数据处理间隔的 5-10 倍。可以通过 dsteam.checkpoint(checkpointInterval) 来设置,参数需要用样本类 Duration 包装下,单位是毫秒。
小技巧
history | grep create 打印所有命令
在window使用工具查看kafka集群
首先使用hosts.exe工具配置本地host 192.168.140.128 node01 zk01 kafka01 192.168.140.129 node02 zk02 kafka02 192.168.140.130 node03 zk03 kafka03 zookeeper_java_client.zip
解压、链接即可。
操作指南:
3、编写测试代码
无论使用哪种语言操作kafka其本质上都是在围绕两个角色进行的,分别是Producer、Consumer
已经在kafka boker里面创建好一个Topic,
1、创建Produer
1)、命令行方式—普通的发送方式
[root@node1 python_app]# python
Python 2.6.6 (r266:84292, Nov 22 2013, 12:16:22)
[GCC 4.4.7 20120313 (Red Hat 4.4.7-4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from kafka import KafkaProducer
>>> producer = KafkaProducer(bootstrap_servers='localhost:9092')
>>> for _ in range(100):
... producer.send('world',b'some_message_bytes')
...
上面的几行功能分别是:
导入KafkaProducer
创建连接到192.168.120.11:9092这个Broker的Producer,
循环向world这个Topic发送100个消息,消息内容都是some_message_bytes’,这种发送方式不指定Partition,kafka会均匀大把这些消息分别写入5个Partiton里面,
更详细的说明可以参考 https://kafka-python.readthedocs.io/en/master/index.html
2)、命令行方式—发送json字符串
json作为一种强大的文本格式,已经得到非常普遍的应用,kafak-python也支持发送json格式的消息
其实如果你参考https://kafka-python.readthedocs.io/en/master/index.html这里的KafkaProducer里面的发送json
一定会报错的,这应该是这个文档的一个bug,
>>> producer = KafkaProducer(value_serializer=lambda v: json.dumps(v).encode('utf-8'))
Traceback (most recent call last):
File "", line 1, in
File "/usr/lib/python2.6/site-packages/kafka_python-1.3.4-py2.6.egg/kafka/producer/kafka.py", line 347, in __init__
**self.config)
File "/usr/lib/python2.6/site-packages/kafka_python-1.3.4-py2.6.egg/kafka/client_async.py", line 220, in __init__
self.config['api_version'] = self.check_version(timeout=check_timeout)
File "/usr/lib/python2.6/site-packages/kafka_python-1.3.4-py2.6.egg/kafka/client_async.py", line 841, in check_version
raise Errors.NoBrokersAvailable()
kafka.errors.NoBrokersAvailable: NoBrokersAvailable
经过测试,应该是这样写才是OK的
>>> producer = KafkaProducer(bootstrap_servers='localhost:9092',value_serializer=lambda v: json.dumps(v).encode('utf-8'))
>>> producer.send('world', {'key1': 'value1'})
>>>
3)、命令行方式—发送普通字符串
>>> producer.send('world', key=b'foo', value=b'bar')
>>>
4)、命令行方式–发送压缩字符串
>>> producer = KafkaProducer(bootstrap_servers='192.168.120.11:9092',compression_type='gzip')
>>> producer.send('world', b'msg 1')
经过测试这种方式发送的内容,在接收方收到的消息仍然是普通的字符串,也许是没有安装python-lz4,原文中有这样的内容:kafka-python supports gzip compression/decompression natively. To produce or consume lz4 compressed messages, you should install python-lz4 (pip install lz4). To enable snappy, install python-snappy (also requires snappy library). See Installation for more information.
上面都是测试各个命令的使用,接下来,我们写一个完整的脚本,这个脚本的功能是把指定目录下的文件名发送到world这个topic
file_monitor.py脚本
#-*- coding: utf-8 -*- from kafka import KafkaProducer import json import os import time from sys import argv producer = KafkaProducer(bootstrap_servers='192.168.120.11:9092') def log(str): t = time.strftime(r"%Y-%m-%d_%H-%M-%S",time.localtime()) print("[%s]%s"%(t,str)) def list_file(path): dir_list = os.listdir(path); for f in dir_list: producer.send('world',f) producer.flush() log('send: %s' % (f)) list_file(argv[1]) producer.close() log('done')
假如我们要监控/opt/jdk1.8.0_91/lib/missioncontrol/features这个目录下的文件,可以这样执行
python file_monitor.py /opt/jdk1.8.0_91/lib/missioncontrol/features
执行结果如下:
[root@node2 python_app]# python file_monitor.py /opt/jdk1.8.0_91/lib/missioncontrol/features [2017-11-07_17-41-04]send: org.eclipse.ecf.filetransfer.ssl.feature_1.0.0.v20140827-1444 [2017-11-07_17-41-04]send: org.eclipse.emf.common_2.10.1.v20140901-1043 [2017-11-07_17-41-04]send: com.jrockit.mc.feature.rcp.ja_5.5.0.165303 [2017-11-07_17-41-04]send: com.jrockit.mc.feature.console_5.5.0.165303 [2017-11-07_17-41-04]send: org.eclipse.ecf.core.feature_1.1.0.v20140827-1444 [2017-11-07_17-41-04]send: org.eclipse.equinox.p2.core.feature_1.3.0.v20140523-0116 [2017-11-07_17-41-04]send: org.eclipse.ecf.filetransfer.httpclient4.ssl.feature_1.0.0.v20140827-1444 [2017-11-07_17-41-04]send: com.jrockit.mc.feature.rcp_5.5.0.165303 [2017-11-07_17-41-04]send: org.eclipse.babel.nls_eclipse_zh_4.4.0.v20140623020002 [2017-11-07_17-41-04]send: com.jrockit.mc.rcp.product_5.5.0.165303 [2017-11-07_17-41-04]send: org.eclipse.help_2.0.102.v20141007-2301 [2017-11-07_17-41-04]send: org.eclipse.ecf.core.ssl.feature_1.0.0.v20140827-1444 [2017-11-07_17-41-04]send: org.eclipse.ecf.filetransfer.httpclient4.feature_3.9.1.v20140827-1444 [2017-11-07_17-41-04]send: org.eclipse.e4.rcp_1.3.100.v20141007-2033 [2017-11-07_17-41-04]send: org.eclipse.babel.nls_eclipse_ja_4.4.0.v20140623020002 [2017-11-07_17-41-04]send: com.jrockit.mc.feature.flightrecorder_5.5.0.165303 [2017-11-07_17-41-04]send: org.eclipse.emf.ecore_2.10.1.v20140901-1043 [2017-11-07_17-41-04]send: org.eclipse.equinox.p2.rcp.feature_1.2.0.v20140523-0116 [2017-11-07_17-41-04]send: org.eclipse.ecf.filetransfer.feature_3.9.0.v20140827-1444 [2017-11-07_17-41-04]send: com.jrockit.mc.feature.core_5.5.0.165303 [2017-11-07_17-41-04]send: org.eclipse.rcp_4.4.0.v20141007-2301 [2017-11-07_17-41-04]send: com.jrockit.mc.feature.rcp.zh_CN_5.5.0.165303 [2017-11-07_17-41-04]done
2、创建Consumer
通常使用Kafka时会创建不同的Topic,并且在Topic里面创建多个Partiton,因此作为Consumer,通常是连接到指定的Broker,指定的Topic来消费消息。
完整的python 脚本
consumer.py
#-*- coding: utf-8 -*- from kafka import KafkaConsumer consumer=KafkaConsumer('world',group_id='consumer-20171017',bootstrap_servers=['localhost:9092']) for msg in consumer: print msg
重新启动file_monitor.py脚本
[root@node2 python_app]# python file_monitor.py /opt/jdk1.8.0_91/lib/missioncontrol/features [2017-11-07_18-00-31]send: org.eclipse.ecf.filetransfer.ssl.feature_1.0.0.v20140827-1444 [2017-11-07_18-00-31]send: org.eclipse.emf.common_2.10.1.v20140901-1043 [2017-11-07_18-00-31]send: com.jrockit.mc.feature.rcp.ja_5.5.0.165303 [2017-11-07_18-00-31]send: com.jrockit.mc.feature.console_5.5.0.165303 [2017-11-07_18-00-31]send: org.eclipse.ecf.core.feature_1.1.0.v20140827-1444 [2017-11-07_18-00-31]send: org.eclipse.equinox.p2.core.feature_1.3.0.v20140523-0116 [2017-11-07_18-00-31]send: org.eclipse.ecf.filetransfer.httpclient4.ssl.feature_1.0.0.v20140827-1444 [2017-11-07_18-00-31]send: com.jrockit.mc.feature.rcp_5.5.0.165303 [2017-11-07_18-00-31]send: org.eclipse.babel.nls_eclipse_zh_4.4.0.v20140623020002 [2017-11-07_18-00-31]send: com.jrockit.mc.rcp.product_5.5.0.165303 [2017-11-07_18-00-31]send: org.eclipse.help_2.0.102.v20141007-2301 [2017-11-07_18-00-31]send: org.eclipse.ecf.core.ssl.feature_1.0.0.v20140827-1444 [2017-11-07_18-00-31]send: org.eclipse.ecf.filetransfer.httpclient4.feature_3.9.1.v20140827-1444 [2017-11-07_18-00-31]send: org.eclipse.e4.rcp_1.3.100.v20141007-2033 [2017-11-07_18-00-31]send: org.eclipse.babel.nls_eclipse_ja_4.4.0.v20140623020002 [2017-11-07_18-00-31]send: com.jrockit.mc.feature.flightrecorder_5.5.0.165303 [2017-11-07_18-00-31]send: org.eclipse.emf.ecore_2.10.1.v20140901-1043 [2017-11-07_18-00-31]send: org.eclipse.equinox.p2.rcp.feature_1.2.0.v20140523-0116 [2017-11-07_18-00-31]send: org.eclipse.ecf.filetransfer.feature_3.9.0.v20140827-1444 [2017-11-07_18-00-31]send: com.jrockit.mc.feature.core_5.5.0.165303 [2017-11-07_18-00-31]send: org.eclipse.rcp_4.4.0.v20141007-2301 [2017-11-07_18-00-31]send: com.jrockit.mc.feature.rcp.zh_CN_5.5.0.165303 [2017-11-07_18-00-31]done
然后启动consumer.py脚本
[root@node1 python_app]# python consumer.py [2017-09-23_11-34-00]start consumer [2017-09-23_11-34-10]world:3:121: key=None value=org.eclipse.ecf.filetransfer.ssl.feature_1.0.0.v20140827-1444 [2017-09-23_11-34-10]world:2:70: key=None value=org.eclipse.emf.common_2.10.1.v20140901-1043 [2017-09-23_11-34-10]world:3:122: key=None value=com.jrockit.mc.feature.rcp.ja_5.5.0.165303 [2017-09-23_11-34-10]world:2:71: key=None value=com.jrockit.mc.feature.console_5.5.0.165303 [2017-09-23_11-34-10]world:0:89: key=None value=org.eclipse.ecf.core.feature_1.1.0.v20140827-1444 [2017-09-23_11-34-10]world:4:101: key=None value=org.eclipse.equinox.p2.core.feature_1.3.0.v20140523-0116 [2017-09-23_11-34-10]world:1:117: key=None value=org.eclipse.ecf.filetransfer.httpclient4.ssl.feature_1.0.0.v20140827-1444 [2017-09-23_11-34-10]world:2:72: key=None value=com.jrockit.mc.feature.rcp_5.5.0.165303 [2017-09-23_11-34-10]world:4:102: key=None value=org.eclipse.babel.nls_eclipse_zh_4.4.0.v20140623020002 [2017-09-23_11-34-10]world:2:73: key=None value=com.jrockit.mc.rcp.product_5.5.0.165303 [2017-09-23_11-34-10]world:3:123: key=None value=org.eclipse.help_2.0.102.v20141007-2301 [2017-09-23_11-34-10]world:3:124: key=None value=org.eclipse.ecf.core.ssl.feature_1.0.0.v20140827-1444 [2017-09-23_11-34-10]world:0:90: key=None value=com.jrockit.mc.feature.flightrecorder_5.5.0.165303 [2017-09-23_11-34-10]world:3:125: key=None value=org.eclipse.ecf.filetransfer.httpclient4.feature_3.9.1.v20140827-1444 [2017-09-23_11-34-10]world:3:126: key=None value=org.eclipse.e4.rcp_1.3.100.v20141007-2033 [2017-09-23_11-34-10]world:3:127: key=None value=org.eclipse.babel.nls_eclipse_ja_4.4.0.v20140623020002 [2017-09-23_11-34-10]world:2:74: key=None value=org.eclipse.emf.ecore_2.10.1.v20140901-1043 [2017-09-23_11-34-10]world:3:128: key=None value=org.eclipse.equinox.p2.rcp.feature_1.2.0.v20140523-0116 [2017-09-23_11-34-10]world:0:91: key=None value=com.jrockit.mc.feature.core_5.5.0.165303 [2017-09-23_11-34-10]world:3:129: key=None value=org.eclipse.ecf.filetransfer.feature_3.9.0.v20140827-1444 [2017-09-23_11-34-11]world:3:130: key=None value=org.eclipse.rcp_4.4.0.v20141007-2301 [2017-09-23_11-34-11]world:3:131: key=None value=com.jrockit.mc.feature.rcp.zh_CN_5.5.0.165303
可以看到file_monitor.py脚本发送了一批文件名到word这个topic,并且consumer.py收到了这些文件名。
Bug指南:
pip install kafka-pythonfrom kafka import KafkaProducer
producer = KafkaProducer(bootstrap_servers='localhost:9092') Traceback (most recent call last): File "", line 1, in File "/usr/lib/python2.7/site-packages/kafka/producer/kafka.py", line 362, in __init__ **self.config) File "/usr/lib/python2.7/site-packages/kafka/client_async.py", line 219, in __init__ self.config['api_version'] = self.check_version(timeout=check_timeout) File "/usr/lib/python2.7/site-packages/kafka/client_async.py", line 819, in check_version raise Errors.NoBrokersAvailable() kafka.errors.NoBrokersAvailable: NoBrokersAvailable producer = KafkaProducer(bootstrap_servers=['localhost:9092'],api_version=(0,10)) kafka server.properties: listeners=PLAINTEXT://:9092 advertised.listeners=PLAINTEXT://kafka.hostname:9092
api_version:可以参考https://kafka.apache.org/documentation/#upgrade
测试:
消息发送者:producer.py
from kafka import KafkaProducer import time producer = KafkaProducer(bootstrap_servers='localhost:9092', api_version=(2, 1, 0)) for _ in range(100): time.sleep(2) producer.send('hello-world', b'some_message_bytes')
消息消费者:consumer.py
from pyspark import SparkContext from pyspark.streaming import StreamingContext from pyspark.streaming.kafka import KafkaUtils sc = SparkContext("local[2]", "KafkaWordCount") # 处理时间间隔为2s ssc = StreamingContext(sc, 2) zookeeper = "localhost:2181" # "localhost:2181,localhost1:2181" # 打开一个TCP socket 地址 和 端口号 topic = {"hello-world": 1} # 要列举出分区 {"hello-world": 1 ,"test1":2,"test2":2} groupid = "test-consumer-group" brokers = "localhost:9092" # "localhost:9092,localhost1:9092" # lines = KafkaUtils.createStream(ssc, zookeeper, groupid, topic) lines = KafkaUtils.createDirectStream(ssc, ['hello-world'], kafkaParams={"metadata.broker.list": "localhost:9092"}) lines.pprint() # 启动spark streaming应用 ssc.start() # 等待计算终止 ssc.awaitTermination()
结束语
本文包含了集成 Spark Streaming 和 Kafka 分布式消息系统的基本知识,但是需要指出的是,在实际问题中,我们可能面临更多的问题,如性能优化,内存不足,以及其他未曾遇到的问题。希望通过本文的阅读,读者能对使用 Spark Streaming 和 Kafka 构建实时数据处理系统有一个基本的认识,为读者进行更深入的研究提供一个参考依据。读者在阅读本文的时候发现任何问题或者有任何建议,请不吝赐教,留下您的评论,我会及时回复。希望我们可以一起讨论,共同进步。
相关主题
- 参考 Spark 官网的 Spark Streaming编程指导,了解 Spark Streaming 编程的基本知识和需要注意的方面。
- 查看 Scala 官网,了解更多关于 Scala 语言的内容。
- developerWorks 开源技术主题:查找丰富的操作信息、工具和项目更新,帮助您掌握开源技术并将其用于 IBM 产品。
参考目录
作者:葡小萄家的猫 来源:简书 链接:https://www.jianshu.com/p/0cb3367b6b5c
官网 http://kafka.apache.org/quickstart
kafka实战教程,kafka配置文件详解 https://blog.csdn.net/luanpeng825485697/article/details/81036028
kafka之 server.properties 配置文件参数说明 https://blog.csdn.net/qq_32252917/article/details/79465339
kafka 消费组:https://www.cnblogs.com/rainwang/p/7496147.html
Kafka的架构原理:https://www.jianshu.com/p/4bf007885116
kafka工作原理介绍:https://blog.csdn.net/qq_29186199/article/details/80827085

浙公网安备 33010602011771号