术语
在kafka中,发布订阅的对象是主题(Topic),你可以为每个业务每个应用都创建专属的主题。
kafka的服务器端由Broker服务进程组成。一个Kafka集群由多个Broker组成,Broker负责接收和处理客户端发送过来的请求,以及对消息进行持久化。虽然多个Broker进程能够运行在同一台机器上,但是更常见的做法是将不同的Broker分散运行在不同的机器上。
Kafka中的分区机制指的是将每个主题划分为多个分区(Partition),每个分区是一组有序的消息日志。生产者生产的每条消息只会被发送到一个分区中。如果向一个双分区的主题发送一条消息,这条消息要么在分区0中,要么在分区1中。Kafka的分区编号是从0开始的。
副本和分区是如何联系在一起的呢?副本是在分区这个层级定义的。每个分区下可以配置若干个副本,其中只有1个领导者副本和N-1个追随者副本。
生产者想分区写入消息,每条消息在分区中的位置信息由一个叫位移(Offset)的数据表示。分区位移总是从0开始。
Kafka的三层消息架构:
- 第一层是主题层,每个主题可以配置M个分区,每个分区又可以配置N个副本。
- 第二层是分区层,每个分区的N个副本只能有一个充当领导者角色,对外提供服务,其他N-1个副本是追随者副本,只是提供数据冗余使用。
- 第三层是消息层,分区中包含若干条消息,每条消息的位移从0开始,依次递增。
- 客户端程序只能与分区的领导者副本进行交互。
Kafka使用消息日志来保存数据。一个日志就是磁盘上一个只能追加鞋消息的物理文件。Kafka会定期删除消息以回收磁盘。简单来说就是通过日志段(Log Segment)机制,在Kafka底层,一个日志又进一步细分成多个日志段,消息被追加到当前最新的日志段中,当写满一个日志段后,Kafka会自动切分出一个新的日志段,并将老的日志段封存起来。后台会有定时任务检查老的日志段是否能够被删除。
所谓消息者组,指的是多个消费者实例共同组成一个组来消费一组主题。这组主题中的每个分区都只会被组内的一个消费实例消费,其他消费者实例不能消费它。每个消费者在消费消息的过程中必然需要又个字段记录它当前消费到了分区的哪个位置上,这个字段就是消费者位移(Consumer Offset)。
重平衡:Rebalance。消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配主题分区的过程。
集群参数配置
Broker 是需要配置存储信息的,即 Broker 使用哪些磁盘。那么针对存储信息的重要参数有以下这么几个:
-
log.dirs:这是非常重要的参数,指定了 Broker 需要使用的若干个文件目录路径。要知道这个参数是没有默认值的,这说明什么?这说明它必须由你亲自指定。
-
log.dir:注意这是 dir,结尾没有 s,说明它只能表示单个路径,它是补充上一个参数用的。
这两个参数应该怎么设置呢?很简单,你只要设置log.dirs,即第一个参数就好了,不要设置log.dir。而且更重要的是,在线上生产环境中一定要为log.dirs配置多个路径,具体格式是一个 CSV 格式,也就是用逗号分隔的多个路径,比如/home/kafka1,/home/kafka2,/home/kafka3这样。这样设置主要是提升读写性能,实现故障转移。
与ZooKeeper相关的设置,zookeeper负责协调管理并保存Kafka集群的所有元数据信息。比如集群都有哪些Broker在运行,创建了哪些Topic,每个Topic都有多少分区以及这些分区的Leader副本都在哪些机器上等信息。
Kafka 与 ZooKeeper 相关的最重要的参数当属zookeeper.connect。这也是一个 CSV 格式的参数,比如我可以指定它的值为zk1:2181,zk2:2181,zk3:2181。2181 是 ZooKeeper 的默认端口。
Broker连接相关的参数:
-
listeners:告诉外部连接者要通过什么协议访问指定主机名和端口开放的 Kafka 服务。
-
advertised.listeners: 表示这组监听器是Broker用于对外发布的。
监听器是有若干个逗号分隔的三元组,每个三元组的格式为<协议名称,主机名,端口号>
关于Topic管理的参数:
-
auto.create.topics.enable:是否允许自动创建 Topic。auto.create.topics.enable参数我建议最好设置成 false,即不允许自动创建 Topic。
-
unclean.leader.election.enable:是否允许 Unclean Leader 选举。设置成false,表示不允许那些落后太多的副本竞选Leader,这样做的后果就是这个分区不可用了。设置为true,表示允许从那些跑的慢的副本中选出一个当Leader,这样做的后果是数据有可能丢失。
-
auto.leader.rebalance.enable:是否允许定期进行 Leader 选举。设置成true表示允许Kafka定期对一些Topic分区进行Leader重选举。此参数与上一个不同之处在于它不是选leader,而是换leader。生产环境建议将此参数设置成false。
最后一组参数是数据留存方面的:
- log.retention.{hours|minutes|ms}:都是控制一条消息数据被保存多长时间。从优先级上来说,ms设置最高,minutes次之,hours最低。
- log.retention.bytes:这是指定 Broker 为消息保存的总磁盘容量大小
- message.max.bytes:控制 Broker 能够接收的最大消息大小。
Topic级别参数
同时设置了Topic级别参数和全局Broker参数的话,Topic级别参数会覆盖Broker参数的值。
retention.ms:规定了该 Topic 消息被保存的时长。默认是 7 天,即该 Topic 只保存最近 7 天的消息。一旦设置了这个值,它会覆盖掉 Broker 端的全局参数值。
retention.bytes:规定了要为该 Topic 预留多大的磁盘空间。和全局参数作用相似,这个值通常在多租户的 Kafka 集群中会有用武之地。当前默认值是 -1,表示可以无限使用磁盘空间。
创建Topic命令:
bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic transaction --partitions 1 --replication-factor 1 --config retention.ms=15552000000 --config max.message.bytes=5242880
假设我们现在要发送最大值是10MB的消息,该如何修改呢?
bin/kafka-configs.sh --zookeeper localhost:2181 --entity-type topics --entity-name transaction --alter --add-config max.message.bytes=10485760
操作系统参数:
首先是ulimit -n。我觉得任何一个 Java 项目最好都调整下这个值。通常情况下将它设置成一个超大的值是合理的做法,比如ulimit -n 1000000。其实设置这个参数一点都不重要,但不设置的话后果很严重,比如你会经常看到“Too many open files”的错误。
swap 的调优。网上很多文章都提到设置其为 0,将 swap 完全禁掉以防止 Kafka 进程使用 swap 空间。
Flush 落盘时间。向 Kafka 发送数据并不是真要等数据被写入磁盘才会认为成功,而是只要数据被写入到操作系统的页缓存(Page Cache)上就可以了,随后操作系统根据 LRU 算法会定期将页缓存上的“脏”数据落盘到物理磁盘上。这个定期就是由提交时间来确定的,默认是 5 秒。
生产者消息分区机制原理剖析
分区策略:
-
轮训策略:Round-robin策略,比如一个主题有三个分区,那么第一条消息被发送到分区0,第二条被发送到分区1,以此类推。这个是生产者API默认题佛那个的分区策略。
-
随机策略:就是随意将消息放置到任意一个分区上。
-
按消息健保存策略:Kafka允许为每条消息定义消息键。一旦消息被定义了key,纳闷就可以保证同一个Key的所有消息进入相同的分区里。
Kafka 只对已提交的消息做有限度的持久化保证。
当Kafka的若干个Broker成功地接收到一条消息并写入到日志文件后,这条消息就变成已提交消息了。
消息丢失的案例:
-
生产者程序丢失数据。目前Kafka Producer 是异步发送消息的,如果调用的是producer.send(msg) 这个api,那么它通常会立即返回,但此时不能认为消息已成功完成。当网络抖动会导致消息压根没有发送到Broker端;或者消息本身不合格导致Broker拒绝接收(比如消息太大,超过Broker的承受能力)。解决此问题的方法非常简单:Producer用友要使用带回调通知的发送API,要使用producer.send(msg,callback).
-
消费者程序丢失数据。Consumer程序有个位移的概念,表示这个Consumer当前消费到Topic分区的位置。如下图:
当Consumer程序从Kafka获得消息后开启了多个线程异步处理消息,而Consumer程序自动向前更新位移,假如某个线程运行失败了,它负责的消息没有被成功处理,但位移更新了,这条消息对于Consumer而言实际上是丢失了。这种情况的解决方案也很简单:如果是多线程异步处理消费消息,Consumer不要开启自动提交位移,而是要应用程序手动提交位移。
Kafka无消息丢失配置的最佳实践:
- 不要使用producer.send(msg),而要使用 producer.send(msg, callback)
- 设置 acks = all。acks 是 Producer 的一个参数,代表了你对“已提交”消息的定义。如果设置成 all,则表明所有副本 Broker 都要接收到消息,该消息才算是“已提交”。这是最高等级的“已提交”定义。
- 设置 retries 为一个较大的值。这里的 retries 同样是 Producer 的参数,对应前面提到的 Producer 自动重试。当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了 retries > 0 的 Producer 能够自动重试消息发送,避免消息丢失。
- 设置 unclean.leader.election.enable = false。这是 Broker 端的参数,它控制的是哪些 Broker 有资格竞选分区的 Leader。如果一个 Broker 落后原先的 Leader 太多,那么它一旦成为新的 Leader,必然会造成消息的丢失。故一般都要将该参数设置成 false,即不允许这种情况的发生。
- 设置 replication.factor >= 3。这也是 Broker 端的参数。其实这里想表述的是,最好将消息多保存几份,毕竟目前防止消息丢失的主要机制就是冗余
- 设置 min.insync.replicas > 1。这依然是 Broker 端参数,控制的是消息至少要被写入到多少个副本才算是“已提交”。设置成大于 1 可以提升消息持久性。在实际环境中千万不要使用默认值 1。
- 确保 replication.factor > min.insync.replicas。如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作了。我们不仅要改善消息的持久性,防止数据丢失,还要在不降低可用性的基础上完成。推荐设置成 replication.factor = min.insync.replicas + 1。
- 确保消息消费完成再提交。Consumer 端有个参数 enable.auto.commit,最好把它设置成 false,并采用手动提交位移的方式。就像前面说的,这对于单 Consumer 多线程处理的场景而言是至关重要的。
Kafka拦截器
Kafka 拦截器分为生产者拦截器和消费者拦截器。生产者拦截器允许你在发送消息前以及消息提交成功后植入你的拦截器逻辑;而消费者拦截器支持在消费消息前以及提交位移后编写特定逻辑。值得一提的是,这两种拦截器都支持链的方式,即你可以将一组拦截器串连成一个大的拦截器,Kafka 会按照添加顺序依次执行拦截器逻辑。
Kafka拦截器的设置方法是通过参数配置完成的,生产者和消费者两端有一个相同的参数,名字叫interceptor.classes。
Java生产者如何管理TCP连接
Kafka的所有通信是基于TCP的。Kafka的Java生产者API主要对象就是KafkaProducer,通常我们开发一个生产者有4步:
-
构造生产者对象所需的参数对象。
-
利用第一步的参数对象,创建KafkaProducer对象实例。
-
使用KafkaProducer的send方法发送消息。
-
调用KafkaProducer的close方法关闭生产者并释放各种系统资源。
大致是这样子:
Properties props = new Properties (); props.put(“参数1”, “参数1的值”); props.put(“参数2”, “参数2的值”); …… try (Producer<String, String> producer = new KafkaProducer<>(props)) { producer.send(new ProducerRecord<String, String>(……), callback); …… }
生产者创建KafkaProducer实例时是会建立与Broker的TCP连接的。具体讲:在创建KafkaProducer实例时,生产者应用会在后台创建一个名为Sender的线程,该Sender线程开始运行时首先会创建与Broker的连接。Producer启动时会发起与所有Broker的TCP连接。
Producer端关闭TCP连接的方式有两种:一种是用户主动关闭;一种是Kafka自动关闭。Producer端参数connections.max.idle.ms与此有关,默认情况下这个参数值为9分钟,即如果在9分钟内没有任何请求“流过”某个TCP连接,那么Kafka会主动帮你把TCP连接关闭。
消费者组
Consumer Group 是Kafka提供的可扩展且有容错性的消费者机制。组内有多个消费者或者消费者实例,它们共享一个公共的ID,Group ID。组内的所有消费者协调在一起来消费订阅主题的所有分区Partition。当然每个分区只能有同一个消费者组内的一个Consumer实例来消费。
Consumer Group 下可以有一个或多个Consumer实例。这里的实例可以是一个单独的进程。也可以是同一个进程下的线程。
Group ID是一个字符串,在一个Kafka集群中,它标示唯一的一个Consumer Group。
Consumer Group下所有实例订阅的主题的单个分区,只能分配给组内的某个consumer实例消费。
如果所有的实例都属于同一个Group,那么它实现的就是消息队列模型,如果所有实例分别属于不同的Group,那么它实现的就是发布/订阅模型。
理想情况下,Consumer实例的数量应该等于该Group订阅主题的分区总数。
关于位移,新版本的Consumer Group将位移保存在Broker端的内部主题中。
Rebalance 本质是一种协议,规定了一个Consumer Group下的所有Consumer如何达成一致,来分配订阅Topic的每个分区。
位移主题
位移主题 _consumer_offsets 主要是用来管理Consumer的位移管理。原理就是 将Consumer的位移数据作为一条普通的Kafka消息,提交到_consumer_offsets中。即,__consumer_offsets主要作用是保存Kafka消费者的位移消息。
位移主题是一个普通的Kafka主题,它的消息格式是Kafka自己定义的。位移主题的key包含3部分内容;<Group ID ,主题名,分区号>
当Kafka集群的第一个Consumer程序启动时,Kafka会自动创建位移主题。这个位移主题的默认分区数是50.副本数是3.
Kafka Consumer提交位移的方式有两种,自动提交位移和手动提交位移。自动提交位移就是Consumer在后台默默顶起提交位移,但是这个就会丧失灵活性和可控性。比如Consumer当前消费了一个位移消息是100的消息,但是后面没有任务消息产生,那么位移主题会不停地写入位移=100的消息,显然kafka只需要保留这类消息的最新一条就可以了,之前的消息是可以删除的。手动提交位移,作为Consumer的开发,就要承担起位移提交的责任,Kafka consumer API 提供了位移提交的方法如 consumer.commitSync等。
Kafka是如何删除位移主题中的过期消息呢?答案是Cōmpaction.compact策略会对同一个key的两条消息M1和M2进行整理。如果M1消息早于M2,那么M1是过期消息。那么M1就要删除掉。
Kafka提供了专门的后台线程定期巡检待Compact的主题,看看是否存在满足条件的可删除数据。这个后台线程叫Log Cleaner