生产者、Broker、消费者、Zookeeper;
注意:Zookeeper中保存Broker id和消费者offsets等信息,但是没有生产者信息。
Apache Kafka是由Apache开发的一种发布订阅消息系统
,它是一个分布式的、分区的和重复的日志服务
。
名称 | 说明 |
---|---|
Topic | 主题,可以理解为一个队列 |
Partition | 分区,为了实现扩展性,一个非常大的topic可以分布到多个broker(即服务器)上,一个topic可以分为多个partition,每个partition是一个有序的队列。partition中的每条消息都会被分配一个有序的id(offset)。kafka只保证按一个partition中的顺序将消息发给consumer,不保证一个topic的整体(多个partition间)的顺序 |
Offset | 偏移量,kafka的存储文件都是按照offset.kafka来命名,用offset做名字的好处是方便查找。例如你想找位于2049的位置,只要找到2048.kafka的文件即可。当然the first offset就是00000000000.kafka |
Broker | 一台kafka服务器就是一个broker。一个集群由多个broker组成。一个broker可以容纳多个topic |
Producer | 消息生产者,向kafka broker发消息的客户端;生产者发布消息时根据消息是否具有键采用不同的分区策略,消息没有键时通过轮询方式进行客户端负载均衡,如果有键则根据分区语义确保相同键发送至同一分区。 |
Consumer | 消息消费者,向kafka broker取消息的客户端;消费者读取一个分区消息的顺序和生产者写入分区的顺序是一致的。 |
Consumer Group | 消费者组,这是kafka用来实现一个topic消息的广播(发给所有的consumer)和单播(发给任意一个consumer)的手段。一个topic可以有多个CG。topic的消息会复制(不是真的复制,是概念上的)到所有的CG,但每个partion只会把消息发给该CG中的一个consumer。如果需要实现广播,只要每个consumer有一个独立的CG就可以了。要实现单播只要所有的consumer在同一个CG。用CG还可以将consumer进行自由的分组而不需要多次发送消息到不同的topic;同一消费者组下的多个消费者互相协调消费工作,每个消费者都会均匀分配到分区,每增加或者减少消费者都会触发消费者管理协议的平衡操作。消费者数量最好和分区数一致,避免有空闲消费者。 |
1.消息分类按不同类别,分成不同的Topic,Topic⼜拆分成多个partition,每个partition均衡分散到不同的服务器(提⾼并发访问的能⼒)
2.消费者按顺序从partition中读取,不⽀持随机读取数据,但可通过改变保存到zookeeper中的offset位置实现从任意位置开始读取
3.服务器消息定时清除(不管有没有消费)
4.每个partition还可以设置备份到其他服务器上的个数以保证数据的可⽤性。通过Leader,Follower⽅式
5.zookeeper保存kafka服务器和客户端的所有状态信息.(确保实际的客户端和服务器轻量级)
6.在kafka中,⼀个partition中的消息只会被group中的⼀个consumer消费;每个group中consumer消息消费互相独⽴;我们可以认为⼀个group是⼀个"订阅"者,⼀个Topic中的每个partions,只会被⼀个"订阅者"中的⼀个consumer消费,不过⼀个consumer可以消费多个partitions中的消息
7.如果所有的consumer都具有相同的group,这种情况和queue模式很像;消息将会在consumers之间负载均衡.
8.如果所有的consumer都具有不同的group,那这就是"发布-订阅";消息将会⼴播给所有的消费者.
9.持久性,当收到的消息时先buffer起来,等到了⼀定的阀值再写⼊磁盘⽂件,减少磁盘IO.在⼀定程度上依赖OS的⽂件系统(对⽂件系统本身优化⼏乎不可能)
10.除了磁盘IO,还应考虑⽹络IO,批量对消息发送和接收,并对消息进⾏压缩。
11.在JMS实现中,Topic模型基于push⽅式,即broker将消息推送给consumer端.不过在kafka中,采⽤了pull⽅式,即consumer在和broker建⽴连接之后,主动去pull(或者说fetch)消息;这种模式有些优点,⾸先consumer端可以根据⾃⼰的消费能⼒适时的去fetch消息并处理,且可以控制消息消费的进度(offset);此外,消费者可以良好的控制消息消费的数量,batch fetch.
12.kafka⽆需记录消息是否接收成功,是否要重新发送等,所以kafka的producer是⾮常轻量级的,consumer端也只需要将fetch后的offset位置注册到zookeeper,所以也是⾮常轻量级的.
对于⼀些常规的消息系统,kafka是个不错的选择;partitons/replication和容错,可以使kafka具有良好的扩展性和性能优势.
不过到⽬前为⽌,我们应该很清楚认识到,kafka并没有提供JMS中的"事务性"“消息传输担保(消息确认机制)”"消息分组"等企业级特性;
kafka只能使⽤作为"常规"的消息系统,在⼀定程度上,尚未确保消息的发送与接收绝对可靠(⽐如,消息重发,消息发送丢失等),kafka的特性决定它⾮常适合作为"⽇志收集中⼼";application可以将操
作⽇志"批量""异步"的发送到kafka集群中,⽽不是保存在本地或者DB中;kafka可以批量提交消息/压缩消息等,这对producer端⽽⾔,⼏乎感觉不到性能的开⽀.
consumer端采⽤批量fetch⽅式,此时consumer端也可以使hadoop等其他系统化的存储和分析系统
Kafka机器数量 = 2 *(峰值生产速度 * 副本数 / 100)+ 1
一般设置成2个或3个,很多企业设置为2个。
副本优势:提高可靠性;
副本劣势:增加了网络IO传输。
Kafka官方自带压力测试脚本(kafka-consumer-perf-test.sh、kafka-producer-perf-test.sh)。Kafka压测时,可以查看到哪个地方出现了瓶颈(CPU,内存,网络IO)。一般都是网络IO达到瓶颈。
默认保存7天;生产环境建议3天
每天总数据量100g,每天产生1亿条日志,10000万/24/60/60=1150条/每秒钟
平均每秒钟:1150条
低谷每秒钟:50条
高峰每秒钟:1150条 *(2-20倍)= 2300条 - 23000条
每条日志大小:0.5k - 2k(取1k)
每秒多少数据量:2.0M - 20MB
每天的数据量(100g) * 副本数(2个副本) * 日志保存时长(3天) / 70%
自己开发监控器;
开源的监控器:KafkaManager、KafkaMonitor、KafkaEagle
分区数一般设置为:3-10个
1)创建一个只有1个分区的topic
2)测试这个topic的producer吞吐量和consumer吞吐量。
3)假设他们的值分别是Tp和Tc,单位可以是MB/s。
4)然后假设总的目标吞吐量是Tt,那么分区数=Tt / min(Tp,Tc)
例如:producer吞吐量 = 20m/s;consumer吞吐量 = 50m/s,期望吞吐量100m/s;则分区数 = 100 / 20 = 5个分区
通常情况:多少个日志类型就多少个Topic。也有对日志类型进行合并的。
Kafka中的ISR(In-Sync Replica)是指与Leader副本保持同步的副本集合。(ISR中包括Leader和Follower)。 ISR中的副本可以参与消息的读取和写入。如果一个副本落后于Leader副本的进度,或者发生了不可恢复的错误,那么这个副本将被移除ISR,存入OSR(Outof-Sync Replicas)列表(新加入的Follower也会先存放在OSR中同时等待恢复或者被替换)。
ISR副本同步队列是Kafka中一种用于同步副本的机制。当ISR中的某个副本出现了错误或者落后于Leader副本时,Kafka需要将这个副本移除ISR,并找到一个新的副本来替换它。Kafka通过ISR副本同步队列来管理新副本的同步进度。
当需要替换ISR中的一个副本时,Kafka会从备份副本中选择一个新的副本来代替它。在新副本与Leader副本同步之前,它不会被添加到ISR中。Kafka会将新副本的同步进度记录到ISR副本同步队列中。当新副本与Leader副本的同步进度达到一定程度时,它就可以加入ISR中,参与消息的读取和写入了。
如果Leader进程挂掉,会在ISR队列中选择一个服务作为新的Leader。有replica.lag.max.messages(延迟条数)
和replica.lag.time.max.ms(延迟时间)
两个参数决定一台服务是否可以加入ISR副本队列,在0.10版本移除了replica.lag.max.messages参数,防止服务频繁的进去队列
。
在 Kafka内部存在两种默认的分区分配策略:Range和 RoundRobin。
Range是默认策略。Range是对每个Topic而言的(即一个Topic一个Topic分),首先对同一个Topic里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。然后用Partitions分区的个数除以消费者线程的总数来决定每个消费者线程消费几个分区。如果除不尽,那么前面几个消费者线程将会多消费一个分区。
例如:我们有10个分区,两个消费者(C1,C2),3个消费者线程,10 / 3 = 3而且除不尽。
C1-0 将消费 0, 1, 2, 3 分区
C2-0 将消费 4, 5, 6 分区
C2-1 将消费 7, 8, 9 分区
RoundRobin方式第一步将所有主题分区组成TopicAndPartition列表,然后对TopicAndPartition列表按照hashCode进行排序,最后按照轮询的方式发给每一个消费线程。
重新启动 Kafka:如果 Kafka 只是因为临时的网络问题、内存不足等原因导致的崩溃,可以尝试重启 Kafka。这可能会解决问题,但不适用于持续出现故障的情况。
切换到备份 Kafka:如果有备份 Kafka 集群,可以尝试将生产者和消费者重定向到备份 Kafka,直到主 Kafka 恢复正常。
Kafka 自动故障转移:Kafka 支持自动故障转移,即在主 Kafka 节点宕机时自动将副本切换为主节点。可以使用 ZooKeeper 实现 Kafka 的自动故障转移,ZooKeeper 会监视 Kafka 集群中的节点,并在检测到主节点宕机时自动将副本切换为主节点。
增加 Kafka 的硬件资源:如果 Kafka 经常出现性能问题,可以尝试增加 Kafka 集群的硬件资源,例如增加内存、CPU、磁盘等,以提高 Kafka 的吞吐量和稳定性。
优化 Kafka 的配置:可以根据实际情况调整 Kafka 的配置参数,例如调整 Kafka 的内存限制、调整 Kafka 的缓存大小、调整 Kafka 的副本数量等,以提高 Kafka 的性能和稳定性。
查找并解决 Kafka 的故障:可以使用 Kafka 的日志、监控工具等来查找并解决 Kafka 的故障,例如查找 Kafka 的磁盘空间是否已满、查找 Kafka 的网络连接是否正常、查找 Kafka 的进程是否崩溃等。
kafka 的 ack 机制:在 kafka producer发送数据的时候,每次发送消息都会有一个确认反馈机制,确保消息正常的能够被收到,其中状态有 0,1,-1。
ack在生产者指定,不同生产者可以不同。
Ack = 0,相当于异步发送,消息发送完毕即offset增加,继续生产。
Ack = 1,leader收到leader replica 对一个消息的接受ack才增加offset,然后继续生产。
Ack = -1,leader收到所有replica 对一个消息的接受ack才增加offset,然后继续生产。
ack设为-1时,需要ISR里的所有follower应答,想要真正不丢数据,需要配合参数min.insync.replicas: n --(ack为-1时生效,ISR里应答的最小follower数量)
,min.insync.replicas默认为1(leader本身也算一个!),所以当ISR里除了leader本身,没有其他的follower,即使ack设为-1,相当于1的效果,不能保证不丢数据。 需要将min.insync.replicas设置大于等于2,才能保证有其他副本同步到数据。
retries = Integer.MAX_VALUE --(无限重试)。
如果上述两个条件不满足,写入一直失败,就会无限次重试,保证数据必须成功的发送给两个副本,如果做不到,就不停的重试,除非是面向金融级的场景,面向企业大客户,或者是广告计费,跟钱的计算相关的场景下,才会通过严格配置保证数据绝对不丢失
kafka-topics.sh --bootstrap-server hadoop1:9092 --create --topic testisr2
--replication-factor 3 --partitions 4 --config min.insync.replicas=2
完全不丢结论:ack=-1 + min.insync.replicas>=2 +无限重试
① 副本数大于1,min.insync.replicas大于1 消息至少要被写入到这么多副本才算成功;
每个 broker 中的 partition 我们一般都会设置有 replication(副本)的个数,生产者写入的时候首先根据分发策略(有 partition 按partition,有 key 按key,都没有轮询)写入到 leader 中,follower(副本)再跟 leader 同步数据,这样有了备份,也可以保证消息数据的不丢失。
② unclean.leader.election.enable=false 关闭unclean leader选举,即不允许非ISR中的副本被选举为leader,以避免数据丢失。
enable.auto.commit=false 关闭自动提交offset,处理完数据之后手动提交
flink结合checkpoint
Kafka数据重复的原因可能有多种,下面列举几种比较常见的情况:
① 消费者在处理数据时出现异常导致消费失败,但是由于Kafka自动提交offset的机制,导致已经消费过的数据的offset没有提交成功,下次启动时会重新消费这些数据。
② 消费者在处理数据时出现重复消费的情况,这可能是由于业务逻辑不严谨或者数据消费的幂等性没有保证。
③ Kafka的消息传输机制导致消息重复。 比如消息在传输过程中重复发送了多次,或者消息发送到一个分区的多个副本都有,导致消费者消费时可能会从不同的副本中消费到相同的消息。
手动提交offset:对于消费者在处理数据时出现异常导致重复消费的情况,可以通过手动提交offset的方式来避免重复消费。
设置 acks 参数为 all:在生产者的配置中,可以设置 acks 参数为 all,表示需要所有的 ISR 副本都确认接收到消息之后,生产者才会收到一个成功的响应。这种方式可以确保消息不会丢失,但可能会导致写入性能下降。
使用幂等性生产者:Kafka 0.11 版本之后,引入了幂等性生产者。幂等性生产者能够保证同一条消息只会被写入一次,即使发送多次请求。开启幂等性生产者后,不需要设置 acks 参数为 all,仅需要将参数 enable.idempotence 设置为 true 即可。
使用事务性生产者:Kafka 0.11 版本之后,引入了事务性生产者。事务性生产者能够保证同一个事务中的所有消息要么全部写入成功,要么全部失败。事务性生产者能够解决消息重复和乱序的问题,但是需要配置 Kafka 和使用者支持事务。
消费端去重:对于业务逻辑不严谨或者数据消费的幂等性没有保证的情况,需要在消费端进行处理,通过SparkStreaming、redis、Flink或者Hive中dwd层去重
,比如通过对消费数据进行去重、利用唯一标识符等方式来保证数据的幂等性。
数据分区:将数据分成多个分区,保证同一分区内的消息按顺序消费。对于需要保证顺序的业务,可以将数据分为一个分区。这种方式可以解决乱序问题,但可能会导致数据倾斜问题。
消费者端顺序消费:在消费端,可以使用顺序消费的方式消费消息。这种方式需要注意并发度和负载均衡的问题,避免出现消费者间负载不均的情况,导致某些消费者处理的消息比其他消费者多,从而影响消息的顺序性。
使用时间戳:在生产端,可以给每个消息设置时间戳,消费端按照时间戳排序消费。这种方式需要保证生产者时钟同步,并且需要注意消息的时效性问题。
1 、如果是Kafka消费能力不足,则可以考虑增加Topic的分区数,并且同时提升消费组的消费者数量,消费者数 = 分区数。(两者缺一不可)
2 、如果是下游的数据处理不及时:提高每批次拉取的数量。批次拉取数据过少(拉取数据/处理时间 < 生产速度),使处理的数据小于生产的数据,也会造成数据积压。
# 保留三天,也可以更短 (log.cleaner.delete.retention.ms)
log.retention.hours=72
default.replication.factor:1 默认副本1个
replica.socket.timeout.ms:30000 #当集群之间网络不稳定时,调大该参数
replica.lag.time.max.ms= 600000# 如果网络不好,或者kafka集群压力较大,会出现副本丢失,然后会频繁复制副本,导致集群压力更大,此时可以调大该参数
compression.type:none gzip snappy lz4
#默认发送不进行压缩,推荐配置一种适合的压缩算法,可以大幅度的减缓网络压力和Broker的存储压力。
默认内存1个G,生产环境尽量不要超过6个G。
export KAFKA_HEAP_OPTS=“-Xms4g -Xmx4g”
1)Kafka本身是分布式集群
,同时采用分区技术
,并发度高
。
2)顺序写磁盘
Kafka的producer生产数据,要写入到log文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能到600M/s,而随机写只有100K/s。
3)零复制技术
读写文件块大小和磁盘读取数据缓存大小一致。
4) 高效文件存储设计
: Kafka 把 topic 中一个 parition 大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。通过索引信息可以快速定位 message 和确定 response的 大 小。通过 index 元数据全部映射到 memory(内存映射文件), 可以避免 segment file 的 IO 磁盘操作。通过索引文件稀疏存储,可以大幅降低 index 文件元数据占用空间大小。
Kafka对于消息体的大小默认为单条最大值是1M但是在我们应用场景中,常常会出现一条消息大于1M,如果不对Kafka进行配置。则会出现生产者无法将消息推送到Kafka或消费者无法去消费Kafka里面的数据,这时我们就要对Kafka进行以下配置:server.properties
replica.fetch.max.bytes: 1048576 broker可复制的消息的最大字节数, 默认为1M
message.max.bytes: 1000012 kafka 会接收单个消息size的最大限制, 默认为1M左右
注意:message.max.bytes必须小于等于replica.fetch.max.bytes,否则就会导致replica之间数据同步失败。
在Kafka中,过期数据的清理是通过两个参数控制的:log.retention.ms和log.retention.bytes。
log.retention.ms
:指定一个日志分段(log segment)最长保留的时间。 默认情况下,这个值是7天。在这个时间之前创建的所有分段都不会被删除,而在这个时间之后创建的分段,如果不再被引用,就会被删除。可以通过修改这个参数的值来控制过期数据的清理。
log.retention.bytes
:指定日志分段的最大大小。 当一个日志分段的大小超过了这个值,且它不再被引用,就会被删除。这个参数的默认值是-1,表示不启用大小限制。
在Kafka中,每个分区都有一个独立的日志文件(log file),日志文件中包含多个日志分段。每个日志分段包含了一段时间内的数据,或者说一定数量的消息。当一个日志分段被创建时,它会被添加到日志文件中,然后写入的消息会被追加到这个分段中。在日志分段中的消息被消费完毕之后,这个分段就不再被使用了,如果它已经过期或者超过了最大大小,就会被删除。
需要注意的是,如果使用Kafka的日志清理功能,那么数据的删除不是立即生效的,而是由一个后台线程定期执行清理操作。这个后台线程默认情况下每隔5分钟执行一次清理操作,可以通过log.cleaner.backoff.ms参数修改清理间隔的时间。另外,日志清理操作并不是实时的,它会在一个时间窗口内处理多个日志分段,因此在清理周期内,一些分段可能会被保留下来,即使它们已经过期或者超过了大小限制。
日志清理的策略只有delete和compact两种
log.cleanup.policy = delete启用删除策略
log.cleanup.policy = compact启用压缩策略
在 Kafka 中,可以通过设置 ConsumerConfig 的参数 timestamp 来按照时间戳消费数据。具体来说,可以通过以下方式实现:
① 设置 ConsumerConfig 中的 enable.auto.commit 参数为 false,这将禁用自动提交偏移量。
② 使用 assign() 方法手动分配分区,而不是使用 subscribe() 方法订阅主题。
③ 使用 seek() 方法将分区的偏移量设置为最接近指定时间戳的消息的偏移量,然后开始消费数据。
④ 在消费完数据后,手动提交偏移量。
下面是一个示例代码,按照时间戳消费 mytopic 主题的数据:
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import java.time.Instant;
import java.util.*;
public class KafkaTimestampConsumer {
public static void main(String[] args) {
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "my-group");
props.put("enable.auto.commit", "false");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 设置时间戳为当前时间
long timestamp = Instant.now().toEpochMilli();
// 创建消费者
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
// 手动分配分区
List<TopicPartition> partitions = Arrays.asList(new TopicPartition("mytopic", 0));
consumer.assign(partitions);
// 查找最接近时间戳的消息偏移量
Map<TopicPartition, Long> offsets = consumer.offsetsForTimes(Collections.singletonMap(new TopicPartition("mytopic", 0), timestamp));
for (Map.Entry<TopicPartition, Long> entry : offsets.entrySet()) {
TopicPartition partition = entry.getKey();
Long offset = entry.getValue();
if (offset != null) {
// 手动设置偏移量并开始消费
consumer.seek(partition, offset);
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("partition = %d, offset = %d, key = %s, value = %s%n", record.partition(), record.offset(), record.key(), record.value());
}
// 手动提交偏移量
consumer.commitSync();
}
}
}
consumer.close();
}
}
在上面的代码中,offsetsForTimes() 方法用于查找最接近指定时间戳的消息的偏移量,然后使用 seek() 方法将分区的偏移量设置为该偏移量。随后,消费者使用 poll() 方法获取消息并进行消费,最后手动提交偏移量。
单分区内有序;多分区,分区与分区间无序。
扩展:
kafka producer发送消息的时候,可以指定key:
这个key的作用是为消息选择存储分区,key可以为空,当指定key且不为空的时候,Kafka是根据key的hash值与分区数取模来决定数据存储到那个分区。
有序解决方案:同一张表的数据 放到 同一个 分区
=> ProducerRecord里传入key,会根据key取hash算出分区号
=> key使用表名,如果有库名,拼接上库名
Kafka 0.11版本之后
Properties props = new Properties();
props.put("bootstrap.servers",
"hadoop1:9092,hadoop2:9092,hadoop3:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//如果要想保证数据不丢失,得如下设置:
// min.insync.replicas = 2
// acks = -1
// retries = Integer.MAX_VALUE
props.put("acks", "-1");
//如果消息发送失败,就会重试,这里的3次代表重试的次数
props.put("retries", 3);
//重试的时间间隔
props.put("retry.backoff.ms",5000);
//设置是否开启压缩,默认是none不压缩
//如果要压缩的话,建议设置lz4,经过实际检验,效果还是不错的
props.put("compression.type","lz4");
//发送一次消息的批次大小,如果批次太小,会导致网络请求频繁,
//建议设置大一些,默认16384Byte(16k),建议调大,这里用32k
props.put("batch.size", 32384);
//批次达到时间就发送。默认是0,意思是消息必须立即被发送,建议100ms
props.put("linger.ms", 100);
//设置的缓冲区大小,默认33554432(32M),一般不用动
//验证何时该调整缓冲区的大小:
//用一般Java获取结束时间和开始时间: System.currentTime()
//当结束时间减去开始时间大于设置的linger.ms(100ms),此时Sender线程处理速度慢,需要调大缓冲区大小。
props.put("buffer.memory", 33554432);
//发送消息的最大大小,默认是1048576(1M),上限可以调大到10M
props.put("max.request.size",10485760);
//保证一个消息发送成功,再发另外一个消息,保证单分区有序
props.put("max.in.flight.requests.per.connection",1);
//最大阻塞时间,RecordAccumulator缓存不足时或者没有可用的元数据时,KafkaProducer的send()方法调用要么被阻塞,要么抛出异常,此参数的默认值为60000,即60s
props.put("max.block.ms", 3000);
// 创建一个Producer实例
KafkaProducer<String, String> producer =
new KafkaProducer<String, String>(props);
// 有序性考虑,可以指定生产者的key
ProducerRecord<String, String> record =
new ProducerRecord<>("mytopic", "mykey", "myvalue");
//可以计算开始时间
long startTime=System.currentTime();
//发送消息的模式有两种,一种是异步的,一种是同步的,我们在实际生产中一般是使用异步的发送方式
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if(exception == null) {
// 消息发送成功
System.out.println("消息发送成功");
} else {
// 消息发送失败,需要重新发送
}
}
});
//计算结束时间
long endTime=System.currentTime();
if(endTime - startTime > 100){//说明内存被压满了
//说明有问题,考虑调大buffer.memory
}
// 这是同步发送的模式
//producer.send(record).get();
producer.close();
要在数据生产过程中从 Kafka 得到准确的信息,需要考虑以下几个方面:
数据生产者的确保消息的完整性和正确性:数据生产者需要确保发送的消息是完整的,没有丢失和损坏,并且包含正确的信息。
使用 Kafka 生产者确认机制:Kafka 生产者确认机制能够在消息发送完成之后,确保消息已经被正确地写入了 Kafka 集群中。生产者可以选择使用不同的确认级别(acks),包括“0”、“1”和“all”,以控制确认机制的可靠性和延迟。当确认级别设置为“all”时,生产者将等待所有的副本都确认了消息的写入,才会认为写入成功。
在消费者端使用正确的 offset:在消费者端,要确保使用正确的 offset 消费数据,避免漏掉消息或者重复消费消息。可以使用 Kafka 提供的 offset 管理机制,手动管理 offset,或者使用 Kafka 提供的消费者组机制,让 Kafka 自动管理 offset。
监控 Kafka 集群的状态:在数据生产和消费过程中,监控 Kafka 集群的状态是非常重要的。可以使用 Kafka 提供的监控工具和指标,包括 Kafka Manager、Kafka Exporter、JMX、Kafka 消费者组指标等,来实时监控集群的健康状况,以及识别和解决问题。
优化 Kafka 集群的性能和可靠性:为了保证 Kafka 集群的性能和可靠性,需要优化集群的配置和参数设置,包括调整 Kafka 的参数、调整操作系统的参数、优化网络连接和磁盘 I/O 等。此外,还可以使用复制和备份等机制来提高 Kafka 集群的可靠性,以确保数据不会因为单点故障而丢失。
Kafka支持三种消息投递语义:
① At most once
消息可能会丢,但绝不会重复传递
② At least one
消息绝不会丢,但可能会重复传递
③ Exactly once
每条消息肯定会被传输一次且仅传输一次,很多时候这是用户想要的
consumer在从broker读取消息后,可以选择commit,该操作会在Zookeeper中存下该consumer在该partition下读取的消息的offset,该consumer下一次再读该partition时会从下一条开始读取。如未commit,下一次读取的开始位置会跟上一次commit之后的开始位置相同。
可以将consumer设置为autocommit,即consumer一旦读到数据立即自动commit。如果只讨论这一读取消息的过程,那Kafka是确保了Exactly once。但实际上实际使用中consumer并非读取完数据就结束了,而是要进行进一步处理,而数据处理与commit的顺序在很大程度上决定了消息从broker和consumer的delivery guarantee semantic。
读完消息先commit再处理消息(At most once)
读完消息先处理再commit消费状态(保存offset)(At least once)
Exactly once经典的做法是引入两阶段提交,但由于许多输出系统不支持两阶段提交,更为通用的方式是将offset和操作输入存在同一个地方。
比如,consumer拿到数据后可能把数据放到HDFS,如果把最新的offset和数据本身一起写到HDFS,那就可以保证数据的输出和offset的更新要么都完成,要么都不完成,间接实现Exactly once。(目前就high level API而言,offset是存于Zookeeper中的,无法存于HDFS,而low level API的offset是由自己去维护的,可以将之存于HDFS中)。
总之,Kafka默认保证At least once,并且允许通过设置producer异步提交来实现At most once,而Exactly once要求与目标存储系统协作,Kafka提供的offset可以较为容易地实现这种方式
。
consumer采用pull(拉)模式从broker中读取数据。
push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据consumer的消费能力以适当的速率消费消息。
对于Kafka而言,pull模式更合适,它可简化broker的设计,consumer可自主控制消费消息的速率,同时consumer可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。
pull模式不足之处是,如果kafka没有数据,消费者可能会陷入循环中,一直等待数据到达。为了避免这种情况,我们在我们的拉请求中有参数,允许消费者请求在等待数据到达的“长轮询”中进行阻塞。
缓冲和削峰:上游数据时有突发流量,下游可能扛不住,或者下游没有足够多的机器来保证冗余,kafka 在中间可以起到一个缓冲的作用,把消息暂存在 kafka 中,下游服务就可以按照自己的节奏进行慢慢处理。
解耦和扩展性:项目开始的时候,并不能确定具体需求。消息队列可以作为一个接口层,解耦重要的业务流程。只需要遵守约定,针对数据编程即可获取扩展能力。
冗余:可以采用一对多的方式,一个生产者发布消息,可以被多个订阅topic 的服务消费到,供多个毫无关联的业务使用。
健壮性:消息队列可以堆积请求,所以消费端业务即使短时间死掉,也不会影响主要业务的正常进行。
异步通信:很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
Kafka消费者在消费消息时,会维护一个offset(偏移量)来表示已经消费过的消息位置。默认情况下,Kafka消费者是以最新的offset开始消费的,也就是说只会消费从启动时开始后生产的消息。如果需要重新消费之前已经消费过的消息,可以通过以下三种方式:
① 将消费者的group.id修改,这样可以让Kafka认为这是一个新的消费者组,从而重新开始消费所有消息。(Kafka的consumer group的名字是在写代码时自定义的,可以根据应用场景来起一个有意义的名字。consumer group的作用是将多个consumer组织在一起,共同消费一个或多个Kafka topic中的消息,实现负载均衡和高可用性。每个consumer group内部的consumer之间负责消费不同的partition,同一个partition只能被一个consumer消费。因此,consumer group的名字需要根据业务需求来设计,保证不同的consumer group之间消费的数据不会有重复。)
② 手动设置消费者的offset。Kafka提供了seek()方法,可以让消费者跳转到指定的offset处,从而重新开始消费之前的消息。
③ 可以在 redis 中自己记录 offset 的 checkpoint 点(n 个),当想重复消费消息时,通过读取 redis 中的 checkpoint 点进行 zookeeper 的 offset 重设,这样就可以达到重复消费消息的目的。
需要注意的是,如果消费者在重复消费时,消费的消息会和之前消费的消息有重复,因此需要在消费者端进行去重处理。
采集层 主要可以使用 Flume, Kafka 等技术。
Flume:Flume 是管道流方式,提供了很多的默认实现,让用户通过参数部署,及扩展 API.
Kafka:Kafka 是一个可持久化的分布式的消息队列。 Kafka 是一个非常通用的系统,可以有许多生产者和很多的消费者共享多个主题 Topics。
相比之下,Flume 是一个专用工具被设计为旨在往 HDFS,HBase 发送数据。它对HDFS 有特殊的优化,并且集成了 Hadoop 的安全特性。
所以,Cloudera 建议如果数据被多个系统消费的话,使用 kafka;如果数据被设计给 Hadoop 使用,使用 Flume
。
Kafka的重启可能会导致数据丢失,具体取决于多种因素,如:
① 集群中副本数量和ISR列表:如果主题的分区副本数量小于3或者ISR列表中没有足够的副本,那么在重启期间可能会出现数据丢失。
② 生产者的acks参数设置:如果生产者的acks参数设置为0,即不需要确认,那么在重启期间可能会出现数据丢失。
③ 日志清理策略 :如果Kafka的日志清理策略设置为删除策略,那么在重启期间可能会出现数据丢失。
④ 在重启 kafka 过程中,如果有消费者消费消息,那么 kafka 如果来不及提交 offset,可能会造成数据的不准确(丢失或者重复消费)。
为了避免数据丢失,可以采取以下措施:
① 将主题的分区副本数量设置为3,并且保证ISR列表中有足够的副本。
② 将生产者的acks参数设置为1或者all,以确保数据已经被写入Kafka。
③ 将Kafka的日志清理策略设置为压缩策略或者合并策略,这样可以保留更多的数据并减少数据丢失的可能性。
④ 在 Kafka 中,可以通过设置消费者参数 auto.commit.offset 来控制消费者的 Offset 提交行为。如果将 auto.commit.offset 设置为 false,那么 Kafka 就不会自动提交 Offset。在这种情况下,消费者需要手动提交 Offset,以确保消息被成功消费并提交 Offset。手动提交 Offset 有两种方式:
同步提交:消费者在消费完一批消息后,通过调用 commitSync() 方法来提交 Offset。这种方式提交 Offset 后会阻塞线程,直到 Kafka 返回提交成功或失败的响应。
异步提交:消费者在消费完一批消息后,通过调用 commitAsync() 方法来提交 Offset。这种方式提交 Offset 后不会阻塞线程,但是需要通过回调函数来获取提交成功或失败的响应。
如果在 Kafka 重启期间有未提交的 Offset,那么消费者在重启后可以使用提交过的 Offset 或者通过设置 auto.offset.reset 参数来重置 Offset。 但是如果消费者在重启前未能提交 Offset,或者提交的 Offset 已经过期,那么就有可能造成数据的丢失或者重复消费。因此,在生产环境中,我们应该合理地设置消费者的 Offset 提交行为,以确保消费者能够及时提交 Offset 并处理异常情况。
在 Kafka 中,生产者写入消息、消费者读取消息的操作都是与 leader 副本进行交互的,从 而实现的是一种主写主读
的生产消费模型。 Kafka 并不支持主写从读,因为主写从读有 2 个很明显的缺点:
1. 数据一致性问题:数据从主节点转到从节点必然会有一个延时的时间窗口,这个时间 窗口会导致主从节点之间的数据不一致。某一时刻,在主节点和从节点中 A 数据的值都为 X, 之后将主节点中 A 的值修改为 Y,那么在这个变更通知到从节点之前,应用读取从节点中的 A 数据的值并不为最新的 Y,由此便产生了数据不一致的问题。
2. 延时问题:类似 Redis 这种组件,数据从写入主节点到同步至从节点中的过程需要经历 网络→主节点内存→网络→从节点内存 这几个阶段,整个过程会耗费一定的时间。而在 Kafka 中,主从同步会比 Redis 更加耗时,它需要经历 网络→主节点内存→主节点磁盘→网络→从节 点内存→从节点磁盘 这几个阶段。对延时敏感的应用而言,主写从读的功能并不太适用。
而 kafka 的主写主读的优点就很多了:
1. 可以简化代码的实现逻辑,减少出错的可能;
2. 将负载粒度细化均摊,与主写从读相比,不仅负载效能更好,而且对用户可控;
3. 没有延时的影响;
4. 在副本稳定的情况下,不会出现数据不一致的情况。
每个分区只能由同一个消费组内的一个消费者(consumer)来消费,可以由不同的消费组的消费者来消费,同组的消费者则起到并发的效果。
连接 ZK 集群,从 ZK 中拿到对应 topic 的 partition 信息和 partition的 Leader 的相关信息
连接到对应 Leader 对应的 broker
consumer 将自⼰已保存的 offset 发送给 Leader
Leader 根据 offset 等信息定位到 segment(索引⽂件和日志文件)
根据索引文件中的内容,定位到日志文件中该偏移量对应的开始位置读取相应长度的数据并返回给 consumer
Kafka 幂等性是指在生产者向 Kafka 发送消息时,通过一定的机制保证相同的消息只会被写入一次,即使生产者重试多次。这样可以避免因为消息重复写入导致的数据错误问题。
在 Kafka 0.11.0.0 版本及以后,Kafka 增加了对幂等性的支持。幂等性机制的实现主要依靠生产者 ID 和序列号(sequence number)两个变量。生产者 ID 用来标识生产者,序列号用来标识生产者发送的消息序列号。
当生产者向 Kafka 发送消息时,如果启用了幂等性机制,则会带上生产者 ID 和序列号。Kafka 将会对序列号进行验证,只有当序列号是未见过的才会被写入到分区中。当写入成功后,Kafka 会将生产者 ID 和序列号存储到磁盘上,以便下次验证序列号时使用。
需要注意的是,幂等性机制并不能完全保证消息不会被重复写入,因为即使在序列号相同的情况下,生产者也可能会重试多次。不过,幂等性机制可以大大减少消息被重复写入的概率,从而提高消息写入的可靠性。
Producer幂等性机制存在的问题:
1) 只能保证 Producer 在单个会话内不丟不重, 如果 Producer 出现意外挂掉再重启是无法保证的(幂等性情况下, 是无法获取之前的状态信息, 因此是无法做到跨会话级别的不丢不重) 。
2) 幂等性不能跨多个 Topic-Partition, 只能保证单个 Partition 内的幂等性, 当涉及多个 Topic-Partition 时, 这中间的状态并没有同步。
Kafka 从 0.11 版本开始引入了事务支持。 事务可以保证 Kafka 在 Exactly Once 语义的基础上, 生产和消费可以跨分区和会话, 要么全部成功, 要么全部失败。
1) Producer 事务
为了实现跨分区跨会话的事务, 需要引入一个全局唯一的 Transaction ID, 并将 Producer获得的 PID 和 Transaction ID 绑定。这样当 Producer重启后就可以通过正在进行的 TransactionID 获得原来的 PID。
为了管理 Transaction, Kafka 引入了一个新的组件 Transaction Coordinator。 Producer 就是通过和 Transaction Coordinator 交互获得 Transaction ID 对应的任务状态。 Transaction Coordinator 还负责将事务所有写入 Kafka 的一个内部 Topic, 这样即使整个服务重启, 由于事务状态得到保存, 进行中的事务状态可以得到恢复, 从而继续进行。
2) Consumer 事务
上述事务机制主要是从 Producer 方面考虑, 对于 Consumer 而言, 事务的保证就会相对较弱, 尤其是无法保证 Commit 的信息被精确消费。 这是由于 Consumer 可以通过 offset 访问任意信息, 而且不同的 Segment File 生命周期不同, 同一事务的消息可能会出现重启后被删除的情况。