解密消息队列:应用场景、优点与比较,揭秘Kafka的构成与消息发送过程

553 阅读19分钟

我正在参加「掘金·启航计划」

之前公司一直都在使用RocketMQ作为主要的三方中间件,现在全部要求切换为kafka,所以就此我们需要详细了解一下kafka的作用,使用方式,以及坑点。在开始讲述之前,我们先来看下什么是消息队列。消息队列(MQ)主要实现了发送、存储和消费,实现了一个消息的转发器功能。如下图所示,可以拆分为生产者,消费者和队列。当然,考虑到实际是需求,会使用gRPC、rpcx这些通信框架,broker水平扩展和存储方案高可用(服务自动注册与发现、负载均衡、超时重试机制、发送和消费消息时的 ack 机制,分区+副本模式,以及可持久化的KV存储系统),现在成熟的消息队列是在这个基础上,随着业务需求演进而来的。

8059750abd33389deb452becbbd02603.png

消息队列应用范围非常广泛,最主要的业务场景是系统解耦、异步通信和流量削峰。不过随之也带来一些问题,比如系统可用性的考虑,消息消费次数、数据一致性和可靠性传输等,都是我们在引入消息队列时需要注意的。

消息队列的应用场景

  • 日志的采集 通过kafka的接口服务接入elk等日志分析平台;
  • 消息系统 生产者和消费者的解耦、缓存消息等;
  • 运营数据 收集运营和监控的数据,CPU占用率、内存使用率、报警和负载报告等反馈信息;
  • 用户活动的追踪 用户在web或者App的活动行为,通过消费kafka的topic做分析或者存储;
  • 限流削峰 对消息的突增压力进行配置和限流;
  • 流式处理 spark streaming 和 storm等。

消息系统的优点

  • 高吞吐量和低延迟 每秒钟可以处理几十万条消息,延迟最低为几毫秒;
  • 解耦 随着项目的发展,加入一个数据接口层,方便实现独立扩展和修改处理过程;
  • 冗余 通过数据持久化直到消息被处理完毕,防止数据丢失带来的风险;
  • 扩展性 通过解耦简化了扩展流程,增大消息队列的入队和消息处理的频率更容易;
  • 灵活性和峰值处理 消息队列可以是关键的组件顶住访问量突增的压力,防止超负荷的请求而使系统崩溃;
  • 可恢复性 进程之间的解耦,如果部分进程挂掉,加入队列的消息可以在系统恢复之后被处理;
  • 顺序保证 kafka可以保证一个partition的消息有顺序;
  • 缓冲 消息队列通过缓冲层实现高效运行,提高数据的处理速度;
  • 异步通信 消息队列可以先把消息放入队列,在需要的时候去做处理;
  • scale out 无需停机即可实现扩展机器;
  • 定期删除机制 通过设定partition的segment 文件的保留时间。

消息模式

点对点和发布订阅模式的区别是在于发送到队列中的消息是否可以被多次的消费(订阅),最初是由JMS程序接口定义实现的。 其中,点对点的方式是消息发送到队列中,消费者从queue中取出并且消费消息,不再被其他的消费者消费使用。也就是单条消息只能被一个消费者使用。 发布订阅模式是多个订阅者可以监听队列的消息,当生产者发布到queue中之后,由多个消息的订阅者来消费这一条消息,也就是该topic的消息被所有的订阅者使用。

0d37c6927a105a03582fb596e8e63e5c.png

bce43c19f4bc20cdd13be1df08495c1a.png

