一、首先我们要理解kafka partition内部消息有序,指的是什么有序?
是消息有序,而不是内容有序,如果你想kafka内部的内容有序,则需要再往kafka发送消息的时候保证内容的发送顺序。
1.kafka保证消息顺序配置
kafka producer:
- 失败重试配置不重试: retries=0这个默认就是0
- max.in.flight.requests.per.connection,这里解释下这个参数的意思,这个参数就是消息生产者将消息发送给broker了,但是broker还没有给回复的消息数量,也就是发送出去还没有响应的个数。它与上面那个重试参数配合使用,如果是不重试的话, max.in.flight.requests.per.connection配置多少都可以,默认是5个,但是你一旦重试的话,你这个参数必须要配置1,只允许一个请求一个响应的来,不能哗哗哗的将请求都发送出去了,这个时候有失败重试的顺序就会乱了。 这里使用画图稍微解释下。
- 比如说现在某个消息生产者是有5个消息,依次发送到broker 上。都还没有响应,这个时候 in.flight.requests就是5 如果这个时候2这个消息失败了。
- 这个时候就会对2这个消息进行重试,重新发送之后到broker就成了这个样子
如果你不重试的话,失败的消息的就丢了。但是能够保证没丢的消息整体顺序性。
如果重试的话,这个max.in.flight.requests.per.connection 就得配置成1,这个时候发送消息之前就会判断待响应的数量是否大于等于1,如果是的话,就先不发送,等着响应回来并成功的时候再发送下一个消息包,如果失败的话,就重试。
kafka broker:这个不用配置,天然支持(这个解释原理的时候会介绍)
kafka consumer:这个从broker拉取回来的消息是顺序的,然后拉取回来消息你自己处理的时候需要根据业务需求来考虑是否要保证顺序性
2.kafka生产者保证消息顺序原理
这里解释下:
首先,可以多个业务线程并发往内存缓冲区中写入消息,也可以单个线程往这个内存缓冲区
内存缓冲区作用就是进行batch,微批,将多个消息打成一个包,然后一次请求发送,这样能够减少网络请求的开销,增加吞吐量,但是你要控制好这个batch的大小,默认是16kb,如果这个batch设置的太大,然后你那个生产的频率还很低的话,这个时候你会发现延迟会很高,不过不用担心,kafka生产者还有个参数是linger.ms它管着就是不管你这个batch有没有满,只要创建时间超过了设置的时间,就会认为是准备好了。
消息到了会根据partition 分组,一个partition对应着一个队列,然后每个队列的元素是batch,一个batch 由若干个消息组成的,这消息在batch中也是有顺序的,毕竟就是内存buffer的追加写。
业务线程写入内存缓冲区就不用管了,可以继续处理下一条消息的发送。这个时候kafka消息生产者有一个sender线程(注意是单线程)它负责的事情贼多,我们主要就是发送请求,接受响应,创建与broker的连接等等。
这里我们就介绍下与消息顺序有关的内容,
他会先找出那种准备好的node(这个node的就是一个broker实例,一个broker进程)怎样算是准备好呢?连接建立,有那种可以发送的batch,从每个partition中获取一个准备好的batch,注意是一个batch。这里这个准备好的意思是如果不是重试的batch,只要对应的node连接ok了,batch满了或者linger.ms时间到了就算是准备好了,重试的batch要过了重试间隔时间才可以,再就是判断这个某个node在InFlightRequests队列的请求数量,不超过max.in.flight.requests.per.connection 才可以。
找出准备好的node之后,接着就是从内存缓冲区中获取batch,每个partition中获取一个准备好的batch,接着就是封装请求,发送请求,发送请求的时候先去InFlightRequests中node对应队列中添加这个请求,接着再将这个请求放到对应的channel中,这个时候还不发送的,只是触发写事件,具体的发送是sender 线程的poll过程来执行的,使用的是java nio事件驱动,真正发送是感知到写事件的时候才会写到对应的输出流中。
再来分析一下这个过程,sender线程是单线程,按照程序的流程顺序执行,每个partition一个请求包中只会取一个batch,接着就是封装请求发送请求,这一轮算是完事了,接着进行下一轮,还是找出准备好的node,取出对应partition中第一个batch,封装请求,发送请求。
接着再说说响应的问题,这个响应与请求的顺序是一样的,比如说我发送了请求1,请求2,先发送的请求1,再发送的请求2。你收的响应顺序绝对是响应1,响应2。
这里我给说下为什么
先是消息生产者,sender单线程保证先往tcp中写入请求1的流,再写入请求2的流,tcp协议是能够保证顺序性,到broker的时候,绝对是先给broker请求1,再给请求2,然后broker 处理的时候也是先处理完请求1,组织响应,放到一个队列中,然后写回来,这块涉及到broker保证消息顺序的机制,我们后面会解释。
响应来了之后会InFlightRequests 中找到对应的请求,如果是失败的,就会判断重试次数,重新放入这个内存缓冲区中,等待下次的发送。
其实可以看到发送消息是顺序的,出问题就出在失败重试这个环节,要么不重试,要么就是max.in.flight.requests.per.connection 这个参数设置成1,这样发送的时候会判断InFlightRequests 中对应node发送请求还未响应的数量,如果是1,说明已经发送出去一个请求还没有收到响应,收到响应之后,如果是成功InFlightRequests 的数量就是0了,接着会发送下一个请求,如果是失败的话,判断要不要重试,如果重试将要重试的batch 到内存缓冲区对应队列的头上去,下次发送消息会先获取这个重试的消息。如果这个一个不成功,就会一直重试,后面的batch就一个不能发送,这样有效的保证了消息在partition内部的顺序性。
本文介绍了kafka保证消息顺序的配置与消息生产者保证消息顺序的原理。
消息生产者retries与max.in.flight.requests.per.connection 这两个参数尤为重要。
要么不重试,丢数据保证顺序性,要么就是max.in.flight.requests.per.connection 设置成1,同时只能一个请求发送,响应回来成功后接着发送下个请求。
1.broker保证消息顺序
1.1分区与副本设计
这里首先介绍下topic与partition
topic就是一类消息,比如说发送短信消息的topic可以是sendMsg,然后partition就是这一类消息可以分为多个partition分区来存储,比如说我们给sendMsg 这个topic分了4个partition,这个时候就会有partition0,partition1,partition2,partition3。再比如说我们kafka集群就两台机器node1与node2
可能就会这个样子分配的,node1里面存储partition0与partition2的消息,node2里面存储partition1与partition3的消息。
好了,介绍完了partition之后我们再介绍下副本机制。
副本机制为了消息高可用性,采取了副本冗余,也就是一个消息存储多份,这样某个broker 节点宕机,它里面的partition在别的broker上面还有,也就是还能继续提供服务。
副本分为leader 与follower角色,消息的写入是写到leader 副本中的, 然后follower 会从leader副本中同步消息。
当leader副本所在的broker 宕机之后,通过zk就能感知到,然后就会从follower 中选择出一个leader继续提供服务。比如说我们把sendMsg 这个topic 设置2个副本(副本数量是与集群broker数量有关的,不能超过broker数量,你创建topic 的时候,代码会有判断),就如下图这个样子
介绍了这么多,与咱们这个消息顺序有什么关系呢?
主要就是同一时刻,只能有一个node处理某个partition消息的写请求。单是多个partition可以是并行的,这就是kafka只支持单partition内部消息顺序的重要因素。1.2网络模型
这里再把里面的一张网络模型图拿出来,介绍下里面的细节,看看是如何保证顺序的。
首先消息生产者要发送消息,就要先与broker三次握手建立连接,这个时候是accpet线程来处理的,会将建立好的连接,根据某种分配规则(这里就是轮询的方式)分配给某个processor
接着就是消息生产者发送消息了,processor专门处理channel的读写事件的,当有消息过来的时候,会将消息放到requestChannel的请求队列中,队列是先进先出的,能够保证顺序,然后由handler线程来处理消息的存储,handler线程默认是8个,processor线程默认是3个
问题:
有个问题来了handler是并发运行的,比如说我们同一个客户端发过来两个请求包。请求1与请求2,根据tcp协议。这两个到processor中还是有顺序的,接着请求1先入队,请求2再入队。handler1线程先拿到请求1,handler2线程再拿到请求2,这个时候cpu时间片给了handler2线程,来处理请求2,这时候就会发现请求2先被执行完成。这个样子就没法保证消息的顺序性了。
看看kafka是怎样处理的
processor线程主要就是读写事件处理的,一开始的时候它是对读事件感兴趣。
这个时候来了消息之后就会读取,现将消息读到stagedReceives 中,这个就是一个连接对应一个队列。比如说我们客户端请求1与请求2都被读到了stagedReceives里面的队列中
这个时候它还是读事件,会将stagedReceives 里面每个channel的对头请求放到completedReceives 集合中(请求1就会被放到completedReceives 这个集合中)
接着就是遍历这个completedReceives集合,将里面请求放到requestChannel里面的请求队列中去。这个时候干了一个非常重要的事就是移除对读事件的监听。这个动作非常重要,请求2还在stagedReceives 中,而请求1已经被搞到handler线程处理业务了。
那么什么时候开始处理请求2?
当handler处理完成之后,会生成一个响应,根据请求是那个processor处理的,将响应放到这个processor对应的队列中,processor 会从这个对列中取出响应,然后放到对应channel中,对写事件感兴趣,等下次poll的时候,发现是写事件,就会将响应真正发送出去,同时对写事件感兴趣改为对读事件感性趣,这个时候会处理请求2了。
总结一下,网络模型保证消息顺序
1.tcp协议
2.某个连接专门交给一个processor处理读写
3.读写事件机制与队列的运用,保证处理完前面的,并且写出去之后,再处理同channel的后一个请求。
1.3存储模型
接着咱们就说说这个存储模型了,在kafka中partition 的leader副本是专门处理客户端读写的,follower副本去leader副本中同步消息。
在kafka中一个partition会有一个log日志,然后分为好多个日志段。而且消息都是有offset的,offset是递增的。
而且在存储的时候采用顺序写的方式,关于存储这块可以看我的《深度解析kafka broker处理发送消息请求并写入磁盘》这篇文章。
顺序写是个重要的点,还有个就是因为我们发送过来的请求里面包含多个partition的一个batch,多个partition我们不用管,其实就是个循环。一个batch是多条消息的,这个batch里面的消息在消息生产者端能够保证顺序性了,而且在消息生产者端会为每个消息分配一个递增的序号,然后在broker 存储的时候会重新校验一下一个batch里面的消息序号是不是递增的,如果不是就抛出异常,如果是的话,重新分配offset,顺序写到磁盘中。
2.消息消费者保证消息顺序
其实这个消息消费者这块没啥好说的。一个消息消费者可能会被分配一个topic的多个partition,但是在拉取的时候,会批量拉取,而且是保证顺序的,也就是在broker中存储的顺序是啥子样子,拉取回来的消息就是啥子样子。而且拉取是单线程。会拉取回来一个集合(list),这个时候你要是业务消费的话,就要根据你的业务需求来看看是否要多线程并发消费,如果多线程并发消费就不能保证顺序了,这个时候你可以根据这个topic消息更细化的字段来保证某个字段的顺序性,比如说我要sendMsg发送短信这个topic消费,我只需要保证单用户的短信是有顺序的就可以了,这个时候你就可以使用用户id%线程数就可以将,相同的用户打到同一个线程上面去。