第4章 Kafka工作机制详解

4.1 消息传递三种语义

4.1.1 消费者

  • at most once: 消费者fetch消息,然后保存offset,然后处理消息。当消费者保存offset之后,但是在消息处理过程中出现了异常,导致部分消息未能继续处理。那么此后“未处理”的消息将不能被重新fetch到,所以存在丢失数据的情况。想实现“至多一次”的做法是:设置消费者自动提交偏移量,并且设置较小的时间间隔。
  • at least once: 消费者fetch消息,然后处理消息,然后保存offset。如果消息处理成功之后,在保存offset阶段,发生异常导致保存操作未能执行成功,这就导致接下来再次fetch时可能获得上次已经处理过的消息。想实现“至少一次”的做法是:设置消费者自动提交偏移量,但设置很长的时间间隔(或者关闭自动提交偏移量),在处理完消息后,手动调用同步模式提交偏移量consumer.commitSync
  • exactly once,需要将消费者的消费进度和处理结果保存在同一介质中,再将这两个操作封装进一个原子性操作。

为了保证消息不丢失,通常情况下,选用at least once。

4.1.2 生产者

kafka的ack机制

  • 0:producer不等待broker的ack,broker一接收到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据;
  • 1:producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,那么将会丢失数据;
  • -1:producer等待broker的ack,partition的leader和follower全部落盘成功后才返回ack,数据一般不会丢失,延迟时间长但是可靠性高。

4.1.3 Broker配置

Broker有三个可以影响到Kafka的消息可靠性存储的设置。

  1. 默认复制因子default.replication.factor,设置更高的复制因子意味着更多的空间换可靠性,会牺牲更多的磁盘空间,还会导致在集群活跃节点少于因子数量的时候不能提供服务,不建议进行设置,而建议创建topic时根据数据重要程度指定副本数量。
  2. 最少同步副本数:min.insync.replicas,默认是1,若保证数据更高可靠性,建议设置为2,在副本数大于等于2个的时候,能避免数据处于只有一个节点活跃的状态,而必须等待有另外一个broker恢复可用时才可以继续进行数据传输。
  3. Unclean leader选举,建议将unclean.leader.election.enable属性为false,避免非同步副本变为leader,导致数据丢失和不一致。非同步副本中的消息远远落后于leader,如果选举这种副本作为leader可能会造成数据丢失,但会保证服务继续可用。

4.2 Kafka恢复与备份机制

kafka把每个parition的消息复制到多个broker上,任何一个parition都有一个leader和多个follower,备份个数可以在创建topic的时候指定。leader负责处理所有read/write请求,follower像consumer一样从leader接收消息并把消息存储在log文件中。leader还负责跟踪所有的follower状态,如果follower “落后”太多或失效,leader将会把它从replicas同步列表中删除。当所有的follower都将一条消息保存成功,此消息才被认为是“committed”。

4.3 Kafka与zookeeper的交互机制

当一个kafka broker启动后,会向zookeeper注册自己的节点信息,当broker和zookeeper断开链接时,zookeeper也会删除该节点的信息。除了自身的信息,broker也会向zookeeper注册自己持有的topic和partitions信息。

当一个consumer被创建时,会向zookeeper注册自己的信息,此作用主要是为了“负载均衡”。一个group中的多个consumer可以交错的消费一个topic的所有partitions。简而言之,保证此topic的所有partitions都能被此group所消费,为了性能的考虑,让partition相对均衡的分撒到每个consumer上。每一个consumer都有一个唯一的ID(host:uuid,可以通过配置文件指定,也可以由系统生成),此ID用来标记消费者信息,主要是topic+partition信息。

Producer端使用zookeeper用来“发现”broker列表,以及和Topic下每个partition leader建立socket连接并发送消息。

zookeeper上还存放partition被哪个consumer所消费的信息,以及每个consumer目前所消费的partition中的最大offset(0.9.0.0版本之前)。

在kafka 0.9版本之后,kafka为了减少与zookeeper的交互,减少网络数据传输,自己实现了在kafka server上存储consumer消费的topic,partitions,offset信息。

4.4 Kafka metircs

对kafka的metrics主要是对lags的分析,lags是topic/partition的logSize与consumer消费到的offset之间的差值,即producer产生数据的量与consumer消费数据的量的差值,差值越来越大,说明消费数据的速度小于产生数据的速度。一般可以认定是consumer出了问题。当然也不能只看某一lags大小,更重要的是关注lags的变化的趋势,当趋势越来越大时,可推断consumer的performance越来越差。

在kafka 0.8.1版本之后,可以通过配置把topic/partition的logsize,offset等信息存储在zookeeper上或存储在kafka server上。在做metrics时,注意可能需要分别从两边获取数据。

获取zookeeper上的kafka数据比较简单,可以通过SimpleConsumer配合zookeeper.getChildren方法获取consumerGroup, topic, paritions信息,然后通过SimpleConsumer的getOffsetsBefore方法获取logSize,fetchOffsets获取topic parition的currentOffsets。