常见的消息队列比较

  • ActiveMQ是一个开源的兼容JMS面向消息的中间件,来自Apache软件公司开发,提供了松耦合的程序应用框架。通过调优允许broker的伸缩来处理大数量的客户端,完成内存的分配。同时事件驱动架构可以获得更好的伸缩性。它还可以作为RPC的替代方案,使用异步消息更容易增加消息数量,支持并发和解耦。
  • RocketMQ是一个低时延、高可靠、可伸缩、易使用的消息中间件,支持发布订阅和点对点的消息模型,队列中的消息是FIFOde严格顺序传递方法,支持push和pull的消费模式,单一队列支持百万级的消息堆积能力,支持JMS、MQTT等的消息协议,高可用的至少一次的消息传递语义,同时支持云集群的部署。
  • RabbitMQ是通过Erlang语言编写的开源消息队列,支持AMQP、SMTP等协议,具有高可靠,路由灵活,高可用,多语言的客户端,提供消息跟踪机制和插件扩展,对数据持久化和负载均衡有较好的支持,适合于重量级的企业业务应用。
  • ZeroMQ适合于大吞吐量的消息传递系统,支持四种消息模型,点对点、请求回应、发布订阅、push-pull,可以跨平台、支持Java、C++等30多种开发语言。它有一个独特的非中间件模式,应用程序就是消息的中间件,只需要引用zeroMQ的程序库就可以。但是zeroMQ的消息队列是非持久性的,如果宕机会导致数据的丢失。
  • Redis是基于key-value的NoSQL的数据库,它不仅可以作为缓存的处理器,还可以用来做消息队列,它的列表类型适合轻量级的消息队列使用。当入队数据量比较小的时候,Redis的性能高于rabbitMQ,但是数据大于10K之后就变得很慢;在出队的时候无论数据多少,都有很好的性能。

对于以上几种消息队列,面向中小型的业务推荐使用rabbitMQ和rocketmq。其中rabbitMQ的性能极其好,界面丰富,作为开源软件社区也很活跃;rocketmq是阿里公司开发的,如果考虑到不再维护而需要定制化开发,还是需要慎重选择。对于大型的业务场景,选择使用kafka、rocketMQ和zeroMQ是比较合适的。

kafka

Kafka最初是由Linkedin公司开发,用scala语言编写,是一个分布式、支持分区的(partition)、多副本的(replica)、基于zookeeper协调的分布式流平台,它可以实时的处理大量数据以满足各种需求场景:比如基于hadoop的批处理系统、低延迟的实时系统、storm/Spark流式处理引擎,web/nginx日志、访问日志,消息服务等等,核心设计就是高吞吐量和低时延。

kafka架构.png

kafka的主要构成

  • producer 消息生产者,向broker发消息的客户端;
  • consumer 消息的消费者,从broker获取消息并消费;
  • broker 存储消息的中间节点,一台服务器就是broker,一个broker包含多个topic。多个broker可以组成集群。
  • topic 主题,一类消息,可以是日志等通过topic来存储; partition 分区, topic上的一个物理分组,一个topic可以有多个,每个partition都是有顺序的队列。提高并行处理能力;
  • segment 段, partition上的物理存储由多个大小相等的segment(段)文件构成,但是段上的消息数量不一定相等,方便无用文件的快速删除,提升磁盘空间的利用率。segment的文件生命周期由服务端的配置参数决定。
  • message 消息,segment由很多message组成;
  • offset 偏移,partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到partition中。partition中的每个消息message都有一个连续的序列号ID叫做offset,用于partition唯一标识一条消息。通过segment和offset可以查询到对应的消息。

segment index file 采取了稀疏索引的存储方式,减少了索引文件大小,通过mmap可以直接进行内存操作。稀疏索引为数据文件的每个对应message设置一个元数据指针,与比稠密索引相比节省存储空间,但查找起来耗时更长。

0721dac5eeb4a40852e5285c3834763c.png

kafka的消息发送过程

首先获取topic的所有的partition,如果客户端不指定partition,同时也没有指定的key,使用自增的数字取余数的方法获取指定的partition。这样就是将平均的向partition生产数据。 如果需要控制发送的partition,可以通过两种方式,一种是指定的partition,另一种是根据自己设置key的算法。继承partition的接口实现其方法。

46c0eb50325fadf0f639077868431081.png

consumer的消费机制

