• Kafka-语义&重复消费&丢失数据&分区有序


    记录下和kafka相关的语义、不重复消息、不丢失数据、分区有序的内容,文中很多理解参考文末博文、书籍还有某前辈。

    kafka语义

    kafka中有三种语义,它对理解下面的不重复消费有帮助。

    最多一次(at most once):消息最多被处理一次,可能有消息丢失的风险。

    至少一次(at least once):这种语义下消息可能被处理多次,可以保证消息不丢失,但是可能导致重复消息。

    精确一次(exactly once):消息只会被处理一次,at least once+幂等性,可以实现精确一次语义。

    重复消费

    这是一个很常见的问题,如果保证消费者不重复消费数据,博客上有很多的方法,简单罗列几条如下。

    (1)给每条消息加一个独一无二的key,如uuid,消费数据的时候同时记录这些key,下次消费数据时需要检查消息的key,是否已经被消费过了,这样是可以避免重复消费的。

    (2)at least once+幂等性,也可以实现,在保证至少一次的语义下,有多种方式实现幂等性,如在关系型数据库如Oracle、MySQL表中设置唯一约束、将数据存储到redis的set、使用set [NX]存储数据到redis,都是一种实现方式。

    • 使用主键约束,当插入数据到关系型数据库的时候,第一次插入成功后,下次再次消费数据后插入数据到数据库,会由于主键的唯一约束导致插入失败,这就保证了幂等性。
    • redis的set,可以不断的set数据到里面,重复消费和一次消费的效果一样,这也可以保证幂等性。
    # 添加set数据 football
    127.0.0.1:6379> sadd likes football
    (integer) 1
    # 查看有一个football
    127.0.0.1:6379> smembers likes
    1) "football"
    # 多次添加football,相当于多次消费到football,进行处理
    127.0.0.1:6379> sadd likes football football football
    (integer) 0
    # 结果依然只有一个,多次sadd,和一次sadd的效果一样,实现幂等
    127.0.0.1:6379> smembers likes
    1) "football"
    
    • redis可以使用set+[NX]也可以实现幂等,NX选项只容许没有这个数据时才能添加,有就不能添加。
    # 添加key=name,value='clyang'成功
    127.0.0.1:6379> set name clyang NX
    OK
    127.0.0.1:6379> get name
    "clyang"
    # 再次添加,失败,实现幂等
    127.0.0.1:6379> set name clyang NX
    (nil)
    

    以上的例子可以看出,不仅仅关系型数据库,nosql也可以实现,只要能实现"INSERT IF NOT EXISTS"语义的存储系统都可以实现幂等效果。

    (3)在消费数据之前,设置一个前提条件,并且消费完数据之后,需要修改前提条件的状态,这样也可以避免重复消费。

    • 比如给用户转账,必须满足余额等于100,余额加100元才能执行,消息里需要带上余额信息,当消息里余额信息和数据库里的余额不相等时,就不会再执行修改余额的操作。当第一次有类似{'余额':100,'加金额':100}的消息要处理时,由于满足条件会执行余额更新的操作变成200,但后面的重复消息,因为消息里的余额不等于数据库里的余额(上一次更新为200了),导致修改失败,这样也保证了幂等性。
    • 上面是数字类型的的情况,如果是非数字或其他复杂的情况,该如何处理,参考前人,通用的方法是给消息里的数据,增加一个版本号,消费消息更新数据前,需要检查数据里的版本号和消息里的版本号,如果不一样就不能更新,并且更新后数据里版本号+1,这样也可以实现幂等性。
    # 第一次消费,数据里没有这条消息,可以消费,并且数据里版本号+1变成2
    {'msg':'当光照进来的时候,你嘴角上扬的骄傲就是最大的回报',versionId=1}
    # 再一次消费,由于消息版本号是1,数据里版本号是2,无法消费
    {'msg':'当光照进来的时候,你嘴角上扬的骄傲就是最大的回报',versionId=1}
    # 同上,无法消费
    {'msg':'当光照进来的时候,你嘴角上扬的骄傲就是最大的回报',versionId=1}
    

    丢失数据

    如何规避数据不丢失,参考文末博文,需要从producer、consumer和broker三方面考虑。关于丢失数据问题,如果当一个学术问题来考虑,是一种情况,如果是实际生产环境,又是另外一种情况,需要注意区分。

    producer

    (1)acks

    生产者可以设置acks=-1或者all,保证发送到broker的消息不丢失。这样设置是当消息发送到了leader副本,所有处于ISR列表中的follower副本都需要同步到这条数据,才可以。

    • acks=-1:代表所有处于ISR列表中的follower partition都会同步写入消息成功,才会返回ack到生产者
    • acks=0:代表消息只要发送出去就行,其他不管,无需返回ack到生产者
    • acks=1:代表发送消息到leader partition写入成功就返回ack到生产者
    //注意代码中配置时,-1是字符串,不是数字
    props.put("acks","-1");
    

    (2)retry

    在kafka中错误分为两种,一种是可恢复的,另一种是不可恢复的。生产时,使用带有回调的send方法,当遇到可以恢复的错误(如网络波动、leader选举中leader副本不可用的情况),设置retry次数和retry时间间隔后,在retry次数范围内都不会进入onCompletition方法,多次尝试(可以设置为Integer.MAX_VALUE)就会大概率成功发送。如果是不可以恢复的错误(如一条消息的最大大小超过max.request.size设置),最后肯定会进入下面的方法,可以做退而求其次的操作保证数据能保存,如将数据存储到redis。

    //生产者部分代码
    producer.send(record, new Callback() {
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    if(exception==null){
                        System.out.println("消息发送到分区成功");
                    }else{
                        System.out.println("消息发送失败");
                        //TODO 写入到redis
                    }
                }
            });
    

    其他参考《Apache Kafka实战》 4.6.1。

    broker

    (1)unclean.leader.election.enable=false

    这个参数设置是否允许从非ISR列表中选取副本为leader副本,默认值是false,如果设置为了true,意味着可以从ISR列表以外选举leader副本,这些ISR列表之外的副本,由于同步赶不上leader副本的更新进度,让它们变成leader副本,就会出现HW水位被截断的情况,导致数据丢失。

    (2)replication.factor>=2

    多副本是保证HA的前提,它使得某些broker即使宕机,依然可以对外提供服务,提高容错性能。上面数值设置为2代表需要设置多个副本,不是说2个就行,一般3-5个,太多也会提高网络开销。

    (3)min.insync.replicas>=2

    分区ISR中至少有多少个副本,它至少有一个,即leader副本,需要设置为大于1个。如果只有1个则leader副本挂掉则不能提供服务。它需要配合上面acks=-1来使用,代表所有IRS中的副本都数据同步,其中一个挂掉,只要能保证有一个能提供服务,就可以。

    注意,上面replication.factor需要配置大于min.insync.replicas,代表容许一些副本可以'掉队',如果设置相等,则系统变脆了,即一个副本都不能落后,只要一个落后就会导致不能满足上面最后一个条件,可用性降低。

    如下图所示,当配置replication.factor=3,min.insync.replicas=2,这样保证在ISR中至少有两个副本(图示为[0,1]),万一当前leader副本宕机了,ISR为1的broker上的副本将顶替成为leader副本,配合acks=-1的设置,数据不会丢失。当配置replication.factor=3,min.insync.replicas=1,则坏的情况是两个follower副本都跟不上leader副本的节奏,导致IRS中只有1个副本,这样万一这个副本宕机,其他的副本由于数据不同步,unclean.leader.election.enable设置为true就会被选举为leader副本就会出现数据丢失,这个时候设置acks=-1也显示没啥意义,该丢还是得丢。

    另外查看主题topicA的1号分区ISR列表[1,2,0],可以看出有三个,数字代表所在的broker id。

    [zk: localhost:2181(CONNECTED) 14] get /brokers/topics/topicA/partitions/1/state
    {"controller_epoch":242,"leader":1,"version":1,"leader_epoch":75,"isr":[1,2,0]}
    cZxid = 0x7500000106
    ctime = Fri Mar 20 20:29:52 CST 2020
    mZxid = 0x9200000064
    mtime = Fri Apr 10 19:44:54 CST 2020
    pZxid = 0x7500000106
    cversion = 0
    dataVersion = 186
    aclVersion = 0
    ephemeralOwner = 0x0
    dataLength = 79
    numChildren = 0
    

    consumer

    消费者部分,一般来说现在使用spark streaming或者flink来处理,很少单独写消费者代码,为了理解问题暂时按照普通的消费者代码来处理。

    一般来说,消费者需要设置enable.auto.commit = false来关闭自动提交消费者offset,改为手动提交的方式,在正常处理完数据再提交,如果处理失败就保存上次处理成功的offset,这次的不提交,这样可以规避丢失数据。

    enable.auto.commit默认是true的,并且设置true的情况下还需要通过auto.commit.interval.ms设置提交的时间间隔来一起使用,代表每隔多久自动提交一次。这样是有丢失风险的,假设消费者代码按照如下的设置,如果某一次消费了100条数据,过了1秒自动提交了消费者offset,但是消费者还没处理完,可能处理到了第80条,就不幸宕机了,再次重启消费,发现81-100这个区间的消息消费不到了,造成数据的丢失。

    //设置自动提交offset
    props.put("enable.auto.commit","true");//注意kafka版本
    
    //多久自动提交offset
    props.put("auto.commit.interval.ms",1000);
    

    分区有序

    同一个分区的数据,默认是有序的,因此有如下两种方案。

    • 方案1:topic只设置一个分区,这样消息就是全局有序,但是consumer group中只有一个consumer能消费,不能多个线程同时消费数据,一定程度上降低了性能。

    • 方案2:topic可能有多个分区,但是可以指定消息的key,使得需要保证顺序的消息都发送到同一个分区,这样消费数据时消息也是有序的。

    但是以上两种情况存在一个坑,参考文末博文,当producer发送消息时因为某些原因如网络延迟导致retry时,消息有重新排列失去有序性的风险,具体需要修改max.in.flight.requests.per.connection参数的值为1(默认为5),以下是官网对这个参数的解释。

    The maximum number of unacknowledged requests the client will send on a single connection before blocking. Note that if this setting is set to be greater than 1 and there are failed sends, there is a risk of message re-ordering due to retries (i.e., if retries are enabled).
    

    翻译一下大概意思就是,在producer向broker发送消息时,在一个connection上不阻塞的前提下,可以存在未及时返回ack的请求的最大数量。如果这个数值超过了1,并且用户设置了retry功能,则在请求发送存在失败时将有消息重排的风险。

    如下所示,假设上面参数设置为4,则一个连接中可以发送4个请求,如果正常发送到了broker,则消息按照正常的顺序保存在log文件,最后是买房->买车->彩礼->成家的顺序。如果消息1、2、3在发送中有retry,多次发送后才成功,则有可能导致最后消息在log文件中顺序是成家->买房->买车->彩礼的顺序,如果按照这个顺序消费,估计作为消费者的丈母娘是不能容忍的,所以上面的参数需要设置为1,这样将保证买房->买车->彩礼->结婚的顺序,即真正的分区有序,但是也是有代价的,会降低一点性能。

    以上,理解不一定正确,学习就是一个不断认识和纠错的过程。

    参考博文:

    (1)《Apache Kafka实战》

    (2)https://segmentfault.com/a/1190000015316545 retry消息重排风险

    (3)https://www.cnblogs.com/youngchaolin/p/11972899.html#_label4_4

    (4)https://www.cnblogs.com/MrRightZhao/p/11498952.html kafka数据不丢失

  • 相关阅读:
    es6
    ES6
    ES6
    css3
    滚动穿透的6种解决方案【已自测】
    css特效
    css布局
    css布局
    js
    【消灭代办】第5周
  • 原文地址:https://www.cnblogs.com/youngchaolin/p/12676425.html
Copyright © 2020-2023  润新知