请教几个关于Kafka架构原理的问题?1.Kafka的topic和partitions内部是如何存储的,有什么特点?2、与传统的消息系统相比,Kafka的消费模型有哪些优势?3、Kafka是如何实现分布式数据存储和数据读取的?多个Producer、多个Broker、多个Consumer,每个Producer可以对应多个Topic,每个Consumer只能对应一个ConsumerGroup。整个Kafka架构对应一个ZK集群,ZK通过该集群管理集群配置,选举Leader,并在consumergroup发生变化时进行rebalance。名称解释Broker消息中间件处理节点。Kafka节点是一个代理。一个或多个Broker可以组成一个Kafka集群Topic主题。Kafka根据主题对消息进行分类。每条发布到Kafka集群的消息都需要指定一个topicProducer消息生产者,客户端Consumer消息消费者向Broker发送消息,客户端ConsumerGroup从Broker读取消息。每个消费者都属于一个特定的消费者组。一条消息可以发送给多个不同的ConsumerGroup,但是一个ConsumerGroup中只有一个Consumer可以消费该消息。Partition的物理概念,一个topic可以划分为多个partition,每个partition内部是有序的。2.Kafka中的每条消息对于Topic和Partition都有一个主题。一般来说,我们的应用中会产生不同类型的数据,可以设置不同的主题。一个主题通常有多个消息订阅者。当生产者向某个主题发布消息时,订阅该主题的消费者可以收到生产者编写的新消息。Kafka为每个topic维护一个分布式分区日志文件,每个分区是Kafka存储层面的一个追加日志。任何发布到这个分区的消息都会被追加到日志文件的末尾,分区中的每条消息都会被分配一个按时间顺序单调递增的序号,也就是我们的偏移量,它是一个long类型的数字。通过这个偏移量,我们可以确定这个分区下的一条消息是唯一的。partition下保证顺序,topic下不保证顺序。在上图中,我们的生产者将决定发送到哪个分区。如果没有Key值,就会发送轮询。如果有Key值,则对Key值进行Hash,然后取分区数的余数,保证相同的Key值会路由到同一个分区。如果你想要队列中的顺序强一致性,你可以将所有消息设置为相同的键。3.消费模型消息被生产者发送到Kafka集群后,会被消费者消费。一般来说,我们有两种消费模型:推模型(psuh)和拉模型(pull)。基于推送模型的消息系统通过消息代理记录消费状态。消息代理将消息推送给消费者后,将消息标记为已消费,但这种方式不能很好地保证消费的处理语义。比如我们发送消息给消费者后,由于消费者进程挂了或者网络原因导致消息收不到。如果我们在消费者代理中将其标记为已消费,则消息将丢失。如果我们采用生产者收到消息后回复的方式,消息代理需要记录消费状态,这是不可取的。如果使用推送,消息消费的速率完全由消费者代理控制。一旦消费者被阻塞,就会出现问题。Kafka采用拉模型(poll),自己控制消费速度和消费进度。消费者可以根据任意一个offset进行消费。比如消费者可以消费已经消费过的消息进行再处理,或者消费最近的消息等等。4、网络模型4.1KafkaClient——单线程Selector单线程模式适用于并发连接数少、逻辑简单、数据量小的场景。在kafka中,consumer和producer都使用上面的单线程模式。这种模式不适用于Kafka服务器。服务器中的请求处理过程比较复杂,会造成线程阻塞。一旦出现后续请求,将得不到处理,导致大量请求超时,造成雪崩。在服务器端,应该充分利用多线程来处理执行逻辑。4.2Kafka--server--多线程Selector在Kafka服务器上使用多线程Selector模型。Acceptor在单独的线程中运行。对于读取操作的线程池,线程池中的线程会在selector中注册读取事件,负责服务端读取请求的逻辑。读取成功后,将请求放入消息队列共享队列。然后在写线程池中,取出这个请求,对其进行逻辑处理。即使某个请求线程被阻塞了,还有后续的郡会从消息队列中获取请求并进行处理。在写线程处理完逻辑处理后,因为注册了OP_WIRTE事件,所以还需要给它发送一个响应。5、高可靠的分布式存储模型在Kafka中,高可靠模型的保证依赖于副本机制。有了拷贝机制,即使机器宕机,也不会出现数据丢失的情况。5.1高性能日志存储Kafka中一个topic下的所有消息都以分区的形式分布存储在多个节点上。同时,在kafka机器上,每个Partition其实对应一个日志目录,目录下有多个日志段(LogSegment)。LogSegment文件由“.index”文件和“.log”文件两部分组成,分别表示为段索引文件和数据文件。这两个文件的命令规则是:partitionglobal的第一个segment从0开始,后面的每个segment文件名都是前一个segment文件中最后一条消息的offset值,值为64位,20个数字。字符长度,如果没有数字,填0,如下,假设有1000条消息,每个LogSegment的大小为100,900-1000的index和Log如下图所示:由于kafka消息数据为太大,如果全部建立索引,会占用空间增加时间消耗,所以Kafka选择了稀疏索引的方式,这样索引可以直接进入内存,加速部分查询。下面简单介绍一下如何读取数据。如果我们要读取第911条数据,第一步就是要找到它属于哪个section,根据二分法找到它所属的文件。找到0000900.index和00000900.log之后,再去索引中找到索引(911-900)=11或者最近的小于11的索引。这里我们通过二分法找到索引是[10,1367],然后我们利用这个索引1367的物理位置开始回溯,直到找到911条数据。以上就是找到某个偏移量的过程,但是很多时候我们不需要找到某个偏移量,我们只需要按顺序读取即可,而在顺序读取时,操作系统会在内存之间加上一个偏移量和磁盘。pagecache就是我们平时看到的预读操作,所以我们的顺序读操作是非常快的。但是,Kafka有一个问题。如果分区太多,日志段就会很多。写的时候,因为是分批写的,所以实际上会变成乱写。此时随机I/O对性能影响很大。所以一般来说,Kafka的分区不能太多。鉴于此,RocketMQ将所有日志写在一个文件中,可以顺序写入。经过一定优化后,阅读也可以接近顺序阅读。大家可以想一想:1、为什么要分区,也就是说一个topic只有一个分区,不是可以吗?2、为什么日志需要分段?5.2复制机制Kafka的复制机制是多个服务器节点复制其他节点主题分区的日志。当集群中的某个节点发生故障时,访问故障节点的请求会被转移到其他正常的节点上(这个过程通常称为Reblance),而kafka中每个topic的每个分区都有一个主副本和0个或多个副本,copy保持数据与mastercopy同步,当mastercopy失效时将被替换。在Kafka中并不是所有的副本都可以用来代替主副本,所以在Kafka的leader节点维护了一个ISR(InsyncReplicas)集合,翻译过来也叫同步集合。这个集合中的需求满足两个条件:节点必须和ZK保持连接。在同步过程中,这个副本不能落后于主副本太远。此外,还有一个AR(AssignedReplicas)来标识完整的副本集,OSR用来表示因落后而被淘汰的副本。集合,所以公式如下:ISR=leader+不落后太远的copy;AR=OSR+ISR;这里是后面两个名词:HW(highwaterlevel)是这个分区消费者能看到的位置,LEO是每个分区日志***消息的位置。HW可以保证leader所在的broker发生故障时,仍然可以从新选出的leader处获取消息,不会造成消息丢失。当producer向leader发送数据时,可以通过request.required.acks参数设置数据可靠性等级:1(默认):表示ISRleader中的producer已经成功接收到数据并在其后发送已确认消息。如果领导者宕机,数据将丢失。0:这意味着生产者不需要等待来自代理的确认来继续发送下一批消息。这种情况下,数据传输效率是最好的,但是数据可靠性确实是最好的。-1:producer需要等待ISR中所有follower确认收到数据才算发送完成,可靠性最好。但这并不能保证数据不会丢失。比如当ISR中只有leader时(其他节点与zk断开,或者没有追上),这就变成了acks=1的情况。
