1.术语
1.broker
broker是Kafka的实例,每个服务器可以有一个或者多个Kafka实例,Kafka集群内的broker有不同的编号
2.topic与消息
Kafka将所有的消息组织成多个topic的形式存储,而每个topic又可以被拆分成多个partition,每个partition又是由一个一个的消息组成,每个消息都被标识了一个递增的序列号代表其进来的先后顺序,并且按照顺序存储在partition中。
3.partition
每个topic划分为一个或多个partition,提前划分合适的partition有利于后续Kafka集群的扩容和提高并发消费能力,且Kafka保证了partition内消息有序性
4.Replication
每一个分区都有多个副本,副本的作用是做备胎。当主分区(Leader)故障的时候会选择一个备胎(Follower)上位,成为Leader。在kafka中默认副本的最大数量是10个,且副本的数量不能大于Broker的数量,follower和leader绝对是在不同的机器,同一机器对同一个分区也只可能存放一个副本(包括自己)
4.producer
生产者生产消息需要以下几个必要参数,指定往哪个topic生产消息,如何根据key将消息分区到不同的partition中,消息的具体内容
5.consumer
每个consumer将自己标记为某一个consumer group,之后系统会将consumer按group分组,将消息发送给所有的分组,每个分组只有一个consumer能消费这条消息。
一般消息系统中consumer存在两种消费模型,push: 优势在于消息实时性高,但是没有考虑consumer消费能力和饱和度,容易出现producer压垮consumer;pull: 优势在于可以控制消费速度,保证consumer不会出现过饱和,但是在没有数据时会出现空轮训,消耗cpu
6.消息发送的语义
-
producer角度
- 消息最多发送一次,producer异步发送消息,或者同步发消息但重试次数为0
- 消息至少发送一次,producer同步发送消息,失败、超时都会出发重试
- 消息发且仅发一次
-
consumer角度
- 消息最多消费一次,consumer先读取消息,再确认position,最后处理消息
- 消息至少消费一次,consumer先读取消息,再处理消息,最后确认position
- 消息消费且消费一次
7.Kafka特性
1.可用性
在Kafka中,正常情况下所有node处于同步中状态,当某个node处于非同步中状态,也就意味着整个系统出问题,需要容错处理。同步中代表了:该node与zookeeper能连通;该node如果是follower,那么consumer position与leader不能差距太大。某个分区内同步中的node组成一个集合,即该分区的ISR(In-Sync Replicas)。
Kafka通过两个手段容错:数据备份,以partition为单位备份,副本数可设置,当副本数为N时,代表一个leader,N-1个followers,followers可以视为leader的consumer,拉取leader的消息,append到自己的系统中;failover,当leader处于非同步中时,系统从ISR中选取新leader,当某个follower处于非同步中时,leader会将此follower剔除ISR,当此follower恢复并完成数据同步后再次进入ISR。当producer生产消息时,只有当消息被所有的ISR确认时,才表示该消息成功提交,只有提交成功的消息才能被consumer消费。
脏leader选举。假设N个副本全挂了,node恢复后面临同步数据的过程,这期间ISR中没有node,会导致该分区服务不可用,Kafka采用一种降级措施来处理,选举第一个恢复的node作为leader提供服务,以他的数据为基准
2.一致性
上面的方案保证的数据的可用性,有时高可用是以牺牲一定的一致性为代价,如果希望达到强一致性,可以采取如下措施:
禁用脏leader选举
设置最小ISR数量min_isr
3.持久性
Kafka依赖磁盘而非内存。
顺序读,数据结构选取queue,操作只有根据offset读和append,基于queue时间复杂度只有O(1),
2.Kafka-Producer
-
参数
-
bootstrap.servers,用于找到Kafka集群
该参数指定了一组host:port 对,用于创建向 Kafka broker 服务器的连接,比如:kl:9092,k2:9092,k3:9092。
如果 Kafka 集群中机器数很多,那么只需要指定部分 broker 即可,不需要列出所有的机器。因为不管指定几台机器,producer 都会通过该参数找到井发现集群中所有的 broker;为该参数指定多台机器只是为了故障转移使用。这样即使某一台 broker 挂掉了,producer 重启后依然可以通过该参数指定的其他 broker 连入 Kafka 集群。
-
key.serializer、value.serialize ,序列化数据 被发送到 broker 端的任何消息的格式都必须是字节数组,因此消息的各个组件必须首先做序列化,然后才能发送到 broker。该参数就是为消息的 key 做序列化之用的。这个参数指定的是实现org.apache.kafka.common.serialization.Serializer接口的类的全限定名称。
-
acks,保证消息持久性
acks 参数用于控制 producer 生产消息的持久性(durability);对于 producer 而言, Kafka在乎的是“己提交”消息的持久性。一旦消息被成功提交,那么只要有任何一个保存了该消息的副本“存活”,这条消息就会被视为“不会丢失的” 。acks 指定了在给 producer 发送响应前, leader broker 必须要确保己成功写入该消息的副本数 。 当前 acks 有 3 个取值: 0、 1和 all 。
-
buffer.memory,缓存待发送数据
该参数指定了 producer 端用于缓存消息的缓冲区大小,单位是字节,默认值是 33554432,即 32MB 。由于采用了异步发送消息的设计架构, Java 版本 producer 启动时会首先创建一块内存缓冲区用于保存待发送的消息,然后由另 一个专属线程负责从缓冲区中读取消息执行真正的发送。这部分内存空间的大小即是由 buffer.memory 参数指定的。若 producer 向缓冲区写消息的速度超过了专属 I/0 线程发送消息的速度,那么必然造成该缓冲区空间的不断增大。此时 producer 会停止手头的工作等待 I/0 线程追上来,若一段时间之后 I/0 线程还是无法追上 producer 的进度, 就会抛出异常;若 producer 程序要给很多分区发送消息,那么就需要仔细地设置这个参数,以防止过小的内存缓冲区降低了producer 程序整体的吞吐量。
-
compression.type,如何压缩数据
设置 producer 端是否压缩消息,默认值是 none ,即不压缩消息 。Kafka 的 producer 端引入压缩后可以显著地降低网络 I/O 传输开销从而提升整体吞吐量,但也会增加 producer 端机器的 CPU 开销。另外,如果 broker 端的压缩参数设置得与 producer 不同, broker 端在写入消息时也会额外使用 CPU 资源对消息进行对应的解压缩-重新压缩操作。目前 Kafka 支持 3 种压缩算法:GZIP、Snappy 和 LZ4。根据实际使用经验来看 producer 结合 LZ4 的性能是最好; LZ4 > Snappy > GZIP;
-
retries,重试次数
Kafka broker 在处理写入请求时可能因为瞬时的故障(比如瞬时的leader选举或者网络抖动)导致消息发送失败。这种故障通常都是可以自行恢复的,如果把这些错误封装进回调函数的异常中返还给 producer,producer程序也并没有太多可以做的,只能简单地在回调函数中重新尝试发送消息。与其这样,还不如 producer 内部自动实现重试。因此 Java 版本 producer 在内部自动实现了重试,当然前提就是要设置retries参数。
-
batch.size,批量发送大小
producer 会将发往同一分区的多条消息封装进一个 batch中,当 batch 满了的时候, producer 会发送 batch 中的所有消息 。不过, producer并不总是等待batch满了才发送消息,很有可能当batch还有很多空闲空间时 producer 就发送该 batch 。显然,batch 的大小就显得非常重要 。通常来说,一个小的 batch 中包含的消息数很少,因而一次发送请求能够写入的消息数也很少,所以 producer 的吞吐量会很低;一个 batch 非常之巨大,那么会给内存使用带来极大的压力,因为不管是否能够填满,producer 都会为该batch 分配固定大小的内存。因此batch.size 参数的设置其实是一种时间与空间权衡的体现 。batch.size 参数默认值是 16384 ,即 16KB 。这其实是一个非常保守的数字。 在实际使用过程中合理地增加该参数值,通常都会发现 producer 的吞吐量得到了相应的增加 。
-
linger.ms,延迟发送 批量发送时满足batch.size和linger.ms之一,producer便开始发送消息。
-
-
自定义partition选择器
3.Kafka-Consumer
-
参数
-
bootstrap.servers 用于找到Kafka集群
和 Java 版本 producer 相同
-
group.id 分组管理consumer
该参数指定的是 consumer group 的名字,同一个消费组内消息只被消费一次
-
key.deserializer,value.deserializer 反序列化数据
反序列化数据
-
session.timeout.ms 协调者检测成员崩溃所需要的时间
consumer group 检测组内成员发送崩溃的时间,假设你设置该参数为 5 分钟,那么当某个 group 成员突然崩攒了(比如被 kill -9 或岩机), 管理 group 的 Kafka 组件(即消费者组协调者,也称 group coordinator)有可能需要 5 分钟才能感知到这个崩溃。显然我们想要缩短这个时间,让coordinator 能够更快地检测到 consumer 失败 consumer 消息处理逻辑的最大时间,倘若 consumer 两次 poll 之间的间隔超过了该参数所设置的阑值,那么 coordinator 就会认为这个 consumer 己经追不上组内其他成员的消费进度了,因此会将该 consumer 实例“踢出”组,该 consumer 负责的分区也会被分配给其他 consumer
两种情况都会导致rebalance,第二种情况是多余的。
在0 .10.1.0 版本及以后的版本中, session.timeout.ms 参数被明确为“ coordinator 检测失败的时间” 。
-
max.poll.interval.ms 消息处理最大时间
session. neout.ms 中“ consumer 处理逻辑最大时间”的含义被剥离出来了,max.poll.interval.ms就承担此职责
-
auto.offset. reset offset非法时的兜底策略
指定了无位移信息或位移越界(即 consumer 要消费的消息的位移不在当前消息日志的合理区间范围)时 Kafka 的应对策略 。 特别要注意这里的无位移信息或位移越界,只有满足这两个条件中的任何一个时该参数才有效果 。
- earliest:指定从最早的位移开始消费 。 注意这里最早的位移不一定就是 0 。
- latest:指定从最新处位移开始消费 。
- none :指定如果未发现位移信息或位移越界,则抛出异常。笔者在实际使用过程中几乎从未见过将该参数设置为 none 的用法,因此该值在真实业务场景中使用甚少。
-
enable.auto.commit 自动提交位移
该参数指定 consumer 是否自动提交位移 。若设置为 true,则 consumer 在后台自动提交位移;否则,用户需要手动提交位移。
-
fetch.max.bytes
一次拉取消息最大字节数
-
max.poll.records
一次拉取最大消息数
-
heartbeat.interval.ms 帮助consumer group组内成员快速感知其他节点的情况,尽快开始rebalance
这里的关键在于要搞清楚 consumer group 的其他成员,如何得知要开启新一轮 rebalance;当 coordinator 决定开启新一轮 rebalance 时,它会将这个决定以 REBALANCE_IN_PROGRESS 异常的形式“塞进” consumer 心跳请求的 response 中,这样其他成员拿到 response 后才能知道它需要重新加入 group。显然这个过程越快越好,而heartbeat. interval.ms 就是用来做这件事情的 。
-
connections.max.idle.ms 连接空闲时间
连接最大空闲时间,socket连接超过这个时间后就会关闭该socket,如果不介意socket开销,可以设置为-1,永远不关闭连接
-
-
@KafkaListener原理
-
KafkaListenerAnnotationBeanPostProcessor负责处理该注解,继承关系如下
//该后置处理器在bean初始化前后调用, //KafkaListenerAnnotationBeanPostProcessor实现postProcessAfterInitialization,将使用该注解的方法注册到 //KafkaListenerEndpointRegistrar registrar 中 public interface BeanPostProcessor { @Nullable default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Nullable default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } } //在KafkaListenerAnnotationBeanPostProcessor实现afterSingletonsInstantiated,初始化MessageListenerContainer public interface SmartInitializingSingleton { void afterSingletonsInstantiated(); } public class KafkaListenerAnnotationBeanPostProcessor<K, V> implements BeanPostProcessor, Ordered, BeanFactoryAware, SmartInitializingSingleton{ } -
MessageListenerContainer间接继承Lifecycle,doStart()方法在初始化完成后被调用,ListenerConsumer循环调用Consumer的poll方法获取消息,然后调用使用Listener注解标注的方法,处理业务逻辑
public class KafkaMessageListenerContainer<K, V> // NOSONAR line count extends AbstractMessageListenerContainer<K, V> { @Override protected void doStart() { if (isRunning()) { return; } if (this.clientIdSuffix == null) { // stand-alone container checkTopics(); } ContainerProperties containerProperties = getContainerProperties(); checkAckMode(containerProperties); Object messageListener = containerProperties.getMessageListener(); if (containerProperties.getConsumerTaskExecutor() == null) { SimpleAsyncTaskExecutor consumerExecutor = new SimpleAsyncTaskExecutor( (getBeanName() == null ? "" : getBeanName()) + "-C-"); containerProperties.setConsumerTaskExecutor(consumerExecutor); } GenericMessageListener<?> listener = (GenericMessageListener<?>) messageListener; ListenerType listenerType = determineListenerType(listener); this.listenerConsumer = new ListenerConsumer(listener, listenerType); setRunning(true); this.startLatch = new CountDownLatch(1); this.listenerConsumerFuture = containerProperties .getConsumerTaskExecutor() .submitListenable(this.listenerConsumer); try { if (!this.startLatch.await(containerProperties.getConsumerStartTimout().toMillis(), TimeUnit.MILLISECONDS)) { this.logger.error("Consumer thread failed to start - does the configured task executor " + "have enough threads to support all containers and concurrency?"); publishConsumerFailedToStart(); } } catch (@SuppressWarnings(UNUSED) InterruptedException e) { Thread.currentThread().interrupt(); } } }
-
4.Kafka服务端原理参数分析
1.参数分析
-
broker.id
kafka使用唯一的一个整数来标识broker。该参数默认值是-1,如果不指定,kafka会自动生成一个唯一值。
-
log.dirs
非常重要的参数,kafka持久化消息的目录,该参数可以设置多个目录,用逗号分隔,这样kafka会将负载均匀地分配到多个目录下。如果每个目录都在不同的磁盘上,那么还能提升整体写消息的吞吐量。默认情况下是/tmp/kafka-logs。
-
zookeeper.connect
此参数没有默认值,必须指定,该参数可以是一个csv列表,如果使用一套zookeeper管理多个kafka集群,则zookeeper的chroot必须指定。
-
listeners,advertised.listeners
listeners是broker监听的csv列表,格式是[协议]://[主机名]:[端口],[协议]://[主机名]:[端口]。该参数用于客户端连接broker使用。如果不指定,默认绑定网卡;如果主机名是0.0.0.0,则表示绑定所有网卡。Kafka当前支持的协议类型包括:PLAINTEXT、SSL以及SASL_SSL等。advertised.listeners和listeners类似,该参数也是用于发布给client的监听器,不过该参数主要用于IaaS环境,比如云上的机器通常都配有多块网卡(私网网卡和公网网卡)。对于这种机器,用户可以设置该参数绑定公网IP供外部client使用。在公司内网部署 kafka 集群只需要用到 listeners,内外网需要作区分时 才需要用到advertised.listeners。
-
unclean.leader.election.enable
是否开启unclean leader选举。
-
delete.topic.enable
是否允许kafka删除topic。默认情况下,Kafka集群允许用户删除topic及其数据。
-
log.retention.{hours|minutes|ms}
这组参数控制了消息数据的存留时间。如果同时设置,优先选取ms的设置,minutes次之,hours最后。Kafka会根据日志文件的最后修改时间(last modified time)进行判断。
-
log.retention.bytes
上面的参数是时间维度,这个参数就是空间维度。它控制着Kafka集群需要为每个消息日志保存多大的数据。对于超过该参数的分区日志而言,Kafka会自动清理该分区的过期日志段文件。该参数默认值为-1,表示kafka永远不会根据消息日志文件总大小来删除日志。
-
min.insync.replicas
该参数实际是和product的acks参数配合使用的。它指定了broker端必须成功响应client消息发送的最小副本数。如果broker段无法满足,则client的消息并不会被认为是成功的。它与product的acks配置使用可以令kafka集群达到最高等级的消息持久化。
-
num.network.threads
控制broker在后台处理来自网络请求的线程数,默认是3。主要处理网络io,读写缓冲区数据,基本没有io等待,配置线程数量建议为cpu核数加1。
-
num.io.threads
该参数控制broker实际处理网络请求的线程数,默认是8。即Kafka创建8个线程,采用轮询的方式监听转发过来的网络请求并进行实时处理。num.io.threads主要进行磁盘io操作,高峰期可能有些io等待,因此配置需要大些。配置线程数量为cpu核数2倍,最大不超过3倍
-
message.max.bytes
Kafka broker能够接受的最大消息数,默认值是977KB。
-
log.flush.interval.messages=10000
每当producer写入10000条消息时,刷数据到磁盘
-
log.flush.interval.ms=1000
每间隔1秒钟时间,刷数据到磁盘
-
socket.send.buffer.bytes,socket.receive.buffer.bytes,socket.request.max.bytes
socket的发送缓冲区; socket的接受缓冲区;socket请求的最大数值,防止serverOOM,message.max.bytes必然要小于socket.request.max.bytes