Kafka基本介绍和一般用途
消息队列(Message Queue),是分布式系统中重要的组件,其通用的使用场景可以简单地描述为:
当不需要立即获得结果,但是并发量又需要进行控制的时候,差不多就是需要使用消息队列的时候。
消息队列的三大主要用途: 异步、削峰、解耦
- 应用解耦:业务逻辑A、和业务逻辑B,B需要利用A的结果进行处理,但A对于B是否成功具有容忍度,将A逻辑封装为应用a, 处理结果以消息的形式写入消息队列,B逻辑封装为应用b,监听和消费消息队列里应用a写入的消息数据,进行处理。a作为生成者,只关心消息是否正确写入队列,b作为消费者,只关心是否有消息写入队列。业务A和业务B通过消息队列进行连接,可以独立的进行逻辑处理而不必关心对方的处理逻辑,实现了逻辑解耦。
- 异步:对于消息队列中的同一条消息,不同的应用可以同时读取它并进行不同的业务逻辑处理,不需某应用处理完再传递给下一个应用,加快整体处理效率。
- 限流削峰:应用系统的数据量或访问量存在时间上的不均衡性,时而大量数据或请求涌入,时而数据或请求量很小,将其写入消息队列中,消费者进程可以以相对均衡的速率进行处理,流量峰值到来时先在消息队列中缓存,消费者消费完先前写入堆积的数据继而处理当前写入的数据。
- 消息驱动:系统分为消息队列、消息生产者、消息消费者,生产者负责产生消息,消费者(可能有多个)负责对消息进行处理
Kafka内部架构实现
Kafka是C/S架构,客户端向kafka写入消息(生产者)或从kafka读取消息(消费者)。服务端响应客户端的请求并负责消息的临时保存。
-
Producer :Kafka消息生产者,就是向kafka broker发消息的客户端;
-
Consumer :Kafka消息消费者,向kafka broker取消息的客户端;
-
Topic :Kafka主题,可以理解为一个队列。在Kafka中消息以主题为单位进行归类,每个主题都有一个 Topic Name,生产者根据Topic Name将消息发送到特定的Topic,消费者则同样根据Topic Name从对应的Topic进行消费;
-
Consumer Group (CG):Kafka消费者组,这是kafka用来实现一个topic消息的广播(发给所有的consumer)和单播(发给任意一个consumer)的手段。一个topic可以有多个CG。topic的消息会复制(不是真的复制,是概念上的)到所有的CG,但每个partion只会把消息发给该CG中的一个consumer。如果需要实现广播,只要每个consumer有一个独立的CG就可以了。要实现单播只要所有的consumer在同一个CG。用CG还可以将consumer进行自由的分组而不需要多次发送消息到不同的topic。
-
Broker :一个 Kafka 服务器也称为 Broker,它接受生产者发送的消息并存入磁盘;Broker 同时服务消费者拉取分区消息的请求,返回目前已经提交的消息。若干个 Broker 组成一个集群(Cluster),其中集群内某个 Broker 会成为集群控制器(Cluster Controller),它负责管理集群,包括分配分区到 Broker、监控 Broker 故障等。在集群内,一个分区由一个 Broker 负责,这个 Broker 也称为这个分区的 Leader;当然一个分区可以被复制到多个 Broker 上来实现冗余,这样当存在 Broker 故障时可以将其分区重新分配到其他 Broker 来负责。
-
Partition :Kafka分区,为了实现扩展性,一个非常大的topic可以分布到多个broker(即服务器)上,一个topic可以分为多个partition,每个partition是一个有序的队列。partition中的每条消息都会被分配一个有序的id(offset)。kafka只保证按一个partition中的顺序将消息发给consumer,不保证一个topic的整体(多个partition间)的顺序。
-
Offset:分区可以看作是一个只进不出的队列(Kafka只保证一个分区内的消息是有序的),消息会往这个队列的尾部追加,每个消息进入分区后都会有一个偏移量,标识该消息在该分区中的位置,消费者要消费该消息就是通过偏移量来识别。
-
Segment: 在 Kafka 的文件存储中,同一个 Topic 下有多个不同的 Partition,每个 Partition 都为一个目录,而每一个目录又被平均分配成多个大小相等的 Segment File 中,Segment File 又由 index file 和 data file 组成,他们总是成对出现,后缀 ".index" 和 ".log" 分表表示 Segment 索引文件和数据文件。Segment 是 Kafka 文件存储的最小单位。 Segment 文件命名规则:Partition 全局的第一个 Segment 从 0 开始,后续每个 Segment 文件名为上一个 Segment 文件最后一条消息的 offset 值。数值最大为 64 位 long 大小,19 位数字字符长度,没有数字用0填充。如 00000000000000368769.index 和 00000000000000368769.log。
应用Kafka需要注意
高性能
- 顺序追加:Kafka需要将生产者提交的数据写入到磁盘上,磁盘写入是个一个寻道、旋转、写入的物理过程,追加写入则是在当前写入之后的位置进行写入,不需要重新寻道和旋转,写入速度快
- 内存映射(mmap):Kafka读写充分利用了操作系统的page cache机制,数据写入写数据时首先写到缓存,将写入的页标记为dirty,然后向外部存储flush,也就是缓存写机制中的write-back;读数据时首先读取缓存,如果未命中,再去外部存储读取,并且将读取来的数据也加入缓存。
- 零拷贝(sendfile):零拷贝技术也是利用page cache,直接从页缓存中读取数据
- 集群分区:通过集群和分区的方式利用各个borker并行处理客户端请求
- 偏移索引:通过分段文件的命名方式和索引文件偏移量和物理偏移的映射关系可以快速定位到消息数据
可靠性
- 副本冗余:每个分区可以配置为几个副本,可以创建topic时指定,也可以通过default.replication.factor指定默认值。在分区的副本中有一个是Leader,其余是follower,数据的读写操作都经过Leader进行,同时follwer会定期地去Leader同步数据。当Leader挂了之后,其中一个follwer会选举成为新的Leader。kafka通过分区副本引入数据冗余提供了数据可靠性, 支持副本数n-1的分区机器崩溃而不丢数据。
- ack机制:producer在向kafka的broker发送消息时提供了消息确认机制,即可以通过配置producer的acks参数来决定消息发送到对应分区的几个副本才认为是成功。acks=0:这种情况如果producer把数据通过网络把消息发送出去就认为成功,这种情况非常容易发生错误,且不会收到提示。此时吞吐量和带宽利用率很高。acks=1:这种情况下若Leader收到了消息并写入了分区中就返回确认响应,不一定同步磁盘。这是若发生Leader崩溃,而follower尚未同步就容易丢消息。acks=all:这种配置下只有Leader收到了所有副本的响应消息才会向producer返回确认或错误的响应。生产者在继续发送其他消息之前需要等待所有副本都收到了,因而这种最可靠,但也是最慢的。取值all也可以为-1。这里也只是保证后面说的ISR中的所有副本都收到了消息,kafka不丢消息的保证可以理解为:对没有提交成功的消息不做任何保证,而对于ISR中至少有一个存活的完全同步的副本的情况下成功提交的消息保证不会丢失。
- ISR和OSR: 每个分区的leader会维护一个ISR(in-sync replicas)的列表,只有能及时与leader消息同步的follower才在ISR列表里,跟不上的就会被移到OSR(Out-of-Sync Replicas)中,这个延时允许的间隔通过replica.lag.time.max.ms配置。不用等待OSR中的副本都保存完成才进行确认,OSR中的副本会尽力追赶进度。
- 重平衡:当集群中有新成员加入或退出,或者某些主题增加了分区之后,消费者组将重新分配分区再进行消费
一致性
- 高水位
HW : Highwatermark, 俗称高水位,它表示了一个特定的消息偏移量(offset), 在一个parttion中consumer只能拉取这个 offset 之前的消息(此 offset 跟 consumer offset 不是一个概念) ;
LEO: LogEndOffset , 日志末端偏移量, 用来表示当前日志文件中下一条写入消息的offset;
leader HW : 该Partititon所有副本的LEO最小值;
follower HW : min(follower自身LEO 和 leader HW);
Leader HW = 所有副本LEO最小值;
Follower HW = min(follower 自身 LEO 和 leader HW);
Leader不仅保存了自己的HW & LEO, 还保存了远端副本的HW & LEO。
- AutoCommit(at most once, commit后挂,实际会丢)
配置自动退提交consumer收到消息就返回正确给 brocker, 但是如果业务逻辑没有走完中断了,实际上这个消息没有消费成功。这种场景适用于可靠性要求不高的业务。设置自动提交的间隔。比如设置为1s提交1次,那么在1s内的故障重启,会从当前消费offset进行重新消费时,1s内未提交但是已经消费的msg, 会被重新消费到。
- 手动Commit(at least once, commit前挂,就会重复, 重启还会丢)
配置为手动提交的场景下,业务开发者需要在消费消息到消息业务逻辑处理整个流程完成后进行手动提交。 如果在流程未处理结束时发生重启,则之前消费到未提交的消息会重新消费到,即消息显然会投递多次。此处应用与业务逻辑明显实现了幂等的场景下使用。
- Exactly once(很难,需要msg持久化和commit是原子的)
消息投递且仅投递一次的语义是很难实现的。 首先要消费消息并且提交保证不会重复投递,其次提交前要完成整体的业务逻辑关于消息的处理。 在Kafka本身没有提供此场景语义接口的情况下,这几乎是不可能有效实现的。一般的解决方案,也是进行原子性的消息存储,业务逻辑异步慢慢地从存储中取出消息进行处理。