获取kafka server上的数据比较麻烦,目前kafka 0.10提供的kafkaConsumer类主要还是关注topic消费,对consumerGroup及Group和topic关系的获取,还没有提供API。不过可以通过kafka-consumer-groups.sh得到group,topic等信息,这个shell文件里面调用了kafka.admin.ConsumerGroupCommand类,这个类确实提供了一个listGroup方法,可惜这个方法的返回值是void,shell文件的输出是打印到控制台的,并没有返回值。再去研究ConsumerGroupCommand是怎么拿到group的,发现它通过AdminClient对象的listAllConsumerGroup获取的group list,所以只要new出来一个AdminClient就能解决问题。

private static AdminClient getAdminClient(){
        if(null != adminClient){
            return adminClient;
        }else{
            Time time = new SystemTime();
            Metrics metrics = new Metrics(time);
            Metadata metadata = new Metadata();
            ConfigDef configs = new ConfigDef();
            configs.define(
                    CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG,
                    Type.LIST,
                    Importance.HIGH,
                    CommonClientConfigs.BOOSTRAP_SERVERS_DOC)
                  .define(
                    CommonClientConfigs.SECURITY_PROTOCOL_CONFIG,
                    ConfigDef.Type.STRING,
                    CommonClientConfigs.DEFAULT_SECURITY_PROTOCOL,
                    ConfigDef.Importance.MEDIUM,
                    CommonClientConfigs.SECURITY_PROTOCOL_DOC)
                  .withClientSslSupport()
                  .withClientSaslSupport();
            HashMap<String, String> originals = new HashMap<String, String>();
            originals.put(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, KAFKA_METRICS_BOOTSTRAP_SERVERS);
            AbstractConfig abstractConfig = new AbstractConfig(configs, originals);
            ChannelBuilder channelBuilder = org.apache.kafka.clients.ClientUtils.createChannelBuilder(abstractConfig.values());
            List<String> brokerUrls = abstractConfig.getList(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG);
            List<InetSocketAddress> brokerAddresses = org.apache.kafka.clients.ClientUtils.parseAndValidateAddresses(brokerUrls);
            Cluster bootstrapCluster = Cluster.bootstrap(brokerAddresses);
            metadata.update(bootstrapCluster, 0);
            
            Long DefaultConnectionMaxIdleMs = 9 * 60 * 1000L;
            int DefaultRequestTimeoutMs = 5000;
            int DefaultMaxInFlightRequestsPerConnection = 100;
            Long DefaultReconnectBackoffMs = 50L;
            int DefaultSendBufferBytes = 128 * 1024;
            int DefaultReceiveBufferBytes = 32 * 1024;
            Long DefaultRetryBackoffMs = 100L;
            String metricGrpPrefix = "admin";
            Map<String, String> metricTags = new LinkedHashMap<String, String>();
            //Selector selector = new Selector(DefaultConnectionMaxIdleMs, metrics, time, metricGrpPrefix, channelBuilder);
            Selector selector = new Selector(DefaultConnectionMaxIdleMs, metrics, time, metricGrpPrefix, metricTags, channelBuilder);
            AtomicInteger AdminClientIdSequence = new AtomicInteger(1);
            NetworkClient client = new NetworkClient(selector, 
                    metadata, 
                    "admin-" + AdminClientIdSequence.getAndIncrement(), 
                    DefaultMaxInFlightRequestsPerConnection, 
                    DefaultReconnectBackoffMs, 
                    DefaultSendBufferBytes, 
                    DefaultReceiveBufferBytes, 
                    DefaultReceiveBufferBytes, 
                    time);
            ConsumerNetworkClient highLevelClient = new ConsumerNetworkClient(client, metadata, time, DefaultRetryBackoffMs);
            //ConsumerNetworkClient highLevelClient = new ConsumerNetworkClient(client, metadata, time, DefaultRetryBackoffMs, DefaultRequestTimeoutMs);
            scala.collection.immutable.List<Node> nList = scala.collection.JavaConverters.asScalaBufferConverter(bootstrapCluster.nodes()).asScala().toList();
            adminClient = new AdminClient(time, DefaultRequestTimeoutMs, highLevelClient, nList);
            return adminClient;
        }
}

4.5 Kafka中的HW、LEO、LSO

HW (High Watermark)俗称高水位,它标识了一个特定的消息偏移量(offset),消费者只能拉取到这个offset之前的消息。

下图表示一个日志文件,这个日志文件中只有9条消息,第一条消息的offset(LogStartOffset)为0,最后一条消息的offset为8,offset为9的消息使用虚线表示,代表下一条待写入的消息。日志文件的 HW 为6,表示消费者只能拉取offset在 0 到 5 之间的消息,offset为6的消息对消费者而言是不可见的。
在这里插入图片描述

LEO(Log End Offset),标识当前日志文件中下一条待写入的消息的offset。上图中offset为9的位置即为当前日志文件的 LEO,LEO 的大小相当于当前日志分区中最后一条消息的offset值加1。分区的ISR 集合中每个副本都会维护自身的 LEO,而 ISR 集合中最小的 LEO 即为分区的 HW,对消费者而言只能消费 HW 之前的消息。