kafka通过broker持久化数据,不需要进行缓存处理,消费者通过pull的机制,定期从服务器拉取数据。不同的consumer group中的消费者可以消费partition相同的消息,相同的group下的消费者只能消费partition中不同的数据信息。通过记录每个消费者在各个topic下的partition的消费offset,每次pull数据的时候都是从上次记录的位置开始拉取数据。

3cdf26f5ddf6a2bb0f233fa003276068.png

kafka高吞吐量的保证

  • 顺序写 操作系统的读和写技术,磁盘的顺序写速度大于随机写内存的方式;
  • 零拷贝(zero-copy) 减少了内存的交换和IO频繁的操作; 批量化处理 合并小的请求,数据的发送和拉取可以批量执行;
  • pull 模式 消费端的消息获取采用pull模式,与消费能力相匹配,提升了消息处理速度;
  • 数据持久化 消息不用存储到内存cache,利用磁盘的读写能力。

原理

  • ISR (in-Sync Replicas) 副本同步列表,是保证高可用(HA)和一致性的重要机制,ISR列表由leader维护。ISR也可以理解为不落后的replica集合,不落后表示为:1距离上次fetchRequest 的时间不大于某一个值和落后的消息数不大于某一个值,2 leader失败后会从ISR中选举出来一个follower作为leader。副本数对Kafka的吞吐率是有一定的影响,但极大的增强了可用性。
  • AR Assigned Replicas 某一个分区的所有副本,也就是已经分配的副本列表;AR = ISR + OSR
  • OSR Outof-Sync Replicas 非同步的副本列表
  • HW HighWaterMark 高水位,partition中对应ISR的最小LEO作为HW,也就是消费者可以消费到的最高的partition偏移量。每个replica都有HW,leader和follower各自负责自己的HW状态。
  • LEO 日志最后消息的偏移量,当前最后一个写入日志文件的消息在partition中的偏移量。

对于新写入的message,consumer不能直接消费,leader会等待消息被所有的ISR的replica同步之后,再更新HW,消息才能被consumer消费。这样保证了leader broker失效之后仍然可以通过新选举的leader获取通知。

metadata

每台broker在内存中都维护了集群上所有节点和topic分区的状态信息,Kafka称这部分状态信息为元数据缓存(metadata cache)。这样客户端(clients)给任何一个broker发送请求都能够获取相同的数据。这个能力可以缩短请求被处理的延时,通过空间换取时间,从而提高整体客户端的吞吐量。Metadata cache中保存的信息十分丰富,几乎囊括了Kafka集群的各个方面,它包含了:

  1. controller所在的broker ID,当前集群中controller所属的broker;
  2. 集群中所有broker的信息:比如每台broker的ID、机架信息以及配置的若干组连接信息;
  3. 集群中所有节点的信息:按照broker ID和监听器类型进行分组。对于超大集群来说,使用这一项缓存可以快速地定位和查找给定节点信息,而无需遍历上一项中的内容;
  4. 集群中所有分区的信息:分区信息是分区的leader、ISR和AR信息以及当前处于offline状态的副本集合。它们按照topic和分区ID进行分组,可以快速地查找到每个分区的当前状态。

另外,数据cache通过发送异步更新请求(UpdateMetadata request)来维护一致性的,只要集群中有broker或分区数据发生了变更就需要更新这些cache。

kafka文件存储机制

partition有自己的replica副本,replica分布在不同的Broker节点上。 多个partition需要选取出lead partition,lead partition负责数据读写,并由zookeeper负责fail over。 通过zookeeper管理broker与consumer的动态注册与删除,使同一个consumer group中消费者的订阅实现负载均衡,并且保留他们的消费关系和信息。当其中的broker或者consumer发生变化时,其他broker和consumer都会收到通知。

每个consumer group 对一个topic进行消费,其中的consumer只能消费不同的partition。这样通过消息分发到不同的consumer,每一条消息被消费一次。同时consumer的状态自己保存,通过横向扩展增加partition和consumer的数量进行消费,可以显著提高吞吐量,实际应用中建议两者的数量保持一致。

