消息中间件
消息中间件是基于队列与消息传递技术,在网络环境中为应用系统提供同步或异步、可靠的消息传输的支撑性软件系统——百度百科
MQ 是“消息队列”(Message Queue)的缩写,是一种用于异步通信的中间件系统。它允许不同应用程序或服务之间通过消息传递进行数据交换,而不需要它们直接连接或实时交互。MQ 的核心功能包括消息的存储、路由和分发,使得消息生产者和消费者可以解耦,从而提高系统的可伸缩性和可靠性。
应用场景
在实际应用中常用的使用场景:异步处理、应用解耦、流量削锋和消息通讯四个场景
-
异步处理
场景说明:用户注册后,需要发注册邮件和注册短信。传统的做法有两种 1.串行的方式;2.并行方式
(1)串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端。
(2)并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间。
假设三个业务节点每个使用50毫秒钟,不考虑网络等其他开销,则串行方式的时间是150毫秒,并行的时间可能是100毫秒。
因为CPU在单位时间内处理的请求数是一定的,假设CPU1秒内吞吐量是100次。则串行方式1秒内CPU可处理的请求量是7次(1000/150),并行方式处理的请求量是10次(1000/100)
小结:如以上案例描述,传统的方式系统的性能(并发量,吞吐量,响应时间)会有瓶颈。如何解决这个问题呢?
引入消息队列,将不是必须的业务逻辑,异步处理。改造后的架构如下:
按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍。
-
应用解耦
场景说明:用户下单后,订单系统需要通知库存系统。传统的做法是,订单系统调用库存系统的接口。假如库存系统无法访问,则订单减库存将失败,从而导致订单失败,缺点在于订单系统与库存系统耦合。
如何解决以上问题呢?引入应用消息队列后的方案,如下图:
(1)订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功
(2)库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作
假如:在下单时库存系统不能正常使用,也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦。
-
流量削锋
应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列:可以控制活动的人数、可以缓解短时间内高流量压垮应用。用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面,秒杀业务根据消息队列中的请求信息,再做后续处理。
-
日志处理
日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。架构简化如下:
(1)日志采集客户端:负责日志数据采集,定时写受写入Kafka队列
(2)Kafka消息队列:负责日志数据的接收,存储和转发
(3)日志处理应用:订阅并消费kafka队列中的日志数据
查看新浪kafka日志处理应用案例:转自(http://cloud.51cto.com/art/201507/484338.htm)
(1)Kafka:接收用户日志的消息队列
(2)Logstash:做日志解析,统一成JSON输出给Elasticsearch
(3)Elasticsearch:实时日志分析服务的核心技术,一个schemaless,实时的数据存储服务,通过index组织数据,兼具强大的搜索和统计功能
(4)Kibana:基于Elasticsearch的数据可视化组件,超强的数据可视化能力是众多公司选择ELK stack的重要原因
-
消息通讯
消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等
点对点通讯:客户端A和客户端B使用同一队列,进行消息通讯。
聊天室通讯:客户端A,客户端B,客户端N订阅同一主题,进行消息发布和接收。实现类似聊天室效果。
以上实际是消息队列的两种消息模式,点对点或发布订阅模式。模型为示意图,供参考。
优缺点
优势:
-
系统解耦
交互系统之间没有直接的调用关系,只是通过消息传输,故系统侵入性不强,耦合度低。
-
提高系统响应时间
例如原来的一套逻辑,完成支付可能涉及先修改订单状态、计算会员积分、通知物流配送几个逻辑才能完成;通过MQ架构设计,就可将紧急重要(需要立刻响应)的业务放到该调用方法中,响应要求不高的使用消息队列,放到MQ队列中,供消费者处理。
-
为大数据处理架构提供服务
通过消息作为整合,大数据背景下,消息队列还与实时处理架构整合,为数据处理提供性能支持。
-
Java消息服务——JMS
Java消息服务(Java Message Service,JMS)应用程序接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。 JMS中的P2P和Pub/Sub消息模式:点对点(point to point, queue)与发布订阅(publish/subscribe,topic)最初是由JMS定义的。这两种模式主要区别或解决的问题就是发送到队列的消息能否重复消费(多订阅)。
缺点:
-
系统可用性降低
RabbitMQ是一个重量级的消息队列系统,它在运行时会占用较多的系统资源,包括内存、CPU和磁盘空间等。在高并发或大规模数据处理的场景下,这可能会成为性能瓶颈。
-
系统复杂度提高
RabbitMQ的架构和配置相对复杂,需要一定的学习和理解成本。对于初学者来说,可能需要花费一定的时间来熟悉其基本概念、组件和配置方式。
-
一致性问题
当RabbitMQ用于跨多个系统或服务进行消息传递时,可能会面临数据一致性的问题。例如,在一个分布式系统中,如果某个服务处理消息失败,可能会导致数据的不一致。因此,需要设计合适的消息处理机制和容错策略来确保数据的一致性。
所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,做好之后,你会发现,系统复杂度提升了一个数量级,也许是复杂了 10 倍。但是关键时刻,用,还是得用的。
模式分类
模式名称 | 特点 | 应用场景 |
点对点模式(P2P) | 消息通过队列传递,每条消息只能被一个消费者消费一次,消费后消息从队列中删除。 | 任务处理、工作队列(如订单处理、任务分发) |
发布-订阅模式(Pub/Sub) | 消息通过主题发布,所有订阅该主题的消费者都能接收到消息。一个消息可以被多个消费者同时消费。 | 广播消息、事件通知(如新闻推送、日志收集) |
请求-响应模式(Request-Response) | 生产者发送请求消息,消费者处理后返回响应消息。用于同步通信,确保请求方可以获取处理结果。 | 同步任务执行、远程过程调用(RPC) |
流式处理模式(Streaming Processing) | 消息作为数据流的一部分连续发布,消费者以流的形式实时处理数据。 | 实时日志分析、金融交易处理 |
事务消息模式(Transactional Messaging) | 支持事务的消息传递,确保一组消息要么全部成功发送,要么全部回滚,保证消息传递的一致性。 | 分布式事务、银行转账 |
延迟消息模式(Delayed Messaging) | 允许消息在指定时间后被消费,用于需要延迟处理的场景。 | 定时任务、延迟执行的业务逻辑 |
死信队列模式(DLQ) | 存储无法被正常消费的消息,方便后续进行分析或人工干预处理。 | 错误消息处理、重试机制 |
负载均衡模式(Load Balancing) | 多个消费者从同一队列中消费消息,负载均衡策略确保每个消费者获得相对均衡的处理任务。 | 高并发任务处理、均衡工作负载 |
常用协议
协议 | 简介 | 特点 | 应用场景 |
AMQP | Advanced Message Queuing Protocol (AMQP) 是一种开放标准的应用层协议,旨在提供统一的消息服务,允许不同平台和语言的客户端与消息中间件之间进行可靠的通信。 | - 可靠性:提供消息确认、事务性消息、持久性支持。 - 通用性:支持多种消息传递模式,如Direct、Topic、Fanout。 | 适用于金融交易系统、物流平台等需要高可靠性和跨平台通信的系统。 |
MQTT | MQTT (Message Queuing Telemetry Transport) 是一种轻量级的即时通讯协议,设计初衷是为低带宽、不稳定网络下的设备通信提供支持。 | - 轻量级:占用带宽小,适合资源受限的设备。 - 支持PUSH机制:适合移动端应用和嵌入式系统。 | 物联网(IoT)设备通信,如智能家居、环境监测传感器网络。 |
STOMP | STOMP (Streaming Text Orientated Messaging Protocol) 是一种简单的基于文本的消息协议,旨在提供一个通用的消息传输方式,便于不同语言的客户端与消息代理进行交互。 | - 简单易用:基于文本格式,易于实现和调试。 - 通用性强:支持多种语言客户端,适用于跨平台的消息传递。 | 适用于快速实现消息通信的应用,如实时数据推送、WebSocket通信。 |
XMPP | XMPP (Extensible Messaging and Presence Protocol) 是一种基于XML的协议,广泛用于即时消息传递和在线状态检测。它的设计目标是提供一个开放的、可扩展的通讯协议。 - 可扩展性:基于XML,支持广泛的扩展功能。 | - 高安全性:支持加密和认证机制,适合安全性要求高的应用场景。 - 通用性:兼容性强,适用于服务器之间的准即时操作。 | 适用于即时通讯系统、在线状态检测,如Google Talk(早期)、Jabber等。 |
在实际应用中,选择合适的协议需要根据具体的需求和系统架构来决定。不同的协议适用于不同的场景:
- AMQP更适合需要高可靠性和复杂消息传递模式的系统;
- MQTT在物联网和移动端应用中表现出色;
- STOMP因其简单易用的特点适合轻量级应用;
- XMPP则因其可扩展性和安全性,在即时通讯和在线状态检测方面具有优势。
MQ积压问题
消息队列(MQ)积压指在消息队列中累积了大量未处理的消息,导致系统处理能力不足,影响正常的消息传递和处理。以下是常见原因和应对方法:
原因:
- 消费者处理速度慢: 消费者处理消息的速度比生产者生成消息的速度慢,导致消息堆积。
- 消费者故障: 消费者出现故障,停止了正常的消息处理,导致消息堆积。
- 消费者数量不足: 消费者的数量不足以处理消息的产生速度,导致积压。
- 生产者发送过快: 生产者发送消息的速度过快,超过了消费者的处理能力。
- 消息处理失败重试: 消息处理失败后会重试,如果重试失败,会导致消息不断堆积。
- 消息体积大: 如果消息体很大,会占用更多的存储空间,导致消息队列积压。
应对方法:
- 增加消费者数量: 增加消费者的数量,以提高消息处理的速度。
- 优化消费者处理逻辑: 优化消费者的处理逻辑,提升处理效率,减少处理时间。
- 监控和报警: 部署监控系统,及时发现消息积压问题,并设置报警机制。
- 消息处理失败策略: 设计合理的消息处理失败策略,避免消息无限重试,可以将处理失败的消息暂时移到死信队列中。
- 增加消息队列容量: 增加消息队列的容量,以适应高峰期的消息产生。
- 限流和熔断: 实施限流和熔断策略,避免过多的请求导致消息队列积压。
- 消息过期处理: 设置消息的过期时间,对于长时间未处理的消息进行清理,避免积压。
- 水平扩展: 根据需求进行消息队列的水平扩展,以提高处理能力。
- 定期清理: 定期清理过期的、无法处理的消息,以避免长期的积压。
综合上述方法,需要根据实际情况进行合理的调整和应对,以确保消息队列的稳定运行,避免积压问题影响整体系统性能。
常用消息中间件
AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。
JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。
AMQP、JMS比较
MQ是消息通信的模型,并发具体实现。现在实现MQ的有两种主流方式:AMQP、JMS。
两者区别和联系:
- JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式
- JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
- JMS规定了两种消息模型;而AMQP的消息模型更加丰富
常见MQ产品
ActiveMQ:基于JMS
RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会
Kafka:分布式消息系统,高吞吐量
在大数据场景主要采用Kafka作为消息队列。在JavaEE开发中主要采用ActiveMQ、RabbitMQ、RocketMQ。
MQ | 主要特性 | 优点 | 缺点 |
Kafka | - 分布式系统,支持自动负载均衡 - 快速持久化,支持批量操作 - 高吞吐量,每秒可处理百万级消息 - 支持数据复制和容错 | - 高性能,支持多种客户端语言 - 支持海量消息堆积 - 良好的扩展性和容错性 - 社区活跃,广泛使用 | - 超过64个队列时负载飙升 - 短轮询方式影响实时性 - 消费失败不支持重试 - 宕机后可能导致消息乱序 |
RabbitMQ | - 基于AMQP协议,消息路由灵活 - 支持消息集群和高可用性 - 支持多种协议和编程语言 - 提供丰富的插件和管理界面 | - 高并发,稳定性强 - 支持复杂路由 - 消息确认机制和持久化 - 管理界面友好 | - 性能不如Kafka - 学习曲线较陡峭 - 消息封装大,中央节点增加延迟 |
RocketMQ | - 支持分布式队列模型 - 提供丰富的消息拉取模式 - 高效订阅,支持亿级消息堆积 - 实现了严格的消息顺序 | - 性能高,支持大量消息堆积 - 提供集群和广播消费模式 - 支持主从和高可用 - 社区更新快,适合国内场景 | - 支持的客户端语言少 - 缺少Web管理界面 - 社区关注度不及Kafka和RabbitMQ |
ActiveMQ | - 完全支持JMS规范 - 提供广泛的连接协议支持 - 支持多种客户端语言和协议 - 提供灵活的持久化和安全机制 | - 跨平台,支持JDBC持久化 - 支持自动重连和错误重试 - 监控和管理工具完善 - 界面友好,易于使用 | - 社区活跃度较低 - 可能出现丢失消息的情况 - 维护集中在新版本Apollo,5.x维护较少 |
RabbitMQ
RabbitMQ
是由erlang
语言开发,基于AMQP
(Advanced Message Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。
RabbitMQ出现后,国内大部分公司都从ActiveMQ切换到了RabbitMQ,基本代替了activeMQ的位置。它的社区还是很活跃的。
它的单机吞吐量也是万级,对于需要支持特别高的并发的情况,它是无法担当重任的。
在高可用上,它使用的是镜像集群模式,可以保证高可用。
在消息可靠性上,它是可以保证数据不丢失的,这也是它的一大优点。
同时它也支持一些消息中间件的高级功能,如:消息重试、死信队列等。
但是,它的开发语言是erlang,国内很少有人精通erlang,所以导致无法阅读源码。
对于大多数中小型公司,不需要面对技术上挑战的情况,使用它还是比较合适的。而对于一些BAT大型互联网公司,显然它就不合适了。
特点
-
可靠性:RabbitMQ采用一系列机制来确保消息的可靠性,如持久化、传输确认和发布确认等方法。可以使用镜像队列,数据能够保证百分之百的不丢失。在互联网金融行业,对数据的稳定性和可靠性要求都非常高的情况下,都会选择RabbitMQ。
-
灵活的路由:消息在进入队列之前会通过交换器进行路由,使得消息能够按照特定的规则进行分发。
-
可扩展性:RabbitMQ支持构建集群,多个节点可以组成一个集群,并可以根据实际业务需求动态地扩展集群中的节点。
-
高可用性:队列可以在集群的多台机器上进行镜像设置,即使其中的某些节点出现故障,队列仍然可用。
-
多种协议:RabbitMQ不仅原生支持AMQP协议,还支持其他多种消息中间件协议,如STOMP、MQTT等。
-
多语言客户端:RabbitMQ提供了广泛的语言客户端支持,几乎涵盖了所有常用编程语言,包括Java、Python、Ruby、PHP、C#、JavaScript等。
四种集群架构
1. 主备模式
主备模式也称为Warren (兔子窝) 模式,是实现RabbitMQ的高可用集群,一般在并发和数据量不高的情况下,这种模式非常的好用且简单。
-
主备模式:主节点提供读写,从节点不提供读写服务,只是负责提供备份服务,备份节点的主要功能是在主节点宕机时,完成自动切换 从-->主
-
主从模式:主节点提供读写,从节点只读
主备模式,也就是一个主/备方案,主节点提供读写,备用节点不提供读写。如果主节点挂了,就切换到备用节点,原来的备用节点升级为主节点提供读写服务,当原来的主节点恢复运行后,原来的主节点就变成备用节点,和 activeMQ 利用 zookeeper 做主/备一样,也可以一主多备。
2. 远程模式(不常用)
远程模式可以实现双活的一种模式,简称 shovel 模式。所谓Shovel就是把消息进行不同数据中心的复制工作,可以跨地域的让两个 MQ 集群互联,远距离通信和复制。Shovel架构模型:
3. 镜像模式(常用)
镜像模式:集群模式非常经典的就是Mirror镜像模式,可以真正做到保证100%数据不丢失,同时做到主节点在某时刻失败自动的去做转换,自动的去做故障转移,保证系统的高可用。在实际工作中用的最多的,并且实现非常的简单,一般互联网大厂都会构建这种镜像集群模式。
镜像队列机制就是将队列在三个节点之间设置主从关系,消息会在三个节点之间进行自动同步,且如果其中一个节点不可用,并不会导致消息丢失或服务不可用的情况,提升MQ集群的整体高可用性。
Mirror 镜像队列,目的是为了保证 rabbitMQ 数据的高可靠性解决方案,主要就是实现数据的同步,一般来讲是 2 - 3 个节点实现数据同步。对于 100% 数据可靠性解决方案,一般是采用 3 个节点。集群架构如下:
4. 多活模式
多活模式:这种模式也是实现异地数据复制的主流模式,因为 shovel 模式配置比较复杂,所以一般来说,实现异地集群的都是采用这种双活 或者 多活模型来实现的。这种模式需要依赖 rabbitMQ 的 federation 插件,可以实现持续的,可靠的 AMQP 数据通信,多活模式在实际配置与应用非常的简单。
RabbitMQ部署架构采用双中心模式(多中心),那么在两套(或多套)数据中心中各部署一套RabbitMQ集群,各中心的rabbitMQ 服务除了需要为业务提供正常的消息服务外,中心之间还需要实现部分队列消息共享。多活集群架构如下:
RabbitMQ镜像集群搭建步骤
-
HAProxy
是一款提供高可用性、负载均衡
以及基于TCP(第四层)和HTTP(第七层)应用的代理软件,支持虚拟主机,他是免费、快速并且可靠的一种解决方案。HAProxy特别适用于那些负载特大的web站点,这些站点通常又需要会话保持或七层处理。HAProxy运行在时下的硬件上,完全可以支撑数以万计的并发连接。并且它的运行模式使得它可以很简单安全的整合进您当前的架构中,同时可以保护你的web服务器不被暴露到网络上。 -
HAProxy借助于OS上几种常见的技术来实现性能的最大化:
-
单进程、时间驱动模型显著降低上下文切换的开销及内存占用
-
在任何可用的情况下,单缓冲(single buffering)机制能以不复制任何数据的方式完成读写操作,这会节约大量的CPU时钟周期及内存带宽
-
借助于Linux2.6上的splice()系统调用,HAProxy可以实现零复制转发(Zero-copy- forwarding),在linux3.5及以上的OS上还可以实现零复制启动(zero-starting)
-
-
KeepAlived
软件主要是通过VRRP协议实现高可用
功能的。VRRP是Virtual Router RedundancyProtocol(虚拟路由器冗余协议)的缩写,VRRP出现的目的就是为了解决静态路由单点故障问题的,它能保证党个别节点宕机时,整个网络可以不间断地运行,所以,KeepAlived一方面具有配置管理LVS的功能,同时还具备对LVS下面节点进行健康检查差的功能,另一方面可实现系统网络服务的高可用功能。 -
KeepAlived服务的三个重要功能:
-
管理LVS负载均衡软件
-
实现LVS集群节点的健康检查
-
作为系统网络服务的高可用性(failover)
-
-
KeepAlived高可用原理
KeepAlived高可用服务对之间的故障转移,是通过VRRP(Virtual Router Redundancy Protocol,虚拟路由器冗余协议)来实现的。在KeepAlived服务正常工作是,主Master节点会不断地向备节点发送(多播的方式)心跳消息,用以告诉备Backup节点自己还活着,当主master节点发生故障时,就无法发送心跳消息,备节点也就因此无法继续监测到来自主Master节点的心跳了,于是调用自身的接管程序,接管主Master节点 的IP资源及服务。当主Master节点恢复时,备Backup节点又会释放主节点故障时自身接管的IP资源和服务,恢复到原来的备用角色。
工作原理
RabbitMQ的基本结构:
组成部分说明:
-
Broker
消息队列服务进程,此进程包括两个部分:Exchange和Queue
-
virtual host
虚拟主机,起到数据隔离作用。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等,每个虚拟主机都有AMQP的全套基础组件,并且可以针对每个虚拟主机进行权限以及数据分配。每个虚拟主机相互独立,有各自的exchange、queue。
-
Exchange
消息队列交换机,生产者将消息发送到Exchange,由Exchange将消息路由到一个或多个Queue中。Exchange根据消息的属性或内容路由消息。
-
Queue
消息队列,存储消息的队列,生产者投递的消息会暂存在消息队列中,等待消费者处理。每个消息都会被投入到一个或多个Queue里
-
Producer
消息生产者,即生产方客户端,生产方客户端将消息发送
-
Consumer
消息消费者,即消费方客户端,接收MQ转发的消息,并处理消息。
-
Connection
生产者或消费者与RabbitMQ进行交互,首先就需要建立一个TPC连接。RabbitMQ为了减少性能开销,也会在一个Connection中建立多个Channel,这样便于客户端进行多线程连接,这些连接会复用同一个Connection的TCP通道,提高性能。
-
Connection会执行认证、IP解析、路由等底层网络任务。
-
应用与消息队列RabbitMQ版完成Connection建立大约需要15个TCP报文交互,因而会消耗大量的网络资源和消息队列RabbitMQ版资源。
-
一个进程对应一个Connection,一个进程中的多个线程则分别对应一个Connection中的多个Channel。
-
Producer和Consumer分别使用不同的Connection进行消息发送和消费;
-
-
Channel
客户端与RabbitMQ建立了连接,就会分配一个AMQP信道 Channel。每个信道都会被分配一个唯一的ID。在客户端的每个物理TCP连接里,可建立多个Channel,每个Channel代表一个会话任务。
-
Channel是物理TCP连接中的虚拟连接。
-
当应用通过Connection与消息队列RabbitMQ版建立连接后,所有的AMQP协议操作(例如创建队列、发送消息、接收消息等)都会通过Connection中的Channel完成。
-
Channel可以复用Connection,即一个Connection下可以建立多个Channel。
-
Channel不能脱离Connection独立存在,而必须存活在Connection中。
-
当某个Connection断开时,该Connection下的所有Channel都会断开。
-
-
Routing Key(路由键)
生产者在向Exchange发送消息时,需要指定一个Routing Key来设定该消息的路由规则。 Routing Key需要与Exchange类型及Binding Key联合使用才能生效。一般情况下,生产者在向Exchange发送消息时,可以通过指定Routing Key来决定消息被路由到哪个或哪些Queue;
-
Binding
一套绑定规则,用于告诉Exchange消息应该被存储到哪个Queue。它的作用是把Exchange和Queue按照路由规则绑定起来。
-
Binding Key(绑定键)
用于告知Exchange应该将消息投递到哪些Queue中(生产者将消息发送给哪个Exchange是需要由RoutingKey决定的,生产者需要将Exchange与哪个队列绑定时需要由BindingKey决定的)
生产者发送消息流程:
1、生产者和Broker建立TCP连接。
2、生产者和Broker建立通道。
3、生产者通过通道消息发送给Broker,由Exchange将消息进行转发。
4、Exchange将消息转发到指定的Queue(队列)
【详细】
1、消息生产者连接到`RabbitMQ Broker`,建立链接(Connection),在链接(Connection)上开启一个信道(Channel);
2、声明一个交换机(Exchange),并设置相关属性,比如交换机类型、是否持久化等;
3、声明一个队列(Queue),并设置相关属性,比如是否排他、是否持久化、是否自动删除等;
4、使用路由键(RoutingKey)将队列(Queue)和交换机(Exchange)绑定起来;
5、生产者发送消息至 RabbitMQ Broker,其中包含路由键、交换器等信息,根据路由键(RoutingKey)发送消息到交换机(Exchange);
6、相应的交换器(Exchange)根据接收到的路由键(RoutingKey)查找相匹配的队列如果找到 ,则将从生产者发送过来的消息存入相应的队列中;
7、如果没有找到 ,则根据生产者配置的属性选择丢弃还是回退给生产者;
8、关闭信道(Channel);
9、关闭链接(Connection);
消费者接收消息流程:
1、消费者和Broker建立TCP连接
2、消费者和Broker建立通道
3、消费者监听指定的Queue(队列)
4、当有消息到达Queue时Broker默认将消息推送给消费者。
5、消费者接收到消息。
6、ack回复
【详细】
- 1、建立链接(Connection);
- 2、在链接(Connection)上开启一个信道(Channel);
- 3、请求消费指定队列(Queue)的消息,并设置回调函数(onMessage);
- 4、[MQ]将消息推送给消费者,消费者接收消息;
- 5、消费者发送消息确定(Ack[acknowledge]);
- 6、[MQ]删除被确认的消息;
- 7、关闭信道(Channel);
- 8、关闭链接(Connection);
MQ消费消息分发原理消费者接收消息流程:
-
一种是Pull模式,对应的方法是basicGet。消息存放在服务端,只有消费者主动获取才能拿到消息。如果每搁一段时间获取一次消息,消息的实时性会降低。但是好处是可以根据自己的消费能力决定消息的频率。
-
另一种是push,对应的方法是BasicConsume,只要生产者发消息到服务器,就马上推送给消费者,消息保存客户端,实时性很高,如果消费不过来有可能会造成消息积压。Spring AMQP是push方式,通过事件机制对队列进行监听,只要有消息到达队列,就会触发消费消息的方法。
生产者确认原理
生产者确认原理:生产者将消息发送到exchange,exchange根据路由规则将消息投递到了queue。
-
Confirm确认:生产者发送消息到交换机时会存在消息丢失的情景,开启事务会导致吞吐量下降,Confirm机制就是消息发送到交换机(Exchange)时会触发Confirm回调。通过 publisher confirm (发送方确认机制)可以确定消息是否被成功路由到MQ broker从而选择是否重发等步骤。当生产者开启 publisher confirm 消息发送到MQ端之后,MQ会回一个ack给生产者,ack是个boolean值,为true消息成功发送到MQ。反之发送失败。
-
Return确认:从交换机到队列也有可能出现路由失败导致消息丢失情景(可能是MQ出问题导致queue和exchange绑定丢失,或者失误删除了绑定关系等),Return机制可解决这个问题,路由失败时可以通过Return回调来将路由失败的消息记录下来。
消费者确认原理
消费者确认原理:指当一条消息投递到消费者处理后,消费者发送给MQ broker的确认
(通俗的说就是 告知服务器这条消息已经被我消费了,可以在队列删掉 ,这样以后就不会再发了, 否则消息服务器以为这条消息没处理掉 重启应用后还会在发)。有auto和manual两种
-
auto则由broker自行选择时机,一般可认为消息发送到消费者后就直接被ack,也即消息会被从队列中移除掉而不顾消息的处理逻辑是否成功;
-
manual则是需要消费者显式的去手动ack后消息才会被从队列中移除掉,通过这个机制可以限制在消息处理完之后再Ack或者nack; 开启手动确认模式,即由消费方自行决定何时应该ack,通过设置autoAck=false开启手动确认模式;
消费者高级特性
- ack
- prefetch
- concurrency
- retry
队列高级特性
1、死信队列DLX
DLX 全称 Dead-Letter-Exchange,死信交换器,死信邮箱。当消息在一个队列中变成死信 (dead message) 之后,可以被重新发送到另一个交换机中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。
被拒收的消息,或者是过期的消息,或者是队列已经满了的消息,都会进入死信队列,死信队列有一个默认的生效时间,如果没有做任务配置,到了时间会自动删除消息。
导致死信的几种原因:
-
队列消息长度到达限制,队列满了无法再添加。
-
消费者拒接消费消息(Basic.Reject /Basic.Nack) ,并且不把消息重新放入原目标队列(requeue=false)
-
原队列存在消息过期设置,消息到达超时时间未被消费
死信队列处理过程:
-
DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。
-
当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。
-
可以监听这个队列中的消息做相应的处理。
死信队列的设置:
-
创建queue1 正常队列 用于接收死信队列过期之后转发过来的消息
-
创建queue2 可以针对他进行参数设置 死信队列
-
创建交换机 死信交换机
-
绑定正常队列到交换机
java中配置死信队列:
//延迟队列,又叫死信队列 “
@Bean
public Queue delayQueue() {
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "test_ex");
arguments.put("x-dead-letter-routing-key", "test_ex.dead");
// 消息过期时间 2分钟
arguments.put("x-message-ttl", 60000);
return new Queue("delayQueue", true, false, false, arguments);
}
以上参数说明:
-
x-dead-letter-exchange:死信队列过期以后往指定交换机发
-
x-dead-letter-routing-key:死信队列过期指定路由键
-
x-message-ttl: 死信队列过期时间,单位是毫秒
2、延迟队列
业务模拟场景:用户下单后,有十分钟的支付时间,会将消息发送到延迟队列,延迟十分钟后让消费者进行消费,判断用户是否支付。
延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。
RabbitMQ 本身是没有延迟队列的,要实现延迟消息,一般有两种方式:
-
使用rabbitmq的死信交换机(DLX)和 消息的存活时间 TTL(Time To Live)。如果消息超时未消费就会变成死信,在RabbitMQ中如果消息成为死信,队列可以绑定一个死信交换机,在死信交换机上可以绑定其他队列,在我们发消息的时候可以按照需求指定TTL的时间,这样就实现了延迟队列的功能了。
-
在 RabbitMQ 3.5.7 及以上的版本提供了一个插件(rabbitmq-delayed-message-exchange)来实现延迟队列功能。同时,插件依赖 Erlang/OPT 18.0 及以上。只需要在声明交互机的时候,指定这个就是死信交换机,然后在发送消息的时候直接指定超时时间就行了,相对于死信交换机+TTL省略了一些步骤
3、优先级队列
RabbitMQ 自 V3.5.0 有优先级队列实现,优先级高的队列会先被消费。不过,当消费速度大于生产速度且 Broker 没有堆积的情况下,优先级显得没有意义。
设置优先级
设置队列的优先级(生产者):
Channel ch = ...;
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-max-priority", 10);
ch.queueDeclare("my-priority-queue", true, false, false, args);
设置消息的优先级:
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("",QUEUE_NAME,properties,message.getBytes());
使用客户端提供的可选参数 x-max-priority 来实现优先级队列。此参数应为介于 1 和 255 之间的正整数,数字越大优先级越高,但推荐值介于: 1 ~ 10。当前使用更多优先级将消耗更多的 CPU 资源,通过使用更多的 Erlang 进程。运行时调度也会受到影响。
然后,发布者可以使用basic.properties
的priority
字段发布优先级消息。没有priority
属性的消息其优先级被视为 0。优先级高于队列最大值的消息将被视为以最大优先级发布。
与消费者的交互
了解使用者使用优先级队列时的工作方式非常重要。默认情况下,消费者在确认任何消息之前可能会收到大量消息,但仅受网络背压限制。
因此,如果这种饥饿的使用者连接到一个空队列,然后将消息发布到该队列中,则消息可能不会花费任何时间在队列中等待。在这种情况下,优先级队列将没有机会优先处理它们。
在大多数情况下,您需要在使用者的手动确认模式下使用 basic.qos 方法,以限制随时可以发送的消息数,从而允许对邮件进行优先级排序。
与其他功能的协作避坑
通常,优先级队列具有标准RabbitMQ队列的所有功能:支持持久性,分页,镜像等。开发人员应该注意几个交互:
- 应该过期的消息仍然只会从队列的开头过期。这意味着与普通队列不同,即使每个队列TTL也会导致过期的低优先级消息滞留在未过期的高优先级消息之后。这些消息将永远不会传递,但是将出现在队列统计信息中。
- 设置了最大长度的队列将照常从队列的开头丢弃消息以强制执行该限制。这意味着较高优先级的消息可能会被丢弃,以取代较低优先级的消息,这可能不是所期望的。
4、队列过期TTL
TTL 全称 Time To Live(存活时间/过期时间)。当消息到达存活时间后,还没有被消费,会被自动清除。
RabbitMQ设置过期时间有两种:
-
针对某一个队列设置过期时间 ;队列中的所有消息在过期时间到之后,如果没有被消费则被全部清除
@Configuration public class TtlConfig { //创建过期队列 @Bean public Queue createqueuettl1(){ //设置队列过期时间为10000 10S钟 return QueueBuilder.durable("queue_demo02").withArgument("x-message-ttl",10000).build(); } //创建交换机 @Bean public DirectExchange createExchangettl(){ return new DirectExchange("exchange_direct_demo02"); } //创建绑定 @Bean public Binding createBindingttl(){ return BindingBuilder.bind(createqueuettl1()).to(createExchangettl()).with("item.ttl"); } } //测试 /** * 发送 ttl测试相关的消息 * @return */ @RequestMapping("/send4") public String send4() { //设置回调函数 //发送消息 rabbitTemplate.convertAndSend("exchange_direct_demo02", "item.ttl", "hello ttl哈哈"); return "ok"; } } //过10S钟之后,该数据就都被清0
-
针对某一个特定的消息设置过期时间;队列中的消息设置过期时间之后,如果这个消息没有被消费则被清除。
//通过MessagePostProcessor方法设置该消息的过期时间 对单独的一条信息设置过期时间 过期后一般让他进入死信队列 rabbitTemplate.convertAndSend("queue_order_queue1", (Object) "哈哈我要检查你是否有支付", new MessagePostProcessor() { @Override public Message postProcessMessage(Message message) throws AmqpException { message.getMessageProperties().setExpiration("10000");//设置该消息的过期时间 return message; } });
注意:针对某一个特定的消息设置过期时间时,一定是消息在队列中在队头的时候进行计算,如果某一个消息A 设置过期时间5秒,消息B在队头,消息B没有设置过期时间,B此时过了已经5秒钟了还没被消费。注意,此时A消息并不会被删除,因为它并没有再队头。
一般在工作当中,单独使用TTL的情况较少。一般配合延时队列使用。
5、队列长度限制
队列的最大长度限制(Queue Length Limit)可以是限制消息的数量(max-length),或是消息的总字节数(max-length-bytes,总字节数表示的是所有的消息体的字节数,忽略消息的属性和任何头部信息),或者两者都进行了限制。
队列长度限制的设置方法
-
声明队列时通过可选参数设置
最大消息数可以通过设置队列声明参数x-max-length,值为非负整数。
最大字节长度可以通过设置队列声明参数x-max-length-bytes,值为非负整数。
如果同时设置了这两个参数值,无论先达到哪个限制,都将强制执行。
Map<String, Object> args = new HashMap<String, Object>(); args.put("x-max-length", 10); //声明配置队列最大长度为10个消息 args.put("max-length-bytes", 1024);//声明配置队列最大字节数为1024个字节 channel.queueDeclare("myqueue", false, false, false, args);
-
在服务器使用配置策略(policies)强制设置。1、2同时设置,将使用两个值中的较小值。
//linux服务器: rabbitmqctl set_policy my-pol "^one-meg$" '{"max-length-bytes":1048576}' --apply-to queues rabbitmqctl set_policy my-pol "^two-messages$" '{"max-length":2,"overflow":"reject-publish"}' --apply-to queues //windows服务器: rabbitmqctl.bat set_policy my-pol "^one-meg$" ^"{""max-length-bytes"":1048576}" ^ --apply-to queues rabbitmqctl.bat set_policy my-pol "^two-messages$" "{""max-length"":2,""overflow"":""reject-publish""}" --apply-to queues
注意,在所有情况中,只有处于ready状态(在RabbitMQ中消息有2种状态:ready 和 unacked)的消息被计数,未被确认的消息不会受到限制。
rabbitmqctl list_queues
中的字段 messages_ready
、 message_bytes_ready
以及管理 API 展示的即为被限制的值。
队列的溢出行为
当队列的消息超过设置的最大长度或大小时,RabbitMQ默认将处于队列头部的信息(队列中最老的消息)丢弃或变成死信。
队列的溢出行为可以在队列声明时设置参数 x-overflow 的值,该值是一个字符串。x-overflow参数的可选值是:
- drop-head:丢弃队列头部的消息(默认的处理策略)。
- reject-publish:丢弃队列尾部的消息。
- reject-publish-dlx:丢弃队列尾部的消息,并拒绝发送消息到死信交换机。
如果overflow设置为reject-publish 或 reject-publish-dlx,则丢弃最近发布的消息。此外,如果开启了发送者确认模式(publisher confirms),那么将使用basic.nack() 通知发布者消息被拒绝。
如果消息被同时路由到多个队列,并被其中至少一个队列拒绝,通道将使用 basic.nack() 通知发布者。但是消息仍可以继续发送给可以接受它的队列。reject-publish、reject-publish-dlx的区别在于reject-publish-dlx也是死信被拒绝的消息。
6种工作模式
① 简单模式
最直接的方式,P端发送一个消息到一个指定的queue,中间不需要任何exchange规则。C端按queue方式进行消费。
在上图的模型中,有以下概念:
-
P:生产者,也就是要发送消息的程序
-
C:消费者:消息的接受者。
-
queue:消息队列,图中红色部分。可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。
-
一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)。
生产者:
public class Producer {
//消息生产者
public static void main(String[] args) throws IOException, TimeoutException {
//创建链接工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置RabbitMQ服务主机地址,默认localhost
connectionFactory.setHost("localhost");
//设置RabbitMQ服务端口,默认5672
connectionFactory.setPort(5672);
//设置虚拟主机名字,默认/
connectionFactory.setVirtualHost("/rabbit");
//设置用户连接名,默认guest
connectionFactory.setUsername("admin");
//设置链接密码,默认guest
connectionFactory.setPassword("admin");
//创建链接
Connection connection = connectionFactory.newConnection();
//创建频道
Channel channel = connection.createChannel();
/**
* 声明队列
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
* **/
channel.queueDeclare("simple_queue",true,false,false,null);
//创建消息
String message = "hello!welcome hello word!";
/**
* 消息发送
* 参数1:交换机名称,如果没有指定则使用默认Default Exchage
* 参数2:路由key,简单模式可以传递队列名称
* 参数3:消息其它属性
* 参数4:消息内容
*/
channel.basicPublish("","simple_queue",null,message.getBytes());
//关闭资源
channel.close();
connection.close();
}
}
消费者:
public class Consumer {
//消息消费者
public static void main(String[] args) throws IOException, TimeoutException {
//创建链接工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置RabbitMQ服务主机地址,默认localhost
connectionFactory.setHost("localhost");
//设置RabbitMQ服务端口,默认5672
connectionFactory.setPort(5672);
//设置虚拟主机名字,默认/
connectionFactory.setVirtualHost("/rabbit");
//设置用户连接名,默认guest
connectionFactory.setUsername("admin");
//设置链接密码,默认guest
connectionFactory.setPassword("admin");
//创建链接
Connection connection = connectionFactory.newConnection();
//创建频道
Channel channel = connection.createChannel();
//创建队列
channel.queueDeclare("simple_queue",true,false,false,null);
//创建消费者,并设置消息处理
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
/***
* @param consumerTag 消息者标签,在channel.basicConsume时候可以指定
* @param envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* @param properties 属性信息
* @param body 消息
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由的key
String routingKey = envelope.getRoutingKey();
//获取交换机信息
String exchange = envelope.getExchange();
//获取消息ID
long deliveryTag = envelope.getDeliveryTag();
//获取消息信息
String message = new String(body,"UTF-8");
System.out.println("routingKey:"+routingKey+",exchange:"+exchange+",deliveryTag:"+deliveryTag+",message:"+message);
}
};
/**
* 消息监听
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume("simple_queue",true,defaultConsumer);
//关闭资源(不建议关闭,建议一直监听消息)
//channel.close();
//connection.close();
}
}
② 工作队列模式(Worke queues)
此模式的核心思想是将任务异步处理,通过将任务封装为消息并发送到队列,避免了立即执行带来的负担。这样,后台的工作进程可以在适当的时候处理这些任务。多个消费者可以并行工作,从而提高处理能力。随着任务量的增加,可以通过增加更多的工作进程来扩展系统的处理能力,确保任务高效地分配和执行。
Work Queues与简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。一个消息只会被一个消费者消费。
一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)。
应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
channel.basicPublish("", TASK_QUEUE_NAME,MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes("UTF-8"));
Consumer: 每次拉取一条消息。
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
channel.basicQos(1);
channel.basicConsume(TASK_QUEUE_NAME, false, consumer);
③ 发布订阅模式(Publish/Subscribe)
发布/订阅模式是一种常见的消息传递模式,它允许消息被广播到多个消费者。这个模式的主要优势在于它的灵活性和扩展性,使得一个消息可以被多个消费者同时接收和处理。
exchange type是 fanout 。
在订阅模型中,多了一个 Exchange 角色,而且过程略有变化:
Exchange只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与 Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失!producer只负责发送消息,至于消息进入哪个queue,由exchange来分配。
应用场景:所有消费者获得相同的消息,例如天气预报。
生产者:
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
消费者:
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");
发布订阅模式、工作队列模式区别:
-
P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
-
C:消费者,消息的接收者
-
Queue:消息队列,接收消息、缓存消息Exchange:交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。
-
Exchange有常见以下3种类型:
-
Fanout:广播,将消息交给所有绑定到交换机的队列,交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到。
-
Direct:定向,把消息交给符合指定routing key 的队列
-
Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
-
工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机
-
发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)
-
发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑 定到默认的交换机
④ 路由模式(Routing)
exchange typ 是 direct 。
-
P:生产者,向 Exchange 发送消息,发送消息时,会指定一个routing key
-
X:Exchange(交换机),接收生产者的消息,然后把消息递交给与 routing key 完全匹配的队列
-
C1:消费者,其所在队列指定了需要 routing key 为 error 的消息
-
C2:消费者,其所在队列指定了需要 routing key 为 info、error、warning 的消息
路由模式要求队列在绑定交换机时要指定 routing key,消息会转发到符合 routing key 的队列。
路由模式特点:
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
- 消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey。
- Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息
生产者:
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
消费者:
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueBind(queueName, EXCHANGE_NAME, routingKey1);
channel.queueBind(queueName, EXCHANGE_NAME, routingKey2);
channel.basicConsume(queueName, true, consumer);
⑤ 主题模式(Topics)
exchange type 是 topic
红色 Queue:绑定的是 usa.# ,因此凡是以 usa. 开头的 routing key 都会被匹配到
黄色 Queue:绑定的是 #.news ,因此凡是以 .news 结尾的 routing key 都会被匹配
对routingKey进行了模糊匹配单词之间用,隔开,* 代表一个具体的单词。# 代表0个或多个单词
Topic 主题模式可以实现 Pub/Sub 发布与订阅模式和 Routing 路由模式的功能,只是 Topic 在配置routing key 的时候可以使用通配符,显得更加灵活。
生产者:
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
消费者:
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
channel.queueBind(queueName, EXCHANGE_NAME, routingKey1);
channel.queueBind(queueName, EXCHANGE_NAME, routingKey2);
channel.basicConsume(queueName, true, consumer);
⑥ 发布者确认模式(Publisher Confirms)
发布者确认是RabbitMQ扩展,可以实现可靠的发布。在通道上启用发布者确认后,代理将异步确认客户端发布的消息,这意味着它们已在服务器端处理。与发布者进行可靠的发布确认
⑦ 远程调用模式(RPC)
远程调用,不太算MQ,不过多介绍
要实现RPC模式中的远程过程调用,需要两个主要组件:客户端和服务器。客户端发送请求到服务器,并且等待服务器的响应。以下是如何搭建一个简单的RPC系统的步骤:
-
客户端:定义一个RPCClient类,其call()方法会发送一个RPC请求,并等待响应。
-
服务器:处理请求并将结果发送回客户端。使用RabbitMQ的队列机制来处理请求和响应。
-
回调队列:客户端会在发送请求时创建一个回调队列,用于接收响应。消息的replyTo属性指定了回调队列,correlationId用来关联请求和响应。
交换机类型
① Direct exchange(直连交换机)
直连型交换机根据消息携带的路由键(routing key)将消息投递给对应队列,步骤:
① 将一个队列绑定到某个交换机上,同时赋予该绑定一个路由键(routing key)
② 当一个携带着路由值为R的消息被发送给直连交换机时,交换机会把它路由给绑定值同样为R的队列。
路由规则:Direct Exchange根据Binding Key和Routing Key完全匹配的规则路由消息。
使用场景:Direct Exchange适用于通过简单字符标识符区分消息的场景。常用于单播路由。
② Fanout exchange(扇型交换机)
扇型交换机将消息路由给绑定到它身上的所有队列。不同于直连交换机,路由键在此类型上不启任务作用。如果N个队列绑定到某个扇型交换机上,当有消息发送给此扇型交换机时,交换机会将消息的发送给这所有的N个队列
路由规则:Fanout Exchange忽略Routing Key和Binding Key的匹配规则,将消息路由到所有绑定的Queue。
使用场景:Fanout Exchange适用于广播消息的场景。例如,分发系统使用Fanout Exchange来广播各种状态和配置更新。
③ Topic exchange(主题交换机)
主题交换机(topic exchanges)中,队列通过路由键绑定到交换机上,然后,交换机根据消息里的路由值,将消息路由给一个或多个绑定队列。
路由规则:Topic Exchange根据Binding Key和Routing Key通配符匹配的规则路由消息。
通配符规则:
- 单个单词的通配符(*):匹配一个英文单词,英文单词间通过英文句号(.)分隔。例如,*.orange.* 可以匹配 quick.orange.rabbit 或 lazy.orange.elephant。
- 多个单词的通配符(#):匹配零个、一个或多个单词,英文单词间通过英文句号(.)分隔。例如,lazy.# 可以匹配 lazy.orange.rabbit、lazy.brown.fox 和 lazy.orange.male.rabbit 等。
使用场景:Topic Exchange适用于通过通配符区分消息的场景。Topic Exchange常用于多播路由。例如,使用Topic Exchange分发有关于特定地理位置的数据。
主题交换机的强大之处
-
当一个队列的绑定键为 #(井号) 时,这个队列将会无视消息的路由键,接收所有的消息。
-
当* (星号) 和#(井号) 这两个特殊字符都未在绑定键中出现的时候,此时主题交换机就拥有的直连交换机的行为。 所以主题交换机也就实现了扇形交换机的功能,和直连交换机的功能。
④ Headers(头部交换机)
路由规则:Headers Exchange可以被视为Direct Exchange的另一种表现形式。 Headers Exchange可以像Direct Exchange一样工作,不同之处在于Headers Exchange使用Headers属性代替Routing Key进行路由匹配。 在绑定Headers Exchange和Queue时,可以设置绑定属性的键值对。然后,在向Headers Exchange发送消息时,设置消息的Headers属性键值对。 Headers Exchange将根据消息Headers属性键值对和绑定属性键值对的匹配情况路由消息。 匹配算法由一个特殊的绑定属性键值对控制。该属性为x-match,只有以下两种取值:
使用场景:Headers Exchange适用于通过多组Headers属性区分消息的场景。Headers Exchange常用于多播路由。例如,涉及到分类或者标签的新闻更新。
-
all:所有除x-match以外的绑定属性键值对必须和消息Headers属性键值对匹配才会路由消息。
-
any:只要有一组除x-match以外的绑定属性键值对和消息Headers属性键值对匹配就会路由消息。
以下两种情况下,认为消息Headers属性键值对和绑定属性键值对匹配:
-
消息Headers属性的键和值与绑定属性的键和值完全相同;
-
消息Headers属性的键和绑定属性的键完全相同,但绑定属性的值为空。
-
扇型交换机、主题交换机异同:
对于扇型交换机路由键是没有意义的,只要有消息,它都发送到它绑定的所有队列上,对于主题交换机,路由规则由路由键决定,只有满足路由键的规则,消息才可以路由到对应的队列上
通配的绑定键是跟队列进行绑定的,举个小例子 队列Q1 绑定键为.TT. 队列Q2绑定键为 TT.# 如果一条消息携带的路由键为 A.TT.B,那么队列Q1将会收到; 如果一条消息携带的路由键为TT.AA.BB,那么队列Q2将会收到;
插件运用
-
rabbitmq_management
-
rabbitmq_tracing
-
rabbitmq_federation
-
rabbitmq_consistent_hash_exchange
-
rabbitmq_sharding
如何保证消息不丢失
数据的丢失问题,可能出现在生产者、MQ、消费者中
-
生产者弄丢了数据
生产者将数据发送到 RabbitMQ 时,可能数据在半路丢了,因为网络问题啥的,都有可能。
可以选择用 RabbitMQ 提供的事务功能,就是生产者发送数据之前开启 RabbitMQ 事务channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit。
// 开启事务 channel.txSelect try { // 这里发送消息 } catch (Exception e) { channel.txRollback // 这里再次重发这条消息 } // 提交事务 channel.txCommit
问题是,RabbitMQ 事务机制(同步,不推荐),会导致吞吐量下来,因为太耗性能。
所以,如果要确保说写 RabbitMQ 的消息别丢,可以开启 confirm 模式(异步,推荐),在生产者设置开启 confirm 模式之后,每次写的消息都会分配一个唯一的 id,如果写入了 RabbitMQ 中,RabbitMQ 会回传一个 ack 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 nack 接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。
事务机制和 confirm 机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是 confirm 机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息 RabbitMQ 接收了之后会异步回调你的一个接口通知你这个消息接收到了。
一般在生产者避免数据丢失,都是用 confirm 机制的。
-
RabbitMQ 弄丢了数据
RabbitMQ 自己弄丢了数据,这个你必须开启 RabbitMQ 的持久化,就是消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。如果RabbitMQ 还没持久化就挂了,可能导致少量数据丢失,但是这个概率较小。
设置持久化有两个步骤:
(1)创建 queue 的时候将其设置为持久化。这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。
(2)发送消息的时候将消息的 deliveryMode 设置为 2。就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。
必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。
注意,即使RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,此时 RabbitMQ 挂了,就会导致内存里的一点点数据丢失。
所以,持久化可以跟生产者那边的 confirm 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 ack 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 ack,你也是可以自己重发的。
-
消费端弄丢了数据
RabbitMQ 如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了,RabbitMQ 认为你都消费了,这数据就丢了。
这时得用 RabbitMQ 提供的 ack 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 ack,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 ack 一把。这样的话,如果你还没处理完,不就没有 ack 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
总结:
如何保证消息顺序性
我举个例子,我们以前做过一个 mysql binlog 同步的系统,压力还是非常大的,日同步数据要达到上亿,就是说数据从一个 mysql 库原封不动地同步到另一个 mysql 库里面去(mysql -> mysql)。常见的一点在于说比如大数据 team,就需要同步一个 mysql 库过来,对公司的业务系统的数据做各种复杂的操作。
你在 mysql 里增删改一条数据,对应出来了增删改 3 条 binlog 日志,接着这三条 binlog 发送到 MQ 里面,再消费出来依次执行,起码得保证人家是按照顺序来的吧?不然本来是:增加、修改、删除;你楞是换了顺序给执行成删除、修改、增加,不全错了么。
本来这个数据同步过来,应该最后这个数据被删除了;结果你搞错了这个顺序,最后这个数据保留下来了,数据同步就出错了。
先看看顺序会错乱的俩场景:
一个 queue,多个 consumer。比如,生产者向 RabbitMQ 发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。三个消费者分别从 MQ 中消费三条数据中的一条,结果消费者2先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了。
解决方案:
-
拆分多个 queue(消息队列),每个 queue(消息队列) 一个 consumer(消费者),就是多一些 queue (消息队列)而已,确实是麻烦点;
-
或者就一个 queue (消息队列)但是对应一个 consumer(消费者),然后这个 consumer(消费者)内部用内存队列做排队,然后分发给底层不同的 worker 来处理。
如何保证消息可靠性
如何确保生产者成功把消息发送到队列:
生产者发送到交换机时出错,confirmCallback模式:
-
发送放可以根据confrim机制来确保是否消息已经发送到交换机
-
confirm机制能保证消息发送到交换机有回调,不能保证消息转发到queue有回调
交换机转发消息给队列时出错,returnCallback模式:
-
returncallback模式,需要手动设置开启
-
该模式 指定 在路由的时候发送错误的时候调用回调函数,不影响消息发送到交换机
如何确保消费者消息消费的可靠性:
-
ACK机制:
自动确认: acknowledge=“none” (默认的,不配置)
手动确认: acknowledge=“manual”。两种方式:签收消息、拒绝消息 批量处理/单个处理
根据异常情况来确认(暂时不怎么用): acknowledge=“auto”
-
自动确认
消费者一旦接收到消息,则自动确认收到,并将消息从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。
-
手动确认
改为手动确认,则需要在业务处理成功后,调用 channel.basicAck() 手动签收,如果出现了异常,则调用 channel.basicNack() 等方法,让其按照业务功能进行处理,比如:重新发送,比如拒绝签收进入死信队列等等。(拒绝签收一般不使用,如果出现异常拒绝签收,下次消费还是异常形成死循环,可以让出现异常的消息重新进入到另一个队列中(死信队列),将来通过业务再次消费处理)
@RabbitListener 和 @RabbitHandler 搭配使用
-
@RabbitListener 可以标注在类上面,需配合 @RabbitHandler 注解一起使用
-
@RabbitListener 标注在类上面表示当有收到消息的时候,就交给 @RabbitHandler 的方法处理,具体使用哪个方法处理,根据 MessageConverter 转换后的参数类型
如何保证高可用性
RabbitMQ是基于主从(非分布式)做高可用性的,以 RabbitMQ 为例讲解第一种 MQ 的高可用性怎么实现。RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。
-
单机模式
单机模式,就是 Demo 级别的,一般就是你本地启动了玩玩的,没人生产用单机模式。
-
普通集群模式(无高可用性)
普通集群模式,就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。
这种方式确实很麻烦,没做到所谓的分布式。因为这导致你要么消费者每次随机连接一个实例然后拉取数据,要么固定连接那个 queue 所在实例消费数据,前者有数据拉取的开销,后者导致单实例性能瓶颈。
而且如果那个放 queue 的实例宕机了,会导致接下来其他实例就无法从那个实例拉取,如果你开启了消息持久化,让 RabbitMQ 落地存储消息的话,消息不一定会丢,得等这个实例恢复了,然后才可以继续从这个 queue 拉取数据。
这就没有什么高可用性,这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个 queue 的读写操作。
-
镜像集群模式(高可用性)
这种模式才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。
那么如何开启这个镜像集群模式呢?其实很简单,RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!第二,这么玩儿,不是分布式的,就没有扩展性可言了,如果某个 queue 负载很重,你加机器,新增的机器也包含了这个 queue 的所有数据,并没有办法线性扩展你的 queue。你想,如果这个 queue 的数据量很大,大到这个机器上的容量无法容纳了,此时该怎么办呢?
幂等性问题
幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果。
解决方案:
-
对数据的设计进行改造
-
使用乐观锁,在表中加入版本号version
消息重复消费问题
RabbitMQ提供了消息去重的机制来解决消息重复消费的问题。可使用以下两种方式实现:
-
消息去重插件
RabbitMQ提供了一个消息去重插件,可以通过在RabbitMQ节点上安装该插件来实现消息去重。该插件会在消息传输之前对消息进行唯一性校验,如果消息已经被消费过,那么该消息将被丢弃。该插件的实现原理是将已经消费过的消息ID保存在内存中,当新消息到达时,会检查该消息ID是否已经存在,如果存在则丢弃该消息。
-
消息幂等性设计
消息幂等性是指对于同一条消息,无论消费多少次,最终的结果都是一致的。因此,可以通过在消息的生产者或消费者端实现消息幂等性来解决消息重复消费的问题。具体实现方式包括:
-
在消息生产者端,为每条消息生成唯一的ID,将该ID与消息一起发送到RabbitMQ,消费者在消费消息时根据该ID进行幂等性校验;
-
在消息消费者端,记录已经消费过的消息ID,当重复消费同一条消息时,直接忽略该消息。
-
需要注意的是,实现消息幂等性需要考虑业务逻辑的复杂性和消息处理的性能。如果业务逻辑比较简单,可以通过对消息进行去重来解决问题;如果业务逻辑比较复杂,可以通过实现消息幂等性来保证消息的正确性。
我们当时消费者是设置了自动确认机制,当服务还没来得及给MQ确认的时候,服务宕机了,导致服务重启之后,又消费了一次消息。这样就重复消费了
因为我们当时处理的支付(订单|业务唯一标识),它有一个业务的唯一标识,我们再处理消息时,先到数据库查询一下,这个数据是否存在,如果不存在,说明没有处理过,这个时候就可以正常处理这个消息了。如果已经存在这个数据了,就说明消息重复消费了,我们就不需要再消费了
消息堆积问题
RabbitMQ消息积压问题通常是由于消费者无法及时消费消息或消费速度过慢或发送者流量太大导致的。产生堆积的情况:
-
生产者突然大量发布消息
-
消费者消费失败
-
消费者宕机
-
消费者性能出现瓶颈
解决方法:
-
增加消费者数量:可以通过增加消费者的数量来提高消费速度,减少消息积压。可以通过添加更多的消费者进程或者增加消费者的线程数来实现。
-
调整消费者的QoS参数:消费者的QoS参数可以控制消费者每次从RabbitMQ服务器获取的消息数量,以及未确认消息的最大数量。可以适当调整这些参数,以减少消息积压。
-
设置消费者的超时时间:可以设置消费者的超时时间,如果消费者在指定的时间内没有消费消息,就将消息重新投递到队列中,以便其他消费者消费。
-
使用死信队列:可以将未能及时消费的消息转移到死信队列中,以便后续处理。可以设置死信队列的超时时间,以便在一定时间内处理这些消息。
-
监控和调整:可以使用RabbitMQ的监控工具来监控队列的状态和消费者的消费速度,及时发现并解决消息积压问题。
-
增加队列的容量:可以增加队列的容量,以便存储更多的消息。但是,如果队列容量过大,可能会导致内存占用过高,影响系统的性能。可以使用RabbitMQ惰性队列,惰性队列的好处主要是:
① 接收到消息后直接存入磁盘而非内存
② 消费者要消费消息时才会从磁盘中读取并加载到内存
③ 支持数百万条的消息存储
-
使用工作队列模式, 设置多个消费者消费同一个队列中的消息
出现丢数据怎么解决?
可以采用仲裁队列,与镜像队列一样,都是主从模式,支持主从数据同步,主从同步基于Raft协议,强一致。并且使用起来也非常简单,不需要额外的配置,在声明队列的时候只要指定这个是仲裁队列即可
如何解决消息队列的延时以及过期失效问题?
RabbtiMQ 可以设置过期时间,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上 12 点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。
为何选择RabbitMQ
-
ActiveMQ,性能不是很好,因此在高并发的场景下,直接被pass掉了。它的Api很完善,在中小型互联网公司可以去使用。
-
kafka,主要强调高性能,如果对业务需要可靠性消息的投递的时候。那么就不能够选择kafka了。但是如果做一些日志收集呢,kafka还是很好的。因为kafka的性能是十分好的。
-
RocketMQ,它的特点非常好。它高性能、满足可靠性、分布式事物、支持水平扩展、上亿级别的消息堆积、主从之间的切换等等。MQ的所有优点它基本都满足。但是它最大的缺点:商业版收费。因此它有许多功能是不对外提供的。
Kafka
Kafka是一个分布式的基于发布/订阅模式的消息队列(Message Queue),功能非常单一,就是消息的接收与发送,不适合应用于许多场景,主要应用于大数据实时处理领域,使用它进行用户行为日志的采集和计算,来实现比如“猜你喜欢”的功能。
kafka的吞吐量被公认为中间件中的翘楚,单机可以支持十几万的并发,相当强悍。在高可用上同样支持分布式集群部署。
在消息可靠性上,如果保证异步的性能,可能会出现消息丢失的情况,因为它保存消息时是先存到磁盘缓冲区的,如果机器出现故障,缓冲区的数据是可能丢失的。
应用场景
-
缓冲/消峰: 有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况
-
解耦:允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
-
异步通信:允许用户把一个消息放入队列,但并不立即处理它,然后再需要的时候再去处理它们。
目录结构分析
- bin:Kafka的所有执行脚本。例如:启动Kafka服务器、创建Topic、生产者、消费者程序等
- config:Kafka的所有配置文件
- libs: 运行Kafka所需要的所有JAR包
- logs: Kafka的所有日志文件,如果Kafka出现一些问题,需要到该目录中去查看异常信息
- site-docs: Kafka的网站帮助文件
配置参数
Kafka服务器配置参数是确保Kafka集群正常运行和优化性能的关键。 这些参数包括broker的身份标识、监听地址、线程数、I/O缓冲、消息大小限制等。
broker.id =0
// 每个broker在集群中的唯一标识,要求是正整数。当该服务器IP地址改变时,只要broker.id没有变化,则不会影响consumers的消息情况
log.dirs=/data/kafka-logs
// kafka数据的存放地址,多个地址的话用逗号分割 /data/kafka-logs-1,/data/kafka-logs-2
port =9092
// broker server服务端口,默认9092
message.max.bytes =6525000
// broker能接收的消息体的最大大小(单条),单位是字节,默认1M。
num.network.threads =4
// broker处理消息的最大线程数,一般情况下不需要去修改,默认值为2。
num.io.threads =8
// broker处理磁盘IO的线程数,数值应该大于你的硬盘数,默认值为8。
background.threads =4
// 一些后台任务处理的线程数,例如过期消息文件的删除等,一般情况下不需要去做修改
queued.max.requests =500
// 等待IO线程处理的请求队列最大数,若是等待IO的请求超过这个数值,那么会停止接受外部消息,应该是一种自我保护机制。
host.name
// broker的主机地址,若是设置了,那么会绑定到这个地址上,若是没有,会绑定到所有的接口上,并将其中之一发送到ZK,一般不设置
socket.send.buffer.bytes=100*1024
// 指定发送数据的socket的缓冲区大小,socket的调优参数SO_SNDBUFF,默认值为102400。
socket.receive.buffer.bytes =100*1024
// 指定接收数据的socket的缓冲区大小,socket的调优参数SO_RCVBUFF,默认值为102400。
socket.request.max.bytes =100*1024*1024
// socket请求的最大数值,防止serverOOM,message.max.bytes必然要小于socket.request.max.bytes,会被topic创建时的指定参数覆盖
log.segment.bytes =1024*1024*1024
// topic的分区是以一堆segment文件存储的,此参数控制每个segment的大小,会被topic创建时的指定参数覆盖。默认值为1073741824。当日志文件达到一定大小时,开辟新的文件来存储(分片存储)
log.roll.hours =24*7
// 这个参数会在日志segment没有达到log.segment.bytes设置的大小,也会强制新建一个segment会被 topic创建时的指定参数覆盖
log.cleanup.policy = delete
// 日志清理策略选择有:delete和compact主要针对过期数据的处理,或是日志文件达到限制的额度,会被 topic创建时的指定参数覆盖
log.retention.minutes=60*24 # 一天后删除
// 数据存储的最大时间超过这个时间会根据log.cleanup.policy设置的策略处理数据,也就是消费端能够多久去消费数据
// log.retention.bytes和log.retention.minutes任意一个达到要求,都会执行删除,会被topic创建时的指定参数覆盖
log.retention.bytes=-1
// topic每个分区的最大文件大小,一个topic的大小限制 = 分区数*log.retention.bytes。-1没有大小限log.retention.bytes和log.retention.minutes任意一个达到要求,都会执行删除,会被topic创建时的指定参数覆盖
log.retention.check.interval.ms=5minutes
// 文件大小检查的周期时间,是否处罚 log.cleanup.policy中设置的策略
log.retention.hours=
// 日志文件保留时间,超时即删除
log.retention.bytes=
// 日志文件最大大小
log.cleaner.enable=false
// 是否开启日志压缩
log.cleaner.threads = 2
// 日志压缩运行的线程数
log.cleaner.io.max.bytes.per.second=None
// 日志压缩时候处理的最大大小
log.cleaner.dedupe.buffer.size=500*1024*1024
// 日志压缩去重时候的缓存空间,在空间允许的情况下,越大越好
log.cleaner.io.buffer.size=512*1024
// 日志清理时候用到的IO块大小一般不需要修改
log.cleaner.io.buffer.load.factor =0.9
// 日志清理中hash表的扩大因子一般不需要修改
log.cleaner.backoff.ms =15000
// 检查是否处罚日志清理的间隔
log.cleaner.min.cleanable.ratio=0.5
// 日志清理的频率控制,越大意味着更高效的清理,同时会存在一些空间上的浪费,会被topic创建时的指定参数覆盖
log.cleaner.delete.retention.ms =1day
// 对于压缩的日志保留的最长时间,也是客户端消费消息的最长时间,同log.retention.minutes的区别在于一个控制未压缩数据,一个控制压缩后的数据。会被topic创建时的指定参数覆盖
log.index.size.max.bytes =10*1024*1024
// 对于segment日志的索引文件大小限制,会被topic创建时的指定参数覆盖
log.index.interval.bytes =4096
// 当执行一个fetch操作后,需要一定的空间来扫描最近的offset大小,设置越大,代表扫描速度越快,但是也更好内存,一般情况下不需要搭理这个参数
log.flush.interval.messages=None
// log文件”sync”到磁盘之前累积的消息条数,因为磁盘IO操作是一个慢操作,但又是一个”数据可靠性"的必要手段,所以此参数的设置,需要在"数据可靠性"与"性能"之间做必要的权衡.如果此值过大,将会导致每次"fsync"的时间较长(IO阻塞),如果此值过小,将会导致"fsync"的次数较多,这也意味着整体的client请求有一定的延迟.物理server故障,将会导致没有fsync的消息丢失.
log.flush.scheduler.interval.ms =3000
// 检查是否需要固化到硬盘的时间间隔
log.flush.interval.ms = None
// 仅仅通过interval来控制消息的磁盘写入时机,是不足的.此参数用于控制"fsync"的时间间隔,如果消息量始终没有达到阀值,但是离上一次磁盘同步的时间间隔达到阀值,也将触发.
log.delete.delay.ms =60000
// 文件在索引中清除后保留的时间一般不需要去修改
log.flush.offset.checkpoint.interval.ms =60000
// 控制上次固化硬盘的时间点,以便于数据恢复一般不需要去修改
auto.create.topics.enable =true
// 是否允许自动创建topic,若是false,就需要通过命令创建topic
default.replication.factor =1
// 是否允许自动创建topic,若是false,就需要通过命令创建topic
num.partitions =1
// 创建topic的时候自动创建多少个分区。可以在创建topic时手动指定,没有指定的话会被topic创建时的指定参数覆盖
num.recovery.threads.per.data.dir:
// 用来读取日志文件的线程数量,对应每一个log.dirs,若此参数为2,log.dirs 为2个目录,那么就会有4个线程来读取
优缺点
优点:
- 基于磁盘的数据存储
- 高伸缩性
- 高性能
- 应用场景 : 收集指标和日志 提交日志 流处理
缺点:
- 运维难度大
- 偶尔有数据混乱的情况
- 多副本模式下对带宽有一定要求
- 由于是批量发送,数据并非真正的实时;
- 对于mqtt协议不支持;
- 不支持物联网传感数据直接接入;
- 仅支持统一分区内消息有序,无法实现全局消息有序;
- 监控不完善,需要安装插件;
- 依赖zookeeper进行元数据管理;3.0版本去除
工作流程
- ⽣产者从Kafka集群获取分区leader信息
- ⽣产者将消息发送给leader
- leader将消息写入本地磁盘
- follower从leader拉取消息数据
- follower将消息写入本地磁盘后向leader发送ACK
- leader收到所有的follower的ACK之后向生产者发送ACK
架构
1、为方便扩展,并提高吞吐量,一个topic分为多个partition
2、配合分区的设计,提出消费者组的概念,组内每个消费者并行消费
3、为提高可用性,为每个partition增加若干副本,类似NameNode HA
4、ZK中记录谁是leader,Kafka2.8.0 以后也可以配置不采用ZK.
-
Broker
一台kafka服务器就是一个broker。一个集群由多个broker组成。一个broker可以有多个topic。broker是无状态(Sateless)的,它们是通过ZooKeeper来维护集群状态。一个Kafka的broker每秒可以处理数十万次读写,每个broker都可以处理TB消息而不影响性能。
-
Producer
消息生产者,向kafka broker发送消息的客户端。
-
Consumer
消息消费者,向kafka broker读取消息的客户端。
-
Topic
主题,生产者和消费者通过此进行对接。每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上,但用户只需指定消息的Topic即可生产或消费数据,而不必关心数据存于何处)。Kafka中的主题必须要有标识符,而且是唯一的,Kafka中可以有任意数量的主题,没有数量上的限制。一旦生产者发送消息到主题中,这些消息就不能被更新(更改)。
-
Consumer Group (CG)
消费者组,若干个Consumer组成的集合。一个消费者组有一个唯一的ID(group Id)。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
这是kafka用来实现一个topic消息的广播(发给所有consumer)和单播(发给任意一个consumer)的手段。一个topic可以有多个CG。topic的消息会复制(不是真的复制,概念上的)到所有的CG,但每个CG只会把消息发给该CG中的一个consumer。如果需要实现广播,只要每个consumer有一个独立的CG就可以了。要实现单播只要所有的consumer在同一个CG。用CG还可以将consumer进行自由的分组而不需要多次发送消息到不同的topic。可为每个Consumer指定group name,若不指定group name则属于默认的group。
-
Partition
分区,为了实现扩展性,一个topic可以分布在多个broker上,每个topic包含一个或多个Partition,每个partition都是一个有序的队列。partition中的每条消息都会被分配一个有序的id(offset)。kafka只保证同一个partition中的消息顺序,不保证一个topic的整体(多个partition之间)的顺序。生产者和消费者使用时可以指定topic中的具体partition。
-
副本
在kafka中,每个主题可以有多个分区,每个分区又可以有多个副本。多个副本中,只有一个是leader,其他的都是follower。仅有leader副本可以对外提供服务。多个follower通常存放在和leader不同的broker中。通过这样的机制实现了高可用,当某台机器挂掉后,其他follower也能迅速”转正“,开始对外提供服务。副本可以确保某个服务器出现故障时,确保数据依然可用,在Kafka中,一般都会设计副本的个数>1。
Leader:每个分区多个副本的 “主”,生产者发送数据的对象,以及消费者消费数据的对象都是Leader。
Follower:每个分区多个副本中的 “从”,实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发生故障时,某个Follower会成为新的 Leader。
-
offset
消费偏移量,是kafka用来确定消息是否被消费过的标识,topic中的每个分区都是有序且顺序不可变的记录集,并且不断地追加到结构化的log文件。在一个分区中,消息是有序的存储着,每个在分区的消费都有一个递增的id,这个就是偏移量offset。offset记录着下一条将要发送给Consumer的消息的序号。可以设置为“自动提交”与“手动提交”。默认Kafka将offset存储在ZooKeeper中。偏移量在分区中才是有意义的,在分区之间,offset是没有任何意义的。
-
zookeeper
ZK用来管理和协调broker,并且存储了Kafka的元数据(例如:有多少topic、partition、consumer)。
ZK服务主要用于通知生产者和消费者Kafka集群中有新的broker加入、或者Kafka集群中出现故障的broker。
Kafka正在逐步想办法将ZooKeeper剥离,维护两套集群成本较高,社区提出KIP-500就是要替换掉ZooKeeper的依赖。“Kafka on Kafka”——Kafka自己来管理自己的元数据
考虑到性能,kafka可以采用打包方式发送消息,也就是说,传统方式是逐条发送消息, 现在可以先把需要发送的消息缓存在客户端,到达一定数值时再一起打包发送, 还可以对发送的数据进行压缩处理,减少在数据传输时的开销
生产者
消息发送原理
在消息发送的过程中,涉及到了两个线程 — main 线程和Sender线程。在main线程中创建了一个双端队列 RecordAccumulator。main线程将消息发送给ResordAccumlator,Sender线程不断从 RecordAccumulator 中拉去消息发送到 Kafka Broker。
生产者重要参数
-
acks
至少要多少个分区副本接收到了消息返回确认消息,一般是:
0:只要消息发送出去了就确认(不管是否失败)
1:只要 有一个broker接收到了消息 就返回
all:所有集群副本都接收到了消息确认
当然 2 3 4 5 这种数字都可以, 就是具体多少台机器接收到了消息返回, 但是一般这种情况很少用到
-
buffer.memory
生产者缓存在本地的消息大小 : 如果生产者生产消息速度过快,快过了往 broker发送消息的速度,就会出现buffer.memory不足的问题,默认值为32M,注意,单位是byte,大概3355000左右
-
max.block.ms
生产者获取kafka元数据(集群数据,服务器数据等) 等待时间 : 当因网络原因导致客户端与服务器通讯时等待的时间超过此值时 会抛出一个TimeOutExctption 默认值为 60000ms
-
retries
设置生产者生产消息失败后重试的次数 默认值 3次
-
retry.backoff.ms
设置生产者每次重试的间隔 默认 100ms
-
batch.size
生产者批次发送消息的大小 默认16k 注意单位还是byte
-
linger.ms
生产者生产消息后等待多少毫秒发送到broker 与batch.size 谁先到达就根据谁 默认值为0
-
compression.type
kafka在压缩数据时使用的压缩算法 可选参数有:none、gzip、snappy。
none即不压缩
gzip、snappy两者比较,gzip压缩率比较高,系统cpu占用比较大,但好处是网络带宽占用少; snappy压缩比没有gzip高,cpu占用率不是很高,性能也还行, 如果网络带宽比较紧张的话,可以选择gzip,一般推荐snappy
-
client.id
一个标识, 可以用来标识消息来自哪, 不影响kafka消息生产
-
max.in.flight.requests.per.connection
指定kafka一次发送请求在得到服务器回应之前,可发送的消息数量
异步发送API
1、普通异步发送
(1)需求:创建Kafka生产者,采用异步的方式发送到Kafka Broker。
(2)代码编程go get github.com/Shopify/sarama
func main() {
config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForAll // 发送完数据需要leader和follow都确认
config.Producer.Partitioner = sarama.NewRandomPartitioner // 新选出一个partition
config.Producer.Return.Successes = true // 成功交付的消息将在success channel返回
// 构造一个消息
msg := &sarama.ProducerMessage{}
msg.Topic = "first"
msg.Value = sarama.StringEncoder("this is a test log")
// 连接kafka
client, err := sarama.NewSyncProducer([]string{
"192.168.71.128:9092", "192.168.71.129:9092", "192.168.71.130:9092",
}, config)
if err != nil {
fmt.Println("producer closed, err:", err)
return
} else {
fmt.Println(client)
}
defer client.Close()
// 发送消息
pid, offset, err := client.SendMessage(msg)
if err != nil {
fmt.Println("send msg failed, err:", err)
return
}
fmt.Printf("pid:%v offset:%v\n", pid, offset)
}
2、带回调函数的异步发送
【注意:】消息发送失败会自动重试,不需要我们在回调函数中手动重试。
同步发送API
分区策略(面试重点)
生产者写入消息到topic,Kafka将依据不同的策略将数据分配到不同的分区中
-
轮询分区策略
默认的策略,也是使用最多的策略,它能最大限度保证所有消息都平均分配到每一个分区。除非有特殊的业务需求,否则使用这种方式即可。所谓轮询策略,即按顺序轮流将每条数据分配到每个分区中。
-
随机分区策略(不用)
随机策略,每次都随机地将消息分配到每个分区。其实大概就是先得出分区的数量,然后每次获取一个随机数,用该随机数确定消息发送到哪个分区。在较早的版本,默认的分区策略就是随机策略,使用随机策略也是为了更好得将消息均衡地写入到每个分区。但后续轮询策略表现更佳,所以后来的默认策略就是轮询策略了。
-
按key分区分配策略
当生产者发送数据时,可以指定一个key,计算这个key的hashCode值,按照hashCode的值对不同消息进行存储。
kafka默认实现了两个策略,没指定key时就是轮询策略,有key就按key分配策略。
上面有说到一个场景,那就是要顺序发送消息到kafka。前面提到的方案是让所有数据存储到一个分区中,但其实更好的做法,就是使用这种按键保存策略。
让需要顺序存储的数据都指定相同的键,而不需要顺序存储的数据指定不同的键,这样既实现了顺序存储的需求,又能够享受到kafka多分区的优势。
按key分配策略,可能会出现「数据倾斜」,例如:某个key包含了大量的数据,因为key值一样,所以全部的数据将都分配到一个分区中,造成该分区的消息数量远大于其他的分区。
-
黏性分区策略
如果使用默认的轮询策略,可能会造成一个大的batch被轮询成多个小的batch的情况。因此,kafka2.4推出一种新的分区策略,即StickyPartitioning Strategy,此策略会随机地选择另一个分区并会尽可能地坚持使用该分区——即所谓的粘住这个分区。
鉴于小batch可能导致延时增加,之前对于无Key消息的分区策略效率很低。社区于2.4版本引入了黏性分区策略(StickyPartitioning Strategy)。该策略是一种全新的策略,能够显著地降低给消息指定分区过程中的延时。使用StickyPartitioner有助于改进消息批处理,减少延迟,并减少broker的负载。
-
自定义分区策略:写入的时候可以指定需要写入的partition,如果有指定,则写入对应的partition。实现partitioner接口
乱序问题:
轮询策略、随机策略都会导致一个问题,生产到Kafka中的数据是乱序存储的。而按key分区可以一定程度上实现数据有序存储——也就是局部有序,但这又可能会导致数据倾斜,所以在实际生产环境中要结合实际情况来做取舍。
生产者副本机制(面试重点)
副本的目的就是冗余备份,当某个Broker上的分区数据丢失时,依然可以保障数据可用。因为在其他的Broker上的副本是可用的。
ACKs参数
对副本关系较大的就是producer配置的acks参数了,acks参数表示当生产者生产消息的时候,写入到副本的要求严格程度。它决定了生产者如何在性能和可靠性之间做取舍。
-
acks配置为0
生产者往集群发送数据不需要等到集群的返回,不确保消息发送成功。安全性最低,但是效率最高。
-
acks配置为1
生产者会等待leader副本确认接收后,才会发送下一条数据,性能中等。
-
acks配置为-1或者all
生产者往集群发送数据需要所有的follower都完成从leader的同步才会发送下一条,确保 leader发送成功和所有的副本都完成备份。安全性最高,但是效率最低。
如果向不存在的topic写数据,kafka会⾃动创建topic,partition和replication的数量默认都是1。
生产者幂等性
幂等性
拿http举例来说,一次或多次请求,得到地响应是一致的(网络超时等问题除外),换句话说,就是执行多次操作与执行一次操作的影响是一样的。
如果,某个系统是不具备幂等性的,如果用户重复提交了某个表格,就可能会造成不良影响。例如:用户在浏览器上点击了多次提交订单按钮,会在后台生成多个一模一样的订单。
生产者幂等性
在生产者生产消息时,如果出现retry时,有可能会一条消息被发送了多次,如果Kafka不具备幂等性的,就有可能会在partition中保存多条一模一样的消息。
幂等性原理
为了实现生产者的幂等性,Kafka引入了 Producer ID(PID)和 Sequence Number的概念。
- PID:每个Producer在初始化时,都会分配一个唯一的PID,这个PID对用户来说是透明的。
- Sequence Number:针对每个生产者(对应PID)发送到指定主题分区的消息都对应一个从0开始递增的Sequence Number。
- 幂等性只能保证的是在单分区单会话内不重复
消费者
工作流程
消费者重要参数
参数 | 描述 |
bootstrap.servers | 生产者向Kafka集群建立初始连接用到的broker地址列表。用于定位集群。格式为host1:port1,host2:port2 |
group.id | 消费者群组的标识符,消费者会加入该群组来读取消息。 Kafka消费者应用程序通常需要指定GroupId,以便Kafka Broker知道它们属于哪个消费者组。 |
client.id | 这个属性可以是任意字符串,broker用它来标识从客户端发送过来的请求,比如获取请求。它通常被用在日志、指标和配额中。 |
group.instance.id | 这个属性可以是任意具有唯一性的字符串,被用于消费者群组的固定名称。 |
enable.auto.commit | 消费者是否自动提交消费者的offset到Kafka。默认值为 true 为了尽量避免出现重复数据和数据丢失,可以把它设为 false。如果设置为 false,则消费者不会自动提交消费的偏移量,您需要手动调用 commit() 方法来提交偏移量。 注意,即使设置了 enable.auto.commit=false,Kafka 仍然会在每次 poll() 方法的调用中记录偏移量。但是,这些偏移量记录不会立即被提交,而是在您调用 commit() 方法时一起提交。 |
auto.commit.interval.ms | 如果enable.auto.commit 为true,则表示自动提交offset的频率。默认 5s |
auto.offset.reset | 当 Kafka 中没有初始偏移量或当前偏移量在服务器中不存在(如数据被删除了),从哪个位置开始读取数据,可选值有:
如果消费者组在同一主题上已经存在偏移量记录,那么无论auto.offset.reset 设置为何值,每次启动都会从已有的最新偏移量开始接着往后消费。这意味着,auto.offset.reset 参数主要影响的是新建消费者组或在没有有效偏移量记录的情况下的行为。 在实际应用中,选择合适的auto.offset.reset 值需要根据具体需求来决定。例如,如果需要从头开始重新处理旧数据,可以选择earliest;如果只关心最新消息,可以选择latest;如果需要严格控制从何处开始消费,可以选择none并通过其他方式手动管理偏移量。 auto.offset.reset.strategy: 使用一个自定义策略决定如何重置偏移量。 |
session.timeout.ms | 用于定义消费者与Kafka集群之间的会话超时时间。默认10s 如果消费者在这个超时时间内未发送心跳到服务器,服务器将认为消费者不再活跃,并将其从消费者组中移除。消费者移除后,集群会进行rebalance,rebalance期间消费者组无法进行消费。 |
heartbeat.interval.ms | 消费者往kafka服务器发送心跳的间隔,默认 3s。 如果不定期发送心跳,Kafka会认为消费者已经死亡并触发重新平衡。 |
key.deserializer和value.deserializer | 指定了Kafka消息的key和value的反序列化方式。一定要写全类名。 默认值org.apache.kafka.common.serialization.ByteArraySerialize。 这个接口表示类将会采用何种方式序列化,它的作用是把对象转换为字节。实现了Serializer接口的类主要有ByteArraySerializer、StringSerializer、IntegerSerializer。这三种序列化器的主要区别在于处理的数据类型不同,具体使用哪种序列化器取决于你需要序列化的数据类型。 |
offsets.topic.num.partitions | __consumer_offsets 的分区数,默认是 50 个分区。 |
fetch.min.bytes | 指定了消费者从服务器获取记录的最小字节数。broker 在收到消费者的数据请求时,如果可用的数据量小于 fetch.min.bytes 指定的大小,会等到有足够的可用数据时才把它返回给消费者。这样可以降低消费者和 broker 的工作负载,因为它们在主题不是很活跃的时候(或者一天里的低谷时段)就不需要来来回回地处理消息。如果没有很多可用数据,但消费者的 CPU 使用率却很高,就需要把该属性的值设得比默认值大。如果消费者的数量比较多,把该属性的值设置得大一点可以降低 broker 的工作负载。 默认值为1 byte。 1、适当调整fetch.min.bytes的值可以提高吞吐量,但也会造成额外的延迟。对于写入量不高的主题,这个参数可以减少broker和消费者的压力。对于有大量消费者的主题,则可以明显减轻broker压力。 |
fetch.max.bytes | 指定了消费者从服务器获取一批消息的最大字节数。如果服务器端一批次的数据大于该值,消费者可能需要执行多次拉取操作才能获取所有消息。因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes (broker config)或 max.message.bytes (topic config)影响。默认为 52428800(50 MB)。 |
fetch.max.wait.ms | 用于指定 broker 的等待时间,默认是如果没有足够的数据流入Kafka,消费者获取最小数据量的要求就得不到满足,最终导致 500ms 的延迟。如果 fetch.max.wait.ms 被设为 100ms,并且 fetch.min.bytes 被设为 1MB,那么 Kafka 在收到消费者的请求后,要么返回 1MB 数据,要么在 100ms 后返回所有可用的数据,就看哪个条件先得到满足。 默认值为500ms。 这个参数的设定和 Consumer 与 Kafka 之间的延迟也有关系,如果业务应用对延迟敏感,那么可以适当调小这个参数。 |
max.poll.interval.ms | 消费者从 broker 拉取数据的最长等待时间,默认是 5 分钟。这个参数可以防止消费者在拉取数据时因为网络问题或其他原因导致的长时间等待。超过该值,该消费者被移除,消费者组执行再平衡。 具体来说,max.poll.interval.ms 参数定义了消费者在连续两次拉取数据之间的最大时间间隔。如果在这个时间内,消费者没有从 broker 拉取到数据,那么就会触发一个超时错误。 |
max.poll.records | 单次调用 poll() 方法返回消息的最大条数,默认是 500 条。 注意,max.poll.records 和 max.poll.interval.ms 是相互关联的。如果 max.poll.records 参数设置得较大,那么 max.poll.interval.ms 参数应该设置得相对较小,以确保消费者能够及时处理数据。反之,如果 max.poll.records 参数设置得较小,那么 max.poll.interval.ms 参数可以设置得相对较大。 |
max.partition.fetch.bytes | 指定了服务器从每个分区返回给消费者的最大字节数。默认值为1048576(1MB)。这意味着,KafkaConsumer.poll()方法从每个分区里返回的记录最多不超过max.partition.fetch.bytes指定的字节。 这个属性必须比broker能够接收的最大消息的字节数(max.message.size)大,否则消费者可能无法读取消息,导致消费者一直挂起重试。 这个参数与 fetch.max.bytes 相似,只不过前者用来限制一次拉取中每个分区的消息大小,而后者用来限制一次拉取中整体消息的大小 |
ssl.keystore.key | key store 文件中私钥的密码。这对于客户端来说是可选的。 |
ssl.keystore.location | key store 文件的位置。这对于客户端来说是可选的,可用于客户端的双向身份验证。 |
ssl.keystore.password | key store 文件的密码。这对于客户端是可选的,只有配置了 ssl.keystore.location 才需要配置该选项。 |
ssl.truststore.type | 用于设置自动提交偏移量的时间间隔。默认值为 5 s。 在 Kafka 中,消费者通过拉取数据并处理,然后提交偏移量来保证消息的可靠消费。auto.commit.interval.ms 参数允许消费者自动提交偏移量,以避免手动提交的繁琐操作。 当消费者拉取数据后,它会检查是否达到了 auto.commit.interval.ms 设定的时间间隔。如果达到了时间间隔,消费者会自动将当前消费的偏移量提交给 Kafka,以确保之前消费的消息得到确认。 |
ssl.truststore.certificates | |
ssl.truststore.location | trust store 文件的位置。 |
ssl.truststore.password | trust store 文件的密码。如果一个密码没有设置到 trust store ,这个密码仍然是可用的,但是完整性检查是禁用的。 |
connections.max.idle.ms | 用来指定在多久之后关闭闲置的连接,默认值是540000(ms),即9分钟 |
default.api.timeout.ms | Kafka 客户端配置中的一个参数,用于指定客户端与 Kafka 服务器交互时的超时时间。默认60s 具体来说,当客户端向 Kafka 服务器发送请求时,它会等待 default.api.timeout.ms 指定的时间。如果服务器在超时时间内没有响应,客户端将认为请求失败并采取相应的措施,例如重试请求或报告错误。 合理设置 default.api.timeout.ms,可以确保客户端与 Kafka 服务器的通信不会因为网络延迟或其他问题而长时间等待。然而,设置太短的超时时间可能会导致频繁的请求失败,而设置太长的超时时间则可能导致客户端等待过长时间。因此,根据您的应用程序的需求和网络环境,选择合适的超时时间是很重要的。 |
exclude.internal.topics | 用来指定 Kafka 中的内部主题是否可以向消费者公开,默认true 如果设置为 true,那么只能使用 subscribe(Collection)的方式而不能使用 subscribe(Pattern)的方式来订阅内部主题,设置为 false 则没有这个限制。 |
isolation.level | 用来配置消费者的事务隔离级别的,默认值为read_uncommitted,也就是消费者可以消费到未提交的事务。 如果将isolation.level设置为read_committed,那么只有已经提交的事务,消费者才能消费到。 |
partition.assignment.strategy | 用于指定分区分配策略,默认值为 RangeAssignor。Kafka 提供了多种分区分配策略,包括 RangeAssignor、RoundRobinAssignor、StickyAssignor 等。
可以使用自定义分配策略,需要把partition.assignment.strategy设置成自定义类的名字。 |
receive.buffer.bytes | 指定了 TCP socket 接收数据包的缓冲区的大小。默认值为65536(B),即64KB。如果设置为 -1,则使用操作系统的默认值。如果 Consumer 与 Kafka 处于不同的机房,则可以适当调大这个参数值。 这个参数主要影响消费者从 broker 拉取数据时的性能。如果接收缓冲区的大小设置得较小,可能会导致消费者频繁地从 broker 拉取数据,这会增加网络开销。如果接收缓冲区的大小设置得较大,则可以减少拉取数据的次数,从而提高性能。 注意,receive.buffer.bytes 和 send.buffer.bytes 是相互关联的。如果 receive.buffer.bytes 参数设置得较大,那么 send.buffer.bytes 参数也应该相应地设置得较大,以确保网络传输的效率。反之,如果 receive.buffer.bytes 参数设置得较小,那么 send.buffer.bytes 参数可以设置得相对较小。 |
send.buffer.bytes | 指定了 TCP socket 发送数据包的缓冲区的大小。默认值为131072(B),即128KB。如果设置为 -1,则使用操作系统的默认值。 |
request.timeout.ms | 指定了消费者等待broker响应的最长时间。默认为 3000 ms。 如果broker在指定时间内没有做出响应,那么客户端就会关闭连接并尝试重连。 不建议把它设置得比默认值小。在放弃请求之前要给broker留有足够长的时间来处理其他请求,因为向已经过载的broker发送请求几乎没有什么好处,况且断开并重连只会造成更大的开销。这个参数对于控制 Consumer 与 Kafka Broker 之间的交互非常关键。 |
allow.auto.create.topics | 是否自动创建topic。默认true |
metadata.max.age.ms | 用来配置元数据的过期时间,默认值为300000(ms),即5分钟。如果元数据在此参数所限定的时间范围内没有进行更新,则会被强制更新,即使没有任何分区变化或有新的 broker 加入。 |
retry.backoff.ms | 用来配置尝试重新发送失败的请求到指定的主题分区之前的等待(退避)时间,避免在某些故障情况下频繁地重复发送,默认值为100(ms)。 |
offsets.retention.minutes | 这是broker端的一个配置属性,需要注意的是,它也会影响消费者的行为。只要消费者群组里有活跃的成员(也就是说,有成员通过发送心跳来保持其身份),群组提交的每一个分区的最后一个偏移量就会被Kafka保留下来,在进行重分配或重启之后就可以获取到这些偏移量。但是,如果一个消费者群组失去了所有成员,则Kafka只会按照这个属性指定的时间(默认为7天)保留偏移量。一旦偏移量被删除,即使消费者群组又“活”了过来,它也会像一个全新的群组一样,没有了过去的消费记忆。 |
消费者组原理
Consumer Group (CG):消费者组,由多个consumer组成。形成一个消费者组的条件是所有消费者的 groupid 相同。
-
消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费。
-
消费者组之间互不影响。所有消费者都属于某个消费者组,即消费者组是逻辑上的订阅者。
消息传递机制
Kafka 的消息传递机制采用的是 Pull 模式。
Pull 模式:
在 Pull 模式下,消费者主动从 Kafka Broker 请求消息。消费者决定何时、从哪个分区获取消息。工作原理:
- 消费者请求: 消费者定期向 Kafka Broker 发送请求,指定要消费的主题和分区。
- 消息拉取: Kafka Broker 根据消费者的请求,将符合条件的消息返回给消费者。
- 消费进度: 消费者会维护自己的消费进度(offset),记录已消费到哪个位置,以便在下次拉取时继续从正确的位置开始消费。
特点:
- 灵活性: 消费者可以控制消息的拉取速率,根据自己的处理能力来调整拉取速度,避免因处理速度过慢而导致的积压。
- 容错性: 如果消费者由于网络问题或故障无法接收消息,消息会保留在 Kafka 中,直到消费者恢复并请求拉取。
- 负载均衡: 可以通过增加消费者实例来实现负载均衡,多个消费者可以共同消费同一个主题的不同分区,提高处理能力。
pull 模式不足之处是,如果Kafka 没有数据,消费者可能会陷入循环中,一直返回空数据。
Push 模式:
在 Push 模式下,消息由生产者或中间件主动推送到消费者。消费者无需主动请求消息,消息会被自动推送到消费者。
特点:
- 实时性: 消息可以被立即推送到消费者,但如果消费者处理能力不足,可能会导致消息积压或丢失。
- 灵活性低: 消费者对消息的处理速率控制较少,可能需要额外的机制来应对处理能力不匹配的问题。
为什么 Kafka 选择 Pull 模式
- 适应高吞吐量: Pull 模式允许消费者以适当的速率拉取消息,适应各种处理能力的消费者,提升系统的整体吞吐量。
- 简单的消息确认: 消费者可以通过偏移量(offset)机制管理自己的消费进度,更容易实现消息确认和重试机制。
- 提高可靠性: 消息在 Kafka 中持久化存储,消费者故障恢复后可以从消息的最新位置继续消费,避免消息丢失。
Kafka 的设计选择 Pull 模式,以提高系统的灵活性、容错性和处理能力。消费者可以根据自己的处理能力来控制消息的拉取速率,从而更好地适应不同的业务场景和负载需求。
特性 | 拉取模式(Kafka) | 推送模式 |
定义 | 消费者主动从 Kafka Broker 请求消息。 | 消息生产者或中间件主动将消息推送到消费者。 |
消息获取 | 消费者定期拉取消息,主动请求指定的主题和分区。 | 消息自动推送到消费者,消费者无需主动请求。 |
控制 | 消费者可以控制消息的拉取速率,根据处理能力调整拉取的频率和批量大小。 | 消费者对消息的接收速度控制较少,可能需要额外的机制来处理消息积压。 |
容错性 | 消息在 Kafka 中持久化存储,即使消费者暂时无法处理消息,消息不会丢失,消费者可以稍后重新拉取。 | 如果消费者处理能力不足或网络问题,可能会导致消息丢失或积压,需额外的处理机制来确保消息的可靠性。 |
负载均衡 | 通过增加消费者实例来实现负载均衡,多个消费者可以共同消费不同分区的消息,提高处理能力。 | 负载均衡通常需要在生产者或中间件端进行控制,消费者端可能难以实现高效的负载均衡。 |
实现复杂度 | 相对简单,消费者控制拉取的频率和批量,易于实现消息确认和处理。 | 需要处理消息推送的失败和重试机制,可能需要复杂的机制来确保消息的完整传输。 |
实时性 | 拉取模式的实时性可能受到消费者拉取间隔的影响。 | 推送模式通常具有更高的实时性,消息可以立即推送到消费者。 |
网络带宽使用 | 拉取模式可以根据需要批量拉取消息,减少网络带宽的使用。 | 推送模式可能导致网络带宽的高峰使用,尤其在高负载情况下。 |
消费者幂等性
Kafka精确一次性(Exactly-once)保障之一
幂等性:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
出现原因:
- Consumer在消费过程中,被强行kill掉消费者线程或异常中断(消费系统宕机、重启等),导致实际消费后的数据,offset没有提交。
- 设置offset为自动提交,关闭kafka时,如果在close之前,调用 consumer.unsubscribe() 则有可能部分offset没提交,下次重启会重复消费。
- 消费超时导致消费者与集群断开连接,offset尚未提交,导致重平衡后重复消费。一般消费超时(session.time.out)有以下原因:并发过大,消费者突然宕机,处理超时等。
解决思路:
- 提高消费能力,提高单条消息的处理速度,例如对消息处理中比 较耗时的步骤可通过异步的方式进行处理、利用多线程处理等。
- 在缩短单条消息消费时常的同时,根据实际场景可将session.time.out(Consumer心跳超时时间)和max.poll.interval.ms(consumer两次poll的最大时间间隔)值设置大一点,避免不必要的rebalance,此外可适当减小max.poll.records的值( 表示每次消费的时候,获取多少条消息),默认值是500,可根据实际消息速率适当调小。这种思路可解决因消费时间过长导致的重复消费问题, 对代码改动较小,但无法绝对避免重复消费问题。
- 根据业务情况制定:引入单独去重机制,例如生成消息时,在消息中加入唯一标识符如主键id。写入时根据逐渐主键判断update还是insert。如果写redis,则每次根据主键id进行set即可,天然幂等性。或者使用redis作为缓冲,将id首先写入redis进行重复判断,然后在进行后续操作。
- 开启生产者的精确一次性,也就是幂等性, 再引入producer事务 ,即客户端传入一个全局唯一的Transaction ID,这样即使本次会话挂掉也能根据这个id找到原来的事务状态
Broker
工作流程
重要参数
分区
分区数可以增加或减少吗
kafka支持增加分区数
例如我们可以使用 bin/kafka-topics.sh -alter --topic --topic topic-name --partitions 3 命令将原本分区数为1得topic-name设置为3。当主题中的消息包含有key时(即key不为null),根据key来计算分区的行为就会有所影响。当topic-config的分区数为1时,不管消息的key为何值,消息都会发往这一个分区中;当分区数增加到3时,那么就会根据消息的key来计算分区号,原本发往分区0的消息现在有可能会发往分区1或者分区2中。如此还会影响既定消息的顺序,所以在增加分区数时一定要三思而后行。对于基于key计算的主题而言,建议在一开始就设置好分区数量,避免以后对其进行调整。
Kafka 不支持减少分区数
按照Kafka现有的代码逻辑而言,此功能完全可以实现,不过也会使得代码的复杂度急剧增大。实现此功能需要考虑的因素很多,比如删除掉的分区中的消息该作何处理?如果随着分区一起消失则消息的可靠性得不到保障;如果需要保留则又需要考虑如何保留。直接存储到现有分区的尾部,消息的时间戳就不会递增,如此对于Spark、Flink这类需要消息时间戳(事件时间)的组件将会受到影响;如果分散插入到现有的分区中,那么在消息量很大的时候,内部的数据复制会占用很大的资源,而且在复制期间,此主题的可用性又如何得到保障?与此同时,顺序性问题、事务性问题、以及分区和副本的状态机切换问题都是不得不面对的。反观这个功能的收益点却是很低,如果真的需要实现此类的功能,完全可以重新创建一个分区数较小的主题,然后将现有主题中的消息按照既定的逻辑复制过去即可。
分区数越多越好吗
并非分区数量越多,效率越高
- Topic 每个 partition 在 Kafka 路径下都有一个自己的目录,该目录下有两个主要的文件:base_offset.log 和 base_offset.index。Kafka 服务端的 ReplicaManager 会为每个 Broker 节点保存每个分区的这两个文件的文件句柄。所以如果分区过多,ReplicaManager 需要保持打开状态的文件句柄数也就会很多。
- 每个 Producer, Consumer 进程都会为分区缓存消息,如果分区过多,缓存的消息越多,占用的内存就越大;
- n 个分区有 1 个 Leader,(n-1) 个 Follower,如果运行过程中 Leader 挂了,则会从剩余 (n-1) 个 Followers 中选举新 Leader;如果有成千上万个分区,那么需要很长时间的选举,消耗较大的性能。
副本
基本信息
-
Kafka副本作用:提高数据可靠性。
-
Kafka默认副本1个,生产环境一般配置为2个,保证数据可靠性;太多副本会增加磁盘存储空间,增加网络上数据传输,降低效率。
-
Kafka中副本为:Leader和Follower。Kafka生产者只会把数据发往 Leader,然后Follower 找 Leader 进行同步数据。
-
Kafka 分区中的所有副本统称为 AR(Assigned Repllicas)。
AR = ISR + OSR
ISR:表示 Leader 保持同步的 Follower 集合。如果 Follower 长时间未 向 Leader 发送通信请求或同步数据,则该 Follower 将被踢出 ISR。该时间阈值由 replica.lag.time.max.ms 参数设定,默认 30s 。Leader 发生故障之后,就会从 ISR 中选举新的 Leader。
OSR:表示 Follower 与 Leader 副本同步时,延迟过多的副本。
Leader 选举流程
Kafka 集群中有一个 broker 的 Controller 会被选举为 Controller Leader ,负责管理集群 broker 的上下线,所有 topic 的分区副本分配 和 Leader 选举等工作。
Leader和Follower故障处理
LEO(Log End Offset): 每个副本的最后一个offset,LEO其实就是最新的 offset + 1。
HW(High Watermark):所有副本中最小的LEO。
LEO(Log End Offset):每个副本的最后一个offset,LEO其实就是最新的offset + 1
HW(High Watermark):所有副本中最小的LEO
活动调整分区副本存储
在生产环境中,每台服务器的配置和性能不一致,但是kafka只会根据自己的代码规则创建对应的分区副本,就会导致个别服务器存储压力较大。所有需要手动调整分区副本的存储。
手动调整分区副本存储的步骤如下:
1、创建一个新的 topic,名称为 three。 [atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --partitions 4 --replication-factor 2 -- topic three 2、查看分区副本存储情况 [atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic three 3、创建副本存储计划(所有副本都指定存储在 broker0、broker1 中)。 [atguigu@hadoop102 kafka]$ vim increase-replication-factor.json 输入如下内容: { "version":1, "partitions":[{"topic":"three","partition":0,"replicas":[0,1]}, {"topic":"three","partition":1,"replicas":[0,1]}, {"topic":"three","partition":2,"replicas":[1,0]}, {"topic":"three","partition":3,"replicas":[1,0]}] } 4、执行副本存储计划。 [atguigu@hadoop102 kafka]$ bin/kafka-reassign-partitions.sh -- bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --execute 5、验证副本存储计划。 [atguigu@hadoop102 kafka]$ bin/kafka-reassign-partitions.sh -- bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --verify 6、查看分区副本存储情况。 [atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic three
Leader Partition 负载平衡
参数名称 | 描述 |
auto.leader.rebalance.enable | 默认是 true。 自动 Leader Partition 平衡。生产环境中,leader 重选举的代价比较大,可能会带来性能影响,建议设置为 false 关闭。 |
leader.imbalance.per.broker.percentage | 默认是 10%。每个 broker 允许的不平衡的 leader的比率。如果每个broker 超过了这个值,控制器会触发 leader 的平衡。 |
leader.imbalance.check.interval.seconds | 默认值 300 秒。检查 leader 负载是否平衡的间隔时间。 |
偏移量
默认维护位置
自动提交offset
自动提交offset的相关参数:
-
enable.auto.commit:是否开启自动提交offset功能,默认是true。
-
auto.commit.interval.ms:自动提交offset的时间间隔,默认是5s
手动提交offset
偏移量是kafka特别重要的一个概念,特别是在消费者端
偏移量是一个自增长ID,用来标识当前分区哪些消息被消费过了,这个ID会保存在kafka的broker中,消费者本地也会存储一份,因为每次消费一条消息都要更新一下偏移量的话,难免会影响整个broker的吞吐量,所以偏移量在每次发生改动时,先由消费者本地改动, 默认情况下,消费者每五秒钟会提交一次改动的偏移量, 这样做虽然吞吐量上来了, 但可能会出现重复消费问题: 因为可能在下一次提交偏移量之前,消费者本地消费了一些消息,然后发生了分区再均衡,那么就会出现一个问题,假设上次提交的偏移量是 2000, 在下一次提交之前 ,其实消费者又消费了500条数据 也就是说当前的偏移量应该是2500 ,但是这个2500只在消费者本地, 也就是说 ,假设其他消费者去消费这个分区的时候拿到的偏移量是2000, 那么又会从2000开始消费消息, 那么 2000到2500之间的消息又会被消费一遍,这就是重复消费的问题.
kafka提供了解决方案:手动提交。可以关闭默认的自动提交 ,然后使用kafka提供的API来进行偏移量提交。kafka提供了两种提交方式:commitSync(同步提交)、commitAsync(异步提交)
//同步提交偏移量
kafkaConsumer.commitSync();
//异步提交偏移量
kafkaConsumer.commitAsync();
相同点:
两者的相同点是,都会将本次提交的一批数据最高的偏移量提交
不同点:
- commitSync:会等待服务器应答,遇到错误会尝试重试,但一定程度上影响性能,不过能确保偏移量到底提交成功与否。必须等待offset提交完毕,再去消费下一批数据。
- commitAsync:对于性能肯定是有利的,但遇到错误没办法重试,因为可能在收到结果的时候又提交过偏移量了,如果这时候重试,又会导致消息重复的问题了。发送完提交offset请求后,就开始消费下一批数据了。
可以采用同步+异步的方式来保证提交的正确性以及服务器的性能
异步提交的话,如果出现问题但是不是致命问题的话,可能下一次提交就不会出现这个问题了, 所以,有些异常是不需要解决的(可能单纯的网络问题 ) 所以,可以采用异步提交方式,等到消费者中断了(遇到了致命问题,或是强制中断消费者) ,再使用同步提交(因为这次如果失败了就没有下次了... 所以要让他重试) 。
注意,手动提交时可以指定偏移量,但非常不建议,因为如果指定的偏移量小于分区所存储的偏移量,会导致消息重复消费, 如果指定的偏移量大于分区所存储的偏移量,会导致消息丢失.
Kafka事务
幂等性可以保证单个Producer会话、单个TopicPartition、单个会话session的不重不漏,如果Producer重启,或者是写入跨Topic、跨Partition的消息,幂等性无法保证。此时需要用到Kafka事务。Kafka 的事务处理,主要是允许应用可以把消费和生产的 batch 处理(涉及多个 Partition)在一个原子单元内完成,操作要么全部完成、要么全部失败。为了实现这种机制,我们需要应用能提供一个唯一 id,即使故障恢复后也不会改变,这个 id 就是 TransactionnalId(也叫 txn.id),txn.id 可以跟内部的 PID 1:1 分配,它们不同的是 txn.id 是用户提供的,而 PID 是 Producer 内部自动生成的(并且故障恢复后这个 PID 会变化),有了 txn.id 这个机制,就可以实现多 partition、跨会话的 EOS 语义。当用户使用 Kafka 的事务性时,Kafka 可以做到的保证:
- 跨会话的幂等性写入:即使中间故障,恢复后依然可以保持幂等性;
- 跨会话的事务恢复:如果一个应用实例挂了,启动的下一个实例依然可以保证上一个事务完成(commit 或者 abort);
- 跨多个 Topic-Partition 的幂等性写入,Kafka 可以保证跨多个 Topic-Partition 的数据要么全部写入成功,要么全部失败,不会出现中间状态。
Kafka事务是2017年Kafka 0.11.0.0引入的新特性。类似于数据库的事务。Kafka事务指的是生产者生产消息以及消费者提交offset的操作可以在一个原子操作中,要么都成功,要么都失败。尤其是在生产者、消费者并存时,事务的保障尤其重要。(consumer-transform-producer模式)
开启事务,必须开启幂等性
事务操作API
Producer接口中定义了以下5个事务相关方法:
- initTransactions(初始化事务):要使用Kafka事务,必须先进行初始化操作
- beginTransaction(开始事务):启动一个Kafka事务
- sendOffsetsToTransaction(提交偏移量):批量地将分区对应的offset发送到事务中,方便后续一块提交
- commitTransaction(提交事务):提交事务
- abortTransaction(放弃事务):取消事务
Kafka中只有两种事务隔离级别:readcommitted、readuncommitted。设置为readcommitted时候是生产者事务已提交的数据才能读取到。在执行 commitTransaction() 或 abortTransaction() 方法前,设置为“readcommitted”的消费端应用是消费不到这些消息的,不过在 KafkaConsumer 内部会缓存这些消息,直到生产者执行 commitTransaction() 方法之后它才能将这些消息推送给消费端应用。同时KafkaConsumer会根据分区对数据进行整合,推送时按照分区顺序进行推送。而不是按照数据发送顺序。反之,如果生产者执行了 abortTransaction() 方法,那么 KafkaConsumer 会将这些缓存的消息丢弃而不推送给消费端应用。设置为read_uncommitted时候可以读取到未提交的数据(报错终止前的数据)
Rebalance分区再均衡
Rebalance 本质上是一种协议,规定了一个 Consumer Group 下的所有 consumer 如何达成一致,来分配订阅 Topic 的每个分区。 例如:某 Group 下有 20 个 consumer 实例,它订阅了一个具有 100 个 partition 的 Topic。正常情况下,kafka 会为每个 Consumer 平均的分配 5 个分区。这个分配的过程就是 Rebalance。
Rebalance 的触发条件
- 组成员发生变更(新consumer加入组、已有consumer主动离开组或已有consumer崩溃了)
- 订阅主题数发生变更,如果你使用了正则表达式的方式进行订阅,那么新建匹配正则表达式的topic就会触发rebalance
- 订阅主题的分区数发生变更
Rebalance 过程
-
JoinGroup 请求
JoinGroup 请求的主要作用是将组成员订阅信息发送给领导者消费者,待领导者制定好分配方案后,重平衡流程进入到 SyncGroup 请求阶段。
-
SyncGroup 请求
SyncGroup 请求主要目的就是让协调者把领导者制定的分配方案下发给各个组内成员。当所有成员都成功接收到分配方案后,消费者组进入到 Stable 状态,即开始正常的消费工作。
当触发Rebalance,kafka重新分配分区所有权
分区所有权:消费者有一个消费者组的概念, 而且一个消费者组在消费一个主题时有以下规则, 一个消费者可以消费多个分区,但是一个分区只能被一个消费者消费, 如果有分区 0 1 2,现在有消费者 A、B ,kafka可能会让消费者A 消费 0,1这两个分区,这时就会说 消费者A 拥有分区 0,1的所有权。
当触发 Rebalance ,kafka会重新分配这个所有权,还是上面的例子,消费者A 拥有分区 0,1的所有权,消费者B会有分区2的所有权,当消费者B离开kafka时,kafka会重新分配所有权,此时整个消费者组只有一个A, 那么 0 1 2 三个分区的所有权都会属于A, 同理如果这时有消费者C进入消费者组,kafka会确保每一个消费者都能消费一个分区.
当触发Rebalance时,由于kafka正在分配所有权,会导致消费者不能消费, 而且还会引发重复消费的问题, 当消费者还没来得及提交偏移量时,分区所有权遭到了重新分配,就会导致一个消息被多个消费者重复消费
解决方案:在消费者订阅时, 添加一个再均衡监听器, 也就是当kafka在做Rebalance 操作前后 均会调用再均衡监听器,这时可以在kafka Rebalance之前提交我们消费者最后处理的消息来解决这个问题。
Close()方法
在Kafka中,close()
主要作用是关闭Kafka连接,释放资源。 无论是生产者还是消费者,使用完之后都应该调用close()
方法来关闭连接,以避免资源泄露。
对于生产者,调用close()
方法可以关闭Producer实例,执行一些资源清理工作,确保所有发送请求完成。如果不调用close()
方法,源码中注释说明close()
方法会一直阻塞主线程,直到之前的所有发送请求全部完成。这意味着,如果不关闭Producer,消息可能无法成功发送到Kafka服务器。
对于消费者,调用close()
方法同样重要。消费者在关闭时需要释放资源,确保消息消费的连贯性和正确性。消费者实例的关闭操作包括设置关闭标志、等待当前处理的消息完成、关闭发送线程等。在关闭之前 close()会发送一个通知告诉kafka这个消费者要退出了, kafka就会准备Rebalance,如果是采用的自动提交偏移量,消费者自身也会在关闭自己之前提交最后所消费的偏移量 。即使没有调用close(),而是直接强制中断了消费者的进程, kafka也会根据系统参数捕捉到消费者退出了。
AR、ISR、OSR
-
AR:
Assigned Replicas 指当前分区中的所有副本。
-
ISR:
In-Sync Replicas 副本同步队列。ISR中包括Leader和Foller。如果Leader进程挂掉,会在ISR队列中选择一个服务作为新的Leader。有replica.lag.max.message(延迟条数)和replica.lag.time.max.ms(延迟时间)两个参数决定一台服务器是否可以加入ISR副本队列,在0.10版本之后移除了replica.lag.max.message(延迟条数)参数,防治服务频繁的进出队列。任意一个维度超过阈值都会把Follower踢出ISR,存入OSR(Outof-Sync Replicas)列表,新加入的Follower也会先存放在OSR中。
-
OSR:
Out-of-Sync Replicas,非同步副本队列。与leader副本同步滞后过多的副本(不包括leader副本)组成OSR。如果OSR集合中有follower副本“追上”了leader副本,leader副本会把它从OSR集合转移至ISR集合。默认情况下,当leader副本发生故障时,只有在ISR集合中的副本才有资格被选举为新的leader,而在OSR集合中的副本没有任何机会(不过这个原则也可以通过修改unclean.leader.election.enable参数配置来改变)。unclean.leader.election.enable 为true的话,意味着非ISR集合的broker 也可以参与选举,这样就有可能发生数据丢失和数据不一致的情况,Kafka的可靠性就会降低;而如果unclean.leader.election.enable参数设置为false,Kafka的可用性就会降低。
ISR的伸缩:
- Leader跟踪维护ISR中follower滞后状态,落后太多或失效时,leade把他们从ISR剔除。
- OSR中follower“追上”Leader,在ISR中才有资格选举leader。
ISR收缩性:
启动 Kafka时候自动开启的两个定时任务,“isr-expiration"和”isr-change-propagation"。
- isr-expiration:isr-expiration任务会周期性的检测每个分区是否需要缩减其ISR集合,相当于一个纪检委员,巡查尖子班时候发现有学生睡觉打牌看小说,就把它的座位移除尖子班,缩减ISR,宁缺毋滥。同样道理,如果follower数据同步赶上leader,那么该follower就能进入ISR尖子班,扩充。上面关于ISR尖子班人员的所见,都会记录到isrChangeSet中,想象成是一个名单列表,谁能进,谁要出,都记录在案。
- isr-change-propagation:作用就是检查isrChangeSet,按照名单上的信息移除和迁入,一般是2500ms检查一次,但是为了防止频繁收缩扩充影响性能,不是每次都能做变动,必须满足:1、上一次ISR集合发生变化距离现在已经超过5秒,2、上一次写入zookeeper的时候距离现在已经超过60秒。这两个条件都满足,那么就开始换座位!这两个条件可以由我们来配置。
- Kafka使用这种ISR收缩的方式有效的权衡了数据可靠性与性能之间的关系。
HW、LEO
- LEO (Log End Offset),标识当前日志文件中下一条待写入的消息的offset。上图中offset为9的位置即为当前日志文件的 LEO,LEO 的大小相当于当前日志分区中最后一条消息的offset值加1.分区 ISR 集合中的每个副本都会维护自身的 LEO ,而 ISR 集合中最小的 LEO 即为分区的 HW,对消费者而言只能消费 HW 之前的消息。
- HW:replica高水印值,副本中最新一条已提交消息的位移。leader 的HW值也就是实际已提交消息的范围,每个replica都有HW值,但仅仅leader中的HW才能作为标示信息。什么意思呢,就是说当按照参数标准成功完成消息备份(成功同步给follower replica后)才会更新HW的值,代表消息理论上已经不会丢失,可以认为“已提交”。
命令行操作
基本管理操作命令
##列出所有主题
kafka-topics.bat --zookeeper localhost:2181/kafka --list
##列出所有主题的详细信息
kafka-topics.bat --zookeeper localhost:2181/kafka --describe
##创建主题 主题名 my-topic,1副本,8分区
kafka-topics.bat --zookeeper localhost:2181/kafka --create --replication-factor 1 --partitions 8 --topic my-topic
##增加分区,注意:分区无法被删除
kafka-topics.bat --zookeeper localhost:2181/kafka --alter --topic my-topic --partitions 16
##删除主题
kafka-topics.bat --zookeeper localhost:2181/kafka --delete --topic my-topic
##列出消费者群组(仅Linux)
kafka-topics.sh --new-consumer --bootstrap-server localhost:9092/kafka --list
##列出消费者群组详细信息(仅Linux)
kafka-topics.sh --new-consumer --bootstrap-server localhost:9092/kafka --describe --group 群组名
主题命令行操作
-
查看操作主题命令参数
./bin/kafka-topics.sh
-
查看当前服务器中的所有topic
./bin/kafka-topics.sh --bootstrap-server localhost:9092 --list
-
创建
first topic
./bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --partitions 1 --replication-factor 1 --topic first
选项说明:
-
–topic 定义 topic 名
-
–replication-factor 定义副本数
-
–partitions 定义分区数
-
-
查看
first
主题的详情./bin/kafka-topics.sh --bootstrap-server localhost:9092 --topic first --describe
-
修改分区数(注意:分区数只能增加,不能减少)
./bin/kafka-topics.sh --bootstrap-server localhost:9092 --alter --topic first --partitions 3
-
查看结果:
./bin/kafka-topics.sh --bootstrap-server localhost:9092 --topic first --describe Topic: first TopicId: _Pjhmn1NTr6ufGufcnsw5A PartitionCount: 3 ReplicationFactor: 1 Configs: segment.bytes=1073741824 Topic: first Partition: 0 Leader: 0 Replicas: 0 Isr: 0 Topic: first Partition: 1 Leader: 0 Replicas: 0 Isr: 0 Topic: first Partition: 2 Leader: 0 Replicas: 0 Isr: 0
-
删除
topic
./bin/kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic first
生产者命令行操作
-
查看操作者命令参数
./bin/kafka-console-producer.sh
-
发送消息
./bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic first >hello world >yooome yooome
消费者命令行操作
-
查看操作消费者命令参数
./bin/kafka-console-consumer.sh
-
消费消息
消费
first
主题中的数据:./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic first
把主题中所有的数据都读取出来(包括历史数据)
./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --from-beginning --topic first
如何保证消息不丢失
Kafka 数据丢失的常见情况:
-
Producer 发送失败: 当 Kafka Producer 发送消息到 Kafka 集群时,如果遇到网络问题、Kafka 集群故障或其他异常情况,可能导致消息发送失败。在这种情况下,消息可能会丢失。
-
Replica 副本不足: Kafka 通过复制数据来保障可靠性。如果某些分区的 Replica 副本数量不足,当 Leader 副本(主副本)出现故障时,无法保障数据可靠性。
-
Producer 未设置acks=all: 如果 Kafka Producer 的 acks 配置未设置为 “all”,即等待所有 Replica 副本确认消息写入成功后再返回 ACK,那么在某些情况下可能出现数据丢失。
-
数据过期: 如果 Kafka Broker 配置了数据保留时间或者数据保留大小,当消息的时间戳超过保留时间或者数据大小超过限制时,消息会被删除,从而导致数据丢失。
-
Producer 重试机制: 在某些情况下,Producer 可能会启用重试机制以确保消息发送成功。但是,如果设置的重试次数过少或者未启用重试机制,可能会导致数据在发送失败后被丢弃。
解决方案:
-
配置 Producer 的 acks 为 “all”,确保数据在 Leader 和所有 Replica 副本上写入成功后才返回 ACK。
-
设置合理的数据保留时间和大小,避免数据过期删除。
-
确保足够的 Replica 副本,保障数据的可靠性。
-
启用 Producer 的重试机制,设置适当的重试次数和重试时间间隔。
-
监控 Kafka 集群的状态,及时发现异常并采取相应措施。
数据的丢失问题,可能出现在生产者、MQ、消费者中
-
消费端弄丢了数据
唯一可能导致消费者弄丢数据的情况,就是说,你消费到了这个消息,然后消费者那边自动提交了 offset,让 Kafka 以为你已经消费好了这个消息,但其实你刚准备处理这个消息,你就挂了,此时这条消息就丢咯。
这不是跟 RabbitMQ 差不多吗,大家都知道 Kafka 会自动提交 offset,那么只要关闭自动提交 offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。但是此时确实还是可能会有重复消费,比如你刚处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。
生产环境碰到的一个问题,就是说我们的 Kafka 消费者消费到了数据之后是写到一个内存的 queue 里先缓冲一下,结果有的时候,你刚把消息写入内存 queue,然后消费者会自动提交 offset。然后此时我们重启了系统,就会导致内存 queue 里还没来得及处理的数据就丢失了。
-
Kafka 弄丢了数据
常见的一个场景,就是 Kafka 某个 broker 宕机,然后重新选举 partition 的 leader。要是此时其他 follower 刚好还有些数据没有同步,结果此时 leader 挂了,然后选举某个 follower 成 leader 之后,不就少了一些数据?这就丢了一些数据啊。
生产环境也遇到过,Kafka 的 leader 机器宕机了,将 follower 切换为 leader后,就会发现这个数据丢了。
此时一般要设置如下 4 个参数:
(1)给 topic 设置 replication.factor 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。
(2)在 Kafka 服务端设置 min.insync.replicas 参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。
(3)在 producer 端设置 acks=all:这个是要求每条数据,必须是写入所有 replica 之后,才能认为是写成功了。
(4)在 producer 端设置 retries=MAX(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了。
生产环境这样配置之后,至少在 Kafka broker 端就可以保证在 leader 所在 broker 发生故障,进行 leader 切换时,数据不会丢失。
-
生产者会不会弄丢数据?
如果按照上述的思路设置了 acks=all,一定不会丢,要求是,你的 leader 接收到消息,所有的 follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次。
如何保证消费顺序性
单分区:
Kafka在 特定条件下 可以保障单分区消息的有序性
kafka在发送消息过程中,正常情况下是有序的,如果消息出现重试,则会造成消息乱序。导致乱序的原因是:max.in.flight.requests.per.connection默认值为5。该参数指定了生产者在收到服务器响应之前,请求队列中可以提交多少个请求,用于提高网络吞吐量。
图中,batch1-5在请求队列中,batch1作为最新数据进行提交,提交失败后如果开启重试机制,则batch1会重新添加到本地缓冲池的头部,然后提交至请求队列中重新发送。此时batch1的顺序会排在batch5之后,发生了乱序。
解决方式是将max.in.flight.requests.per.connection设置为1,消息队列中只允许有一个请求,这样消息失败后,可以第一时间发送,不会产生乱序,但是会降低网络吞吐量。
或者开启生产者幂等性设置,开启后,该Producer发送的消息都对应一个单调增的Sequence Number。同样的Broker端也会为每个生产者的每条消息维护一个序号,并且每commit一条数据时就会将其序号递增。对于接收到的数据,如果其序号比Borker维护的序号大一(即表示是下一条数据),Broker会接收它,否则将其丢弃。如果消息序号比Broker维护的序号差值比一大,说明中间有数据尚未写入,即乱序,此时Broker拒绝该消息,Producer抛出InvalidSequenceNumber 如果消息序号小于等于Broker维护的序号,说明该消息已被保存,即为重复消息,Broker直接丢弃该消息,Producer抛出DuplicateSequenceNumber Sender发送失败后会重试,这样可以保证每个消息都被发送到broker
多分区:
Kafka本身无法保障多分区的有序性,可以通过业务设计进行保证,例如需要单表数据通过自定义partition的方式发送至同一个分区
先看看顺序会错乱的俩场景:
比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。
消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞多个线程来并发处理消息。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。
解决方案:
一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用。
分配内存队列(Memory Queue):为了解决多线程处理中的顺序问题,可以在 Consumer 内部分配多个内存队列,每个内存队列处理一个 key,从而保证具有相同 key 的消息始终进入同一个内存队列。然后,再由各自的线程消费内存队列中的消息,确保消息处理顺序一致。
如何保证高可用性
Kafka 最基本的架构:由多个 broker 组成,每个 broker 是一个节点;创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。
这就是天然的分布式消息队列,一个 topic 的数据,是分散放在多个机器上的,每个机器就放一部分数据。
实际上 RabbmitMQ 之类的,并不是分布式消息队列,它就是传统的消息队列,只不过提供了一些集群、HA(High Availability, 高可用性) 的机制而已,因为无论怎么玩儿,RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。
Kafka 0.8 以前,是没有 HA 机制的,就是任何一个 broker 宕机了,那个 broker 上的 partition 就废了,没法写也没法读,没有什么高可用性可言。
比如说,我们假设创建了一个 topic,指定其 partition 数量是 3 个,分别在三台机器上。但是,如果第二台机器宕机了,会导致这个 topic 的 1/3 的数据就丢了,因此这个是做不到高可用的。
kafka-before
Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。
这么搞,就有所谓的高可用性了,因为如果某个 broker 宕机了,没事儿,那个 broker上面的 partition 在其他机器上都有副本的。如果这个宕机的 broker 上面有某个 partition 的 leader,那么此时会从 follower 中重新选举一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就有所谓的高可用性了。
-
写数据的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack 之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为)
-
消费的时候,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。
主要是有两个层面
- kafka集群指的是由多个broker实例组成,即使某一台宕机,也不耽误其他broker继续对外提供服务
- 复制机制是可以保证kafka的高可用的,一个topic有多个分区,每个分区有多个副本,有一个leader,其余的是follower,副本存储在不同的broker中;所有的分区副本的内容是都是相同的,如果leader发生故障时,会自动将其中一个follower提升为leader,保证了系统的容错性、高可用性
如何保证可靠性
Kafka 在设计上提供了多种机制来保证消息传递的可靠性。以下是不同层面来保障可靠性的说明:
-
数据复制(Replication)
原理:
(1)Kafka 的每个主题(Topic)可以配置多个分区(Partition),每个分区的数据会在多个 Kafka Broker 上进行复制。每个分区有一个主副本(Leader)和多个从副本(Follower)。
(2)Leader 负责处理生产者和消费者的读写请求,Follower 副本同步 Leader 的数据。
(3)当 Leader 发生故障时,Kafka 会从 Follower 中选举一个新的 Leader,确保数据的高可用性和可靠性。
可靠性保证:
即使某个 Broker 发生故障,消息仍然可以从其他副本中读取,避免数据丢失。
-
消息确认机制(Acknowledgments, acks)
原理:
生产者在发送消息时,可以配置消息的确认级别(acks)。常见的配置包括:
acks=0: 不等待任何确认,生产者发送消息后即认为成功,最低可靠性。
acks=1: 只等待 Leader 副本的确认,即认为消息成功,适中可靠性。
acks=all(或-1):等待所有副本(Leader 和 Follower)确认后,生产者才认为消息发送成功,最高可靠性。
可靠性保证:
使用 acks=all 可以确保消息被所有副本接收后才算成功,即使 Leader 出现故障,新 Leader 也可以提供一致的数据。
-
ISR(In-Sync Replicas)机制
原理:
(1)ISR 是指当前与 Leader 保持同步的 Follower 副本集合。当 Follower 复制滞后太久或故障时,会从 ISR 中移除,直到重新同步。
(2)Kafka 仅允许在 ISR 中的副本成为新的 Leader,确保新 Leader 始终持有最新的消息数据。
可靠性保证:
保证在副本故障或滞后时,不会选举数据不完整的 Follower 作为新的 Leader,避免数据不一致。
-
日志留存与分段(Log Retention and Segmentation)
原理:
(1)Kafka 将消息存储在日志文件中,并支持基于时间或大小的日志留存策略。即使消息被消费,也可以在指定时间内保留,以供数据恢复或回溯使用。
(2)日志文件被分为多个段(Segment),每个段可以独立管理,便于高效的日志清理和压缩。
可靠性保证:
通过日志留存机制,Kafka 能够保留消息历史数据,防止因消费错误或其他原因导致的数据丢失。
-
生产者幂等性(Idempotence)
原理:
Kafka 生产者可以配置幂等性(Idempotence)模式,在这种模式下,生产者会为每条消息生成唯一的序列号,并在发送时携带该序列号。Kafka 会检查该序列号,确保相同的消息不会被重复写入。
可靠性保证:
避免网络抖动或重试机制导致的消息重复,从而保证消息的一致性和准确性。
-
事务支持(Transactions)
原理:
(1)Kafka 提供了事务支持,可以确保一组消息要么全部成功写入,要么全部失败回滚。生产者可以开启事务,将多个消息批次写入作为一个原子操作。
(2)Kafka 通过两个阶段提交协议(Two-Phase Commit Protocol, 2PC)实现事务一致性。
可靠性保证:
确保在分布式环境下的多分区写入操作具有原子性,避免出现部分成功部分失败的情况,保证数据的一致性。
-
严格的顺序保证
原理:
在每个分区内,Kafka 保证消息按照生产者的发送顺序进行存储和消费,即消息的顺序性得到保证。
可靠性保证:
保证在关键业务场景下(如金融交易系统)的消息顺序,避免因乱序导致的数据不一致问题。
这些机制共同作用,使得 Kafka 能够在各种复杂场景下保障消息的可靠传递和一致性,适应高可用、高性能的分布式系统需求。
如何实现高吞吐率
机制/原理 | 描述 |
分布式架构 | 分区机制: 将每个主题分成多个分区,每个分区在不同 Broker 上存储,实现并行处理。 副本机制: 每个分区有一个主副本和多个从副本,主副本处理读写请求,从副本负责数据同步。 |
高效的磁盘存储 | 顺序写入: 消息顺序写入磁盘,减少磁盘寻道时间,提高 I/O 性能。 日志分段: 将日志文件分为多个段,提高日志管理效率,减少磁盘碎片化。 |
内存优化 | 批量处理: 生产者将多个消息打包成一个批次发送,消费者批量拉取消息,减少网络开销和磁盘 I/O。 内存缓存: 使用内存缓存消息索引和元数据,提高读写性能。 |
高效的网络传输 | 压缩机制: 支持 GZIP、Snappy、LZ4 等压缩格式,减少网络带宽和磁盘存储占用。 网络传输优化: 使用高效的网络协议和传输方式,减少网络延迟和开销。 |
可扩展性 | 水平扩展: 通过增加 Broker 节点实现水平扩展,处理更多数据和负载。 负载均衡: 自动均衡分区和 Broker 之间的负载,避免单点瓶颈。 |
生产者和消费者优化 | 异步发送: 生产者异步发送消息,减少网络延迟,提高吞吐量。 消费者并行处理: 消费者并行处理不同分区的数据,多个消费者组成消费者组,共同消费数据。 |
数据压缩与索引 | 消息压缩: 支持消息压缩,减少消息大小,提高存储和传输效率。 索引优化: 使用高效的索引结构加速消息查找,索引存储在内存中,定期更新。 |
消息重复消费问题
kafka消费消息都是按照offset进行标记消费的,消费者默认是自动按期提交已经消费的偏移量,默认是每隔5s提交一次,如果出现重平衡的情况,可能会重复消费或丢失数据。我们一般都会禁用掉自动提价偏移量,改为手动提交,当消费成功以后再报告给broker消费的位置,这样就可以避免消息丢失和重复消费了
为了消息的幂等,我们也可以设置唯一主键来进行区分,或者是加锁,数据库的锁,或者是redis分布式锁,都能解决幂等的问题
选举机制
1、控制器(Broker)选举机制
控制器是Kafka的核心组件,它的主要作用是在Zookeeper的帮助下管理和协调整个Kafka集群包括所有分区与副本的状态。集群中任意一个Broker都能充当控制器的角色,但在运行过程中,只能有一个Broker成为控制器。集群中第一个启动的Broker会通过在Zookeeper中创建临时节点/controller来让自己成为控制器,其他Broker启动时也会在zookeeper中创建临时节点,但是发现节点已经存在,所以它们会收到一个异常,意识到控制器已经存在,那么就会在Zookeeper中创建watch对象,便于它们收到控制器变更的通知。如果控制器与Zookeeper断开连接或异常退出,其他broker通过watch收到控制器变更的通知,就会尝试创建临时节点/controller,如果有一个Broker创建成功,那么其他broker就会收到创建异常通知,代表控制器已经选举成功,其他Broker只需创建watch对象即可。
控制器作用:
- 主题管理:创建、删除Topic,以及增加Topic分区等操作都是由控制器执行。
- 分区重分配:执行Kafka的reassign脚本对Topic分区重分配的操作,也是由控制器实现。如果集群中有一个Broker异常退出,控制器会检查这个broker是否有分区的副本leader,如果有那么这个分区就需要一个新的leader,此时控制器就会去遍历其他副本,决定哪一个成为新的leader,同时更新分区的ISR集合。如果有一个Broker加入集群中,那么控制器就会通过Broker ID去判断新加入的Broker中是否含有现有分区的副本,如果有,就会从分区副本中去同步数据。
- Preferred leader选举:因为在Kafka集群长时间运行中,broker的宕机或崩溃是不可避免的,leader就会发生转移,即使broker重新回来,也不会是leader了。在众多leader的转移过程中,就会产生leader不均衡现象,可能一小部分broker上有大量的leader,影响了整个集群的性能,所以就需要把leader调整回最初的broker上,这就需要Preferred leader选举。
- 集群成员管理:控制器能够监控新broker的增加,broker的主动关闭与被动宕机,进而做其他工作。这也是利用Zookeeper的ZNode模型和Watcher机制,控制器会监听Zookeeper中/brokers/ids下临时节点的变化。同时对broker中的leader节点进行调整。
- 元数据服务:控制器上保存了最全的集群元数据信息,其他所有broker会定期接收控制器发来的元数据更新请求,从而更新其内存中的缓存数据。
2、分区副本选举机制
发生副本选举的情况:
- 创建主题
- 增加分区
- 分区下线(分区中原先的leader副本下线,此时分区需要选举一个新的leader上线来对外提供服务)
- 分区重分配
分区leader副本的选举由Kafka控制器负责具体实施。主要过程如下:
- 从Zookeeper中读取当前分区的所有ISR(in-sync replicas)集合。
- 调用配置的分区选择算法选择分区的leader。
分区副本分为ISR(同步副本)和OSR(非同步副本),当leader发生故障时,只有“同步副本”才可以被选举为leader。选举时按照集合中副本的顺序查找第一个存活的副本,并且这个副本在ISR集合中。同时kafka支持OSR(非同步副本)也参加选举,Kafka broker端提供了一个参数unclean.leader.election.enable,用于控制是否允许非同步副本参与leader选举;如果开启,则当 ISR为空时就会从这些副本中选举新的leader,这个过程称为 Unclean leader选举。可以根据实际的业务场景选择是否开启Unclean leader选举。开启 Unclean 领导者选举可能会造成数据丢失,但好处是,它使得分区 Leader 副本一直存在,不至于停止对外提供服务,因此提升了高可用性。一般建议是关闭Unclean leader选举,因为通常数据的一致性要比可用性重要。
3、消费组(Consumer Group)选主机制
在Kafka的消费端,会有一个消费者协调器以及消费组,组协调器(Group Coordinator)需要为消费组内的消费者选举出一个消费组的leader。如果消费组内还没有leader,那么第一个加入消费组的消费者即为消费组的leader,如果某一个时刻leader消费者由于某些原因退出了消费组,那么就会重新选举leader,选举源码如下:
private val members = new mutable.HashMap[String, MemberMetadata]
leaderId = members.keys.headOption
在组协调器中消费者的信息是以HashMap的形式存储的,其中key为消费者的member_id,而value是消费者相关的元数据信息。而leader的取值为HashMap中的第一个键值对的key(这种选举方式等同于随机)。
消费组的Leader和Coordinator没有关联。消费组的leader负责Rebalance过程中消费分配方案的制定。
数据存储机制
Kafka使用文件存储机制来持久化消息数据。下面是对Kafka文件存储机制的分析:
-
分区文件:Kafka中的消息被组织成一个个分区(partition),每个分区对应一个磁盘上的文件。每个分区文件以固定大小的片段(segment)进行存储,通常是一系列连续的文件片段。
-
文件片段(Segment):每个分区文件由多个文件片段组成,每个文件片段对应一个时间范围内的消息。文件片段的大小可以配置,通常为一定的存储容量或时间长度。当一个文件片段达到指定的大小或时间限制时,Kafka会创建一个新的文件片段。
-
日志索引:为了快速定位消息在分区文件中的位置,Kafka使用了日志索引。每个文件片段都有对应的日志索引文件,它包含了消息在文件片段中的偏移量(offset)和物理位置的索引。通过使用日志索引,Kafka可以快速定位和查找消息,提高读写的效率。
-
文件清理和压缩:Kafka通过一系列的策略来管理分区文件,包括文件清理和压缩。文件清理是指删除已经过时的文件片段,即已经被所有消费者消费完的文件片段。文件压缩是将多个文件片段合并为一个更大的文件,以减少存储空间和提高读写性能。
-
零拷贝技术:Kafka使用零拷贝(Zero-Copy)技术来提高数据的传输效率。在消息写入和读取过程中,Kafka避免了数据在内核空间和用户空间之间的多次拷贝,减少了CPU和内存的开销,提高了性能和吞吐量。
-
快速写入和追加:Kafka采用追加写入(Append-Only)的方式来存储消息。新的消息会被追加到分区文件的末尾,不涉及文件的修改和移动操作,提高了写入的效率。
通过以上文件存储机制,Kafka能够高效地持久化消息数据,并提供快速的读写能力。文件分段、日志索引、文件清理和压缩等策略和优化措施,进一步提升了Kafka的性能和存储效率。这使得Kafka在大规模的消息处理场景下具备了良好的可扩展性和可靠性。
数据清理机制
Kafka中topic的数据存储在分区上,分区如果文件过大会分段存储segment。每个分段都在磁盘上以索引(xxxx.index)和日志文件(xxxx.log)的形式存储,这样分段的好处是,第一能够减少单个文件内容的大小,查找数据方便,第二方便kafka进行日志清理。
Kafka 中默认的日志保存时间为 7 天,可以通过调整如下参数修改保存时间。
- Log.retention.hours,最低优先级小时,默认7天。
- log.retention.minutes,分钟。
- log.retention.ms,最高优先级毫秒。
- log.retention.check.interval.ms,负责设置检查周期,默认 5 分钟。
那么日志一旦超过了设置的时间,怎么处理呢?kafka中提供了两个日志的清理策略:
-
log.cleanup.policy = delete 所有数据启用阐述策略
(1) 基于时间:默认打开。以 segment 中所有记录中的最大时间戳作为该文件时间戳。根据消息的保留时间,当消息保存的时间超过了指定的时间,就会触发清理,默认是168小时( 7天)。
(2) 基于大小:默认关闭。超过设置的所有日志总大小,阐述最早的 segment 。根据topic存储的数据大小,当topic所占的日志文件大小大于一定的阈值,则开始删除最久的消息。
log.retention.bytes,默认等于-1,表示无穷大。
-
log.cleanup.policy = compact所有数据启动压缩策略
compact日志压缩:对于相同 key 的不同 value 值,值保留最后一个版本。
压缩后的offset可能是不连续的,比如上图中没有6,当从这些offset消费消息时,将会拿到比这个 offset 大的 offset 对应的消息,实际上会拿到 offset 为 7 的消息,并从这个位置开始消费。
这种策略只适合特殊场景,比如消息的 key 是用户 ID,value 是用户的资料,通过这种压缩策略,整个消息集里就保存了所有用户最新的资料。
零拷贝
零拷贝(Zero-Copy) 是一种优化技术,旨在减少数据在内存中的复制操作,从而提高系统的性能。Netty 和 Kafka 都使用了零拷贝技术,以提高网络数据的传输效率。
零拷贝指的是在数据传输过程中避免将数据从一个内存区域复制到另一个内存区域。传统的数据传输通常涉及多个内存拷贝操作,例如,从用户空间到内核空间,再到网络接口,零拷贝技术通过减少这些拷贝操作来提高性能。
零拷贝的实现通常依赖以下几个底层原理和技术:
-
文件映射(Memory-Mapped Files)
原理:文件映射技术允许将文件内容直接映射到进程的内存空间,这样应用程序可以像操作内存一样操作文件数据。这种映射避免了将文件数据从磁盘读取到内核缓冲区,然后再从内核缓冲区复制到用户空间的过程。
使用:在 Netty 和 Kafka 中,文件映射用于高效读取大文件或传输大量数据。
-
直接内存访问(Direct Memory Access, DMA)
原理:直接内存访问允许设备直接读写内存中的数据,而不需要经过 CPU 的干预。网络接口卡(NIC)可以使用 DMA 将数据直接写入内存缓冲区,从而避免了 CPU 参与数据的复制。
使用:Netty 使用直接内存来减少内存拷贝,特别是在处理大量的网络数据时,数据可以直接从网络接口卡写入应用程序的内存中。
-
sendfile 系统调用
原理:sendfile 是一个系统调用,它允许将文件内容直接从磁盘发送到网络套接字,而不需要将数据读入用户空间。该调用通过内核空间中的直接内存拷贝来完成文件到网络的传输。
使用:Kafka 使用 sendfile 来优化日志文件的传输,从而减少数据的拷贝次数,提高写入性能。
-
内核级别的 I/O 操作
原理:某些操作系统提供内核级别的 I/O 操作支持,例如 splice 系统调用,它允许将数据从一个文件描述符直接移动到另一个文件描述符,而不需要经过用户空间。
使用:Netty 利用这些内核级别的 I/O 操作来进一步减少内存拷贝的需求。
零拷贝技术通过减少数据在内存中的复制次数,利用文件映射、直接内存访问、内核级 I/O 操作等技术来优化数据传输效率。Netty 和 Kafka 通过使用这些技术,提高了网络数据传输和日志处理的性能。
脑裂问题
controller挂掉后,Kafka集群会重新选举一个新的controller。这里面存在一个问题,很难确定之前的controller节点是挂掉还是只是短暂性的故障。如果之前挂掉的controller又正常了,他并不知道自己已经被取代了,那么此时集群中会出现两台controller。
其实这种情况是很容易发生。比如,某个controller由于GC而被认为已经挂掉,并选择了一个新的controller。在GC的情况下,在最初的controller眼中,并没有改变任何东西,该Broker甚至不知道它已经暂停了。因此,它将继续充当当前controller,这是分布式系统中的常见情况,称为脑裂。
假如,处于活跃状态的controller进入了长时间的GC暂停。它的ZooKeeper会话过期了,之前注册的/controller节点被删除。集群中其他Broker会收到zookeeper的这一通知。
由于集群中必须存在一个controller Broker,所以现在每个Broker都试图尝试成为新的controller。假设Broker 2速度比较快,成为了最新的controller Broker。此时,每个Broker会收到Broker2成为新的controller的通知,由于Broker3正在进行"stop the world"的GC,可能不会收到Broker2成为最新的controller的通知。
等到Broker3的GC完成之后,仍会认为自己是集群的controller,在Broker3的眼中好像什么都没有发生一样。
现在,集群中出现了两个controller,它们可能一起发出具有冲突的命令,就会出现脑裂的现象。如果对这种情况不加以处理,可能会导致严重的不一致。所以需要一种方法来区分谁是集群当前最新的Controller。
Kafka是通过使用epoch number(纪元编号,也称为隔离令牌)来完成的。epoch number只是单调递增的数字,第一次选出Controller时,epoch number值为1,如果再次选出新的Controller,则epoch number将为2,依次单调递增。
每个新选出的controller通过Zookeeper 的条件递增操作获得一个全新的、数值更大的epoch number 。其他Broker 在知道当前epoch number 后,如果收到由controller发出的包含较旧(较小)epoch number的消息,就会忽略它们,即Broker根据最大的epoch number来区分当前最新的controller。
上图,Broker3向Broker1发出命令:让Broker1上的某个分区副本成为leader,该消息的epoch number值为1。于此同时,Broker2也向Broker1发送了相同的命令,不同的是,该消息的epoch number值为2,此时Broker1只听从Broker2的命令(由于其epoch number较大),会忽略Broker3的命令,从而避免脑裂的发生。
Kafka中高性能原因
Kafka 高性能,是多方面协同的结果,包括宏观架构、分布式存储、ISR 数据同步、以及高效的利用磁盘、操作系统特性等。主要体现有这么几点:
- 消息分区:不受单台服务器的限制,可以不受限的处理更多的数据
- 顺序读写:磁盘顺序读写,提升读写效率
- 页缓存:把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问
- 零拷贝:减少上下文切换及数据拷贝
- 消息压缩:减少磁盘IO和网络IO
- 分批发送:将消息打包批量发送,减少网络开销
Kafka为什么这么快
- 利用 Partition 实现并行处理 不同 Partition 可位于不同机器,因此可以充分利用集群优势,实现机器间的并行处理。另一方面,由于 Partition 在物理上对应一个文件夹,即使多个 Partition 位于同一个节点,也可通过配置让同一节点上的不同 Partition 置于不同的磁盘上,从而实现磁盘间的并行处理,充分发挥多磁盘的优势。
- 利用了现代操作系统分页存储 Page Cache 来利用内存提高 I/O 效率
- 顺序写 kafka的消息是不断追加到文件中的,这个特性使kafka可以充分利用磁盘的顺序读写性能 由于现代的操作系统提供了预读和写技术,磁盘的顺序写大多数情况下比随机写内存还要快。顺序读写不需要硬盘磁头的寻道时间,只需很少的扇区旋转时间,所以速度远快于随机读写
- Zero-copy 零拷技术减少拷贝次数
- 数据批量处理。合并小的请求,然后以流的方式进行交互,直顶网络上限。在很多情况下,系统的瓶颈不是 CPU 或磁盘,而是网络IO。因此,除了操作系统提供的低级批处理之外,Kafka 的客户端和 broker 还会在通过网络发送数据之前,在一个批处理中累积多条记录 (包括读和写)。记录的批处理分摊了网络往返的开销,使用了更大的数据包从而提高了带宽利用率。
- Pull 拉模式 使用拉模式进行消息的获取消费,与消费端处理能力相符。
- 数据压缩 Kafka还支持对消息集合进行压缩,Producer可以通过GZIP、Snappy、LZ4格式对消息集合进行压缩,数据压缩一般都是和批处理配套使用来作为优化手段的。压缩的好处就是减少传输的数据量,减轻对网络传输的压力 Producer压缩之后,在Consumer需进行解压,虽然增加了CPU的工作,但在对大数据处理上,瓶颈在网络上而不是CPU,所以这个成本很值得
解释一下复制机制中的ISR
ISR的意思是in-sync replica,就是需要同步复制保存的follower
其中分区副本有很多的follower,分为了两类,一个是ISR,与leader副本同步保存数据,另外一个普通的副本,是异步同步数据,当leader挂掉之后,会优先从ISR副本列表中选取一个作为leader,因为ISR是同步保存数据,数据更加的完整一些,所以优先选择ISR副本列表
follower如何与leader同步数据
Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制。完全同步复制要求All Alive Follower都复制完,这条消息才会被认为commit,这种复制方式极大的影响了吞吐率。而异步复制方式下,Follower异步的从Leader复制数据,数据只要被Leader写入log就被认为已经commit,这种情况下,如果leader挂掉,会丢失数据,kafka使用ISR的方式很好的均衡了确保数据不丢失以及吞吐率。Follower可以批量的从Leader复制数据,而且Leader充分利用磁盘顺序读以及send file(zero copy)机制,这样极大的提高复制性能,内部批量写磁盘,大幅减少了Follower与Leader的消息量差。
如何增强消费者的消费能力
1、如果是Kafka消费能力不足,则可以考虑增加Topic的分区数,并且同时提升消费组的消费者数量,消费者数==分区数。两者缺一不可。
2、如果是下游的数据处理不及时:则提高每批次拉取的数量。批次拉取数据过少(拉取数据/处理时间<生产速度),使处理的数据小于生产的数据,也会造成数据积压。
3、优化消费者的处理逻辑,提高处理效率
如何为Kafka集群选择合适的Topics/Partitions数量
1、根据当前topic的消费者数量确认
在kafka中,单个patition是kafka并行操作的最小单元。在producer和broker端,向每一个分区写入数据是可以完全并行化的,此时,可以通过加大硬件资源的利用率来提升系统的吞吐量,例如对数据进行压缩。在consumer段,kafka只允许单个partition的数据被一个consumer线程消费。因此,在consumer端,每一个Consumer Group内部的consumer并行度完全依赖于被消费的分区数量。综上所述,通常情况下,在一个Kafka集群中,partition的数量越多,意味着可以到达的吞吐量越大。
2、根据consumer端的最大吞吐量确定
我们可以粗略地通过吞吐量来计算kafka集群的分区数量。假设对于单个partition,producer端的可达吞吐量为p,Consumer端的可达吞吐量为c,期望的目标吞吐量为t,那么集群所需要的partition数量至少为max(t/p,t/c)。在producer端,单个分区的吞吐量大小会受到批量大小、数据压缩方法、 确认类型(同步/异步)、复制因子等配置参数的影响。经过测试,在producer端,单个partition的吞吐量通常是在10MB/s左右。在consumer端,单个partition的吞吐量依赖于consumer端每个消息的应用逻辑处理速度。因此,我们需要对consumer端的吞吐量进行测量。
如何实现负载均衡
生产者层面:
分区器是生产者层面的负载均衡。Kafka 生产者生产消息时,根据分区器将消息投递到指定的分区中,所以 Kafka 的负载均衡很大程度上依赖于分区器。Kafka 默认的分区器是 Kafka 提供的 DefaultPartitioner。它的分区策略是根据 Key 值进行分区分配的:
如果 key 不为 null:对 Key 值进行 Hash 计算,从所有分区中根据 Key 的 Hash 值计算出一个分区号;拥有相同 Key 值的消息被写入同一个分区;如果 key 为 null:消息将以轮询的方式,在所有可用分区中分别写入消息。如果不想使用 Kafka 默认的分区器,用户可以实现 Partitioner 接口,自行实现分区方法。
注:在笔者的理解中,分区器的负载均衡与顺序性有着一定程度上的矛盾。
- 负载均衡的目的是将消息尽可能平均分配,对于 Kafka 而言,就是尽可能将消息平均分配给所有分区;
- 如果使用 Kafka 保证顺序性,则需要利用到 Kafka 的分区顺序性的特性。
- 对于需要保证顺序性的场景,通常会利用 Key 值实现分区顺序性,那么所有 Key值相同的消息就会进入同一个分区。这样的情况下,对于大量拥有相同 Key值的消息,会涌入同一个分区,导致一个分区消息过多,其他分区没有消息的情况,即与负载均衡的思想相悖。
消费者层面:
主要根据消费者的Rebalance机制实现
判断一个节点活着的条件?
Kafka判断一个节点是否还活着通常依赖于两个条件:
- 心跳机制:Kafka使用心跳机制来检测节点的活跃状态。每个Broker节点会定期发送心跳信号给集群的控制器(Controller),以表示它仍然活着。如果一段时间内没有收到来自节点的心跳信号,Kafka会认为该节点不再活跃,并将其标记为失效。
- ISR(In-Sync Replicas)列表:ISR是指与Leader副本保持同步的副本集合。Kafka通过监测ISR列表来判断节点的活跃状态。如果一个节点的ISR列表中没有该节点的副本,说明该节点与集群的同步已经中断,Kafka会认为该节点不再活跃,并将其标记为失效。
这两个条件的结合可以帮助Kafka监测节点的状态,及时发现失效节点并进行相应的故障处理。当一个节点被标记为失效后,Kafka可以执行副本重分配和重新选举等操作,以确保集群的可用性和数据的一致性。
为什么不支持读写分离
在 Kafka 中,生产者写入消息、消费者读取消息的操作都是与 leader 副本进行交互的,从而实现的是一种主写主读的生产消费模型。
Kafka 并不支持主写从读,因为主写从读有 2 个很明显的缺点:
- 数据一致性问题。数据从主节点转到从节点必然会有一个延时的时间窗口,这个时间 窗口会导致主从节点之间的数据不一致。某一时刻,在主节点和从节点中 A 数据的值都为 X, 之后将主节点中 A 的值修改为 Y,那么在这个变更通知到从节点之前,应用读取从节点中的 A 数据的值并不为最新的 Y,由此便产生了数据不一致的问题。
- 延时问题。类似 Redis 这种组件,数据从写入主节点到同步至从节点中的过程需要经 历网络→主节点内存→网络→从节点内存这几个阶段,整个过程会耗费一定的时间。而在 Kafka 中,主从同步会比 Redis 更加耗时,它需要经历网络→主节点内存→主节点磁盘→网络→从节 点内存→从节点磁盘这几个阶段。对延时敏感的应用而言,主写从读的功能并不太适用。
ActiveMQ
ActiveMQ。其实一般早些的项目需要引入消息中间件,都是使用的这个MQ,但是现在用的确实不多了,说白了就是有些过时了。我们去它的官网看一看,你会发现官网已经不活跃了,好久才会更新一次。
它的单机吞吐量是万级,一些小的项目已经够用了,但对于高并发的互联网项目完全不够看。
在高可用上,使用的主从架构的实现。
在消息可靠性上,有较低的概率会丢失数据。
综合以上,其实这个产品基本可以弃用掉了,我们完全可以使用RabbitMQ来代替它。
RocketMQ
RocketMQ,它是阿里开源的消息中间件,久经沙场,非常靠谱。
它支持高吞吐量,能达到10万级,能承受互联网项目高并发的挑战。
在高可用上,它使用的是分布式架构,可以搭建大规模集群,性能很高。
在消息可靠性上,通过配置,可以保证数据的绝对不丢失。
同时它支持大量的高级功能,如:延迟消息、事务消息、消息回溯、死信队列等等。
它非常适合应用于java系统架构中,因为它使用java语言开发的,我们可以去阅读源码了解更深的底层原理。
目前来看,它没有什么特别的缺点,可以支持高并发下的技术挑战,可以基于它实现分布式事务,大型互联网公司和中小型公司都可以选择使用它来作为消息中间件使用,如果我来做技术选型,我首选的中间件就是它。