消息系统
- 串行
- 并行
消息模式
-
点到点:
- 主要采用的队列的方式
- 对点消息系统中,消息被保留在队列中,一个或者多个消费者可以消费队列中的消息,但是特定的消息只能有最多的一个消费者消费。
- 该系统的典型应用就是订单处理系统
-
发布-订阅消息系统
-
主题
-
发布者
-
订阅者
-
应用场景:
-
应用解耦
-
流量控制
-
日志处理
-
消息通讯
-
-
kafka应用
- 搜索日志,
- 监控日志,
- 访问日志等。
- 指标分析
- 日志聚合解决方法
- 流式处理
kafka优点
可靠性:分布式的,分区,复制和容错的。
可扩展性:kafka消息传递系统轻松缩放,无需停机。
耐用性:kafka使用分布式提交日志,这意味着消息会尽可能快速的保存在磁盘上,因此它是持久的。
性能:kafka对于发布和定于消息都具有高吞吐量。即使存储了许多TB的消息,他也爆出稳定的性能。
kafka非常快:保证零停机和零数据丢失。
KafKa架构四大核心
- 生产者API:允许应用程序发布记录流至一个或者多个kafka的主题(topics)
- 消费者API:允许应用程序订阅一个或者多个主题,并处理这些主题接收到的记录流。
- StreamsAPI:允许应用程序充当流处理器(stream processor),从一个或者多个主题获取输入流,并生产一个输出流到一个或者多个主题,能够有效的变化输入流为输出流。
- ConnectorAPI:允许构建和运行可重用的生产者或者消费者,能够把kafka主题连接到现有的应用程序或数据系统。
kafka支持消息持久化,消费端为拉模型来拉取数据,消费状态和订阅关系有客户端负责维护,消息消费完 后,不会立即删除,会保留历史消息。因此支持多订阅时,消息只会存储一份就可以了
kafka整体架构
- 典型的kafka集群中包含若干个Producer,若干个Broker,若干个Consumer,以及一个zookeeper集群
- kafka通过zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行Rebalance(负载均 衡);Producer使用push模式将消息发布到Broker;Consumer使用pull模式从Broker中订阅并消费消息。
Kafka术语
-
broker,消息队列中常用的概念,在Kafka中指【部署了Kafka实例的服务器节点】。
-
topic,用来区分不同类型信息的主题。比如应用程序A订阅了主题t1,应用程序B订阅了主题t2而没有订阅t1,那么发送到主题t1中的数据将只能被应用程序A读到,而不会被应用程序B读到。
-
partiton,每个topic可以有一个或多个partition(分区)。分区是在物理层面上的,不同的分区对应着不同的数据文件。Kafka使用分区支持物理上的并发写入和读取,从而大大提高了吞吐量。
-
replica,Kafka在建立topic時,可以設定replica(副本)的数量,(控制消息保存在几个broker(服务器)上,一般情况下等于broker的个数)
-
副本因子过程图?
-
replica操作以partition为单位,每个partition都有各自的主副本和从副本,主副本为leader,从副本叫做follower(在有多个副本的情况下,kafka会为同一个分区下的分区,设定角色关系:一个leader和N个 follower),处于同步状态的副本叫做in-sync-replicas(ISR,表示当前可用的副本);follower通过拉的方式从leader同步数据。消费 者和生产者都是从leader读写数据,不与follower交互。
-
作用:让kafka读取数据和写入数据时的可靠性。
-
副本因子是包含本身,同一个副本因子不能放在同一个Broker中。
-
如果某一个分区有三个副本因子,就算其中一个挂掉,那么只会剩下的两个钟,选择一个leader,但不会在其 他的broker中,另启动一个副本(因为在另一台启动的话,存在数据传递,只要在机器之间有数据传递,就 会长时间占用网络IO,kafka是一个高吞吐量的消息系统,这个情况不允许发生)所以不会在零个broker中启 动。
-
-
segement,kafka中实际用来存储消息的文件
-
producer将数据存入到kafka集群broker上,consumer才能消费。消息是存储在segment段上
-
消息是按照什么形式或怎么方式存储到segment段呢?
-
实际中有config/server.properties文件的配置如下:
-
##日志滚动的周期时间,到达指定周期时间时,强制生成一个新的segment log.roll.hours=72 ##segment的索引文件最大尺寸限制,即时log.segment.bytes没达到,也会生成一个新的segment log.index.size.max.bytes=10*1024*1024 ##控制日志segment文件的大小,超出该大小则追加到一个新的日志segment文件中(-1表示没有限制) 、 log.segment.bytes=1014*1024*1024
-
-
offset,消费位置,它唯一标识了一条消息,消费者通过(offset,partition,topic)跟踪记录。
- 任何发布到此partition的消息都会被直接追加到log文件的尾部,每条消息在文件中的位置称为offset
-
LogEndOffset,简称LEO, 代表Partition的最高日志位移,其值对消费者不可见。
-
LogSize,已经存入该partition的数据量。已经写到该分区的消息数
-
消费组:由一个或者多个消费者组成,同一个组中的消费者对于同一条消息只消费一次。
- 某一个topic下的partition数,对消费组而言,应小于等于该主题下的分区数。
- 某一topic有4个partition,那么消费组中消费者应该小于4,且最好与分区数成整数倍。同一个partition下的数据,在同一时刻,不能同一个消费组的不同消费者消费
- 总结:分区数越多,同一时间可以有越多的消费者来进行消费,消费数据的速度就会越快,提高消费的性能
Partition 接收消息的时候是有顺序的吗?需要进行怎么的处理?不处理的话是怎样接收的
一个broker服务下,可以创建多个partition,在kafka中,每一个分区会有一个编号:编号从0开始,某一个分区的数据是有序的
- 说明-数据是有序,生产是什么样的顺序,那么消费的时候也是什么样的顺序
如何保证一个主题下的数据是有序的?
-
方案1:
- 一个主题(topic)下面有一个分区(partition)即可(单线程)
- 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个
-
方案2:(多个线程来并发处理消息)
- 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。
产生消息
熟悉offset的提交方式(自动提交以及手动提交)
-
自动提交
props.put("enable.auto.commit", "true"); props.put("auto.commit.interval.ms", "1000");
-
手动提交
//关闭自动提交确认选项 props.put("enable.auto.commit", "false"); // 手动提交offset值 kafkaConsumer.commitSync(); consumer.commitSync(); buffer.clear();
-
了解生产消息时的几种消息格式(string,arvo,json)
* Json
* 当我们的数据格式完全不固定,而且大量的文字性的消息,例如从网上爬取一篇文章,然后分词,做摘要,
* 最后存储到ElasticSearch里面做检索,这么一个流程,使用JSON其实是免去了很多转换工作
* 优势
* 所以采用JSON在数据转换方面的工作量肯定是比较小的
* 劣势
* 1.由于它每条记录都需要Key,所以在大数据时,其实它有大量的数据冗余
* 2.它不够明白,需要额外的文档来说明每一个字段。
*
* string
* org.apache.kafka.common.serialization.StringSerializer String 序列化
* org.apache.kafka.common.serialization.StringDeserializer String 反序列化
* arvo
* 解决了JSON的一些问题,但增加了一道转换的工序。
* 当我们的数据格式固定时,应该避免直接JSON,可以考虑使用Avro。
消费消息
-
按partition消费以及指定partition和offset进行消费
-
指定partition进行消息消费
//手动指定消费指定分区的数据---start String topic = "foo"; TopicPartition partition0 = new TopicPartition(topic, 0); TopicPartition partition1 = new TopicPartition(topic, 1); consumer.assign(Arrays.asList(partition0, partition1)); //手动指定消费指定分区的数据---end
-
指定partition的offset进行消费
//从指定的partition的指定的offset开始消费数据: KafkaConsummer consummer.subscribe(Arrays.asList(topic)); ConsummerRecords<String,User> records = consummer.poll(1000); consummer.seek(new TopicPartition(topic,partitionNum),offset); consummer.commitSync(); // 向ZK提交offset
-
熟悉topic数据过期策略以及实现过程
- 过期策略
- 基于时间或者日志大小,具体底层怎么实现的, 就是有个定时任务一样的线程,检测日志大小或者检测时间,记录在一个文件里面,数据超过一定的大小 生成新的文件
- 压缩合并日志的操作
- https://medium.com/@sunny_81705/kafka-log-retention-and-cleanup-policies-c8d9cb7e09f8
kafka partition和replica分配策略(可选)
partition分配策略
-
一个topic里面有好多个分区,数据究竟写入到哪一个分区里面去???
-
kafka的partition默认规则
/** * The default partitioning strategy: * <ul> * <li>If a partition is specified in the record, use it * <li>If no partition is specified but a key is present choose a partition based on a hash of the key * <li>If no partition or key is present choose a partition in a round-robin fashion */
总结:如果指定了数据的分区号,那么数据直接生产到对应的分区里面去
如果没有指定分区好,出现了数据key,通过key取hashCode来计算数据究竟该落在哪一个分区里面
如果既没有指定分区号,也没有指定数据的key,使用round-robin轮询 的这种机制来是实现
-
kafka当中四种分区策略
//分区策略第一种,如果既没有指定分区号,也没有指定数据key,那么就会使用轮询的方式将数据均匀的发送到不同的分区里面去 //ProducerRecord<String, String> producerRecord1 = new ProducerRecord<>("mypartition", "mymessage" + i); //kafkaProducer.send(producerRecord1); //第二种分区策略 如果没有指定分区号,指定了数据key,通过key.hashCode % numPartitions来计算数据究竟会保存在哪一个分区里面 //注意:如果数据key,没有变化 key.hashCode % numPartitions = 固定值 所有的数据都会写入到某一个分区里面去 //ProducerRecord<String, String> producerRecord2 = new ProducerRecord<>("mypartition", "mykey", "mymessage" + i); //kafkaProducer.send(producerRecord2); //第三种分区策略:如果指定了分区号,那么就会将数据直接写入到对应的分区里面去 // ProducerRecord<String, String> producerRecord3 = new ProducerRecord<>("mypartition", 0, "mykey", "mymessage" + i); // kafkaProducer.send(producerRecord3); //第四种分区策略:自动以分区策略.如果不自定义分区规则,那么会将数据使用轮询的方式均匀的发送到各个分区里面去 //配置我们自定义分区类 props.put("partitioner.class","cn.itcast.kafka.partition.MyPartitioner"); kafkaProducer.send(new ProducerRecord<String, String>("mypartition","mymessage"+i));
replica 分配策略
-
1.数据同步
- kafka 0.8后提供了Replication机制来保证Broker的failover。
- 引入Replication之后,同一个Partition可能会有多个Replica,而这时需要在这些Replication之间选出一个Leader,Producer和Consumer只与这个Leader交互,其它Replica作为Follower从Leader中复制数据。
- kafka 0.8后提供了Replication机制来保证Broker的failover。
-
副本放置策略
-
Kafka分配Replica的算法如下,下面的broker、partition副本数这些编号都是从0开始编号的
-
将所有存活的N个Brokers和待分配的Partition排序
-
将第i个Partition分配到第(i mod n)个Broker上,这个Partition的第一个Replica存在于这个分配的Broker上,并且会作为partition的优先副本( 这里就基本说明了一个topic的partition在集群上的大致分布情况 )
-
将第i个Partition的第j个Replica分配到第((i + j) mod n)个Broker上
-
假设集群一共有4个brokers,一个topic有4个partition,每个Partition有3个副本。下图是每个Broker上的副本分配情况。
-
-
-
对于Kafka而言,定义一个Broker是否“活着”包含两个条件:
- 一是它必须维护与ZooKeeper的session(这个通过ZooKeeper的Heartbeat机制来实现)。
- 二是Follower必须能够及时将Leader的消息复制过来,不能“落后太多”。
kafka producer的幂等性(可选)
-
引入目的
- 生产者重复生产消息。生产者进行retry会产生重试时,会重复产生消息。有了幂等性之后,在进行retry重试时,只会生成一个消息。
-
幂等性实现
- PID和Sequence Number
- PID。每个新的Producer在初始化的时候会被分配一个唯一的PID,这个PID对用户是不可见的。
- Sequence Numbler。(对于每个PID,该Producer发送数据的每个<Topic, Partition>都对应一个从0开始单调递增的Sequence Number。
- PID和Sequence Number
-
Kafka 中,Producer 默认不是幂等性的
-
在 0.11 之后,指定 Producer 幂等性的方法很简单,仅需要设置一个参数即可,即 props.put(“enable.idempotence”,ture)
-
开启条件
- enable.idempotence=true
- ack=all
- retries>1
-
解幂等性 Producer 的作用范围。
- 它只能保证单分区上的幂等性,即一个幂等性Producer 能够保证某个主题的一个分区上不出现重复消息,它无法实现多个分区的幂等性
- 只能实现单会话上的幂等性,不能实现跨会话的幂等性。
- Producer 进程的一次运行。当你重启了Producer 进程之后,这种幂等性保证就丧失了。
- 如果想实现多分区以及多会话上的消息无重复,应该怎么做呢?
- 答案就是事务(transaction)或者依赖事务型 Producer。这也是幂等性 Producer 和事务型 Producer 的最大区别。
使用多线程增加kafka消费能力
- 某些业务的执行速度实在是太慢,这个时候我们就要用到多线程去消费,提高应用机器的利用率,而不是一味的给kafka增加压力。
- 使用Spring创建一个kafka消费者是非常简单的。我们选择的方式是继承kafka的ShutdownableThread,然后实现它的doWork方法即可
- 多线程消费某个分区的数据
- 创建了一个最大容量为20的线程池,其中有两个参数需要注意一下
- 使用了了零容量的SynchronousQueue,一进一出,避免队列里缓冲数据,这样在系统异常关闭时,就能排除因为阻塞队列丢消息的可能。
- 使用了CallerRunsPolicy饱和策略,使得多线程处理不过来的时候,能够阻塞在kafka的消费线程上。
- 将真正处理业务的逻辑放在任务中多线程执行,每次执行完毕,我们都手工的commit一次ack,表明这条消息我已经处理了。由于是线程池认领了这些任务,顺序性是无法保证的,可能有些任务没有执行完毕,后面的任务就已经把它的offset给提交了
- Error due to java.util.ConturrentModificationException: kafkaConsumer is not safe for multi-thread access
- kafka的消费端不是线程安全的,它拒绝你这么调用它的api。kafka的初衷是好的,想要避免一些并发环境的问题,但我确实需要使用多线程处理。
- kafka消费者通过比较调用者的线程id来判断是否是由外部线程发起请求。
- 将commitSync函数放在线程外面了,先提交ack、再执行任务。
- 创建了一个最大容量为20的线程池,其中有两个参数需要注意一下
- 加入管道
- 参数配置
- 消息保证
- 使用关闭钩子
- 使用日志处理
- 借助redis处理
kafka topic 创建
创建topic的时候,kafka如何分配partition以及replica所在的位置.
要是一个broker down了,那它的replica该怎么重新分配.
-
KafkaApis.handleCreateTopicsRequest()
-
adminManager.createTopics()
-
AdminUtils.assignReplicasToBrokers() assignReplicas就在assignReplicasToBrokers这个函数中完成
-
AdminUtils这个类中,作者在源码上的注释
-
从broker-list中选定一个随机的位置,从这个位置开始,将每一个partition的第一个replica依次赋予brokerList中的broker.
-
比如现在有broker0~4,同时该topic有10个partition,随机选定的起始位置是broker0,那么就从broker0开始依次赋予partition,当partition超过了broker的数目时,再回到一开始选定的broker开始分配
-
* broker-0 broker-1 broker-2 broker-3 broker-4 * p0 p1 p2 p3 p4 (1st replica) * p5 p6 p7 p8 p9 (1st replica)
-
-
当分配好了第一个replica之后,剩下的replica以第一个replica所在的broker为基准,依次赋予之后的broker
-
比如partition0的replica0给了broker2,那么partion0的replica1与replica2依次赋予broker3和broker4
-
* broker-0 broker-1 broker-2 broker-3 broker-4 * p0 p1 p2 p3 p4 (1st replica) * p5 p6 p7 p8 p9 (1st replica) * p4 p0 p1 p2 p3 (2nd replica) * p8 p9 p5 p6 p7 (2nd replica) * p3 p4 p0 p1 p2 (3nd replica) * p7 p8 p9 p5 p6 (3nd replica)
-
-
kafka集群会通过zookeeper选出一个集群中的leader,由这个leader与zookeeper交互.选举或者加入集群成为follower在一个broker初始化的时候完成.
-
private def assignReplicasToBrokersRackUnaware(nPartitions: Int, replicationFactor: Int, brokerList: Seq[Int], fixedStartIndex: Int, startPartitionId: Int): Map[Int, Seq[Int]] = { val ret = mutable.Map[Int, Seq[Int]]() val brokerArray = brokerList.toArray val startIndex = if (fixedStartIndex >= 0) fixedStartIndex else rand.nextInt(brokerArray.length) var currentPartitionId = math.max(0, startPartitionId) var nextReplicaShift = if (fixedStartIndex >= 0) fixedStartIndex else rand.nextInt(brokerArray.length) for (_ <- 0 until nPartitions) { if (currentPartitionId > 0 && (currentPartitionId % brokerArray.length == 0)) nextReplicaShift += 1 val firstReplicaIndex = (currentPartitionId + startIndex) % brokerArray.length val replicaBuffer = mutable.ArrayBuffer(brokerArray(firstReplicaIndex)) for (j <- 0 until replicationFactor - 1) replicaBuffer += brokerArray(replicaIndex(firstReplicaIndex, nextReplicaShift, j, brokerArray.length)) ret.put(currentPartitionId, replicaBuffer) currentPartitionId += 1 } ret }
-
-
总之经过了某些的运算,replica并不是在broker之间依次分配下去的.而是间隔了nextReplicaShift个broker分配的.
private def replicaIndex(firstReplicaIndex: Int, secondReplicaShift: Int, replicaIndex: Int, nBrokers: Int): Int = { val shift = 1 + (secondReplicaShift + replicaIndex) % (nBrokers - 1) (firstReplicaIndex + shift) % nBrokers }
-
每两个replica之间所间隔的broker数目取决于
- 1.nextReplicaShit的大小
- 2.该replica是该partition的第几个replica
-
最后的shif在不考虑shift超出了broker数目的情况下t为1+nextReplicaShift+replicaindex