kafka集群中的任何一个broker,都可以向producer提供metadata信息,这些metadata中包含集群中存活的servers列表、partitions leader列表等信息。 当producer获取到metadata信息之后,producer将会和Topic下所有partition leader保持socket连接,消息由producer直接通过socket发送到broker,中间不会经过任何"路由层"。消息通过路由发送到producer客户端指定的partition那里,可以采用随机、键值哈希、轮询等方式,对于有多个partition的topic,实现消息的均衡分发是很有必要的。

副本复制机制

kafka分区partition引入了副本机制,增加副本数量可以提升容灾能力。kafka的partition有一个预写式的日志文件,每个partition有一系列的有序不可变的消息组成,它们被连续追加到partition中,每个消息的序列号是offset,用来标记它在partition中的惟一的位置。

Kafka每个topic的partition有N个副本,其中N是topic的复制因子。Kafka通过多副本机制实现故障自动转移,当Kafka集群中一个Broker失效情况下仍然保证服务可用。在Kafka中发生复制时确保partition的预写式日志有序地写到其他节点上。N个replicas其中的一个replica为leader,其他都为follower,leader处理partition的所有读写请求,同时follower被动定期地复制leader上的数据。

Kafka必须提供数据复制算法保证,如果leader发生故障或挂掉,一个新leader被选举并接收客户端的消息成功写入。Kafka确保从同步副本列表中选举一个副本为leader,或者换句话说,follower追赶leader数据。leader负责维护和跟踪ISR中所有follower滞后状态。当生产者发送一条消息到Broker,leader写入消息并复制到所有follower。消息提交之后才被成功复制到所有的同步副本。消息复制延迟受最慢的follower限制,重要的是快速检测慢副本,如果follower”落后”太多或者失效,leader将会把它从replicas从ISR移除。

2a46ba09180a1afa3fa767dd015278a6.png

副本不同步的原因有

  • 慢副本 周期时间内follower不能追上leader,常见的原因是I/O瓶颈引起的follower的复制速度小于从leader的拉取速度。
  • 卡住副本 一定周期时间内follower停止从leader拉取请求。可能的原因是GC的暂停或follower的失效或者挂掉。
  • 新启动的副本 当用户给topic添加副本因子的时候,新的follower不再ISR的列表中,直到他们完全赶上leader的日志。

当一个partition的follower落后于leader太多,就是处于不再ISR或者滞后的状态中。通常我们通过最大消息数量来判断缓慢的副本,通过最长等待时间来检测失效或者死亡的副本

kafka的 Rebalance 机制

Rebalance 本质上是一种协议,规定了一个 Consumer Group 下的所有 consumer 如何达成一致,来分配订阅 Topic 的每个分区。 Rebalance的分配过程可以理解为,某 Group 下有 10 个 consumer 实例,它订阅了一个具有 50 个 partition 的 Topic,正常情况下,kafka 会为每个 Consumer 平均的分配 5 个分区。

触发 Rebalance 的时机

Rebalance 的触发条件有3个

  • 组成员个数发生变化,例如新的 consumer 实例加入该消费组或者离开组。
  • 订阅的 Topic 个数发生变化。
  • 订阅 Topic 的分区数发生变化。

Rebalance 发生时,Group 下所有 consumer 实例都会协调在一起共同参与,kafka 能够保证尽量达到最公平的分配。但是 Rebalance 过程对 consumer group 会造成比较严重的影响。在 Rebalance 的过程中 consumer group 下的所有消费者实例都会停止工作,等待 Rebalance 过程完成。

首先介绍一下Coordinator的概念。Group Coordinator是一个服务,每个Broker在启动的时候都会启动一个该服务。Group Coordinator的作用是用来存储Group的相关Meta信息,并将对应Partition的offset信息记录到Kafka内置Topic(__consumer_offsets)中。

