1. Kafka
1.1 什么是Kafka
- Apache Kafka是分布式发布-订阅消息系统(消息中间件)。它是一种快速、可扩展的、设计内在就是分布式的,分区的和可复制的提交日志服务。
- Kafka是一个分布式的、支持分区的(partition)、多副本(replica),基于zookeeper协调的分布式消息系统,它的最大的特性就是可以实时的处理大量数据以满足各种需求场景:eg:
基于Hadoop的批处理系统、低延迟的实时系统、storm/spark流式处理引擎、web/nginx日志、访问日志、消息服务等等,使用是scala语言编写的。
简单说明:
举个例子,生产者消费者,生产者生产鸡蛋,消费者消费鸡蛋,生产者生产一个鸡蛋,消费者就消费一个鸡蛋,假设消费者消费鸡蛋的时候噎住了(系统宕机了),生产者还在生产鸡蛋,那新生产的鸡蛋就丢失了。再比如生产者很强劲(大交易量的情况),生产者1秒钟生产100个鸡蛋,消费者1秒钟只能吃50个鸡蛋,那要不了一会,消费者就吃不消了(消息堵塞,最终导致系统超时),消费者拒绝再吃了,”鸡蛋“又丢失了,这个时候我们放个篮子在它们中间,生产出来的鸡蛋都放到篮子里,消费者去篮子里拿鸡蛋,这样鸡蛋就不会丢失了,都在篮子里,而这个篮子就是”Kafka“。
鸡蛋其实就是“数据流”,系统之间的交互都是通过“数据流”来传输的(就是tcp、http什么的),也称为报文,也叫“消息”。
1.2 Kafka的优点:
- 它是分布式系统,易于向外扩展
- 它同时为发布和订阅提供高吞吐量
- 它支持多订阅者,当失败时能自动平衡消费者
- 它将消息持久化到磁盘,因此可以用于批量消费,eg:ETL , 以及实时应用程序, 容错
1.3 Kafka的特性
- 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,他的延迟最低只有几毫秒,每个topic可以分为多个partition、consumer、group对partition进行consume操作
- 可扩展性:kafka集群支持热扩展
- 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份以防止数据丢失
- 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败)
- 高并发:支持数千个客户端同时读写
1.4 Kafka的使用场景
- 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、Hbase、Solr等
- 消息系统:解耦和生产者和消费者、缓存消息等。
- 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘
- 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告
- 流式处理:比如spark streaming和storm
- 事件源
1.5 Kafka 的术语及解释
- Broker: Kafka集群包含一个或多个服务器,这种服务器被称为broker
- Topic: 每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)
- Partition:Partition是物理上的概念,每个Topic包含一个或多个Partition
- Producer:负责发布消息到Kafka broker
- Consumer:消息消费者,向Kafka broker读取消息的客户端
- Consumer Group:每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)
- replica: partition 的副本,保障 partition 的高可用
- leader: replica 中的一个角色, producer 和 consumer 只跟 leader 交互
- follower: replica 中的一个角色,从 leader 中复制数据
- controller: Kafka 集群中的其中一个服务器,用来进行 leader election 以及各种 failover
小白理解:
- producer:生产者,就是它来生产“鸡蛋”的
- consumer:消费者,生出的“鸡蛋”它来消费
- topic:把它理解为标签,生产者每生产出来一个鸡蛋就贴上一个标签(topic),消费者可不是谁生产的“鸡蛋”都吃的,这样不同的生产者生产出来的“鸡蛋”,消费者就可以选择性的“吃”了
- broker:就是篮子了
- 如果从技术角度,topic标签实际就是队列,生产者把所有“鸡蛋(消息)”都放到对应的队列里了,消费者到指定的队列里取
2. Kafka的安装、启动、测试
2.1 安装Kafka
1. 下载kafka的安装包
Apache kafka 官方: http://kafka.apache.org/downloads.html Scala 2.11 - kafka_2.11-0.10.2.0.tgz (asc, md5)
2. 解压
tar -zxvf kafka_2.11-0.10.2.1.tgz -C /apps/
3. 修改配置文件
vi server.properties
broker.id=0 //为依次增长的:0、1、2、3、4,集群中唯一id
delete.topic.enable=true #删除主题的配置,默认是false
listeners=PLAINTEXT://kk-01:9092 # 监听的主机及端口号
log.dirs=/kafkaData/logs // Kafka的消息数据存储路径
num.partitions=3 #创建主题的时候,默认有3个分区
zookeeper.connect=master:2181,slave1:2181,slave2:2181 //zookeeperServers列表,各节点以逗号分开
4. 将Kafka server.properties 文件拷贝到其他节点机器
KAFKA_HOME/config>scp server.properties xx:$PWD
5. Kafka集群的环境准备:
- 安装JDK,配置JAVA_HOME
- 搭建zookeeper集群
2.2 启动Kafka
- 先启动zookeeper集群
bin/zkServer.sh start conf/zoo.cfg
2 再启动Kafka集群:
bin/kafka-server-server-start.sh [-daemon] config/server.properties
//-daemon:后台程序运行(添加守护进程)
2.3 测试kafka集群
- 进入kafka根目录,创建Topic名称为test的主题
bin/kafka-topic.sh –create –zookepper 192.168.8.11:2181 –replication-factor 3 –partition 2 –topic test
- 列出已经创建的topic列表
bin/kafka-topics.sh –list –zookeeper localhost:2181 –topic test
- 查看topic的详细信息:
bin/kafka –topics.sh –describe –zookeeper localhost:2181 –topic test
Topic:test PartitionCount:1 ReplicationFactor:3 Configs:
Topic: test Partition: 0 Leader: 1 Replicas: 1,2,0 Isr: 1,2,0
说明:第一行是对所有分区的一个描述。每个分区对应一行,因为只有一个分区所以下面只有一行。
Leader:负责处理消息的读和写,leader是从所有节点中随机选取的
Replica:列出了所有的副本节点,不管节点是否在服务中
Iisr:正在服务的节点
2.4 kafka中的生产者与消费者:
- 生产者—发送消息
bin/kafka-console-producer.sh –broker-list kafka集群中所有节点的IP:9092 --topic test
- 消费者—读取消息
bin/kafka-console-consumer.sh –bootstrap-server 生产者的IP地址:9092 –from-beginning (从最早位置开始消费)
2.5 kafka集群的容错能力
2.51 故障转移
Kill -9 pid[leader节点
另外一个节点被选做了leader,node 1 不再出现在 in-sync 副本列表中:
bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic test
Topic:test PartitionCount:1 ReplicationFactor:3 Configs:
Topic: test Partition: 0 Leader: 2 Replicas: 1,2,0 Isr: 2,0
虽然最初负责续写消息的leader down掉了,但之前的消息还是可以消费的:
bin/kafka-console-consumer.sh --zookeeper localhost:2181 --from-beginning --topic test2
2.52 负载均衡
等down掉的leader重新启动了,consumer还是会把它重新设置为leader。实现了负载均衡,以防止其他节点处理的数据量太大。
3 Kafka的客户端开发(使用java语言开发)
添加pom依赖:
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.11</artifactId>
<version>0.10.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.10.2.0</version>
</dependency>
3.1 Producer生产者
//生产者客户端--往kafka发送数据
public class ProducerClient {
public static void main(String[] args) {
Properties props = new Properties();
props.setProperty("bootstrap.servers","" +
"hdp-01:9092,hdp-02:9092,hdp-03:9092");
//0是不获取反馈(消息有可能传输失败)
//1是获取消息传递给leader后反馈(其他副本有可能接受消息失败)
//-1 | all是所有in-sync replicas接受到消息时的反馈
props.put("acks", "all");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<String,String>(props);
for(int i=0;i<100;i++){
ProducerRecord<String, String> record = new ProducerRecord<>("atopic", UUID.randomUUID().toString(), "hello hello");
kafkaProducer.send(record);
System.out.println("发送:"+i+"条信息:"+record.topic());
}
kafkaProducer.flush();
kafkaProducer.close();
}
}
3.2 ConsumerClient-消费者
//消费者客户端--接受数据
public class ConsumerClient {
public static void main(String[] args) {
Properties props = new Properties();
props.setProperty("bootstrap.servers","hdp-01:9092,hdp-02:9092,hdp-03:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("session.timeout.ms", "30000");
props.put("auto.offset.reset", "earliest");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(props);
kafkaConsumer.subscribe(Arrays.asList("atopic"));
ConsumerRecords<String, String> records = kafkaConsumer.poll(3000);
Iterator<ConsumerRecord<String, String>> iterator = records.iterator();
while (iterator.hasNext()){
ConsumerRecord<String, String> next = iterator.next();
System.out.println("读取next:"+next);
}
}
}
4 Kafka原理
4.1 Kafka的拓扑结构
说明:如图所示-一个典型的kafka集群中包含若干Producer、若干Broker(kafka支持水平扩展,一般broker数量越多,集群吞吐率越高),若干Consumer Group以及一个zookeeper集群。通过zookeeper管理集群配置,选举leader。Producer使用push模式将消息发布到broker,consumer使用pull模式从broker订阅并消费消息。
4.2 Zookeeper节点
4.3 Producer发布消息
- Producer采用push模式将消息发布到broker,每条消息都被append到partition中,属于顺序写入磁盘
- Producer 发送消息到broker时,会根据分区算法选择将其存储到哪一个partition:
1. 指定了partition,则直接使用
2. 未指定partition,但指定了key,通过对key的value进行hash,选出一个partition
3. Partition和key都未指定,使用轮询选出一个partition
4.4 写数据流程
- Producer先从zookeeper的“/brokers/…/state”节点找到该partition的leader
- Producer将消息发送给该leader
- Leader将消息写入本地的log
- Followers从leader里pull消息,写入本地的log后发送ACK
- Leader收到所有的ISR(in-sync replicas)中的ACK后向producer发送ACK
4.5 Broker存储消息
4.41 消息存储方式
4.42 消息存储策略
无论消息是否被消费,kafka都会保留所有消息。有两种策略可以删除就数据:
Log.retention.hours=168 #基于时间
Log.retention.bytes=1073741824 #基于大小
4.6 Kafka log的存储解析
Partition中每条Message由offset来表示它在这个partition中的偏移量,这个offset不改是message在partition数据文件中的实际存储位置,而逻辑上一个值,它唯一确定了partition中的每一条message。因此:可以认为offset是partition中message的ID。Partition 中每条message包含了以下三个属性:
1. Offset
2. MessageSize
3. Data
其中,offset为long类型,messageSize为int32,表示data的大小,data为massage的具体内容
思考:
如果一个partition只有一个数据文件会怎么样?
- 新数据添加在文件末尾,不论文件数据文件有多大,这个操作永远都是高效的
- 查找某个offset的message是顺序查找的。因此:如果数据文件很大的话,查找的效率就很低
Kafka解决查找效率的2种方案:分段 + 索引
- 数据文件的分段
Kafka解决高效查询效率的手段之一是将数据文件分段:eg:100条Message,它们的offset是从0到99。假设将数据文件分成5段:第一段是:0-19,第二段是:20-39,以此类推。每段放在一个单独的数据文件里,数据文件以该段中最小的offset命名。这样在查找指定的offset的message的时候,用二分查找就可以定位到该message在哪个段中。 - 为数据文件建立索引
数据文件分段使得可以在一个较小的数据文件中查找对应offset的message了,但这依然是需要顺序扫描文件才能找到对应offset的message。为了进一步提高查询的效率,kafka为每个分段后的数据文件建立了索引文件,文件名与数据文件是一样的,只是文件的扩展名是:.index
索引文件中包含若干个索引条目,每个条目表示数据文件中一条message的索引。索引包含两不跟:相对offset 和 position
- 相对offset:因为数据文件分段后,每个分区的数据文件的起始offset不为0,相对offset表示这条message相对其所属数据文件中最小的offset的大小。Eg: 分段后的一个数据文件的offset是从20开始,那么offset为25的message在index文件中的相对offset就是25-20= 5。存储相对的offset可以减少索引文件占用的空间。
- Position:表示该条message在数据文件中的绝对位置。只要打开文件并移动文件指针到这个position就可以读取相应的message了
Index文件中并没有为数据文件的每条message建立索引,而是采用了稀疏存储的方式,每隔一定字节的数据建立一条索引。这样也避免了索引文件占用太多空间,从而可以将索引文件保留在内存中。但是缺点就是没有建立索引的massage也不能一次定位到其所在数据文件的位置,从而需要做一次顺序扫描,但这次顺序扫描的范围就很小了。
4.7 总结:
4.7.1 kafka存储message
Message是按照topic来组织,每个topic可以分成多个partition。Eg:有5个partition的名为page_visits的topic的目录结构:
Partition是分段的,每个段叫segment,包括了一个数据文件和一个索引文件:
如图,该partition有4个segment
4.7.2 Kafka如何查找message
Eg: 查找绝对offset为7 的message:
首先二分查找确定了他是在哪个logsegment中,自然是在第一个segment;
打开这个segment的index文件,也是用二分查找找到offset小于或者等于指定offset的索引条目中最大的那个offset。自然offset为6的那个索引是我们要找的,通过索引文件知道offset为6的message在数据文件中的位置为9807
打开这个数据文件,从9807的位置开始顺序扫描直到找到offset为7的那条massage。
这套机制是建立在offset是有序的,索引文件被映射到内存中,所以查找的速度还是很快的。
总而言之:kafka的message存储采用了分区(partition),分段(LogSegment和稀疏索引这个手段来达到了高效性。
5 Kafka和Sparkstreming的整合
添加jar包:
<!--导入kafka和SparkStreaming整合的jar包-->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
<version>2.2.0</version>
</dependency>
生产者随机生成a~z个单词,发送到kafka的atopic主题下:
//kafka和Streaming整合的生产者:随机发送单词
public class StreamingKafkaProducer {
public static void main(String[] args) throws InterruptedException {
Properties props = new Properties();
props.setProperty("bootstrap.servers",
"hdp-01:9092,hdp-02:9092,hdp-03:9092");
props.put("acks", "all");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer<String, String> stringKafkaProducer = new KafkaProducer<>(props);
while (true){
int index = new Random().nextInt(26);
char word= (char) (index+97);
ProducerRecord<String, String> record = new ProducerRecord<String, String>("atopic",UUID.randomUUID().toString(),String.valueOf(word));
Future<RecordMetadata> send = stringKafkaProducer.send(record);
System.out.println("word:"+word);
Thread.sleep(1000);
}
}
}
进行Wordcount:
//Kafka和SparkStreaming整合做WordCOunt
object KafksStreamingWC {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf()
.setAppName("Kafka和SparkStreaming整合做WordCOunt")
.setMaster("local[*]")
val ssc: StreamingContext = new StreamingContext(conf,Seconds(2))
val topics: Array[String] = Array("atopic")
var groupId="song"
val kafkaParams: Map[String, Object] = Map[String, Object](
"bootstrap.servers" -> "hdp-01:9092,hdp-02:9092,hdp-03:9092",
"group.id" -> groupId,
"enable.auto.commit" -> "true", // 不记录G9529这个consumer消费的数据信息
"auto.offset.reset" -> "earliest",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer"
)
从kafka中获取数据
val stream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(
ssc,
LocationStrategies.PreferConsistent, ConsumerStrategies.Subscribe(topics.split(","),kafkaParams,OffsetHandlerMySql.findCurrentOffset(topics.split(","),groupId)))
//将stream转成RDD进行处理
stream.foreachRDD(rdd=>{
//业务逻辑---wordCount
val wordCount: RDD[(String, Int)] = rdd.flatMap(_.value().split(" ")).map((_,1)).reduceByKey(_+_)
val conn: Connection = OffsetHandlerMySql.getConn()
//存储当前批次的偏移量--存入Mysql
OffsetHandlerMySql.saveCurrentOffset(ranges,groupId,conn)
//写入Redis数据库
wordCount.foreachPartition(partition=>{
val jedis = RedisPools.getRedis()
partition.foreach(wc=>{
jedis.hincrBy("wordcount",wc._1,wc._2)
})
jedis.close()
})*/
})
ssc.start()
ssc.awaitTermination()
}
}
不自动提交offset偏移量,把偏移量存入数据库中,再从数据库中读取:
1. Redis数据库
object OffsetHandler {
//保存当前偏移量
def saveCurrentOffset(offSetRanges:Array[OffsetRange],groupId:String)={
//1.获取redis数据库连接
val jedis: Jedis = RedisPools.getRedis()
offSetRanges.foreach(os=>{
jedis.hset(os.topic+"-"+groupId,os.partition.toString,os.untilOffset.toString)
})
}
//读取当前的偏移量
def findCurrentOffset(topics:Array[String],groupId:String)={
//1.获取redis数据库连接
val jedis: Jedis = RedisPools.getRedis()
val jmap: util.Map[String, String] = jedis.hgetAll(topics.head+"-"+groupId)
jmap.asScala.map(tp=>(new TopicPartition(topics.head,tp._1.toInt),tp._2.toLong))
}
}
2. Mysql数据库
//基于mysql操作偏移量
object OffsetHandlerMySql {
//获取数据库连接
def getConn()={
DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mysql?characterEncoding=utf-8",
"root", "admin"
)
}
//保存当前偏移量
def saveCurrentOffset(offSetRanges:Array[OffsetRange],groupId:String,conn:Connection)={
//1.获取MySql数据库连接
/* val url="jdbc:mysql://localhost:3306/mysql?characterEncoding=utf-8"
val conn: Connection = DriverManager.getConnection(url,"root","admin")
*/
val psmt: PreparedStatement = conn.prepareStatement("replace into offset_manager values (?,?,?,?)")
offSetRanges.foreach(os=>{
//往mysql数据库里进行插入
//jedis.hset(os.topic+"-"+groupId,os.partition.toString,os.untilOffset.toString)
//wordcount表结构:topic、partition、groupId、offset
psmt.setString(1,os.topic)
psmt.setInt(2,os.partition)
psmt.setInt(3,os.untilOffset.toInt )
psmt.setString(4,groupId)
psmt.executeUpdate()
})
if (null != psmt) psmt.close()
if (null != conn) conn.close()
}
读取当前的偏移量
def findCurrentOffset(topics:Array[String],groupId:String)={
//定义一个map存储和封装数据库中的数据
val offsets: mutable.Map[TopicPartition, Long] = mutable.Map[TopicPartition,Long]()
//1.获取MySql数据库连接
val conn: Connection = getConn()
//执行查询
val stmt: Statement = conn.createStatement()
val rs=stmt.executeQuery(s"select * from offset_manager where groupId= '${groupId}' and topic='${topics.head}'")
while(rs.next()){
val pId: Int = rs.getInt("partitionId")
val offset: Int = rs.getInt("offset")
//封装数据
val topicPartition = new TopicPartition(topics.head,pId)
offsets.put(topicPartition,offset.toLong)
}
//释放资源
if(null != rs) rs.close()
if(null != stmt) stmt.close()
if(null != conn) conn.close()
offsets
/* psmt.setString(1,)
jmap.asScala.map(tp=>(new TopicPartition(topics.head,tp._1.toInt),tp._2.toLong))*/
}
}