ISR 集合和 HW、LEO的关系
假设某分区的 ISR 集合中有 3 个副本,即一个 leader 副本和 2 个 follower 副本,此时分区的 LEO 和 HW 都分别为2 。消息a和消息b从生产者出发之后先被存入leader副本。
在这里插入图片描述
在这里插入图片描述

在消息被写入leader副本之后,follower副本会发送请求来拉取消息a和消息b进行消息同步。

在同步过程中不同的副本同步的效率不尽相同,在某一时刻follower1完全跟上了leader副本而follower2只同步了消息a,如此leader副本的LEO为4,follower1的LEO为4,follower2的LEO 为3,那么当前分区的HW取最小值3,此时消费者可以消费到offset0至3之间的消息(offset为0、1、2)。

当所有副本都成功写入消息a和消息b之后,整个分区的HW和LEO都变为4,因此消费者可以消费到offset为3的消息了。
在这里插入图片描述

由此可见kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制。事实上,同步复制要求所有能工作的follower副本都复制完,这条消息才会被确认已成功提交,这种复制方式极大的影响了性能。而在异步复制的方式下,follower副本异步的从leader副本中复制数据,数据只要被leader副本写入就会被认为已经成功提交。在这种情况下,如果follower副本都还没有复制完而落后于leader副本,然后leader副本宕机,则会造成数据丢失。kafka使用这种ISR的方式有效的权衡了数据可靠性和性能之间的关系。

4.6 Kafka中的ISR、AR

分区中的所有副本统称为AR(Assigned Replicas)。所有与leader副本保持一定程度同步的副本(包括Leader)组成ISR(In-Sync Replicas),ISR集合是AR集合中的一个子集。消息会先发送到leader副本,然后follower副本才能从leader副本中拉取消息进行同步,同步期间内follower副本相对于leader副本而言会有一定程度的滞后。前面所说的“一定程度”是指可以忍受的滞后范围,这个范围可以通过参数进行配置。与leader副本同步滞后过多的副本(不包括leader副本),组成OSR(Out-Sync Replicas),由此可见:AR=ISR+OSR。在正常情况下,所有的follower副本都应该与leader副本保持一定程度的同步,即AR=ISR,OSR集合为空

Leader副本负责维护和跟踪ISR集合中所有的follower副本的滞后状态,当follower副本落后太多或者失效时,leader副本会把它从ISR集合中剔除。如果OSR集合中follower副本“追上”了Leader副本,之后在ISR集合中的副本才有资格被选举为leader,而在OSR集合中的副本则没有机会(这个原则可以通过修改对应的参数配置来改变)

4.7 ISR伸缩

Kafka启动的时候会开启两个与ISR相关的定时任务,名称分别为isr-expirationisr-change-propagation。isr-expiration任务会周期性的检测每个分区是否需要缩减其ISR集合。这个周期和replica.lag.time.max.ms参数有关,大小是这个参数一半。默认值为5000ms,当检测到ISR中有失效的副本的时候,就会缩减ISR集合。如果某个分区的ISR集合发生变更,则会将变更后的数据记录到ZooKeeper对应/brokers/topics/partition/state节点中。节点中数据示例如下

{“controller_cpoch":26,“leader”:0,“version”:1,“leader_epoch”:2,“isr”:{0,1}}

其中controller_epoch表示的是当前kafka控制器epoch。leader表示当前分区的leader副本所在broker的id编号,version表示版本号,(当前版本固定位1),leader_epoch表示当前分区的leader纪元,isr表示变更后的isr列表

除此之外,当ISR集合发生变更的时候还会将变更后的记录缓存到isrChangeSet中,isr-change-propagation任务会周期性(固定值为2500ms)地检查isrChangeSet,如果发现isrChangeSet中有ISR 集合的变更记录,那么它会在Zookeeper的/isr_change_notification的路径下创建一个以isr_change开头的持久顺序节点(比如/isr_change_notification/isr_change_0000000000), 并将isrChangeSet中的信息保存到这个节点中。kafka控制器为/isr_change_notification添加了一个Watcher,当这个节点中有子节点发生变化的时候会触发Watcher动作,以此通知控制器更新相关的元数据信息并向它管理的broker节点发送更新元数据信息的请求。最后删除/isr_change_notification的路径下已经处理过的节点。频繁的触发Watcher会影响kafka控制器、zookeeper甚至其他的broker性能。为了避免这种情况,kafka添加了指定的条件,当检测到分区ISR集合发生变化的时候,还需要检查一下两个条件:

  1. 上一次ISR集合发生变化距离现在已经超过5秒
  2. 上一次写入zookeeper的时候距离现在已经超过60秒。

满足以上两个条件之一者可以将ISR变更集合写入目标节点。

随着follower副本不断进行消息同步,follower副本LEO也会逐渐后移,并且最终赶上leader副本,此时follower副本就有资格进入ISR集合,追赶上leader副本的判定准侧是此副本的LEO是否小于leader副本HW,这里并不是和leader副本LEO相比。ISR扩充之后同样会更新ZooKeeper中的/broker/topics/partition/state节点和isrChangeSet,之后的步骤就和ISR收缩时的相同。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值