Rebalance 过程分为两步:Join 和 Sync。 Join 顾名思义就是加入组。这一步中,所有成员都向coordinator发送JoinGroup请求,请求加入消费组。一旦所有成员都发送了JoinGroup请求,coordinator会从中选择一个consumer担任leader的角色,并把组成员信息以及订阅信息发给leader——注意leader和coordinator不是一个概念。leader负责消费分配方案的制定。

ea38cc176a6f03da2a96be6de0ddbf08.png

Sync,这一步leader开始分配消费方案,即分配哪些consumer去负责消费哪些topic的partition。一旦完成分配,leader会将这个方案封装进SyncGroup请求中发给coordinator,非leader也会发SyncGroup请求,只是内容为空。coordinator接收到分配方案之后会把方案塞进SyncGroup的response中发给各个consumer。这样组内的所有成员就都知道自己应该消费哪些分区了。

d5d4571fc5f5965195244a7ee354b050.png

可靠性

消息投递的保障机制

消息投递的语义有以下几种情况:

  • At least once(默认) 可能会丢失消息,但不会重复发送;
  • At most once 不会丢失消息,但可能导致重复投递,所以消费端要做幂等;
  • Exactly once 不会丢失消息,且保证消息只会投递一次。

整体的消息投递语义需要Producer端和Consumer端两者来保证。当Producer向broker发送消息时,一旦这条消息被commit,因为replication的存在就不会丢失。kafka默认是At most once,也可以通过配置事务达到Exactly once,但效率低一般不推荐使用。

943172a7e494c1cd6c0ce622ae5e911f.png

消息传递的可靠性

生产者向leader发送消息的设置级别,通过request.required.acks来设置。

  • acks=1(默认) 数据发送到Kafka后,经过leader成功接收消息的的确认,就算是发送成功了。在这种情况下,如果leader宕机了,则会丢失数据。
  • acks=0 生产者将数据发送出去就不管了,不去等待任何返回。这种情况下数据传输效率最高,但是数据可靠性确是最低的。
  • acks=-1 producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。

689c6d6a2603ee1f3058ce65d500eaed.png

数据的一致性问题

kafka的数据复制机制不是通常所说的同步或异步,而是通过ISR的方式均衡了数据不丢失和吞吐率的特性。因此follower可以通过批量的从leader获取数据,而leader可以通过磁盘的顺序存储读取和实现 zero copy 机制,提高了消息的赋值性能,减少了leader和follower之间消息数量的差距。

kafka的producer的消息发送到broker,有三种返回方式:noack、leader commit 成功之后就ack、leader和follower同时commit成功才返回ack。第三种是数据的强一致性。

零拷贝(zero copy)技术只用将磁盘文件的数据复制到页面缓存中一次,然后将数据从页面缓存直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面缓存),避免了重复的复制操作。减少了内核态与用户态的内存拷贝和进程切换。kafka使用的 zero copy 底层是使用了DMA(Direct Memory Access)来实现的,DMA是指外部设备不通过CPU而直接与系统内存交换数据的接口技术,网卡(NIC)就使用了这个方法,因此数据传送的速度取决于存储器和外设的工作速度。

b7421a4e36de8474aa799f0f90a2b76d.png

kafka监控管理工具

  • Kafka Manager manger是目前功能最全的管理工具,可管理多个Kafka集群。但是当Topic太多时,监控数据会占用大量的带宽,造成机器负载增高,整体的监控功能偏弱。
  • kafka eagle 监控kafka集群和主题,组合消费监控、阻塞告警和健康状态查看等情况。
  • Kafka Offset Monitor 通过一个jar程序包的形式运行,监控部署方便。因为只有监控功能,使用起来较为安全。
  • Kafka Web Console Console的监控功能较多,可以预览消息,监控Offset、Lag等信息,但是不建议应用在生产环境中。
  • Burrow LinkedIn开源的一款专门监控consumer lag的框架。支持报警,只提供HTTP接口,没有webUI。
  • Availability Monitor for Kafka 微软公司开源的一个Kafka可用性、延迟性的监控框架,提供JMX接口,用的不多。