├── .nojekyll ├── INTRODUCTION.md ├── 03~Kafka ├── 集群部署 │ ├── 集群与高可用.md │ ├── 部署配置.md │ └── 参数配置.md ├── 消息代理 │ ├── ZooKeeper.md │ ├── README.md │ ├── 网络模型.md │ ├── 磁盘读写优化.md │ ├── 日志文件.md │ └── 副本.md ├── 消息生产与消费 │ ├── README.md │ ├── 事务消息.md │ ├── 消费者.md │ └── 生产者.md ├── Kafka Streams │ └── README.md ├── README.md └── 架构概念.md ├── 05~Pulsar ├── 集群部署 │ └── 部署配置.md ├── 消息生产与消费 │ ├── README.md │ └── 消息消费模型.md ├── Pulsar 设计概览.md └── README.md ├── 10~实践案例 ├── 哈啰单车 │ ├── README.md │ └── 2021-哈啰在分布式消息治理和微服务治理中的实践.md └── 从零设计消息队列 │ └── README.md ├── 02~RabbitMQ ├── 接口使用 │ ├── Java.md │ └── Python.md ├── 架构模型 │ ├── 消息消费.md │ ├── 消息存储.md │ └── 消息类型.md ├── 集群部署 │ ├── 集群与高可用.md │ └── 部署配置.md └── README.md ├── 04~RocketMQ ├── 04~内部机制 │ ├── README.md │ └── 99~参考资料 │ │ ├── 2021-Vivo-深入剖析 RocketMQ 源码 │ │ └── 存储模块.md │ │ └── 2022-深度解读 RocketMQ 存储机制.md ├── README.md ├── 02~消息发送与消费 │ ├── 99~参考资料 │ │ ├── 2023-RocketMQ 消息短暂而又精彩的一生.md │ │ └── 2021-RocketMQ 5.0 POP 消费模式探秘.md │ └── 消息消费.md ├── 10~版本变迁 │ └── RocketMQ 5.0 │ │ └── 2022-RocketMQ 5.0: 存储计算分离新思路.md ├── 03~集群与高可用 │ └── 集群与高可用.md └── 01~领域模型 │ └── README.md ├── 01~概念与设计 ├── 02.消息生产与消费 │ ├── 顺序消息.md │ ├── 消息积压处理.md │ ├── 重复消息处理.md │ ├── 消息防丢失.md │ └── 消息传输模型 │ │ ├── 队列模型与流式模型.md │ │ ├── 不同系统的消息传输模型.md │ │ └── 点对点模型与发布订阅模型.md ├── 01~背景与价值 │ ├── README.md │ ├── 消息传递的不同方式.md │ └── 消息队列的应用场景.md ├── README.md ├── 04.开源系统对比 │ ├── 性能对比.md │ ├── README.md │ └── 功能对比.md └── 03.消息存储 │ ├── 消息存储.md │ └── 分区日志.md ├── 99~参考资料 └── 2019~温柔的风~消息队列系统学习笔记 │ └── README.md ├── 06~ActiveMQ ├── README.md └── 架构设计.md ├── .gitignore ├── _sidebar.md ├── README.md ├── index.html ├── header.svg └── LICENSE /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /INTRODUCTION.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /03~Kafka/集群部署/集群与高可用.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /05~Pulsar/集群部署/部署配置.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /10~实践案例/哈啰单车/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02~RabbitMQ/接口使用/Java.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /04~RocketMQ/04~内部机制/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /01~概念与设计/02.消息生产与消费/顺序消息.md: -------------------------------------------------------------------------------- 1 | # 顺序消息 2 | -------------------------------------------------------------------------------- /02~RabbitMQ/架构模型/消息消费.md: -------------------------------------------------------------------------------- 1 | # 消息消费模型 2 | -------------------------------------------------------------------------------- /01~概念与设计/02.消息生产与消费/消息积压处理.md: -------------------------------------------------------------------------------- 1 | # 消息积压处理 2 | -------------------------------------------------------------------------------- /01~概念与设计/02.消息生产与消费/重复消息处理.md: -------------------------------------------------------------------------------- 1 | # 重复消息处理 2 | -------------------------------------------------------------------------------- /02~RabbitMQ/接口使用/Python.md: -------------------------------------------------------------------------------- 1 | # Routing | 路由 2 | -------------------------------------------------------------------------------- /02~RabbitMQ/架构模型/消息存储.md: -------------------------------------------------------------------------------- 1 | # RabbitMQ 数据存储 2 | -------------------------------------------------------------------------------- /02~RabbitMQ/集群部署/集群与高可用.md: -------------------------------------------------------------------------------- 1 | # RabbitMQ 集群与高可用 2 | -------------------------------------------------------------------------------- /05~Pulsar/消息生产与消费/README.md: -------------------------------------------------------------------------------- 1 | # Pulsar 消息生产与消费 2 | -------------------------------------------------------------------------------- /04~RocketMQ/README.md: -------------------------------------------------------------------------------- 1 | # RocketMQ 2 | 3 | # Links 4 | -------------------------------------------------------------------------------- /03~Kafka/消息代理/ZooKeeper.md: -------------------------------------------------------------------------------- 1 | # Links 2 | 3 | - https://cubox.pro/c/LUhyY9 消息系统兴起二次革命:Kafka 不需要 ZooKeeper 4 | -------------------------------------------------------------------------------- /99~参考资料/2019~温柔的风~消息队列系统学习笔记/README.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://www.cnblogs.com/wt645631686/category/1534014.html) 2 | -------------------------------------------------------------------------------- /04~RocketMQ/04~内部机制/99~参考资料/2021-Vivo-深入剖析 RocketMQ 源码/存储模块.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://juejin.cn/post/7028385090036039717) 2 | -------------------------------------------------------------------------------- /06~ActiveMQ/README.md: -------------------------------------------------------------------------------- 1 | # ActiveMQ 2 | 3 | ActiveMQ 是最常用、特性最丰富的消息中间件,通常用于消息异步通信、削峰解耦等多种场景,是 JMS 规范的实现者之一。功能丰富到什么程度呢?支持大部分消息协议,而且支持 XA。 4 | -------------------------------------------------------------------------------- /04~RocketMQ/04~内部机制/99~参考资料/2022-深度解读 RocketMQ 存储机制.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://juejin.cn/post/7117168703678414862) 2 | 3 | # 深度解读 RocketMQ 存储机制 4 | -------------------------------------------------------------------------------- /04~RocketMQ/02~消息发送与消费/99~参考资料/2023-RocketMQ 消息短暂而又精彩的一生.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://juejin.cn/post/7186880907582636069) 2 | 3 | # RocketMQ 消息短暂而又精彩的一生 4 | -------------------------------------------------------------------------------- /04~RocketMQ/02~消息发送与消费/99~参考资料/2021-RocketMQ 5.0 POP 消费模式探秘.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://developer.aliyun.com/article/801815) 2 | 3 | # RocketMQ 5.0 POP 消费模式探秘 4 | -------------------------------------------------------------------------------- /04~RocketMQ/10~版本变迁/RocketMQ 5.0/2022-RocketMQ 5.0: 存储计算分离新思路.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://juejin.cn/post/7102855046823411719) 2 | 3 | # RocketMQ 5.0: 存储计算分离新思路 4 | -------------------------------------------------------------------------------- /05~Pulsar/Pulsar 设计概览.md: -------------------------------------------------------------------------------- 1 | # Pulsar 设计概览 2 | 3 | ![Pulsar 流程概览](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/superbed/2021/07/30/6103ff985132923bf8ddae56.jpg) 4 | 5 | # Links 6 | 7 | - https://mp.weixin.qq.com/s/toUvzn9mc_jmamxrFmLSPA 5000 字阐述云原生消息中间件 Apache Pulsar 的核心特性和设计概览 8 | -------------------------------------------------------------------------------- /04~RocketMQ/03~集群与高可用/集群与高可用.md: -------------------------------------------------------------------------------- 1 | # 集群与高可用 2 | 3 | # 负载均衡 4 | 5 | Producer 端的负载均衡实际是对于 Broker 的选择,目前包含以下两种: 6 | 7 | - Round Robin 容错:即按照队列列表轮询,当上一次请求的 Broker 出现异常时,本次请求会通过简单容错机制进行跳过,这是默认的策略。 8 | - RT 排序与延时容错:维护一个 Map,统计发送消息的每个 broker 的 RT,优先从 RT 较低的几个 broker 中选择,当某个 broker 出现异常时,通过延时容错机制,使其在一定时间内更难被选中。 9 | -------------------------------------------------------------------------------- /06~ActiveMQ/架构设计.md: -------------------------------------------------------------------------------- 1 | # ActiveMQ 架构设计概要 2 | 3 | ActiveMQ 提供两种可供实施的架构模型:“M-S”和“network bridge”;其中“M-S”是 HA 方案,“网络转发桥”用于实现“分布式队列”。 4 | 5 | # links 6 | 7 | - ActiveMQ 架构设计与最佳实践,需要一万字 https://mp.weixin.qq.com/s?__biz=MzA4MTc4NTUxNQ==&mid=2650520899&idx=1&sn=48c5de709f20b97466353debc3ef2ce3&scene=21#wechat_redirect 8 | -------------------------------------------------------------------------------- /02~RabbitMQ/README.md: -------------------------------------------------------------------------------- 1 | # RabbitMQ 2 | 3 | RabbitMQ 是一个由 Erlang 开发的 AMQP(Advanved Message Queue)的开源实现,是经典的消息代理(Message Broker)/消息队列(Message Queue);AMQP 高级消息队列协议是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。 4 | 5 | ## 特性 6 | 7 | # Links 8 | 9 | - https://cloud.tencent.com/developer/article/1452656 10 | -------------------------------------------------------------------------------- /01~概念与设计/01~背景与价值/README.md: -------------------------------------------------------------------------------- 1 | # 消息队列的通用概念 2 | 3 | - AMQP:AMQP 是一种开放的 Internet 协议,用于可靠地发送和接收消息。 4 | 5 | - MOM:面向消息的中间件是一种方法,是一种分布式系统的体系结构,即整个分布式系统的中间层,其中存在大量内部通信(一个组件正在查询数据,然后需要将其发送给另一个 组件,它将对数据进行一些处理),因此组件必须在它们之间共享信息/数据。 6 | 7 | - Message Broker 消息代理:是在 MOM 中处理消息(发送和接收)的任何系统,或更确切地说,是将消息路由到特定使用者/收件人的任何系统。消息代理通常基于 MOM 构建。MOM 提供了应用程序之间的基本通信,以及诸如消息持久性和保证的传递之类的东西。“消息代理是面向消息的中间件的构建块。” 8 | -------------------------------------------------------------------------------- /03~Kafka/消息代理/README.md: -------------------------------------------------------------------------------- 1 | # Kafka 架构模型 2 | 3 | 如下图所示,一个典型的 Kafka 集群中包含若干 Producer(可以是 web 前端产生的 Page View,或者是服务器日志,系统 CPU、Memory 等),若干 broker(Kafka 支持水平扩展,一般 broker 数量越多,集群吞吐率越高),若干 Consumer Group,以及一个 Zookeeper 集群。Kafka 通过 Zookeeper 管理集群配置,选举 leader,以及在 Consumer Group 发生变化时进行 rebalance。Producer 使用 push 模式将消息发布到 broker,Consumer 使用 pull 模式从 broker 订阅并消费消息。 4 | 5 | ![Kafka 体系架构](https://pic.imgdb.cn/item/6076e7628322e6675ceae6a3.jpg) 6 | -------------------------------------------------------------------------------- /01~概念与设计/README.md: -------------------------------------------------------------------------------- 1 | # 消息队列基础概念 2 | 3 | # Links 4 | 5 | - https://mp.weixin.qq.com/s/hYfTl8eR2Vkue8-EpgZY7g 6 | - https://mp.weixin.qq.com/s/y3CheyPMJpLpD3pB3lTT9g 7 | - https://zhuanlan.zhihu.com/p/34700753 8 | - https://zhuanlan.zhihu.com/p/72728396 9 | - https://mp.weixin.qq.com/s/nAjYhuN3ptAqaE67bqc4aQ 10 | - https://github.com/qunarcorp/qmq 学习 qmq 中的实践 11 | - https://mp.weixin.qq.com/s/j905XZcZewI-0m-mcU7Wng 消息中间件的四种投递模式对比 12 | -------------------------------------------------------------------------------- /02~RabbitMQ/集群部署/部署配置.md: -------------------------------------------------------------------------------- 1 | # RabbitMQ 部署配置 2 | 3 | # 单机 Docker 化部署 4 | 5 | ```sh 6 | # 单机方式启动 7 | $ docker run -d -p 4369:4369 -p 5671:5671 -p 5672:5672 --hostname rabbit --name rabbit rabbitmq:3 8 | 9 | # 集群方式启动 10 | ``` 11 | 12 | 常用控制: 13 | 14 | ```sh 15 | # 清除当前队列中消息 16 | $ docker exec -it rabbit rabbitmqctl stop_app && \ 17 | docker exec -it rabbit rabbitmqctl reset && \ 18 | docker exec -it rabbit rabbitmqctl start_app 19 | 20 | # 查看当前队列 21 | $ docker exec -it rabbit rabbitmqctl list_queues 22 | ``` 23 | -------------------------------------------------------------------------------- /05~Pulsar/README.md: -------------------------------------------------------------------------------- 1 | # Pulsar 2 | 3 | Apache Pulsar 是一个开源的 pub-sub(发布-订阅)消息与流媒体平台,与 Apache Kafka 在同一领域展开竞争。它提供了我们所期望的功能,如低延迟的异步及同步消息传递,可进行容量伸缩的消息持久化存储,以及多种客户端程序库。Pulsar 吸引我们的地方,是能轻易实现容量伸缩,尤其适合在多用户类型的大型组织中使用。Pulsar 原生支持多租户、异地备份、基于角色的访问控制和计费隔离。此外,我们还期望 Pulsar 能解决高容量数据系统中,消息日志无限增长的问题。在这些系统中,事件一旦持久化,就需要无限期地保存,并且订阅者可以从历史节点开始订阅消息。这是通过一个分层存储的模型来实现的。尽管 Pulsar 对于大型组织是一个有前景的平台,但依然有提升的空间。目前,安装 Pulsar 需要管理 ZooKeeper 和 BookKeeper 以及其他工具。我们期待随着 Pulsar 的日益普及,用户很快能够得到更广泛的社区支持。 4 | 5 | # Links 6 | 7 | - https://mp.weixin.qq.com/s/v4A--nGiDTt58pZyIzepeg 比拼 Kafka,大数据分析新秀 Pulsar 到底好在哪 8 | -------------------------------------------------------------------------------- /03~Kafka/消息代理/网络模型.md: -------------------------------------------------------------------------------- 1 | # Kafka 网络模型 2 | 3 | # Kafka Client:单线程 Selector 4 | 5 | ![单线程 Selector](https://pic.imgdb.cn/item/6077b06b8322e6675c188f53.png) 6 | 7 | 单线程模式适用于并发链接数小,逻辑简单,数据量小。在 kafka 中,consumer 和 producer 都是使用的上面的单线程模式。这种模式不适合 kafka 的服务端,在服务端中请求处理过程比较复杂,会造成线程阻塞,一旦出现后续请求就会无法处理,会造成大量请求超时,引起雪崩。而在服务器中应该充分利用多线程来处理执行逻辑。 8 | 9 | # Kafka Server:多线程 Selector 10 | 11 | ![Kafka Server](https://pic.imgdb.cn/item/6077dc738322e6675c6e637c.png) 12 | 13 | 在 kafka 服务端采用的是多线程的 Selector 模型,Acceptor 运行在一个单独的线程中,对于读取操作的线程池中的线程都会在 selector 注册 read 事件,负责服务端读取请求的逻辑。成功读取后,将请求放入 message queue 共享队列中。然后在写线程池中,取出这个请求,对其进行逻辑处理,即使某个请求线程阻塞了,还有后续的县城从消息队列中获取请求并进行处理,在写线程中处理完逻辑处理,由于注册了 OP_WIRTE 事件,所以还需要对其发送响应。 14 | -------------------------------------------------------------------------------- /01~概念与设计/04.开源系统对比/性能对比.md: -------------------------------------------------------------------------------- 1 | # 框架性能对比 2 | 3 | 在 [2020~Benchmarking Apache Kafka,Apache Pulsar,and RabbitMQ: Which is the Fastest?](https://www.confluent.io/blog/kafka-fastest-messaging-system/) 一文中对 Kafka、RabbitMQ 和 Pulsar 这三种消息系统进行了全面、均衡的分析: 4 | 5 | - 吞吐量:Kafka 在三个系统中的吞吐量最高,是 RabbitMQ 的 15 倍,Pulsar 的 2 倍。 6 | - 延迟:Kafka 在较高的吞吐量下提供了最低的延迟,同时还提供了强大的持久性和高可用性。在默认配置下,Kafka 在所有延迟基准测试中都要比 Pulsar 快,而且,当设置为 fsync 每条消息时,一直到 p99.9 百分位,它都更快。RabbitMQ 可以实现比 Kafka 更低的端到端延迟,但只能在吞吐量低很多的情况下。 7 | - 成本/复杂性:成本往往是性能的逆函数。作为具有最高稳定吞吐量的系统,由于其高效的设计,Kafka 提供了所有系统中最好的价值(即每字节写入成本)。事实上,Twitter 的 Kafka 之 旅 远离了像 Pulsar 这样的基于 BookKeeper 的架构,这证实了我们的观察:Kafka 相对较少的移动部件显著降低了它的成本(在 Twitter 的情况下高达 75%)。此外,将 ZooKeeper 从 Apache Kafka 中移除的工作(参见 KIP-500)正在进行中,这进一步简化了 Kafka 的架构。 8 | -------------------------------------------------------------------------------- /01~概念与设计/02.消息生产与消费/消息防丢失.md: -------------------------------------------------------------------------------- 1 | # 消息防丢失 2 | 3 | ## 确认与重新交付 4 | 5 | 消费随时可能会崩溃,所以有一种可能的情况是:代理向消费者递送消息,但消费者没有处理,或者在消费者崩溃之前只进行了部分处理。为了确保消息不会丢失,消息代理使用确认(acknowledgments):客户端必须显式告知代理消息处理完毕的时间,以便代理能将消息从队列中移除。如果与客户端的连接关闭,或者代理超出一段时间未收到确认,代理则认为消息没有被处理,因此它将消息再递送给另一个消费者。请注意可能发生这样的情况,消息实际上是处理完毕的,但确认在网络中丢失了。需要一种原子提交协议才能处理这种情况。 6 | 7 | 当与负载均衡相结合时,这种重传行为对消息的顺序有种有趣的影响。在下图中,消费者通常按照生产者发送的顺序处理消息。然而消费者 2 在处理消息 m3 时崩溃,与此同时消费者 1 正在处理消息 m4。未确认的消息 m3 随后被重新发送给消费者 1,结果消费者 1 按照 m4,m3,m5 的顺序处理消息。因此 m3 和 m4 的交付顺序与以生产者 1 的发送顺序不同。 8 | 9 | ![在处理m3时消费者2崩溃,因此稍后重传至消费者1](https://s2.ax1x.com/2020/02/13/1OgJ7F.md.png) 10 | 11 | 即使消息代理试图保留消息的顺序(如 JMS 和 AMQP 标准所要求的),负载均衡与重传的组合也不可避免地导致消息被重新排序。为避免此问题,你可以让每个消费者使用单独的队列(即不使用负载均衡功能)。如果消息是完全独立的,则消息顺序重排并不是一个问题。但正如我们将在本章后续部分所述,如果消息之间存在因果依赖关系,这就是一个很重要的问题。 12 | -------------------------------------------------------------------------------- /10~实践案例/哈啰单车/2021-哈啰在分布式消息治理和微服务治理中的实践.md: -------------------------------------------------------------------------------- 1 | # 哈啰在分布式消息治理和微服务治理中的实践 2 | 3 | 公司之前使用RabbitMQ,下面在使用RabbitMQ时的痛点,其中很多事故由于RabbitMQ集群限流引起的。 4 | 5 | - 积压过多是清理还是不清理?这是个问题,我再想想 6 | - 积压过多触发集群流控?那是真的影响业务了 7 | - 想消费前两天的数据?请您重发一遍吧 8 | - 要统计哪些服务接入了?您要多等等了,我得去捞IP看看 9 | - 有没有使用风险比如大消息?这个我猜猜 10 | 11 | # 打造分布式消息治理平台 12 | 13 | 旨在屏蔽底层各个中间件(RocketMQ/Kafka)的复杂性,通过唯一标识动态路由消息。同时打造集资源管控、检索、监控、告警、巡检、容灾、可视化运维等一体化的消息治理平台,保障消息中间件平稳健康运行。 14 | 消息治理平台设计需要考虑的点 15 | 16 | - 提供简单易用API 17 | - 有哪些关键点能衡量客户端的使用没有安全隐患 18 | - 有哪些关键指标能衡量集群健康不健康 19 | - 有哪些常用的用户/运维操作将其可视化 20 | - 有哪些措施应对这些不健康 21 | 22 | ## 尽可能简单易用 23 | 24 | - 设计指南:把复杂的问题搞简单,那是能耐 25 | - 极简统一API:提供统一的SDK封装了(Kafka/RocketMQ)两种消息中间件。 26 | - 一次申请:主题消费组自动创建不适合生产环境,自动创建会导致失控,不利于整个生命周期管理和集群稳定。需要对申请流程进行控制,但是应尽可能简单。例如:一次申请各个环境均生效、生成关联告警规则等。 27 | 28 | ## 客户端治理 29 | 30 | 监控客户端使用是否规范,找到合适的措施治理。 31 | 32 | -------------------------------------------------------------------------------- /04~RocketMQ/01~领域模型/README.md: -------------------------------------------------------------------------------- 1 | # RocketMQ 领域模型概述 2 | 3 | Apache RocketMQ 是一款典型的分布式架构下的中间件产品,使用异步通信方式和发布订阅的消息传输模型。通信方式和传输模型的具体说明,请参见下文通信方式介绍和消息传输模型介绍。Apache RocketMQ 产品具备异步通信的优势,系统拓扑简单、上下游耦合较弱,主要应用于异步解耦,流量削峰填谷等场景。 4 | 5 | ![领域模型](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230409175705.png) 6 | 7 | 如上图所示,Apache RocketMQ 中消息的生命周期主要分为消息生产、消息存储、消息消费这三部分。生产者生产消息并发送至 Apache RocketMQ 服务端,消息被存储在服务端的主题中,消费者通过订阅主题消费消息。 8 | 9 | - 消息生产 10 | 11 | - 生产者(Producer):Apache RocketMQ 中用于产生消息的运行实体,一般集成于业务调用链路的上游。生产者是轻量级匿名无身份的。 12 | 13 | - 消息存储 14 | 15 | - 主题(Topic):Apache RocketMQ 消息传输和存储的分组容器,主题内部由多个队列组成,消息的存储和水平扩展实际是通过主题内的队列实现的。 16 | - 队列(MessageQueue):Apache RocketMQ 消息传输和存储的实际单元容器,类比于其他消息队列中的分区。Apache RocketMQ 通过流式特性的无限队列结构来存储消息,消息在队列内具备顺序性存储特征。 17 | - 消息(Message):Apache RocketMQ 的最小传输单元。消息具备不可变性,在初始化发送和完成存储后即不可变。 18 | 19 | - 消息消费 20 | 21 | - 消费者分组(ConsumerGroup):Apache RocketMQ 发布订阅模型中定义的独立的消费身份分组,用于统一管理底层运行的多个消费者(Consumer)。同一个消费组的多个消费者必须保持消费逻辑和配置一致,共同分担该消费组订阅的消息,实现消费能力的水平扩展。 22 | - 消费者(Consumer):Apache RocketMQ 消费消息的运行实体,一般集成在业务调用链路的下游。消费者必须被指定到某一个消费组中。 23 | - 订阅关系(Subscription):Apache RocketMQ 发布订阅模型中消息过滤、重试、消费进度的规则配置。订阅关系以消费组粒度进行管理,消费组通过定义订阅关系控制指定消费组下的消费者如何实现消息过滤、消费重试及消费进度恢复等。Apache RocketMQ 的订阅关系除过滤表达式之外都是持久化的,即服务端重启或请求断开,订阅关系依然保留。 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all 2 | * 3 | 4 | # Unignore all with extensions 5 | !*.* 6 | 7 | # Unignore all dirs 8 | !*/ 9 | 10 | .DS_Store 11 | 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | 71 | # next.js build output 72 | .next 73 | -------------------------------------------------------------------------------- /04~RocketMQ/02~消息发送与消费/消息消费.md: -------------------------------------------------------------------------------- 1 | # 消息消费 2 | 3 | # 超时时间 4 | 5 | ```java 6 | // Producer 级别配置 7 | DefaultMQProducer producer = new DefaultMQProducer(); 8 | producer.setSendMsgTimeout(1000); 9 | 10 | // 请求级别配置 11 | // 同步发送 12 | defaultMQProducer.send(message, 100); 13 | // 异步发送 14 | defaultMQProducer.send(message, sendCallback, 100); 15 | ``` 16 | 17 | 需要注意的是,async 和 oneway 两种发送方式的超时时间和同步发送有比较大的区别,这两种方式下,每个消息发送者都有一个资源信号量来控制发送的并发度,获取资源锁可能存在等待耗时。 18 | 19 | # 失败重试 20 | 21 | 发送失败在不同的场景和需求下的定义都不相同,于是我们需要具体问题具体分析,以同步发送为例: 22 | 23 | - 如果我们希望保证消息不丢,那么 Broker.Master 就必须同步刷盘成功; 24 | - 如果我们希望消息不丢的同时,如果 Master 故障,消费者也能立马消费到消息,那么 Broker.Slave 也必须同步刷盘成功; 25 | - 如果我们能容忍掉电级别导致的消息丢失,那么 Broker.Master 只需要写入 PageCache 即可。 26 | 27 | 对高可靠的要求不同,Broker 的刷盘策略及 HA 策略也各不相同,Producer 的处理逻辑自然也就不同,失败又可以分为以下几类: 28 | 29 | - 系统失败:客户端异常:Producer 无法获取 broker 的地址;通讯层面的异常:连接不可用、请求超时等;Broker 异常:磁盘满了、创建文件失败、写入 PageCache 超时等。可能抛出 MQClientException、RemotingException、MQBrokerException。 30 | - 业务失败:消息 Topic 长度超过上限;消息体大小超过上限;消息的 properties 长度超过上限等。可能抛出 MQClientException、MQBrokerException。 31 | - 节点失败:Broker.Master 刷盘失败,Broker.Slave 不可用或刷盘超时;无异常,根据发送返回值 SendResult.sendStatus 来判断。 32 | 33 | 针对系统失败和业务失败,我们可以通过 DefaultMQProducer.retryTimesWhenSendFailed 来配置重试次数,对于高可用失败,可以通过 DefaultMQProducer.retryAnotherBrokerWhenNotStoreOK 来配置切换 broker 的重试。 34 | 35 | 如果为了保证消息不丢,只要消息在 Master 同步落盘即可: 36 | 37 | - Broker 的刷盘策略需要配置为同步刷盘,即 `FlushDiskType==SYNC_FLUSH`。 38 | - Producer 在发送消息时,properties 中的“WAIT”属性设置为“true”,表示客户端同步等待刷盘完成。 39 | - 客户端需要手动检查发送状态,保证 `SendResult.sendStatus=SEND_OK`。 40 | 41 | 为了性能,我们都是采用同步写 PageCache 与异步刷盘的策略,甚至是同步写预分配内存与异步写、PageCache 与异步刷盘。 42 | -------------------------------------------------------------------------------- /02~RabbitMQ/架构模型/消息类型.md: -------------------------------------------------------------------------------- 1 | # RabbitMQ 消息模型 2 | 3 | AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP 中增加了 Exchange 和 Binding 的角色。生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送到那个队列。 4 | 5 | ![image](https://user-images.githubusercontent.com/5803001/51668768-e32bdb80-1ffd-11e9-9a32-486690a335d7.png) 6 | 7 | Exchange 分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers。headers 匹配 AMQP 消息的 header 而不是路由键,此外 headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了,所以直接看另外三种类型: 8 | 9 | ![image.png](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230430222238.png) 10 | 11 | - 消息中的路由键(routing key)如果和 Binding 中的 binding key 一致,交换器就将消息发到对应的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发 routing key 标记为“dog”的消息,不会转发“dog.puppy”,也不会转发“dog.guard”等等。它是完全匹配、单播的模式。 12 | 13 | - 每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。 14 | 15 | - topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“#”和符号“”。#匹配 0 个或多个单词,匹配不多不少一个单词。 16 | 17 | ![image.png](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230430222221.png) 18 | 19 | 在某个实际的分布式多目标任务处理场景中,我们会以 Pub/Sub 模式,从主节点向子节点发送目标信息,此时子节点会开始监听不同的队列。然后使用 Routing 模式针对某个具体的任务队列拉取并且执行任务,而通过 Topic 模式来搜集处理日志与统计信息。 20 | 21 | ## 优先级队列 22 | 23 | 生产者生成消息打到交换机里面(如果没有声明交换机,会打到 default exchange 里面),交换机绑定一个或多个队列,消息进入队列里面,消费者一直在监听队列,发现队列里面有消息就开始消费,这里就是一个消息传递的过程,queue 是一个栈队列,栈是先进先出的,就是说消息来了依次排队,一个队列并不能实现消息的插队和优先推送的功能。但是如果说我们的多个队列有不同的优先级,不同优先级的消息通过 roatingkey 进入不同的队列,优先级高的队列消息被优先消费,这样也能形成一个相对意义上的优先级,所以说这里不是消息的优先级而是队列的优先级。 24 | 25 | # 消息分发 26 | 27 | # 消息消费 28 | -------------------------------------------------------------------------------- /01~概念与设计/02.消息生产与消费/消息传输模型/队列模型与流式模型.md: -------------------------------------------------------------------------------- 1 | # 队列与流式 2 | 3 | 以单个管道(也就是后续的分区的语义)为视角,可以分为队列与流式这两种语义,这两种语义最适合使用的场景有所不同。 4 | 5 | - 队列(Queue)语义:队列语义主要是采用无序或者共享的方式来消费消息。通过队列语义,用户可以创建多个消费者从单个管道中接收消息;当一条消息从队列发送出来后,多个消费者中的只有一个(任何一个都有可能)接收和消费这条消息。消息系统的具体实现决定了最终哪个消费者实际接收到消息。队列语义通常与无状态应用程序一起结合使用。无状态应用程序不关心排序,但它们确实需要能够确认(ack)或删除单条消息,以及尽可能地扩展消费并行性的能力。典型的基于队列语义的消息系统包括 RabbitMQ 和 RocketMQ。在队列消息系统中,一个队列可能有多个 Producer 和 Consumer。Producer 向队列发送消息,Consumer 从队列中接收消息。接收消息后,Consumer 开始处理消息,并在处理完每条消息后向队列消息系统发送 ack。由于多个 Consumer 共用一个队列,消息顺序并不重要,因此基于队列的系统很容易对 Consumer 进行扩展。消息队列系统适用于不需要按特定顺序执行任务的队列,例如,发送同一封邮件给多个收件人。RabbitMQ 和 Amazon SQS 都是基于队列的消息系统。 6 | 7 | - 流式(Stream)语义:相比之下,流式语义要求消息的消费严格排序或独占消息消费。对于一个管道,使用流式语义,始终只会有一个消费者使用和消费消息。消费者按照消息写入管道的确切顺序接收从管道发送的消息。流语义通常与有状态应用程序相关联。有状态的应用程序更加关注消息的顺序及其状态。消息的消费顺序决定了有状态应用程序的状态。消息的顺序将影响应用程序处理逻辑的正确性。在流消息系统中,Producer 追加数据到“仅追加”消息流中。在每个消息流中,必须按特定顺序处理消息,Consumer 在消息流中标记消息的位置。我们可以采取某种策略(如对用户 ID 进行哈希处理)对消息进行分区,使分区成为单独的数据流,增加并行度。由于每个流中的数据不可变,且只保存偏移 entry,因此处理时不会遗漏消息。流适用于重视消息顺序(如提取数据)的场景。Kafka 和 Amazon Kinesis 都使用流语义处理消息。 8 | 9 | 基于不同的消息语义,我们又可以从通讯模式的角度,理解消息队列可能会具备以下模式: 10 | 11 | - 点对点通讯:点对点方式是最为传统和常见的通讯方式,它支持一对一、一对多、多对多、多对一等多种配置方式,支持树状、网状等多种拓扑结构。 12 | - 多点广播:MQ 适用于不同类型的应用。其中重要的,也是正在发展中的是"多点广播"应用,即能够将消息发送到多个目标站点 (Destination List)。可以使用一条 MQ 指令将单一消息发送到多个目标站点,并确保为每一站点可靠地提供信息。MQ 不仅提供了多点广播的功能,而且还拥有智能消息分发功能,在将一条消息发送到同一系统上的多个用户时,MQ 将消息的一个复制版本和该系统上接收者的名单发送到目标 MQ 系统。目标 MQ 系统在本地复制这些消息,并将它们发送到名单上的队列,从而尽可能减少网络的传输量。 13 | - 发布/订阅(Publish/Subscribe)模式:发布/订阅功能使消息的分发可以突破目的队列地理指向的限制,使消息按照特定的主题甚至内容进行分发,用户或应用程序可以根据主题或内容接收到所需要的消息。发布/订阅功能使得发送者和接收者之间的耦合关系变得更为松散,发送者不必关心接收者的目的地址,而接收者也不必关心消息的发送地址,而只是根据消息的主题进行消息的收发。 14 | - 集群(Cluster):为了简化点对点通讯模式中的系统配置,MQ 提供 Cluster(群集) 的解决方案。群集类似于一个域 (Domain),群集内部的队列管理器之间通讯时,不需要两两之间建立消息通道,而是采用群集 (Cluster) 通道与其它成员通讯,从而大大简化了系统配置。此外,群集中的队列管理器之间能够自动进行负载均衡,当某一队列管理器出现故障时,其它队列管理器可以接管它的工作,从而大大提高系统的高可靠性。 15 | -------------------------------------------------------------------------------- /03~Kafka/消息生产与消费/README.md: -------------------------------------------------------------------------------- 1 | # Kafka 接口使用 2 | 3 | Kafka 有四个核心 API,它们分别是 4 | 5 | - Producer API,它允许应用程序向一个或多个 topics 上发送消息记录 6 | - Consumer API,允许应用程序订阅一个或多个 topics 并处理为其生成的记录流 7 | - Streams API,它允许应用程序作为流处理器,从一个或多个主题中消费输入流并为其生成输出流,有效的将输入流转换为输出流。 8 | - Connector API,它允许构建和运行将 Kafka 主题连接到现有应用程序或数据系统的可用生产者和消费者。例如,关系数据库的连接器可能会捕获对表的所有更改 9 | 10 | ![Kafka APIs](https://pic.imgdb.cn/item/6076e7c08322e6675ceba736.jpg) 11 | 12 | 作为一个消息系统,Kafka 遵循了传统的方式,选择由 Producer 向 broker push 消息并由 Consumer 从 broker pull 消息。push 模式很难适应消费速率不同的消费者,因为消息发送速率是由 broker 决定的。push 模式的目标是尽可能以最快速度传递消息,但是这样很容易造成 Consumer 来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而 pull 模式则可以根据 Consumer 的消费能力以适当的速率消费消息。对于 Kafka 而言,pull 模式更合适。pull 模式可简化 broker 的设计,Consumer 可自主控制消费消息的速率,同时 Consumer 可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。 13 | 14 | # 批量提交 15 | 16 | 我们知道,批量处理是一种非常有效的提升系统吞吐量的方法。在 Kafka 内部,消息都是以“批”为单位处理的。一批消息从发送端到接收端,是如何在 Kafka 中流转的呢? 17 | 18 | ## Producer 端 19 | 20 | 在 Kafka 的客户端 SDK(软件开发工具包)中,Kafka 的 Producer 只提供了单条发送的 send() 方法,并没有提供任何批量发送的接口。原因是,Kafka 根本就没有提供单条发送的功能,是的,你没有看错,虽然它提供的 API 每次只能发送一条消息,但实际上,Kafka 的客户端 SDK 在实现消息发送逻辑的时候,采用了异步批量发送的机制。 21 | 22 | 当你调用 send() 方法发送一条消息之后,无论你是同步发送还是异步发送,Kafka 都不会立即就把这条消息发送出去。它会先把这条消息,存放在内存中缓存起来,然后选择合适的时机把缓存中的所有消息组成一批,一次性发给 Broker。简单地说,就是攒一波一起发。在 Kafka 的服务端,也就是 Broker 这一端,又是如何处理这一批一批的消息呢? 23 | 24 | ## Broker 端 25 | 26 | 在服务端,Kafka 不会把一批消息再还原成多条消息,再一条一条地处理,这样太慢了。Kafka 这块儿处理的非常聪明,每批消息都会被当做一个“批消息”来处理。也就是说,在 Broker 整个处理流程中,无论是写入磁盘、从磁盘读出来、还是复制到其他副本这些流程中,批消息都不会被解开,一直是作为一条“批消息”来进行处理的。 27 | 28 | ## 消费端 29 | 30 | 在消费时,消息同样是以批为单位进行传递的,Consumer 从 Broker 拉到一批消息后,在客户端把批消息解开,再一条一条交给用户代码处理。比如说,你在客户端发送 30 条消息,在业务程序看来,是发送了 30 条消息,而对于 Kafka 的 Broker 来说,它其实就是处理了 1 条包含 30 条消息的“批消息”而已。显然处理 1 次请求要比处理 30 次请求要快得多。 31 | 32 | 构建批消息和解开批消息分别在发送端和消费端的客户端完成,不仅减轻了 Broker 的压力,最重要的是减少了 Broker 处理请求的次数,提升了总体的处理能力。这就是 Kafka 用批量消息提升性能的方法。 33 | -------------------------------------------------------------------------------- /03~Kafka/集群部署/部署配置.md: -------------------------------------------------------------------------------- 1 | # Kafka 集群的配置与部署 2 | 3 | 本假设你本机还没有安装任何的 Zookeeper 节点或者 Kafka 节点,首先我们需要下载 Kafka 源代码然后将其解压缩: 4 | 5 | ```sh 6 | > tar -xzf kafka_2.11-0.9.0.0.tgz 7 | > cd kafka_2.11-0.9.0.0 8 | ``` 9 | 10 | Kafka 默认使用 ZooKeeper 作为服务协调器,并且 Kafka 内置了一个 ZooKeeper 的运行脚本,可以让你快速地启动允许一个单节点的 ZooKeeper 实例: 11 | 12 | ```sh 13 | > bin/zookeeper-server-start.sh config/zookeeper.properties 14 | [2013-04-22 15:01:37,495] INFO Reading configuration from: config/zookeeper.properties (org.apache.zookeeper.server.quorum.QuorumPeerConfig) 15 | ... 16 | ``` 17 | 18 | 然后启动 Kafka 服务器: 19 | 20 | ```sh 21 | > bin/kafka-server-start.sh config/server.properties 22 | [2013-04-22 15:01:47,028] INFO Verifying properties (kafka.utils.VerifiableProperties) 23 | [2013-04-22 15:01:47,051] INFO Property socket.send.buffer.bytes is overridden to 1048576 (kafka.utils.VerifiableProperties) 24 | ... 25 | ``` 26 | 27 | # 使用 28 | 29 | 在启动了 Kafka 服务器之后,我们首先来尝试创建一个名为`test`的主题,设置只有一个 Partition 与一个 Replica。 30 | 31 | ``` 32 | > bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test 33 | ``` 34 | 35 | 然后我们在查询主题列表的时候就可以看到该主题了: 36 | 37 | ``` 38 | > bin/kafka-topics.sh --list --zookeeper localhost:2181 39 | test 40 | ``` 41 | 42 | 除了手动地指定创建主题,我们也可以配置 Brokers 在收到不存在的主题的时候自动创建该主题。在 Topics 创建好之后,就可以通过其来发送消息。Kafka 为我们提供了一个命令行工具,可以允许我们从某个文件或者其他标准的输入流中抓取数据然后将其以消息形式发送到 Kafka 集群,默认情况下每个单独的行会被认为是一条单独的消息。 43 | 44 | ``` 45 | > bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test 46 | This is a message 47 | This is another message 48 | ``` 49 | 50 | 消息发送完毕之后,我们需要设置 Consumer 端来读取消息,Kafka 同样为我们提供了这样一个便捷的命令行工具, 51 | 52 | ``` 53 | > bin/kafka-console-consumer.sh --zookeeper localhost:2181 --topic test --from-beginning 54 | This is a message 55 | This is another message 56 | ``` 57 | 58 | 如果你在不同的命令行中分别运行上述命令,那么你的 Consumer 已经能够接收到这些数据了。 59 | -------------------------------------------------------------------------------- /01~概念与设计/02.消息生产与消费/消息传输模型/不同系统的消息传输模型.md: -------------------------------------------------------------------------------- 1 | # 不同系统的消息模型 2 | 3 | ## RabbitMQ 4 | 5 | RabbitMQ 是少数依然坚持使用队列模型的产品之一。在 RabbitMQ 中,Exchange 位于生产者和队列之间,生产者并不关心将消息发送给哪个队列,而是将消息发送给 Exchange,由 Exchange 上配置的策略来决定将消息投递到哪些队列中。 6 | 7 | ![RabbitMQ 的消息模型](https://pic.imgdb.cn/item/60856b2dd1a9ae528fedd017.jpg) 8 | 9 | 同一份消息如果需要被多个消费者来消费,需要配置 Exchange 将消息发送到多个队列,每个队列中都存放一份完整的消息数据,可以为一个消费者提供消费服务。这也可以变相地实现新发布-订阅模型中,“一份消息数据可以被多个订阅者来多次消费”这样的功能。 10 | 11 | ## RocketMQ & Kafka 的消息模型 12 | 13 | RocketMQ 使用的消息模型是标准的发布-订阅模型,在 RocketMQ 的术语表中,生产者、消费者和主题与我在上面讲的发布-订阅模型中的概念是完全一样的。但是,在 RocketMQ 也有队列(Queue)这个概念,并且队列在 RocketMQ 中是一个非常重要的概念。 14 | 15 | 几乎所有的消息队列产品都使用一种非常朴素的“请求-确认”机制,确保消息不会在传递过程中由于网络或服务器故障丢失。具体的做法也非常简单。在生产端,生产者先将消息发送给服务端,也就是 Broker,服务端在收到消息并将消息写入主题或者队列中后,会给生产者发送确认的响应。如果生产者没有收到服务端的确认或者收到失败的响应,则会重新发送消息;在消费端,消费者在收到消息并完成自己的消费业务逻辑(比如,将数据保存到数据库中)后,也会给服务端发送消费成功的确认,服务端只有收到消费确认后,才认为一条消息被成功消费,否则它会给消费者重新发送这条消息,直到收到对应的消费成功确认。 16 | 17 | 这个确认机制很好地保证了消息传递过程中的可靠性,但是,引入这个机制在消费端带来了一个不小的问题。为了确保消息的有序性,在某一条消息被成功消费之前,下一条消息是不能被消费的,否则就会出现消息空洞,违背了有序性这个原则。也就是说,每个主题在任意时刻,至多只能有一个消费者实例在进行消费,那就没法通过水平扩展消费者的数量来提升消费端总体的消费性能。为了解决这个问题,RocketMQ 在主题下面增加了队列的概念。 18 | 19 | 每个主题包含多个队列,通过多个队列来实现多实例并行生产和消费。需要注意的是,RocketMQ 只在队列上保证消息的有序性,主题层面是无法保证消息的严格顺序的。每个主题包含多个队列,通过多个队列来实现多实例并行生产和消费。需要注意的是,RocketMQ 只在队列上保证消息的有序性,主题层面是无法保证消息的严格顺序的。 20 | 21 | RocketMQ 中,订阅者的概念是通过消费组(Consumer Group)来体现的。每个消费组都消费主题中一份完整的消息,不同消费组之间消费进度彼此不受影响,也就是说,一条消息被 Consumer Group1 消费过,也会再给 Consumer Group2 消费。消费组中包含多个消费者,同一个组内的消费者是竞争消费的关系,每个消费者负责消费组内的一部分消息。如果一条消息被消费者 Consumer1 消费了,那同组的其他消费者就不会再收到这条消息。 22 | 23 | 在 Topic 的消费过程中,由于消息需要被不同的组进行多次消费,所以消费完的消息并不会立即被删除,这就需要 RocketMQ 为每个消费组在每个队列上维护一个消费位置(Consumer Offset),这个位置之前的消息都被消费过,之后的消息都没有被消费过,每成功消费一条消息,消费位置就加一。这个消费位置是非常重要的概念,我们在使用消息队列的时候,丢消息的原因大多是由于消费位置处理不当导致的。 24 | 25 | ![RocketMQ 消息消费模型](https://pic.imgdb.cn/item/60856bbad1a9ae528ff39b2f.jpg) 26 | 27 | Kafka 的消息模型和 RocketMQ 是完全一样的,我刚刚讲的所有 RocketMQ 中对应的概念,和生产消费过程中的确认机制,都完全适用于 Kafka。唯一的区别是,在 Kafka 中,队列这个概念的名称不一样,Kafka 中对应的名称是“分区(Partition)”,含义和功能是没有任何区别的。 28 | -------------------------------------------------------------------------------- /10~实践案例/从零设计消息队列/README.md: -------------------------------------------------------------------------------- 1 | # 消息队列的设计 2 | 3 | 消息队列中核心的三个角色为: 4 | 5 | - Broker(服务端):MQ 中最核心的部分,是 MQ 的服务端,核心逻辑几乎全在这里,它为生产者和消费者提供 RPC 接口,负责消息的存储、备份和删除,以及消费关系的维护等。 6 | - Producer(生产者):MQ 的客户端之一,调用 Broker 提供的 RPC 接口发送消息。 7 | - Consumer(消费者):MQ 的另外一个客户端,调用 Broker 提供的 RPC 接口接收消息,同时完成消费确认。 8 | 9 | 不同的消息队列采取各种各样的方法,并没有针对所有目的的通用答案。为了区分这些系统,问一下这两个问题会特别有帮助: 10 | 11 | - 如果生产者发送消息的速度比消费者能够处理的速度快会发生什么?一般来说,有三种选择:系统可以丢掉消息,将消息放入缓冲队列,或使用背压(backpressure)(也称为流量控制(flow control),即阻塞生产者,以免其发送更多的消息)。例如 Unix 管道和 TCP 使用背压:它们有一个固定大小的小缓冲区,如果填满,发送者会被阻塞,直到接收者从缓冲区中取出数据。 12 | 13 | - 如果节点崩溃或暂时脱机,会发生什么情况?是否会有消息丢失?与数据库一样,持久性可能需要写入磁盘和/或复制的某种组合,这是有代价的。如果你能接受有时消息会丢失,则可能在同一硬件上获得更高的吞吐量和更低的延迟。是否可以接受消息丢失取决于应用。例如,对于周期传输的传感器读数和指标,偶尔丢失的数据点可能并不重要,因为更新的值会在短时间内发出。但要注意,如果大量的消息被丢弃,可能无法立刻意识到指标已经不正确了。如果你正在对事件计数,那么更重要的是它们能够可靠送达,因为每个丢失的消息都意味着使计数器的错误扩大。批处理系统的一个很好的特性是,它们提供了强大的可靠性保证:失败的任务会自动重试,失败任务的部分输出会自动丢弃。这意味着输出与没有发生故障一样,这有助于简化编程模型。 14 | 15 | 下面,再展开讨论下一些具体的技术难点和可行的解决方案。 16 | 17 | - RPC 通信:解决的是 Broker 与 Producer 以及 Consumer 之间的通信问题。如果不重复造轮子,直接利用成熟的 RPC 框架 Dubbo 或者 Thrift 实现即可,这样不需要考虑服务注册与发现、负载均衡、通信协议、序列化方式等一系列问题了。当然,你也可以基于 Netty 来做底层通信,用 Zookeeper、Euraka 等来做注册中心,然后自定义一套新的通信协议(类似 Kafka),也可以基于 AMQP 这种标准化的 MQ 协议来做实现(类似 RabbitMQ)。对比直接用 RPC 框架,这种方案的定制化能力和优化空间更大。 18 | 19 | - 高可用设计:高可用主要涉及两方面:Broker 服务的高可用、存储方案的高可用。可以拆开讨论。Broker 服务的高可用,只需要保证 Broker 可水平扩展进行集群部署即可,进一步通过服务自动注册与发现、负载均衡、超时重试机制、发送和消费消息时的 ack 机制来保证。存储方案的高可用有两个思路: 20 | 21 | - 参考 Kafka 的分区 + 多副本模式,但是需要考虑分布式场景下数据复制和一致性方案(类似 Zab、Raft 等协议),并实现自动故障转移; 22 | - 还可以用主流的 DB、分布式文件系统、带持久化能力的 KV 系统,它们都有自己的高可用方案。 23 | 24 | - 存储设计:消息的存储方案是 MQ 的核心部分,可靠性保证已经在高可用设计中谈过了,可靠性要求不高的话直接用内存或者分布式缓存也可以。这里重点说一下存储的高性能如何保证?这个问题的决定因素在于存储结构的设计。目前主流的方案是:追加写日志文件(数据部分) + 索引文件的方式(很多主流的开源 MQ 都是这种方式),索引设计上可以考虑稠密索引或者稀疏索引,查找消息可以利用跳转表、二份查找等,还可以通过操作系统的页缓存、零拷贝等技术来提升磁盘文件的读写性能。如果不追求很高的性能,也可以考虑现成的分布式文件系统、KV 存储或者数据库方案。 25 | 26 | - 消费关系管理:为了支持发布-订阅的广播模式,Broker 需要知道每个主题都有哪些 Consumer 订阅了,基于这个关系进行消息投递。由于 Broker 是集群部署的,所以消费关系通常维护在公共存储上,可以基于 Zookeeper、Apollo 等配置中心来管理以及进行变更通知。 27 | 28 | - 高性能设计:存储的高性能前面已经谈过了,当然还可以从其他方面进一步优化性能。比如 Reactor 网络 IO 模型、业务线程池的设计、生产端的批量发送、Broker 端的异步刷盘、消费端的批量拉取等等。 29 | -------------------------------------------------------------------------------- /03~Kafka/Kafka Streams/README.md: -------------------------------------------------------------------------------- 1 | # Kafka Streams 2 | 3 | # 概览 4 | 5 | ## 不足 6 | 7 | ### 检查点机制 8 | 9 | 我认为这个问题是客观存在的,无可辩驳,Kafka Sterams 确实存在检查点机制问题。检查点可以说是操作分布式系统的基础。那么,到底什么是检查点呢?Kafka 是一套分布式日志记录系统。在消息处理方面,Kafka 同时支持有状态与无状态两类。在无状态处理方面,用户只会接收一条消息,然后进行实际处理,非常简单。但一旦涉及有状态处理,情况就立刻变得不同。现在,开发者面对的第一个难题就是存储状态,只有这样才能在遇到错误时配合对应状态完成系统恢复。 10 | 11 | 对于 Kafka Streams,恢复工作看似非常简单,因为拥有重构状态所需要的全部消息。理论上,甚至能够通过一些方法将保存在 Kafka 中的消息控制在每键约一条的水平。 12 | 但这就没问题了吗?当然不是。因为每键一条消息,状态量仍然相当夸张。如果大家拥有上千亿个键,那就需要在状态主题中保存超过 1 千亿条消息,毕竟所有状态变更都被放置在了对应的状态变更主题内。随着键数量的进一步增加,状态的体积也会随之膨胀。 13 | 14 | 这种运营思路总结起来就是,一旦某个节点无法正常运行,则必须从主题中重播所有消息并将其插入数据库内。只有执行完成整个流程,处理才能恢复至原有状态并继续进行。在极端情况或者发生人为错误的前提下,一切运行 Kafka Streams 作业的设备都有可能崩溃或者宕机,这意味着所有节点都必须重播所有状态变更消息,而后才能继续正常处理新的消息。 15 | 16 | 这种大规模重播可能会带来长达数小时的停机时间。不少 Kafka Streams 的潜在用户表示,他们估算出的停机时间至少达到 4 个小时。好的,就算 4 个小时,完成恢复流程之后,接下来还有这一时段内新增的大量消息。追上这部分进度,系统才算是真正回到运行正轨之上。 17 | 18 | 有鉴于此,数据库与处理框架往往采用检查点机制(在 Flink 中,这一机制被称为快照)。所谓检查点,是将当前整体态定入至持久存储(S3/HDFS)中。因此,一旦发生大规模故障,恢复程序将直接读取前一个检查点,重播该检查点之后的所有消息(通常在 1000 秒以内),以便快速恢复后续处理能力。总体而言,检查点支持下的恢复流程一般仅耗时几秒钟到几分钟不等。 19 | 20 | 可以看到,在检查点的帮助下,系统的停机时间将由 Kafka Streams 的数小时显著缩短至几秒和几分钟水平。对于实时系统,停机时间必须尽可能短,我们也必须尽最大努力确保分布式系统能够尽快从故障中恢复过来。 21 | 22 | ## 随机排序 23 | 24 | 随机排序是分布式处理流程的重要组成部分,其本质是将数据与同一个键整合起来的实现方法。如果需要对数据进行分析,则很可能会接触到随机排序。 25 | 26 | 事实上,Kafka Streams 的随机排序与 Flink 或者 Spark Streaming 中的随机排序存在巨大差异。下面来看看 JavaDoc 中关于其工作原理的描述: 27 | 28 | > 如果某个键变更运算符在实际使用之前发生了变化(例如 selectKey(KeyValueMapper)、map(KeyValueMapper), flatMap(KeyValueMapper) 或者 transform(TransformerSupplier, String…)),且此后没有发生数据重新分发(例如通过 through(String)),那么在 Kafka 当中创建一个内部重新分区主题。该主题将被命名为“\${applicationId}-XXX-repartition”的形式,其中,“applicationId”由用户在 StreamsConfig 中通过 APPLICATION_ID_CONFIG 参数进行指定,“XXX”为内部生成的名称,而“-repartition”则为固定后缀。开发者可以通过 KafkaStreams.toString() 检索所有已生成的内部主题名称。 29 | 30 | 这意味着只要变更键(一般用于分析),Kafka Streams 就会新建一个主题来实现随机排序。这种随机排序实现方法,证实了我在与开发者沟通时做出的几个基本假设:我曾与多位前 Kafka Streams 开发者进行过交流,他们并不清楚这种新的主题机制。他们直接在集群上执行实时分析,但这会快速增加代理上的负载与数据总量,并最终导致系统崩溃。但从使用者的角度来看,他们只是在正常执行数据处理。 31 | 32 | 如果开发者对性能并不关注,那么这种方法似乎也能接受。但是,其他一些处理框架(例如 Apache Flink)显然更加理想,它们提供更完善的内置随机排序功能,而且也能与 Kafka 配合使用。这些系统,无疑能够带来更顺畅的使用体验。 33 | 34 | 结论:由于缺少两大关键功能,Kafka Streams 实用性会大打折扣。我们不可能接受在实时生产系统上经历长达数小时的停机,无法接受随机排序功能导致集群崩溃。而且除非在每一项 KSQL 查询之前都进行解释,否则我们也弄不明白可能出现哪些随机排序操作。 35 | 36 | # Links 37 | 38 | - http://www.jasongj.com/kafka/kafka_stream/ 2017-Kafka设计解析(七)- Kafka Stream 39 | -------------------------------------------------------------------------------- /01~概念与设计/02.消息生产与消费/消息传输模型/点对点模型与发布订阅模型.md: -------------------------------------------------------------------------------- 1 | ## 点对点队列模型 2 | 3 | 这里的队列更符合数据结构中所谓先进先出(FIFO, First-In-First-Out)的线性表(Linear List)的这个定义。即在消息入队出队过程中,需要保证这些消息严格有序,按照什么顺序写进队列,必须按照同样的顺序从队列中读出来。不过,队列是没有“读”这个操作的,“读”就是出队,也就是从队列中“删除”这条消息。早期的消息队列,就是按照“队列”的数据结构来设计的。我们一起看下这个图,生产者(Producer)发消息就是入队操作,消费者(Consumer)收消息就是出队也就是删除操作,服务端存放消息的容器自然就称为“队列”。 4 | 5 | ![队列模型示意](https://pic.imgdb.cn/item/6086256bd1a9ae528fded288.jpg) 6 | 7 | 如果有多个生产者往同一个队列里面发送消息,这个队列中可以消费到的消息,就是这些生产者生产的所有消息的合集。消息的顺序就是这些生产者发送消息的自然顺序。如果有多个消费者接收同一个队列的消息,这些消费者之间实际上是竞争的关系,每个消费者只能收到队列中的一部分消息,也就是说任何一条消息只能被其中的一个消费者收到。如果需要将一份消息数据分发给多个消费者,要求每个消费者都能收到全量的消息,例如,对于一份订单数据,风控系统、分析系统、支付系统等都需要接收消息。这个时候,单个队列就满足不了需求,一个可行的解决方式是,为每个消费者创建一个单独的队列,让生产者发送多份。 8 | 9 | 显然这是个比较蠢的做法,同样的一份消息数据被复制到多个队列中会浪费资源,更重要的是,生产者必须知道有多少个消费者。为每个消费者单独发送一份消息,这实际上违背了消息队列“解耦”这个设计初衷。 10 | 11 | 以 RocketMQ 为例,点对点模型也叫队列模型,具有如下特点: 12 | 13 | - 消费匿名:消息上下游沟通的唯一的身份就是队列,下游消费者从队列获取消息无法申明独立身份。 14 | - 一对一通信:基于消费匿名特点,下游消费者即使有多个,但都没有自己独立的身份,因此共享队列中的消息,每一条消息都只会被唯一一个消费者处理。因此点对点模型只能实现一对一通信。 15 | 16 | ![队列模型](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230409180605.png) 17 | 18 | ## 发布-订阅模型 19 | 20 | 在发布-订阅模型中,消息的发送方称为发布者(Publisher),消息的接收方称为订阅者(Subscriber),服务端存放消息的容器称为主题(Topic)。发布者将消息发送到主题中,订阅者在接收消息之前需要先订阅主题。订阅在这里既是一个动作,同时还可以认为是主题在消费时的一个逻辑副本,每份订阅中,订阅者都可以接收到主题的所有消息。 21 | 22 | ![发布-订阅模型](https://pic.imgdb.cn/item/6086a867d1a9ae528f0ea7b1.jpg) 23 | 24 | 在消息领域的历史上很长的一段时间,队列模式和发布-订阅模式是并存的,有些消息队列同时支持这两种消息模型,比如 ActiveMQ。我们仔细对比一下这两种模型,生产者就是发布者,消费者就是订阅者,队列就是主题,并没有本质的区别。它们最大的区别其实就是,一份消息数据能不能被消费多次的问题。实际上,在这种发布-订阅模型中,如果只有一个订阅者,那它和队列模型就基本是一样的了。也就是说,发布-订阅模型在功能层面上是可以兼容队列模型的。 25 | 26 | 以 RocketMQ 为例,发布订阅模型具有如下特点: 27 | 28 | - 消费独立:相比队列模型的匿名消费方式,发布订阅模型中消费方都会具备的身份,一般叫做订阅组(订阅关系),不同订阅组之间相互独立不会相互影响。 29 | - 一对多通信:基于独立身份的设计,同一个主题内的消息可以被多个订阅组处理,每个订阅组都可以拿到全量消息。因此发布订阅模型可以实现一对多通信。 30 | 31 | ![发布订阅模型](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230409180654.png) 32 | 33 | ## 多个消费者 34 | 35 | 当多个消费者从同一主题中读取消息时,有使用两种主要的消息传递模式: 36 | 37 | - 负载均衡(load balance):每条消息都被传递给消费者之一,所以处理该主题下消息的工作能被多个消费者共享。代理可以为消费者任意分配消息。当处理消息的代价高昂,希望能并行处理消息时,此模式非常有用(在 AMQP 中,可以通过让多个客户端从同一个队列中消费来实现负载均衡,而在 JMS 中则称之为共享订阅(shared subscription))。 38 | 39 | - 扇出(fan-out):每条消息都被传递给所有消费者。扇出允许几个独立的消费者各自收听相同的消息广播,而不会相互影响:这个流处理中的概念对应批处理中多个不同批处理作业读取同一份输入文件(JMS 中的主题订阅与 AMQP 中的交叉绑定提供了这一功能)。 40 | 41 | ![(a)负载平衡:在消费者间共享消费主题;(b)扇出:将每条消息传递给多个消费者。](https://s2.ax1x.com/2020/02/13/1Oc2Q0.md.png) 42 | 43 | 两种模式可以组合使用:例如,两个独立的消费者组可以每组各订阅一个主题,每一组都共同收到所有消息,但在每一组内部,每条消息仅由单个节点处理。 44 | -------------------------------------------------------------------------------- /01~概念与设计/01~背景与价值/消息传递的不同方式.md: -------------------------------------------------------------------------------- 1 | # 消息传递:点对点与消息代理 2 | 3 | 向消费者通知新事件的常用方式是使用消息传递系统(messaging system):生产者发送包含事件的消息,然后将消息推送给消费者。 4 | 5 | ## 通过管道或数据库 6 | 7 | 像生产者和消费者之间的 Unix 管道或 TCP 连接这样的直接信道,是实现消息传递系统的简单方法。但是,大多数消息传递系统都在这一基本模型上进行扩展。特别的是,Unix 管道和 TCP 将恰好一个发送者与恰好一个接收者连接,而一个消息传递系统允许多个生产者节点将消息发送到同一个主题,并允许多个消费者节点接收主题中的消息。 8 | 9 | 同理,文件或数据库就足以连接生产者和消费者:生产者将其生成的每个事件写入数据存储,且每个消费者定期轮询数据存储,检查自上次运行以来新出现的事件。这实际上正是批处理在每天结束时处理当天数据时所做的事情。但当我们想要进行低延迟的连续处理时,如果数据存储不是为这种用途专门设计的,那么轮询开销就会很大。轮询的越频繁,能返回新事件的请求比例就越低,而额外开销也就越高。相比之下,最好能在新事件出现时直接通知消费者。数据库在传统上对这种通知机制支持的并不好,关系型数据库通常有 触发器(trigger),它们可以对变化作出反应(如,插入表中的一行),但是它们的功能非常有限,并且在数据库设计中有些后顾之忧。 10 | 11 | ## 直接从生产者传递给消费者 12 | 13 | 许多消息传递系统使用生产者和消费者之间的直接网络通信,而不通过中间节点: 14 | 15 | - UDP 组播广泛应用于金融行业,例如股票市场,其中低时延非常重要。虽然 UDP 本身是不可靠的,但应用层的协议可以恢复丢失的数据包(生产者必须记住它发送的数据包,以便能按需重新发送数据包)。 16 | - 无代理的消息库,如 ZeroMQ 和 nanomsg 采取类似的方法,通过 TCP 或 IP 多播实现发布/订阅消息传递。 17 | - StatsD 和 Brubeck 使用不可靠的 UDP 消息传递来收集网络中所有机器的指标并对其进行监控。(在 StatsD 协议中,只有接收到所有消息,才认为计数器指标是正确的;使用 UDP 将使得指标处在一种最佳近似状态。 18 | - 如果消费者在网络上公开了服务,生产者可以直接发送 HTTP 或 RPC 请求将消息推送给使用者。这就是 webhooks 背后的想法,一种服务的回调 URL 被注册到另一个服务中,并且每当事件发生时都会向该 URL 发出请求。 19 | 20 | ![RPC 与 MQ 区别](https://pic.imgdb.cn/item/6086a913d1a9ae528f14105b.jpg) 21 | 22 | 尽管这些直接消息传递系统在设计它们的环境中运行良好,但是它们通常要求应用代码意识到消息丢失的可能性。它们的容错程度极为有限:即使协议检测到并重传在网络中丢失的数据包,它们通常也只是假设生产者和消费者始终在线。如果消费者处于脱机状态,则可能会丢失其不可达时发送的消息。一些协议允许生产者重试失败的消息传递,但当生产者崩溃时,它可能会丢失消息缓冲区及其本应发送的消息,这种方法可能就没用了。 23 | 24 | ## 消息代理/消息中间件 25 | 26 | 一种广泛使用的替代方法是通过消息代理(message broker)(也称为消息队列(message queue))发送消息,消息代理实质上是一种针对处理消息流而优化的数据库。它作为服务器运行,生产者和消费者作为客户端连接到服务器。生产者将消息写入代理,消费者通过从代理那里读取来接收消息。 27 | 28 | 通过将数据集中在代理上,这些系统可以更容易地容忍来来去去的客户端(连接,断开连接和崩溃),而持久性问题则转移到代理的身上。一些消息代理只将消息保存在内存中,而另一些消息代理(取决于配置)将其写入磁盘,以便在代理崩溃的情况下不会丢失。针对缓慢的消费者,它们通常会允许无上限的排队(而不是丢弃消息或背压),尽管这种选择也可能取决于配置。 29 | 30 | 排队的结果是,消费者通常是**异步(asynchronous)**的:当生产者发送消息时,通常只会等待代理确认消息已经被缓存,而不等待消息被消费者处理。向消费者递送消息将发生在未来某个未定的时间点:通常在几分之一秒之内,但有时当消息堆积时会显著延迟。 31 | 32 | ### 消息代理与数据库对比 33 | 34 | 有些消息代理甚至可以使用 XA 或 JTA 参与两阶段提交协议,这个功能与数据库在本质上非常相似,尽管消息代理和数据库之间仍存在实践上很重要的差异: 35 | 36 | - 数据库通常保留数据直至显式删除,而大多数消息代理在消息成功递送给消费者时会自动删除消息。这样的消息代理不适合长期的数据存储。 37 | - 由于它们很快就能删除消息,大多数消息代理都认为它们的工作集相当小—— 即队列很短。如果代理需要缓冲很多消息,比如因为消费者速度较慢(如果内存装不下消息,可能会溢出到磁盘),每个消息需要更长的处理时间,整体吞吐量可能会恶化。 38 | - 数据库通常支持二级索引和各种搜索数据的方式,而消息代理通常支持按照某种模式匹配主题,订阅其子集。机制并不一样,对于客户端选择想要了解的数据的一部分,这是两种基本的方式。 39 | - 查询数据库时,结果通常基于某个时间点的数据快照;如果另一个客户端随后向数据库写入一些改变了查询结果的内容,则第一个客户端不会发现其先前结果现已过期(除非它重复查询或轮询变更)。相比之下,消息代理不支持任意查询,但是当数据发生变化时(即新消息可用时),它们会通知客户端。 40 | 41 | 这是关于消息代理的传统观点,它被封装在诸如 JMS 和 AMQP 的标准中,并且被诸如 RabbitMQ,ActiveMQ,HornetQ,Qpid,TIBCO 企业消息服务,IBM MQ,Azure Service Bus 和 Google Cloud Pub/Sub 实现 。 42 | -------------------------------------------------------------------------------- /03~Kafka/消息生产与消费/事务消息.md: -------------------------------------------------------------------------------- 1 | # 高可用及幂等 2 | 3 | 在分布式系统中一般有三种处理语义: 4 | 5 | - at-least-once:至少一次,有可能会有多次。如果 producer 收到来自 ack 的确认,则表示该消息已经写入到 Kafka 了,此时刚好是一次,也就是我们后面的 exactly-once。但是如果 producer 超时或收到错误,并且 request.required.acks 配置的不是-1,则会重试发送消息,客户端会认为该消息未写入 Kafka。如果 broker 在发送 Ack 之前失败,但在消息成功写入 Kafka 之后,这一次重试将会导致我们的消息会被写入两次,所以消息就不止一次地传递给最终 consumer,如果 consumer 处理逻辑没有保证幂等的话就会得到不正确的结果。在这种语义中会出现乱序,也就是当第一次 ack 失败准备重试的时候,但是第二消息已经发送过去了,这个时候会出现单分区中乱序的现象,我们需要设置 Prouducer 的参数 max.in.flight.requests.per.connection,flight.requests 是 Producer 端用来保存发送请求且没有响应的队列,保证 Producer 端未响应的请求个数为 1。 6 | 7 | - at-most-once:如果在 ack 超时或返回错误时 producer 不重试,也就是我们讲 request.required.acks=-1,则该消息可能最终没有写入 kafka,所以 consumer 不会接收消息。 8 | 9 | - exactly-once:刚好一次,即使 producer 重试发送消息,消息也会保证最多一次地传递给 consumer。该语义是最理想的,也是最难实现的。在 0.10 之前并不能保证 exactly-once,需要使用 consumer 自带的幂等性保证。0.11.0 使用事务保证了。 10 | 11 | # 如何实现 exactly-once 12 | 13 | 要实现 exactly-once 在 Kafka 0.11.0 中有两个官方策略。 14 | 15 | ## 单 Producer 单 Topic 16 | 17 | 每个 producer 在初始化的时候都会被分配一个唯一的 PID,对于每个唯一的 PID,Producer 向指定的 Topic 中某个特定的 Partition 发送的消息都会携带一个从 0 单调递增的 sequence number。在我们的 Broker 端也会维护一个维度为,每次提交一次消息的时候都会对齐进行校验: 18 | 19 | - 如果消息序号比 Broker 维护的序号大一以上,说明中间有数据尚未写入,也即乱序,此时 Broker 拒绝该消息,Producer 抛出 InvalidSequenceNumber。 20 | - 如果消息序号小于等于 Broker 维护的序号,说明该消息已被保存,即为重复消息,Broker 直接丢弃该消息,Producer 抛出 DuplicateSequenceNumber 21 | - 如果消息序号刚好大一,就证明是合法的。 22 | 23 | 上面所说的解决了两个问题: 24 | 25 | - 当 Prouducer 发送了一条消息之后失败,broker 并没有保存,但是第二条消息却发送成功,造成了数据的乱序。 26 | - 当 Producer 发送了一条消息之后,broker 保存成功,ack 回传失败,producer 再次投递重复的消息。 27 | 28 | 上面所说的都是在同一个 PID 下面,意味着必须保证在单个 Producer 中的同一个 seesion 内,如果 Producer 挂了,被分配了新的 PID,这样就无法保证了,所以 Kafka 中又有事务机制去保证。 29 | 30 | # 事务 31 | 32 | 在 kafka 中事务的作用是: 33 | 34 | - 实现 exactly-once 语义 35 | - 保证操作的原子性,要么全部成功,要么全部失败。 36 | - 有状态的操作的恢复 37 | 38 | 事务可以保证就算跨多个,在本次事务中的对消费队列的操作都当成原子性,要么全部成功,要么全部失败。并且,有状态的应用也可以保证重启后从断点处继续处理,也即事务恢复。在 kafka 的事务中,应用程序必须提供一个唯一的事务 ID,即 Transaction ID,并且宕机重启之后,也不会发生改变,Transactin ID 与 PID 可能一一对应。区别在于 Transaction ID 由用户提供,而 PID 是内部的实现对用户透明。为了 Producer 重启之后,旧的 Producer 具有相同的 Transaction ID 失效,每次 Producer 通过 Transaction ID 拿到 PID 的同时,还会获取一个单调递增的 epoch。由于旧的 Producer 的 epoch 比新 Producer 的 epoch 小,Kafka 可以很容易识别出该 Producer 是老的 Producer 并拒绝其请求。 39 | 40 | 为了实现这一点,Kafka 0.11.0.0 引入了一个服务器端的模块,名为 Transaction Coordinator,用于管理 Producer 发送的消息的事务性。该 Transaction Coordinator 维护 Transaction Log,该 log 存于一个内部的 Topic 内。由于 Topic 数据具有持久性,因此事务的状态也具有持久性。Producer 并不直接读写 Transaction Log,它与 Transaction Coordinator 通信,然后由 Transaction Coordinator 将该事务的状态插入相应的 Transaction Log。Transaction Log 的设计与 Offset Log 用于保存 Consumer 的 Offset 类似。 41 | -------------------------------------------------------------------------------- /01~概念与设计/03.消息存储/消息存储.md: -------------------------------------------------------------------------------- 1 | # 消息中间件中的数据存储 2 | 3 | # 顺序读写 4 | 5 | 消息系统数据持久化一般采用为每个消费者队列提供一个 B 树或其他通用的随机访问数据结构来维护消息的元数据,B 树操作的时间复杂度为 O(log n),O(log n)的时间复杂度可以看成是一个常量时间,而且 B 树可以支持各种各样的事务性和非事务性语义消息的传递。尽管 B 树具有这些优点,但这并不适合磁盘操作。目前的磁盘寻道时间一般在 10ms 以内,对一块磁盘来说,在同一时刻只能有一个磁头来读写磁盘,这样在并发 IO 能力上就有问题。同时,对树结构性能的观察结果表明:其性能会随着数据的增长而线性下降。鉴于消息系统本身的作用考虑,数据的持久化队列可以建立在简单地对文件进行追加的实现方案上。 6 | 7 | 操作系统每次从磁盘读写数据的时候,需要先寻址,也就是先要找到数据在磁盘上的物理位置,然后再进行数据读写。如果是机械硬盘,这个寻址需要比较长的时间,因为它要移动磁头,这是个机械运动,机械硬盘工作的时候会发出咔咔的声音,就是移动磁头发出的声音。顺序读写相比随机读写省去了大部分的寻址时间,它只要寻址一次,就可以连续地读写下去,所以说,性能要比随机读写要好很多。 8 | 9 | 以 Kafka 为例,因为是顺序追加,所以 Kafka 在设计上是采用时间复杂度 O(1)的磁盘结构,它提供了常量时间的性能,即使是存储海量的信息(TB 级)也如此,性能和数据的大小关系也不大,同时 Kafka 将数据持久化到磁盘上,这样只要磁盘空间足够大数据就可以一直追加,而不会像一般的消息系统在消息被消费后就删除掉,Kafka 提供了相关配置让用户自己决定消息要保存多久,这样为消费者提供了更灵活的处理方式,因此 Kafka 能够在没有性能损失的情况下提供一般消息系统不具备的特性。 10 | 11 | ## WAL 12 | 13 | 包括数据库内很多的具有持久化能力的中间件都会采用 WAL,Writing Ahead Log 策略来保证数据的安全性与一致性。从客户端获取到的数据往往是会被首先写入到类似于 Commit Log 这样的文件中,该文件是实时顺序追加写入的。当系统发生了某些异常的崩溃后,即可以从这样的 Commit Log 中进行数据恢复。在写入 Commit Log 之后,数据或者对于数据的描述信息才会被写入到实际的表或分区文件中。数据文件往往采用异步刷盘的策略,而刷盘的时候也是依据时间或者数据的策略: 14 | 15 | - Flush driven by timer: There is a backend timer which flushes data in cache periodically to disks. The period is configurable via parameter commitTime in system configuration file taos.cfg. 16 | 17 | - Flush driven by data: Data in the cache is also flushed to disks when the left buffer size is below a threshold. Flush driven by data can reset the timer of flush driven by the timer. 18 | 19 | ![](https://tva3.sinaimg.cn/large/007DFXDhgy1g50s4k8hdvj30h4072t90.jpg) 20 | 21 | # Flush 22 | 23 | # Kafka 与 Pulsar 存储对比 24 | 25 | Apache Kafka 和 Apache Pulsar 都有类似的消息概念。客户端通过主题与消息系统进行交互。每个主题都可以分为多个分区。然而,Apache Pulsar 和 Apache Kafka 之间的根本区别在于 Apache Kafka 是以分区为存储中心,而 Apache Pulsar 是以 Segment 为存储中心。 26 | 27 | ![image](https://user-images.githubusercontent.com/5803001/50600083-f3fb9c80-0eea-11e9-999d-b9e511f03edd.png) 28 | 29 | 在 Apache Kafka 中,分区只能存储在单个节点上并复制到其他节点,其容量受最小节点容量的限制。这意味着容量扩展需要对分区重新平衡,这反过来又需要重新复制整个分区,以平衡新添加的代理的数据和流量。重新传输数据非常昂贵且容易出错,并且会消耗网络带宽和 IO。维护人员在执行此操作时必须非常小心,以避免破坏生产系统。 30 | 31 | Kafka 中分区数据的重新拷贝不仅发生在以分区为中心的系统中的群集扩展上。许多其他事情也会触发数据重新拷贝,例如副本故障,磁盘故障或计算机的故障。在数据重新复制期间,分区通常不可用,直到数据重新复制完成。例如,如果您将分区配置为存储为 3 个副本,这时,如果丢失了一个副本,则必须重新复制完整个分区后,分区才可以再次可用。 32 | 33 | 在用户遇到故障之前,通常会忽略这种缺陷,因为许多情况下,在短时间内仅是对内存中缓存数据的读取。当数据被保存到磁盘后,用户将越来越多地不可避免地遇到数据丢失,故障恢复的问题,特别是在需要将数据长时间保存的场合。 34 | 35 | 相反,在 Apache Pulsar 中,同样是以分区为逻辑单元,但是以 Segment 为物理存储单元。分区随着时间的推移会进行分段,并在整个集群中均衡分布,旨在有效地迅速地扩展。Pulsar 是以 Segment 为中心的,因此在扩展容量时不需要数据重新平衡和拷贝,旧数据不会被重新复制,这要归功于在 Apache BookKeeper 中使用可扩展的以 Segment 为中心的分布式日志存储系统。 36 | 37 | 通过利用分布式日志存储,Pulsar 可以最大化 Segment 放置选项,实现高写入和高读取可用性。例如,使用 BookKeeper,副本设置等于 2,只要任何 2 个 Bookie 启动,就可以对主题分区进行写入。对于读取可用性,只要主题分区的副本集中有 1 个处于活动状态,用户就可以读取它,而不会出现任何不一致。 38 | 39 | 总之,Apache Pulsar 这种独特的基于分布式日志存储的以 Segment 为中心的发布/订阅消息系统可以提供许多优势,例如可靠的流式系统,包括无限制的日志存储,无需分区重新平衡的即时扩展,快速复制修复以及通过最大化数据放置实现高写入和读取可用性选项。 40 | -------------------------------------------------------------------------------- /01~概念与设计/04.开源系统对比/README.md: -------------------------------------------------------------------------------- 1 | # 常见的消息系统对比 2 | 3 | 现在市面上有非常多的消息队列产品,虽然这些消息队列产品在功能和特性方面各有优劣,但我们在选择的时候要有一个最低标准,保证入选的产品至少是及格的。 4 | 5 | - 首先,必须是开源的产品,这个非常重要。开源意味着,如果有一天你使用的消息队列遇到了一个影响你系统业务的 Bug,你至少还有机会通过修改源代码来迅速修复或规避这个 Bug,解决你的系统火烧眉毛的问题,而不是束手无策地等待开发者不一定什么时候发布的下一个版本来解决。 6 | - 其次,这个产品必须是近年来比较流行并且有一定社区活跃度的产品。流行的好处是,只要你的使用场景不太冷门,你遇到 Bug 的概率会非常低,因为大部分你可能遇到的 Bug,其他人早就遇到并且修复了。你在使用过程中遇到的一些问题,也比较容易在网上搜索到类似的问题,然后很快的找到解决方案。 7 | - 还有一个优势就是,流行的产品与周边生态系统会有一个比较好的集成和兼容,比如,Kafka 和 Flink 就有比较好的兼容性,Flink 内置了 Kafka 的 Data Source,使用 Kafka 就很容易作为 Flink 的数据源开发流计算应用,如果你用一个比较小众的消息队列产品,在进行流计算的时候,你就不得不自己开发一个 Flink 的 Data Source。 8 | 9 | 最后,作为一款及格的消息队列产品,必须具备的几个特性包括: 10 | 11 | - 消息的可靠传递:确保不丢消息; 12 | - Cluster:支持集群,确保不会因为某个节点宕机导致服务不可用,当然也不能丢消息; 13 | - 性能:具备足够好的性能,能满足绝大多数场景的性能要求。 14 | 15 | # 快速对比 16 | 17 | 目前业界使用比较多的是 Kafka,主要场景是大数据日志处理,较少用于金融场景。RocketMQ 对 Topic 运营不太友好,特别是不支持按 Topic 删除失效消息,以及不具备宕机 Failover 能力。我们选 Pulsar 是因为其原生的高一致性,基于 BookKeeper 提供高可用存储服务,采用了存储和服务分离架构方便扩容,同时还支持多种消费模式和多域部署模式。Kafka、RocketMQ 和 Pulsar 的对比如下: 18 | 19 | ![Kafka、RocketMQ、Pulsar 大致对比如下](https://s2.ax1x.com/2019/09/02/n92zUe.jpg) 20 | 21 | - Kafka:Kafka 是 Apache 下的一个子项目,是一个高性能跨语言分布式发布/订阅消息队列系统,而 Jafka 是在 Kafka 之上孵化而来的,即 Kafka 的一个升级版。具有以下特性:快速持久化,可以在 O(1)的系统开销下进行消息持久化;高吞吐,在一台普通的服务器上既可以达到 10W/s 的吞吐速率;完全的分布式系统,Broker、Producer、Consumer 都原生自动支持分布式,自动实现负载均衡;支持 Hadoop 数据并行加载,对于像 Hadoop 的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka 通过 Hadoop 的并行加载机制统一了在线和离线的消息处理。Apache Kafka 相对于 ActiveMQ 是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。 22 | 23 | - RocketMQ:RocketMQ 是阿里巴巴在 2012 年开源的消息队列产品,后来捐赠给 Apache 软件基金会,2017 正式毕业,成为 Apache 的顶级项目。阿里内部也是使用 RocketMQ 作为支撑其业务的消息队列,经历过多次“双十一”考验,它的性能、稳定性和可靠性都是值得信赖的。作为优秀的国产消息队列,近年来越来越多的被国内众多大厂使用。 24 | 25 | - Pulsar:Pulsar 旨在取代 Apache Kafka 多年的主宰地位。Pulsar 在很多情况下提供了比 Kafka 更快的吞吐量和更低的延迟,并为开发人员提供了一组兼容的 API,让他们可以很轻松地从 Kafka 切换到 Pulsar。Pulsar 的最大优点在于它提供了比 Apache Kafka 更简单明了、更健壮的一系列操作功能,特别在解决可观察性、地域复制和多租户方面的问题。在运行大型 Kafka 集群方面感觉有困难的企业可以考虑转向使用 Pulsar。 26 | 27 | - RabbitMQ:RabbitMQ 是使用 Erlang 编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正因如此,它非常重量级,更适合于企业级的开发。同时实现了 Broker 构架,这意味着消息在发送给客户端时先在中心队列排队。对路由,负载均衡或者数据持久化都有很好的支持。 28 | 29 | - Redis:Redis 是一个基于 Key-Value 对的 NoSQL 数据库,开发维护很活跃。虽然它是一个 Key-Value 数据库存储系统,但它本身支持 MQ 功能,所以完全可以当做一个轻量级的队列服务来使用。对于 RabbitMQ 和 Redis 的入队和出队操作,各执行 100 万次,每 10 万次记录一次执行时间。测试数据分为 128Bytes、512Bytes、1K 和 10K 四个不同大小的数据。实验表明:入队时,当数据比较小时 Redis 的性能要高于 RabbitMQ,而如果数据大小超过了 10K,Redis 则慢的无法忍受;出队时,无论数据大小,Redis 都表现出非常好的性能,而 RabbitMQ 的出队性能则远低于 Redis。 30 | 31 | - ZeroMQ:ZeroMQ 号称最快的消息队列系统,尤其针对大吞吐量的需求场景。ZeroMQ 能够实现 RabbitMQ 不擅长的高级/复杂的队列,但是开发人员需要自己组合多种技术框架,技术上的复杂度是对这 MQ 能够应用成功的挑战。ZeroMQ 具有一个独特的非中间件的模式,你不需要安装和运行一个消息服务器或中间件,因为你的应用程序将扮演这个服务器角色。你只需要简单的引用 ZeroMQ 程序库,可以使用 NuGet 安装,然后你就可以愉快的在应用程序之间发送消息了。但是 ZeroMQ 仅提供非持久性的队列,也就是说如果宕机,数据将会丢失。其中,Twitter 的 Storm 0.9.0 以前的版本中默认使用 ZeroMQ 作为数据流的传输(Storm 从 0.9 版本开始同时支持 ZeroMQ 和 Netty 作为传输模块)。 32 | 33 | - ActiveMQ 是 Apache 下的一个子项目。类似于 ZeroMQ,它能够以代理人和点对点的技术实现队列。同时类似于 RabbitMQ,它少量代码就可以高效地实现高级应用场景。ActiveMQ 太过于复杂,在使用过程中经常出现消息丢失或者整个进程挂起的情况,并且难以定位。 34 | -------------------------------------------------------------------------------- /01~概念与设计/04.开源系统对比/功能对比.md: -------------------------------------------------------------------------------- 1 | # 不同开源系统的功能对比 2 | 3 | # RabbitMQ 4 | 5 | RabbitMQ 是使用一种比较小众的编程语言:Erlang 语言编写的,它最早是为电信行业系统之间的可靠通信设计的,也是少数几个支持 AMQP 协议的消息队列之一。RabbitMQ 就像它的名字中的兔子一样:轻量级、迅捷,它的 Slogan,也就是宣传口号,也很明确地表明了 RabbitMQ 的特点:Messaging that just works,“开箱即用的消息队列”。也就是说,RabbitMQ 是一个相当轻量级的消息队列,非常容易部署和使用。RabbitMQ 一个比较有特色的功能是支持非常灵活的路由配置,和其他消息队列不同的是,它在生产者(Producer)和队列(Queue)之间增加了一个 Exchange 模块,你可以理解为交换机。这个 Exchange 模块的作用和交换机也非常相似,根据配置的路由规则将生产者发出的消息分发到不同的队列中。路由的规则也非常灵活,甚至你可以自己来实现路由规则。基于这个 Exchange,可以产生很多的玩儿法,如果你正好需要这个功能,RabbitMQ 是个不错的选择。 6 | 7 | 接下来说下 RabbitMQ 的几个问题: 8 | 9 | - 第一个问题是,RabbitMQ 对消息堆积的支持并不好,在它的设计理念里面,消息队列是一个管道,大量的消息积压是一种不正常的情况,应当尽量去避免。当大量消息积压的时候,会导致 RabbitMQ 的性能急剧下降。 10 | - 第二个问题是,RabbitMQ 的性能是我们介绍的这几个消息队列中最差的,根据官方给出的测试数据综合我们日常使用的经验,依据硬件配置的不同,它大概每秒钟可以处理几万到十几万条消息。其实,这个性能也足够支撑绝大多数的应用场景了,不过,如果你的应用对消息队列的性能要求非常高,那不要选择 RabbitMQ。 11 | - 最后一个问题是 RabbitMQ 使用的编程语言 Erlang,这个编程语言不仅是非常小众的语言,更麻烦的是,这个语言的学习曲线非常陡峭。大多数流行的编程语言,比如 Java、C/C++、Python 和 JavaScript,虽然语法、特性有很多的不同,但它们基本的体系结构都是一样的,你只精通一种语言,也很容易学习其他的语言,短时间内即使做不到精通,但至少能达到“会用”的水平。 12 | 13 | # RocketMQ 14 | 15 | RocketMQ 是阿里巴巴在 2012 年开源的消息队列产品,后来捐赠给 Apache 软件基金会,2017 正式毕业,成为 Apache 的顶级项目。阿里内部也是使用 RocketMQ 作为支撑其业务的消息队列,经历过多次“双十一”考验,它的性能、稳定性和可靠性都是值得信赖的。作为优秀的国产消息队列,近年来越来越多的被国内众多大厂使用。RocketMQ 就像一个品学兼优的好学生,有着不错的性能,稳定性和可靠性,具备一个现代的消息队列应该有的几乎全部功能和特性,并且它还在持续的成长中。 16 | 17 | RocketMQ 有非常活跃的中文社区,大多数问题你都可以找到中文的答案,也许会成为你选择它的一个原因。另外,RocketMQ 使用 Java 语言开发,它的贡献者大多数都是中国人,源代码相对也比较容易读懂,你很容易对 RocketMQ 进行扩展或者二次开发。RocketMQ 对在线业务的响应时延做了很多的优化,大多数情况下可以做到毫秒级的响应,如果你的应用场景很在意响应时延,那应该选择使用 RocketMQ。RocketMQ 的性能比 RabbitMQ 要高一个数量级,每秒钟大概能处理几十万条消息。 18 | 19 | RocketMQ 的一个劣势是,作为国产的消息队列,相比国外的比较流行的同类产品,在国际上还没有那么流行,与周边生态系统的集成和兼容程度要略逊一筹。 20 | 21 | # Redis 22 | 23 | Redis 是一个基于 Key-Value 对的 NoSQL 数据库,开发维护很活跃。虽然它是一个 Key-Value 数据库存储系统,但它本身支持 MQ 功能,所以完全可以当做一个轻量级的队列服务来使用。对于 RabbitMQ 和 Redis 的入队和出队操作,各执行 100 万次,每 10 万次记录一次执行时间。测试数据分为 128Bytes、512Bytes、1K 和 10K 四个不同大小的数据。实验表明:入队时,当数据比较小时 Redis 的性能要高于 RabbitMQ,而如果数据大小超过了 10K,Redis 则慢的无法忍受;出队时,无论数据大小,Redis 都表现出非常好的性能,而 RabbitMQ 的出队性能则远低于 Redis。 24 | 25 | # ZeroMQ & ActiveMQ 26 | 27 | ZeroMQ 号称最快的消息队列系统,尤其针对大吞吐量的需求场景。ZeroMQ 能够实现 RabbitMQ 不擅长的高级/复杂的队列,但是开发人员需要自己组合多种技术框架,技术上的复杂度是对这 MQ 能够应用成功的挑战。ZeroMQ 具有一个独特的非中间件的模式,你不需要安装和运行一个消息服务器或中间件,因为你的应用程序将扮演这个服务器角色。你只需要简单的引用 ZeroMQ 程序库,可以使用 NuGet 安装,然后你就可以愉快的在应用程序之间发送消息了。但是 ZeroMQ 仅提供非持久性的队列,也就是说如果宕机,数据将会丢失。其中,Twitter 的 Storm 0.9.0 以前的版本中默认使用 ZeroMQ 作为数据流的传输(Storm 从 0.9 版本开始同时支持 ZeroMQ 和 Netty 作为传输模块)。严格来说 ZeroMQ 并不能称之为一个消息队列,而是一个基于消息队列的多线程网络库,如果你的需求是将消息队列的功能集成到你的系统进程中,可以考虑使用 ZeroMQ。 28 | 29 | ActiveMQ 是 Apache 下的一个子项目。类似于 ZeroMQ,它能够以代理人和点对点的技术实现队列。同时类似于 RabbitMQ,它少量代码就可以高效地实现高级应用场景。ActiveMQ 是最老牌的开源消息队列,是十年前唯一可供选择的开源消息队列,目前已进入老年期,社区不活跃。无论是功能还是性能方面,ActiveMQ 都与现代的消息队列存在明显的差距,它存在的意义仅限于兼容那些还在用的爷爷辈儿的系统。 30 | 31 | # Kafka 32 | 33 | Kafka 最早是由 LinkedIn 开发,目前也是 Apache 的顶级项目。Kafka 最初的设计目的是用于处理海量的日志。在早期的版本中,为了获得极致的性能,在设计方面做了很多的牺牲,比如不保证消息的可靠性,可能会丢失消息,也不支持集群,功能上也比较简陋,这些牺牲对于处理海量日志这个特定的场景都是可以接受的。这个时期的 Kafka 甚至不能称之为一个合格的消息队列。但是,请注意,重点一般都在后面。随后的几年 Kafka 逐步补齐了这些短板,你在网上搜到的很多消息队列的对比文章还在说 Kafka 不可靠,其实这种说法早已经过时了。当下的 Kafka 已经发展为一个非常成熟的消息队列产品,无论在数据可靠性、稳定性和功能特性等方面都可以满足绝大多数场景的需求。 34 | 35 | Kafka 与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域,几乎所有的相关开源软件系统都会优先支持 Kafka。Kafka 使用 Scala 和 Java 语言开发,设计上大量使用了批量和异步的思想,这种设计使得 Kafka 能做到超高的性能。Kafka 的性能,尤其是异步收发的性能,是三者中最好的,但与 RocketMQ 并没有量级上的差异,大约每秒钟可以处理几十万条消息。我曾经使用配置比较好的服务器对 Kafka 进行过压测,在有足够的客户端并发进行异步批量发送,并且开启压缩的情况下,Kafka 的极限处理能力可以超过每秒 2000 万条消息。 36 | 37 | 但是 Kafka 这种异步批量的设计带来的问题是,它的同步收发消息的响应时延比较高,因为当客户端发送一条消息的时候,Kafka 并不会立即发送出去,而是要等一会儿攒一批再发送,在它的 Broker 中,很多地方都会使用这种“先攒一波再一起处理”的设计。当你的业务场景中,每秒钟消息数量没有那么多的时候,Kafka 的时延反而会比较高。所以,Kafka 不太适合在线业务场景。 38 | -------------------------------------------------------------------------------- /03~Kafka/README.md: -------------------------------------------------------------------------------- 1 | ![Kafka 示意图](https://www.confluent.io/wp-content/uploads/streaming_platform_rev.png) 2 | 3 | # Kafka 4 | 5 | Kafka 是由 Linkedin 公司开发的,它是一个分布式的,支持多分区、多副本,基于 Zookeeper 的分布式消息流平台,它同时也是一款开源的基于发布订阅模式的消息引擎系统。与传统的消息系统相比,Kafka 能够很好地处理活跃的流数据,使得数据在各个子系统中高性能、低延迟地不停流转。它最初由 LinkedIn 公司开发,后来成为 Apache 项目的一部分。Kafka 核心模块使用 Scala 语言开发,支持多语言(如 Java、C/C++、Python、Go、Erlang、Node.js 等)客户端,它以可水平扩展和具有高吞吐量等特性而被广泛使用。 6 | 7 | ![Kafka 管道应用示意图](https://pic.imgdb.cn/item/6077f7a18322e6675cb26c90.png) 8 | 9 | 据 Kafka 官方网站介绍,当前的 Kafka 已经定位为一个分布式流式处理平台(a distributed streaming platform),在官方看来,作为一个流式处理平台,必须具备以下 3 个关键特性。 10 | 11 | - 能够允许发布和订阅流数据。从这个角度来讲,平台更像一个消息队列或者企业级的消息系统。 12 | - 存储流数据时提供相应的容错机制。 13 | - 当流数据到达时能够被及时处理。 14 | 15 | Kafka 能够很好满足以上 3 个特性,通过 Kafka 能够很好地建立实时流式数据通道,由该通道可靠地获取系统或应用程序的数据,也可以通过 Kafka 方便地构建实时流数据应用来转换或是对流式数据进行响应处理。特别是在 0.10 版本之后,Kafka 推出了 Kafka Streams,这让 Kafka 对流数据处理变得更加方便。 16 | 17 | # 特性 18 | 19 | Kafka 是一种分布式的,基于发布/订阅的消息系统。主要设计目标与特性如下: 20 | 21 | - 高吞吐、低延迟:高吞吐量是 Kafka 设计的主要目标,即使在非常廉价的商用机器上也能做到单机支持每秒 100K 条以上消息的传输。Kafka 将数据写到磁盘,充分利用磁盘的顺序读写。同时,Kafka 在数据写入及数据同步采用了零拷贝(zero-copy)技术,采用 sendFile() 函数调用,sendFile() 函数是在两个文件描述符之间直接传递数据,完全在内核中操作,从而避免了内核缓冲区与用户缓冲区之间数据的拷贝,操作效率极高。Kafka 还支持数据压缩及批量发送,同时 Kafka 将每个主题划分为多个分区,这一系列的优化及实现方法使得 Kafka 具有很高的吞吐量。经大多数公司对 Kafka 应用的验证,Kafka 支持每秒数百万级别的消息。 22 | 23 | - 持久性与可靠性:Kafka 能够允许数据的持久化存储,消息被持久化到磁盘,并支持数据备份防止数据丢失;以时间复杂度为 O(1)的方式提供消息持久化能力,即使对 TB 级以上数据也能保证常数时间复杂度的访问性能。Kafka 可以为每个主题指定副本数,对数据进行持久化备份,这可以一定程度上防止数据丢失,提高可用性。 24 | 25 | - 扩展性与高伸缩性:每个主题(topic) 包含多个分区(partition),主题中的分区可以分布在不同的主机(broker)中。Kafka 要支持对大规模数据的处理,就必须能够对集群进行扩展,分布式必须是其特性之一,这样就可以将多台廉价的 PC 服务器搭建成一个大规模的消息系统。Kafka 依赖 ZooKeeper 来对集群进行协调管理,这样使得 Kafka 更加容易进行水平扩展,生产者、消费者和代理都为分布式,可配置多个。同时在机器扩展时无需将整个集群停机,集群能够自动感知,重新进行负责均衡及数据复制。 26 | 27 | - 轻量级:Kafka 的代理是无状态的,即代理不记录消息是否被消费,消费偏移量的管理交由消费者自己或组协调器来维护。同时集群本身几乎不需要生产者和消费者的状态信息,这就使得 Kafka 非常轻量级,同时生产者和消费者客户端实现也非常轻量级。 28 | 29 | - 消息压缩:Kafka 支持 Gzip、Snappy、LZ4 这 3 种压缩方式,通常把多条消息放在一起组成 MessageSet,然后再把 MessageSet 放到一条消息里面去,从而提高压缩比率进而提高吞吐量。 30 | 31 | 由于 Kafka 本身是存储计算耦合的架构,使得数据不均衡的问题经常凸显,集群扩容、故障恢复也变得异常麻烦,给运维工作带来不少痛苦;同时,由于 Consumer 的 Rebalance 算法每次都是全部重新计算,使得业务的消费体验也不是很好。存储计算耦合的架构在扩容和故障转移时都需要进行数据搬迁;故障恢复时一般要经历复杂的算法先选举 Leader,且提供服务前要先保证各副本数据是一致的。 32 | 33 | ## 与 RabbitMQ 对比 34 | 35 | 与 RabbitMQ 这样的传统消息中间件相比,Kafka 并不适合做 Task Queue。Kafka 将一组消息抽象归纳为一个主题(Topic),每个主题又被分成一个或多个分区(Partition)。每个分区由一系列有序、不可变的消息组成,是一个有序队列;Kafka 只能保证一个分区之内消息的有序性,并不能保证跨分区消息的有序性。同一个分区的一条消息只能被同一个消费组下某一个消费者消费,但不同消费组的消费者可同时消费该消息。 36 | 37 | Kafka Broker 本身并不会记录消息的消费情况,而是交由消费者通过偏移量记录,这样使其非常方便地实现了 "at-least-once" 处理模型;但是也意味着我们并不能随机地获取某条消息。 38 | 39 | ## Kafka 应用 40 | 41 | 消息系统或是说消息队列中间件是当前处理大数据一个非常重要的组件,用来解决应用解耦、异步通信、流量控制等问题,从而构建一个高效、灵活、消息同步和异步传输处理、存储转发、可伸缩和最终一致性的稳定系统。当前比较流行的消息中间件有 Kafka、RocketMQ、RabbitMQ、ZeroMQ、ActiveMQ、MetaMQ、Redis 等,这些消息中间件在性能及功能上各有所长。如何选择一个消息中间件取决于我们的业务场景、系统运行环境、开发及运维人员对消息中件间掌握的情况等。我认为在下面这些场景中,Kafka 是一个不错的选择。 42 | 43 | - 消息系统:Kafka 作为一款优秀的消息系统,具有高吞吐量、内置的分区、备份冗余分布式等特点,为大规模消息处理提供了一种很好的解决方案。 44 | - 应用监控:利用 Kafka 采集应用程序和服务器健康相关的指标,如 CPU 占用率、IO、内存、连接数、TPS、QPS 等,然后将指标信息进行处理,从而构建一个具有监控仪表盘、曲线图等可视化监控系统。例如,很多公司采用 Kafka 与 ELK(ElasticSearch、Logstash 和 Kibana)整合构建应用服务监控系统。 45 | - 网站用户行为追踪:为了更好地了解用户行为、操作习惯,改善用户体验,进而对产品升级改进,将用户操作轨迹、内容等信息发送到 Kafka 集群上,通过 Hadoop、Spark 或 Strom 等进行数据分析处理,生成相应的统计报告,为推荐系统推荐对象建模提供数据源,进而为每个用户进行个性化推荐。 46 | - 流处理:需要将已收集的流数据提供给其他流式计算框架进行处理,用 Kafka 收集流数据是一个不错的选择,而且当前版本的 Kafka 提供了 Kafka Streams 支持对流数据的处理。 47 | - 持久性日志:Kafka 可以为外部系统提供一种持久性日志的分布式系统。日志可以在多个节点间进行备份,Kafka 为故障节点数据恢复提供了一种重新同步的机制。同时,Kafka 很方便与 HDFS 和 Flume 进行整合,这样就方便将 Kafka 采集的数据持久化到其他外部系统 48 | 49 | # Links 50 | 51 | - https://mp.weixin.qq.com/s/fX26tCdYSMgwM54_2CpVrw 52 | 53 | - https://www.cnblogs.com/huxi2b/p/6223228.html 54 | -------------------------------------------------------------------------------- /03~Kafka/消息代理/磁盘读写优化.md: -------------------------------------------------------------------------------- 1 | # Kafka 性能优化之道 2 | 3 | # 顺序读写 4 | 5 | Kafka 高度依赖于文件系统来存储和缓存消息。对于磁盘来说,它有一个特性,就是顺序读写的性能要远远好于随机读写。在 SSD(固态硬盘)上,顺序读写的性能要比随机读写快几倍,如果是机械硬盘,这个差距会达到几十倍。据 Kafka 官方网站介绍:6 块 7200r/min SATA RAID-5 阵列的磁盘线性写的速度为 600 MB/s,而随机写的速度为 100KB/s,线性写的速度约是随机写的 6000 多倍。由此看来磁盘的快慢取决于我们是如何去应用磁盘。加之现代的操作系统提供了预读(read-ahead)和延迟写(write-behind)技术,使得磁盘的写速度并不是大家想象的那么慢。操作系统每次从磁盘读写数据的时候,需要先寻址,也就是先要找到数据在磁盘上的物理位置,然后再进行数据读写。如果是机械硬盘,这个寻址需要比较长的时间,因为它要移动磁头,这是个机械运动,机械硬盘工作的时候会发出咔咔的声音,就是移动磁头发出的声音。顺序读写相比随机读写省去了大部分的寻址时间,它只要寻址一次,就可以连续地读写下去,所以说,性能要比随机读写要好很多。 6 | 7 | Kafka 就是充分利用了磁盘的这个特性。它的存储设计非常简单,对于每个分区,它把从 Producer 收到的消息,顺序地写入对应的 log 文件中,一个文件写满了,就开启一个新的文件这样顺序写下去。消费的时候,也是从某个全局的位置开始,也就是某一个 log 文件中的某个位置开始,顺序地把消息读出来。 8 | 9 | ![顺序写入示意](https://pic.imgdb.cn/item/6077a25c8322e6675cfc796a.jpg) 10 | 11 | 对于传统的 message queue 而言,一般会删除已经被消费的消息,而 Kafka 是不会删除数据的,它会把所有的数据都保留下来,每个消费者(Consumer)对每个 Topic 都有一个 offset 用来表示读取到了第几条数据。 12 | 13 | ![基于偏移的读取](https://pic.imgdb.cn/item/6077a28e8322e6675cfcfb8d.jpg) 14 | 15 | 即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以 Kafka 的数据并不是实时的写入硬盘,它充分利用了现代操作系统分页存储来利用内存提高 I/O 效率。 16 | 17 | # 利用 PageCache 加速消息读写 18 | 19 | 在 Kafka 中,它会利用 PageCache 加速消息读写。PageCache 是现代操作系统都具有的一项基本特性。通俗地说,PageCache 就是操作系统在内存中给磁盘上的文件建立的缓存。无论我们使用什么语言编写的程序,在调用系统的 API 读写文件的时候,并不会直接去读写磁盘上的文件,应用程序实际操作的都是 PageCache,也就是文件在内存中缓存的副本。应用程序在写入文件的时候,操作系统会先把数据写入到内存中的 PageCache,然后再一批一批地写到磁盘上。读取文件的时候,也是从 PageCache 中来读取数据,这时候会出现两种可能情况。 20 | 21 | - 一种是 PageCache 中有数据,那就直接读取,这样就节省了从磁盘上读取数据的时间; 22 | - 另一种情况是,PageCache 中没有数据,这时候操作系统会引发一个缺页中断,应用程序的读取线程会被阻塞,操作系统把数据从文件中复制到 PageCache 中,然后应用程序再从 PageCache 中继续把数据读出来,这时会真正读一次磁盘上的文件,这个读的过程就会比较慢。 23 | 24 | 用户的应用程序在使用完某块 PageCache 后,操作系统并不会立刻就清除这个 PageCache,而是尽可能地利用空闲的物理内存保存这些 PageCache,除非系统内存不够用,操作系统才会清理掉一部分 PageCache。清理的策略一般是 LRU 或它的变种算法,这个算法我们不展开讲,它保留 PageCache 的逻辑是:优先保留最近一段时间最常使用的那些 PageCache。 25 | 26 | Kafka 在读写消息文件的时候,充分利用了 PageCache 的特性。一般来说,消息刚刚写入到服务端就会被消费,按照 LRU 的“优先清除最近最少使用的页”这种策略,读取的时候,对于这种刚刚写入的 PageCache,命中的几率会非常高。也就是说,大部分情况下,消费读消息都会命中 PageCache,带来的好处有两个:一个是读取的速度会非常快,另外一个是,给写入消息让出磁盘的 IO 资源,间接也提升了写入的性能。 27 | 28 | # ZeroCopy:零拷贝技术 29 | 30 | 在 Linux Kernal 2.2 之后出现了一种叫做“零拷贝(zero-copy)”系统调用机制,就是跳过“用户缓冲区”的拷贝,建立一个磁盘空间和内存空间的直接映射,数据不再复制到“用户态缓冲区”系统上下文切换减少 2 次,可以提升一倍性能。 31 | 32 | ![零拷贝示意](https://pic.imgdb.cn/item/6077a2f98322e6675cfdcd1a.jpg) 33 | 34 | > 更多零拷贝相关介绍参阅《[Concurrent-Notes/零拷贝](https://github.com/wx-chevalier/Concurrent-Notes?q=)》 35 | 36 | 我们知道,在服务端,处理消费的大致逻辑是这样的: 37 | 38 | - 首先,从文件中找到消息数据,读到内存中; 39 | - 然后,把消息通过网络发给客户端。 40 | 41 | 这个过程中,数据实际上做了 2 次或者 3 次复制: 42 | 43 | - 从文件复制数据到 PageCache 中,如果命中 PageCache,这一步可以省掉; 44 | - 从 PageCache 复制到应用程序的内存空间中,也就是我们可以操作的对象所在的内存; 45 | - 从应用程序的内存空间复制到 Socket 的缓冲区,这个过程就是我们调用网络应用框架的 API 发送数据的过程。 46 | 47 | Kafka 使用零拷贝技术可以把这个复制次数减少一次,上面的 2、3 步骤两次复制合并成一次复制。直接从 PageCache 中把数据复制到 Socket 缓冲区中,这样不仅减少一次数据复制,更重要的是,由于不用把数据复制到用户内存空间,DMA 控制器可以直接完成数据复制,不需要 CPU 参与,速度更快。下面是这个零拷贝对应的系统调用: 48 | 49 | ```c 50 | #include 51 | ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); 52 | ``` 53 | 54 | 它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。如果你遇到这种从文件读出数据后再通过网络发送出去的场景,并且这个过程中你不需要对这些数据进行处理,那一定要使用这个零拷贝的方法,可以有效地提升性能。 55 | 56 | ## 消费者(读取数据) 57 | 58 | 传统模式下我们从硬盘读取一个文件是这样的:先复制到内核空间(read 是系统调用,放到了 DMA,所以用内核空间),然后复制到用户空间(1、2);从用户空间重新复制到内核空间(你用的 socket 是系统调用,所以它也有自己的内核空间),最后发送给网卡(3、4)。Zero Copy 中直接从内核空间(DMA 的)到内核空间(Socket 的),然后发送网卡,Nginx 也是用的这种技术。 59 | 60 | ![传统调用与零拷贝对比](https://pic.imgdb.cn/item/6077adfb8322e6675c13715d.jpg) 61 | 62 | 实际上,Kafka 把所有的消息都存放在一个一个的文件中,当消费者需要数据的时候 Kafka 直接把“文件”发送给消费者。当不需要把整个文件发出去的时候,Kafka 通过调用 Zero Copy 的 sendfile 这个函数,这个函数包括: 63 | 64 | - out_fd 作为输出(一般及时 socket 的句柄) 65 | - in_fd 作为输入文件句柄 66 | - off_t 表示 in_fd 的偏移(从哪里开始读取) 67 | - size_t 表示读取多少个 68 | -------------------------------------------------------------------------------- /01~概念与设计/01~背景与价值/消息队列的应用场景.md: -------------------------------------------------------------------------------- 1 | # 消息队列的价值 2 | 3 | - 异步通信:很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。 4 | 5 | - 解耦:防止引入过多的 API 给系统的稳定性带来风险;调用方使用不当会给被调用方系统造成压力,被调用方处理不当会降低调用方系统的响应能力。消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口。这允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。另一方面,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的"插入-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。 6 | 7 | - 冗余:有些情况下,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的"插入-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。 8 | 9 | - 扩展性:因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。不需要改变代码、不需要调节参数。扩展就像调大电力按钮一样简单。一次发布多方订阅,消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。不需要改变代码、不需要调节参数。系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。 10 | 11 | - 可恢复性:系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。 12 | 13 | - 顺序保证:在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。Kafka 保证一个 Partition 内的消息的有序性。 14 | 15 | - 缓冲:在任何重要的系统中,都会有需要不同的处理时间的元素。例如,加载一张图片比应用过滤器花费更少的时间。消息队列通过一个缓冲层来帮助任务最高效率的执行———写入队列的处理会尽可能的快速。该缓冲有助于控制和优化数据流经过系统的速度。 16 | 17 | - 削峰和流控:在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见;如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。消息生产者不会堵塞,突发消息缓存在队列中,消费者按照实际能力读取消息。 18 | 19 | # 异步通信 20 | 21 | 以经典的秒杀系统为例,需要解决的核心问题是,如何利用有限的服务器资源,尽可能多地处理短时间内的海量请求。我们知道,处理一个秒杀请求包含了很多步骤,例如:风险控制、库存锁定、生成订单、短信通知、更新统计数据。如果没有任何优化,正常的处理流程是:App 将请求发送给网关,依次调用上述 5 个流程,然后将结果返回给 APP。对于对于这 5 个步骤来说,能否决定秒杀成功,实际上只有风险控制和库存锁定这 2 个步骤。只要用户的秒杀请求通过风险控制,并在服务端完成库存锁定,就可以给用户返回秒杀结果了,对于后续的生成订单、短信通知和更新统计数据等步骤,并不一定要在秒杀请求中处理完成。所以当服务端完成前面 2 个步骤,确定本次请求的秒杀结果后,就可以马上给用户返回响应,然后把请求的数据放入消息队列中,由消息队列异步地进行后续的操作。 22 | 23 | ![秒杀系统不同服务间异步请求](https://pic.imgdb.cn/item/6076972f8322e6675c40b1b6.jpg) 24 | 25 | 处理一个秒杀请求,从 5 个步骤减少为 2 个步骤,这样不仅响应速度更快,并且在秒杀期间,我们可以把大量的服务器资源用来处理秒杀请求。秒杀结束后再把资源用于处理后面的步骤,充分利用有限的服务器资源处理更多的秒杀请求。 26 | 27 | # 流量控制 28 | 29 | 我们还面临一个问题:如何避免过多的请求压垮我们的秒杀系统?一个设计健壮的程序有自我保护的能力,也就是说,它应该可以在海量的请求下,还能在自身能力范围内尽可能多地处理请求,拒绝处理不了的请求并且保证自身运行正常。不幸的是,现实中很多程序并没有那么“健壮”,而直接拒绝请求返回错误对于用户来说也是不怎么好的体验。因此,我们需要设计一套足够健壮的架构来将后端的服务保护起来。我们的设计思路是,使用消息队列隔离网关和后端服务,以达到流量控制和保护后端服务的目的。加入消息队列后,整个秒杀流程变为: 30 | 31 | - 网关在收到请求后,将请求放入请求消息队列; 32 | - 后端服务从请求消息队列中获取 APP 请求,完成后续秒杀处理过程,然后返回结果。 33 | 34 | ![秒杀系统流量控制](https://pic.imgdb.cn/item/607697968322e6675c41b011.jpg) 35 | 36 | 秒杀开始后,当短时间内大量的秒杀请求到达网关时,不会直接冲击到后端的秒杀服务,而是先堆积在消息队列中,后端服务按照自己的最大处理能力,从消息队列中消费请求进行处理。对于超时的请求可以直接丢弃,APP 将超时无响应的请求处理为秒杀失败即可。运维人员还可以随时增加秒杀服务的实例数量进行水平扩容,而不用对系统的其他部分做任何更改。 37 | 38 | 这种设计的优点是:能根据下游的处理能力自动调节流量,达到“削峰填谷”的作用。但这样做同样是有代价的: 39 | 40 | - 增加了系统调用链环节,导致总体的响应时延变长。 41 | - 上下游系统都要将同步调用改为异步消息,增加了系统的复杂度。 42 | 43 | 那还有没有更简单一点儿的流量控制方法呢?如果我们能预估出秒杀服务的处理能力,就可以用消息队列实现一个令牌桶,更简单地进行流量控制。令牌桶控制流量的原理是:单位时间内只发放固定数量的令牌到令牌桶中,规定服务在处理请求之前必须先从令牌桶中拿出一个令牌,如果令牌桶中没有令牌,则拒绝请求。这样就保证单位时间内,能处理的请求不超过发放令牌的数量,起到了流量控制的作用。 44 | 45 | ![令牌桶机制](https://pic.imgdb.cn/item/607697d38322e6675c423a6b.jpg) 46 | 47 | 实现的方式也很简单,不需要破坏原有的调用链,只要网关在处理 APP 请求时增加一个获取令牌的逻辑。令牌桶可以简单地用一个有固定容量的消息队列加一个“令牌发生器”来实现:令牌发生器按照预估的处理能力,匀速生产令牌并放入令牌队列(如果队列满了则丢弃令牌),网关在收到请求时去令牌队列消费一个令牌,获取到令牌则继续调用后端秒杀服务,如果获取不到令牌则直接返回秒杀失败。 48 | 49 | # 服务解耦 50 | 51 | 消息队列的另外一个作用,就是实现系统应用之间的解耦。举一个实际例子,比如说电商业务中最常见的订单支付场景:在订单支付成功后,需要更新订单状态、更新用户积分、通知商家有新订单、更新推荐系统中的用户画像等等。 52 | 53 | ![服务解耦](https://pic.imgdb.cn/item/6086a980d1a9ae528f172679.jpg) 54 | 55 | 这些订单下游的系统都需要实时获得订单数据。随着业务不断发展,这些订单下游系统不断的增加,不断变化,并且每个系统可能只需要订单数据的一个子集,负责订单服务的开发团队不得不花费很大的精力,应对不断增加变化的下游系统,不停地修改调试订单系统与这些下游系统的接口。任何一个下游系统接口变更,都需要订单模块重新进行一次上线,对于一个电商的核心服务来说,这几乎是不可接受的。所有的电商都选择用消息队列来解决类似的系统耦合过于紧密的问题。引入消息队列后,订单服务在订单变化时发送一条消息到消息队列的一个主题 Order 中,所有下游系统都订阅主题 Order,这样每个下游系统都可以获得一份实时完整的订单数据。无论增加、减少下游系统或是下游系统需求如何变化,订单服务都无需做任何更改,实现了订单服务与下游服务的解耦。 56 | -------------------------------------------------------------------------------- /01~概念与设计/03.消息存储/分区日志.md: -------------------------------------------------------------------------------- 1 | # 分区日志 2 | 3 | 通过网络发送数据包或向网络服务发送请求通常是短暂的操作,不会留下永久的痕迹。尽管可以永久记录(通过抓包与日志),但我们通常不这么做。即使是将消息持久地写入磁盘的消息代理,在送达给消费者之后也会很快删除消息,因为它们建立在短暂消息传递的思维方式上。数据库和文件系统采用截然相反的方法论:至少在某人显式删除前,通常写入数据库或文件的所有内容都要被永久记录下来。 4 | 5 | 这种思维方式上的差异对创建衍生数据的方式有巨大影响。批处理过程的一个关键特性是,你可以反复运行它们,试验处理步骤,不用担心损坏输入(因为输入是只读的)。而 AMQP/JMS 风格的消息传递并非如此:收到消息是具有破坏性的,因为确认可能导致消息从代理中被删除,因此你不能期望再次运行同一个消费者能得到相同的结果。如果你将新的消费者添加到消息系统,通常只能接收到消费者注册之后开始发送的消息。先前的任何消息都随风而逝,一去不复返。作为对比,你可以随时为文件和数据库添加新的客户端,且能读取任意久远的数据(只要应用没有显式覆盖或删除这些数据)。 6 | 7 | 为什么我们不能把它俩杂交一下,既有数据库的持久存储方式,又有消息传递的低延迟通知?这就是基于日志的消息代理(log-based message brokers)背后的想法。 8 | 9 | ## 使用日志进行消息存储 10 | 11 | 日志只是磁盘上简单的仅追加记录序列,在[《数据库](https://github.com/wx-chevalier/Database-Notes)》中我们也讨论了日志结构存储引擎和预写式日志的应用。同样的结构可以用于实现消息代理:生产者通过将消息追加到日志末尾来发送消息,而消费者通过依次读取日志来接收消息。如果消费者读到日志末尾,则会等待新消息追加的通知。Unix 工具 tail -f 能监视文件被追加写入的数据,基本上就是这样工作的。 12 | 13 | 为了扩展到比单个磁盘所能提供的更高吞吐量,可以对日志进行分区,不同的分区可以托管在不同的机器上,且每个分区都拆分出一份能独立于其他分区进行读写的日志。一个主题可以定义为一组携带相同类型消息的分区。在每个分区内,代理为每个消息分配一个单调递增的序列号或偏移量(offset),这种序列号是有意义的,因为分区是仅追加写入的,所以分区内的消息是完全有序的。没有跨不同分区的顺序保证。 14 | 15 | ![生产者通过将消息追加写入主题分区文件来发送消息,消费者依次读取这些文件](https://s2.ax1x.com/2020/02/14/1XJRn1.md.png) 16 | 17 | Apache Kafka,Amazon Kinesis Streams 和 Twitter 的 DistributedLog 都是基于日志的消息代理。Google Cloud Pub/Sub 在架构上类似,但对外暴露的是 JMS 风格的 API,而不是日志抽象。尽管这些消息代理将所有消息写入磁盘,但通过跨多台机器分区,每秒能够实现数百万条消息的吞吐量,并通过复制消息来实现容错性。 18 | 19 | ## 日志与传统消息相比 20 | 21 | 基于日志的方法天然支持扇出式消息传递,因为多个消费者可以独立读取日志,而不会相互影响:读取消息不会将其从日志中删除。为了在一组消费者之间实现负载平衡,代理可以将整个分区分配给消费者组中的节点,而不是将单条消息分配给消费者客户端。 22 | 23 | 每个客户端消费指派分区中的所有消息。然后使用分配的分区中的所有消息。通常情况下,当一个用户被指派了一个日志分区时,它会以简单的单线程方式顺序地读取分区中的消息。这种粗粒度的负载均衡方法有一些缺点: 24 | 25 | - 共享消费主题工作的节点数,最多为该主题中的日志分区数,因为同一个分区内的所有消息被递送到同一个节点。 26 | - 如果某条消息处理缓慢,则它会阻塞该分区中后续消息的处理。 27 | 28 | 因此在消息处理代价高昂,希望逐条并行处理,以及消息的顺序并没有那么重要的情况下,JMS/AMQP 风格的消息代理是可取的。另一方面,在消息吞吐量很高,处理迅速,顺序很重要的情况下,基于日志的方法表现得非常好。 29 | 30 | ## 消费者偏移量 31 | 32 | 顺序消费一个分区使得判断消息是否已经被处理变得相当容易:所有偏移量小于消费者的当前偏移量的消息已经被处理,而具有更大偏移量的消息还没有被看到。因此,代理不需要跟踪确认每条消息,只需要定期记录消费者的偏移即可。在这种方法减少了额外簿记开销,而且在批处理和流处理中采用这种方法有助于提高基于日志的系统的吞吐量。 33 | 34 | 实际上,这种偏移量与单领导者数据库复制中常见的日志序列号非常相似。在数据库复制中,日志序列号允许跟随者断开连接后,重新连接到领导者,并在不跳过任何写入的情况下恢复复制。这里原理完全相同:消息代理的表现得像一个主库,而消费者就像一个从库。 35 | 36 | 如果消费者节点失效,则失效消费者的分区将指派给其他节点,并从最后记录的偏移量开始消费消息。如果消费者已经处理了后续的消息,但还没有记录它们的偏移量,那么重启后这些消息将被处理两次。 37 | 38 | ## 磁盘空间使用 39 | 40 | 如果只追加写入日志,则磁盘空间终究会耗尽。为了回收磁盘空间,日志实际上被分割成段,并不时地将旧段删除或移动到归档存储。这就意味着如果一个慢消费者跟不上消息产生的速率而落后的太多,它的消费偏移量指向了删除的段,那么它就会错过一些消息。实际上,日志实现了一个有限大小的缓冲区,当缓冲区填满时会丢弃旧消息,它也被称为循环缓冲区(circular buffer)或环形缓冲区(ring buffer)。不过由于缓冲区在磁盘上,因此可能相当的大。 41 | 42 | 让我们做个简单计算。在撰写本文时,典型的大型硬盘容量为 6TB,顺序写入吞吐量为 150MB/s。如果以最快的速度写消息,则需要大约 11 个小时才能填满磁盘。因而磁盘可以缓冲 11 个小时的消息,之后它将开始覆盖旧的消息。即使使用多个磁盘和机器,这个比率也是一样的。实践中的部署很少能用满磁盘的写入带宽,所以通常可以保存一个几天甚至几周的日志缓冲区。 43 | 44 | 不管保留多长时间的消息,日志的吞吐量或多或少保持不变,因为无论如何,每个消息都会被写入磁盘。这种行为与默认将消息保存在内存中,仅当队列太长时才写入磁盘的消息传递系统形成鲜明对比。当队列很短时,这些系统非常快;而当这些系统开始写入磁盘时,就要慢的多,所以吞吐量取决于保留的历史数量。 45 | 46 | ## 当消费者跟不上生产者时 47 | 48 | 在[《微服务调用》](https://github.com/wx-chevalier/MicroService-Notes)中我们介绍过如果消费者无法跟上生产者发送信息的速度时,我们讨论了三种选择:丢弃信息,进行缓冲或施加背压。在这种分类法里,基于日志的方法是缓冲的一种形式,具有很大,但大小固定的缓冲区(受可用磁盘空间的限制)。如果消费者远远落后,而所要求的信息比保留在磁盘上的信息还要旧,那么它将不能读取这些信息,所以代理实际上丢弃了比缓冲区容量更大的旧信息。你可以监控消费者落后日志头部的距离,如果落后太多就发出报警。由于缓冲区很大,因而有足够的时间让人工运维来修复慢消费者,并在消息开始丢失之前让其赶上。 49 | 50 | 即使消费者真的落后太多开始丢失消息,也只有那个消费者受到影响;它不会中断其他消费者的服务。这是一个巨大的运维优势:你可以实验性地消费生产日志,以进行开发,测试或调试,而不必担心会中断生产服务。当消费者关闭或崩溃时,会停止消耗资源,唯一剩下的只有消费者偏移量。这种行为也与传统的信息代理形成了鲜明对比,在那种情况下,你需要小心地删除那些消费者已经关闭的队列,否则那些队列就会累积不必要的消息,从其他仍活跃的消费者那里占走内存。 51 | 52 | ## 重播旧信息 53 | 54 | 我们之前提到,使用 AMQP 和 JMS 风格的消息代理,处理和确认消息是一个破坏性的操作,因为它会导致消息在代理上被删除。另一方面,在基于日志的消息代理中,使用消息更像是从文件中读取数据:这是只读操作,不会更改日志。 55 | 56 | 除了消费者的任何输出之外,处理的唯一副作用是消费者偏移量的前进。但偏移量是在消费者的控制之下的,所以如果需要的话可以很容易地操纵:例如你可以用昨天的偏移量跑一个消费者副本,并将输出写到不同的位置,以便重新处理最近一天的消息。你可以使用各种不同的处理代码重复任意次。 57 | 58 | 这一方面使得基于日志的消息传递更像上一章的批处理,其中衍生数据通过可重复的转换过程与输入数据显式分离。它允许进行更多的实验,更容易从错误和漏洞中恢复,使其成为在组织内集成数据流的良好工具。 59 | -------------------------------------------------------------------------------- /_sidebar.md: -------------------------------------------------------------------------------- 1 | - [1 01.概念与设计 [4]](/01.概念与设计/README.md) 2 | - [1.1 01~背景与价值 [2]](/01.概念与设计/01~背景与价值/README.md) 3 | - [1.1.1 消息传递的不同方式](/01.概念与设计/01~背景与价值/消息传递的不同方式.md) 4 | - [1.1.2 消息队列的应用场景](/01.概念与设计/01~背景与价值/消息队列的应用场景.md) 5 | - 1.2 02.消息生产与消费 [5] 6 | - 1.2.1 消息传输模型 [3] 7 | - [1.2.1.1 不同系统的消息传输模型](/01.概念与设计/02.消息生产与消费/消息传输模型/不同系统的消息传输模型.md) 8 | - [1.2.1.2 点对点模型与发布订阅模型](/01.概念与设计/02.消息生产与消费/消息传输模型/点对点模型与发布订阅模型.md) 9 | - [1.2.1.3 队列模型与流式模型](/01.概念与设计/02.消息生产与消费/消息传输模型/队列模型与流式模型.md) 10 | - [1.2.2 消息积压处理](/01.概念与设计/02.消息生产与消费/消息积压处理.md) 11 | - [1.2.3 消息防丢失](/01.概念与设计/02.消息生产与消费/消息防丢失.md) 12 | - [1.2.4 重复消息处理](/01.概念与设计/02.消息生产与消费/重复消息处理.md) 13 | - [1.2.5 顺序消息](/01.概念与设计/02.消息生产与消费/顺序消息.md) 14 | - 1.3 03.消息存储 [2] 15 | - [1.3.1 分区日志](/01.概念与设计/03.消息存储/分区日志.md) 16 | - [1.3.2 消息存储](/01.概念与设计/03.消息存储/消息存储.md) 17 | - [1.4 04.开源系统对比 [2]](/01.概念与设计/04.开源系统对比/README.md) 18 | - [1.4.1 功能对比](/01.概念与设计/04.开源系统对比/功能对比.md) 19 | - [1.4.2 性能对比](/01.概念与设计/04.开源系统对比/性能对比.md) 20 | - [2 02.RabbitMQ [3]](/02.RabbitMQ/README.md) 21 | - 2.1 接口使用 [2] 22 | - [2.1.1 Java](/02.RabbitMQ/接口使用/Java.md) 23 | - [2.1.2 Python](/02.RabbitMQ/接口使用/Python.md) 24 | - 2.2 架构模型 [3] 25 | - [2.2.1 消息存储](/02.RabbitMQ/架构模型/消息存储.md) 26 | - [2.2.2 消息消费](/02.RabbitMQ/架构模型/消息消费.md) 27 | - [2.2.3 消息类型](/02.RabbitMQ/架构模型/消息类型.md) 28 | - 2.3 集群部署 [2] 29 | - [2.3.1 部署配置](/02.RabbitMQ/集群部署/部署配置.md) 30 | - [2.3.2 集群与高可用](/02.RabbitMQ/集群部署/集群与高可用.md) 31 | - [3 03.Kafka [5]](/03.Kafka/README.md) 32 | - [3.1 Kafka Streams](/03.Kafka/Kafka%20Streams/README.md) 33 | 34 | - [3.2 架构概念](/03.Kafka/架构概念.md) 35 | - [3.3 消息代理 [5]](/03.Kafka/消息代理/README.md) 36 | - [3.3.1 ZooKeeper](/03.Kafka/消息代理/ZooKeeper.md) 37 | - [3.3.2 副本](/03.Kafka/消息代理/副本.md) 38 | - [3.3.3 日志文件](/03.Kafka/消息代理/日志文件.md) 39 | - [3.3.4 磁盘读写优化](/03.Kafka/消息代理/磁盘读写优化.md) 40 | - [3.3.5 网络模型](/03.Kafka/消息代理/网络模型.md) 41 | - [3.4 消息生产与消费 [3]](/03.Kafka/消息生产与消费/README.md) 42 | - [3.4.1 事务消息](/03.Kafka/消息生产与消费/事务消息.md) 43 | - [3.4.2 消费者](/03.Kafka/消息生产与消费/消费者.md) 44 | - [3.4.3 生产者](/03.Kafka/消息生产与消费/生产者.md) 45 | - 3.5 集群部署 [3] 46 | - [3.5.1 参数配置](/03.Kafka/集群部署/参数配置.md) 47 | - [3.5.2 部署配置](/03.Kafka/集群部署/部署配置.md) 48 | - [3.5.3 集群与高可用](/03.Kafka/集群部署/集群与高可用.md) 49 | - [4 04.RocketMQ [5]](/04.RocketMQ/README.md) 50 | - [4.1 01~领域模型](/04.RocketMQ/01~领域模型/README.md) 51 | 52 | - 4.2 02.消息发送与消费 [2] 53 | - 4.2.1 99~参考资料 [2] 54 | - [4.2.1.1 RocketMQ 5.0 POP 消费模式探秘](/04.RocketMQ/02.消息发送与消费/99~参考资料/2021-RocketMQ%205.0%20POP%20消费模式探秘.md) 55 | - [4.2.1.2 RocketMQ 消息短暂而又精彩的一生](/04.RocketMQ/02.消息发送与消费/99~参考资料/2023-RocketMQ%20消息短暂而又精彩的一生.md) 56 | - [4.2.2 消息消费](/04.RocketMQ/02.消息发送与消费/消息消费.md) 57 | - 4.3 03.集群与高可用 [1] 58 | - [4.3.1 集群与高可用](/04.RocketMQ/03.集群与高可用/集群与高可用.md) 59 | - [4.4 04.内部机制 [1]](/04.RocketMQ/04.内部机制/README.md) 60 | - 4.4.1 99~参考资料 [2] 61 | - 4.4.1.1 Vivo 深入剖析 RocketMQ 源码 [1] 62 | - [4.4.1.1.1 存储模块](/04.RocketMQ/04.内部机制/99~参考资料/2021-Vivo-深入剖析%20RocketMQ%20源码/存储模块.md) 63 | - [4.4.1.2 深度解读 RocketMQ 存储机制](/04.RocketMQ/04.内部机制/99~参考资料/2022-深度解读%20RocketMQ%20存储机制.md) 64 | - 4.5 10.版本变迁 [1] 65 | - 4.5.1 RocketMQ 5.0 [1] 66 | - [4.5.1.1 RocketMQ 5.0: 存储计算分离新思路](/04.RocketMQ/10.版本变迁/RocketMQ%205.0/2022-RocketMQ%205.0:%20存储计算分离新思路.md) 67 | - [5 05~Pulsar [3]](/05~Pulsar/README.md) 68 | - [5.1 Pulsar 设计概览](/05~Pulsar/Pulsar%20设计概览.md) 69 | - [5.2 消息生产与消费 [1]](/05~Pulsar/消息生产与消费/README.md) 70 | - [5.2.1 消息消费模型](/05~Pulsar/消息生产与消费/消息消费模型.md) 71 | - 5.3 集群部署 [1] 72 | - [5.3.1 部署配置](/05~Pulsar/集群部署/部署配置.md) 73 | - [6 06.ActiveMQ [1]](/06.ActiveMQ/README.md) 74 | - [6.1 架构设计](/06.ActiveMQ/架构设计.md) 75 | - 7 10.实践案例 [2] 76 | - [7.1 从零设计消息队列](/10.实践案例/从零设计消息队列/README.md) 77 | 78 | - [7.2 哈啰单车 [1]](/10.实践案例/哈啰单车/README.md) 79 | - [7.2.1 哈啰在分布式消息治理和微服务治理中的实践](/10.实践案例/哈啰单车/2021-哈啰在分布式消息治理和微服务治理中的实践.md) 80 | - [8 INTRODUCTION](/INTRODUCTION.md) -------------------------------------------------------------------------------- /05~Pulsar/消息生产与消费/消息消费模型.md: -------------------------------------------------------------------------------- 1 | # Pulsar 消息消费模型 2 | 3 | Apache Pulsar 通过订阅,抽象出了统一的: producer-topic-subscription-consumer 消费模型。Pulsar 的消息模型既支持队列模型,也支持流模型。在 Pulsar 的消息消费模型中,Topic 是用于发送消息的通道。每一个 Topic 对应着 Apache BookKeeper 中的一个分布式日志。发布者发布的每条消息只在 Topic 中存储一次;存储的过程中,BookKeeper 会将消息复制存储在多个存储节点上;Topic 中的每条消息,可以根据消费者的订阅需求,多次被使用,每个订阅对应一个消费者组(Consumer Group)。 4 | 5 | 主题(Topic)是消费消息的真实来源。尽管消息仅在主题(Topic)上存储一次,但是用户可以有不同的订阅方式来消费这些消息: 6 | 7 | - 消费者被组合在一起以消费消息,每个消费组是一个订阅。 8 | - 每个 Topic 可以有不同的消费组。 9 | - 每组消费者都是对主题的一个订阅。 10 | - 每组消费者可以拥有自己不同的消费方式: 独占(Exclusive),故障切换(Failover)或共享(Share)。 11 | 12 | Pulsar 通过这种模型,将队列模型和流模型这两种模型结合在了一起,提供了统一的 API 接口。这种模型,既不会影响消息系统的性能,也不会带来额外的开销,同时还为用户提供了更多灵活性,方便用户程序以最匹配模式来使用消息系统。 13 | 14 | # 三种订阅模式 15 | 16 | ## 独占订阅(Stream 流模型) 17 | 18 | 顾名思义,独占订阅中,在任何时间,一个消费者组(订阅)中有且只有一个消费者来消费 Topic 中的消息。下图是独占订阅的示例。在这个示例中有一个有订阅 A 的活跃消费者 A-0,消息 m0 到 m4 按顺序传送并由 A-0 消费。如果另一个消费者 A-1 想要附加到订阅 A,则是不被允许的。 19 | 20 | ![独占订阅](https://pic.imgdb.cn/item/60853f51d1a9ae528f8d9d04.jpg) 21 | 22 | ## 故障切换(Stream 流模型) 23 | 24 | 使用故障切换订阅,多个消费者(Consumer)可以附加到同一订阅。但是,一个订阅中的所有消费者,只会有一个消费者被选为该订阅的主消费者。其他消费者将被指定为故障转移消费者。当主消费者断开连接时,分区将被重新分配给其中一个故障转移消费者,而新分配的消费者将成为新的主消费者。发生这种情况时,所有未确认(ack)的消息都将传递给新的主消费者。这类似于 Apache Kafka 中的 Consumer partition rebalance。 25 | 26 | 下图是故障切换订阅的示例。消费者 B-0 和 B-1 通过订阅 B 订阅消费消息。B-0 是主消费者并接收所有消息。B-1 是故障转移消费者,如果消费者 B-0 出现故障,它将接管消费。 27 | 28 | ![故障切换模型](https://pic.imgdb.cn/item/60853f77d1a9ae528f8ef049.jpg) 29 | 30 | ## 共享订阅(Queue 队列模型) 31 | 32 | 使用共享订阅,在同一个订阅背后,用户按照应用的需求挂载任意多的消费者。订阅中的所有消息以循环分发形式发送给订阅背后的多个消费者,并且一个消息仅传递给一个消费者。当消费者断开连接时,所有传递给它但是未被确认(ack)的消息将被重新分配和组织,以便发送给该订阅上剩余的剩余消费者。 33 | 34 | 下图是共享订阅的示例。消费者 C-1,C-2 和 C-3 都在同一主题上消费消息。每个消费者接收大约所有消息的 1/3。如果想提高消费的速度,用户不需要不增加分区数量,只需要在同一个订阅中添加更多的消费者。 35 | 36 | ![共享订阅模型](https://pic.imgdb.cn/item/60853fa3d1a9ae528f90cb60.jpg) 37 | 38 | ## 三种订阅模式的选择 39 | 40 | 独占和故障切换订阅,仅允许一个消费者来使用和消费,每个对主题的订阅。这两种模式都按主题分区顺序使用消息。它们最适用于需要严格消息顺序的流(Stream)用例。共享订阅允许每个主题分区有多个消费者。同一订阅中的每个消费者仅接收主题分区的一部分消息。共享订阅最适用于不需要保证消息顺序的队列(Queue)的使用模式,并且可以按照需要任意扩展消费者的数量。 41 | 42 | Pulsar 中的订阅实际上与 Apache Kafka 中的 Consumer Group 的概念类似。创建订阅的操作很轻量化,而且具有高度可扩展性,用户可以根据应用的需要创建任意数量的订阅。对同一主题的不同订阅,也可以采用不同的订阅类型。比如用户可以在同一主题上可以提供一个包含 3 个消费者的故障切换订阅,同时也提供一个包含 20 个消费者的共享订阅,并且可以在不改变分区数量的情况下,向共享订阅添加更多的消费者。 43 | 44 | 下图描绘了一个包含 3 个订阅 A,B 和 C 的主题,并说明了消息如何从生产者流向消费者。 45 | 46 | ![不同订阅模式下的演示](https://pic.imgdb.cn/item/60854050d1a9ae528f953d0e.jpg) 47 | 48 | 除了统一消息 API 之外,由于 Pulsar 主题分区实际上是存储在 Apache BookKeeper 中,它还提供了一个读取 API(Reader),类似于消费者 API(但 Reader 没有游标管理),以便用户完全控制如何使用 Topic 中的消息。 49 | 50 | # Pulsar 的消息确认(ACK) 51 | 52 | 由于分布式系统的特性,当使用分布式消息系统时,可能会发生故障。比如在消费者从消息系统中的主题消费消息的过程中,消费消息的消费者和服务于主题分区的消息代理(Broker)都可能发生错误。消息确认(ACK)的目的就是保证当发生这样的故障后,消费者能够从上一次停止的地方恢复消费,保证既不会丢失消息,也不会重复处理已经确认(ACK)的消息。 53 | 54 | - 在 Apache Kafka 中,恢复点通常称为 Offset,更新恢复点的过程称为消息确认或提交 Offset。 55 | - 在 Apache Pulsar 中,每个订阅中都使用一个专门的数据结构–游标(Cursor)来跟踪订阅中的每条消息的确认(ACK)状态。每当消费者在主题分区上确认消息时,游标都会更新。更新游标可确保消费者不会再次收到消息。 56 | 57 | Apache Pulsar 提供两种消息确认方法,单条确认(Individual Ack)和累积确认(Cumulative Ack)。通过累积确认,消费者只需要确认它收到的最后一条消息。主题分区中的所有消息(包括)提供消息 ID 将被标记为已确认,并且不会再次传递给消费者。累积确认与 Apache Kafka 中的 Offset 更新类似。Apache Pulsar 可以支持消息的单条确认,也就是选择性确认。消费者可以单独确认一条消息。被确认后的消息将不会被重新传递。下图说明了单条确认和累积确认的差异(灰色框中的消息被确认并且不会被重新传递)。在图的上半部分,它显示了累计确认的一个例子,M12 之前的消息被标记为 acked。在图的下半部分,它显示了单独进行 acking 的示例。仅确认消息 M7 和 M12 - 在消费者失败的情况下,除了 M7 和 M12 之外,其他所有消息将被重新传送。 58 | 59 | ![单条确认与累计确认](https://pic.imgdb.cn/item/6085415cd1a9ae528f9bc500.jpg) 60 | 61 | 独占订阅或故障切换订阅的消费者能够对消息进行单条确认和累积确认;共享订阅的消费者只允许对消息进行单条确认。单条确认消息的能力为处理消费者故障提供了更好的体验。对于某些应用来说,处理一条消息可能需要很长时间或者非常昂贵,防止重新传送已经确认的消息非常重要。这个管理 Ack 的专门的数据结构–游标(Cursor),由 Broker 来管理,利用 BookKeeper 的 Ledger 提供存储,在后面的文章中我们会介绍更多的关于游标(Cursor)的细节。Apache Pulsar 提供了灵活的消息消费订阅类型和消息确认方法,通过简单的统一的 API,就可以支持各种消息和流的使用场景。 62 | 63 | # Pulsar 的消息保留(Retention) 64 | 65 | 在消息被确认后,Pulsar 的 Broker 会更新对应的游标。当 Topic 里面中的一条消息,被所有的订阅都确认 ack 后,才能删除这条消息。Pulsar 还允许通过设置保留时间,将消息保留更长时间,即使所有订阅已经确认消费了它们。下图说明了如何在有 2 个订阅的主题中保留消息。订阅 A 在 M6 和订阅 B 已经消耗了 M10 之前的所有消息之前已经消耗了所有消息。这意味着 M6 之前的所有消息(灰色框中)都可以安全删除。订阅 A 仍未使用 M6 和 M9 之间的消息,无法删除它们。如果主题配置了消息保留期,则消息 M0 到 M5 将在配置的时间段内保持不变,即使 A 和 B 已经确认消费了它们。 66 | 67 | ![消息保留](https://pic.imgdb.cn/item/60854548d1a9ae528fb34af6.jpg) 68 | 69 | 在消息保留策略中,Pulsar 还支持消息生存时间(TTL)。如果消息未在配置的 TTL 时间段内被任何消费者使用,则消息将自动标记为已确认。消息保留期消息 TTL 之间的区别在于:消息保留期作用于标记为已确认并设置为已删除的消息,而 TTL 作用于未 ack 的消息。上面的图例中说明了 Pulsar 中的 TTL。例如,如果订阅 B 没有活动消费者,则在配置的 TTL 时间段过后,消息 M10 将自动标记为已确认,即使没有消费者实际读取该消息。 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Contributors][contributors-shield]][contributors-url] 2 | [![Forks][forks-shield]][forks-url] 3 | [![Stargazers][stars-shield]][stars-url] 4 | [![Issues][issues-shield]][issues-url] 5 | [![license: CC BY-NC-SA 4.0](https://img.shields.io/badge/license-CC%20BY--NC--SA%204.0-lightgrey.svg)][license-url] 6 | 7 | 8 |
9 |

10 | 11 | Logo 12 | 13 | 14 |

15 | 在线阅读 >> 16 |
17 |
18 | 代码实践 19 | · 20 | 参考资料 21 | 22 |

23 |

24 | 25 | 26 | 27 | # Introduction | 前言 28 | 29 | 随着信息技术的快速发展及互联网用户规模的急剧增长,计算机所存储的信息量正呈爆炸式增长,目前数据量已进入大规模和超大规模的海量数据时代,如何高效地存储、分析、处理和挖掘海量数据已成为技术研究领域的热点和难点问题。当前出现的云存储、分布式存储系统、NoSQL 数据库及列存储等前沿技术在海量数据的驱使下,正日新月异地向前发展,采用这些技术来处理大数据成为一种发展趋势。而如何采集和运营管理、分析这些数据也是大数据处理中一个至关重要的组成环节,这就需要相应的基础设施对其提供支持。在现代微服务化的分布式应用中,不同服务之间往往需要进行频繁而广泛的通信,我们可以把消息队列比作是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。 30 | 31 | > 参考维基百科的定义,在计算机科学中,消息队列和邮箱是用于进程间通信(IPC)的软件工程组件,或用于同一进程内的线程间通信的软件工程组件。与标准的请求-应答模式的 [RPC](http://ngte-be.gitbook.io/?q=RPC) 相比,RPC 更强调点对点交互、强事务保证和延迟敏感的服务/应用之间的通信,消息队列则更关注于异步通信、内容投递。 32 | 33 | 消息队列作为服务/应用之间的通信中间件,可以起到业务耦合、广播消息、保证最终一致性以及错峰流控(克服短板瓶颈)等作用。这里所谓的消息,可以看做一个小的,自包含的,不可变的对象,包含某个时间点发生的某件事情的细节:它通常包含一个来自时钟的时间戳,以指明消息发生的时间。例如,发生的消息可能是用户采取的行动,例如查看页面或进行购买。它也可能来源于机器,例如对温度传感器或 CPU 利用率的周期性测量。一个消息由生产者(producer)(也称为 发布者(publisher)或发送者(sender))生成一次,然后可能由多个消费者(consumer)(订阅者(subscribers)或接收者(recipients))进行处理。 34 | 35 | ![消息队列示意](https://pic.imgdb.cn/item/6086a9ead1a9ae528f1a0422.jpg) 36 | 37 | 更多介绍与导览参阅 [INTRODUCTION](./INTRODUCTION.md)。 38 | 39 | ## Nav | 关联导航 40 | 41 | # About | 关于 42 | 43 | 44 | 45 | ## Contributing 46 | 47 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 48 | 49 | 1. Fork the Project 50 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 51 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 52 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 53 | 5. Open a Pull Request 54 | 55 | 56 | 57 | ## Acknowledgements 58 | 59 | - [Awesome-Lists](https://github.com/wx-chevalier/Awesome-Lists): 📚 Guide to Galaxy, curated, worthy and up-to-date links/reading list for ITCS-Coding/Algorithm/SoftwareArchitecture/AI. 💫 ITCS-编程/算法/软件架构/人工智能等领域的文章/书籍/资料/项目链接精选。 60 | 61 | - [Awesome-CS-Books](https://github.com/wx-chevalier/Awesome-CS-Books): :books: Awesome CS Books/Series(.pdf by git lfs) Warehouse for Geeks, ProgrammingLanguage, SoftwareEngineering, Web, AI, ServerSideApplication, Infrastructure, FE etc. :dizzy: 优秀计算机科学与技术领域相关的书籍归档。 62 | 63 | ## Copyright & More | 延伸阅读 64 | 65 | 笔者所有文章遵循[知识共享 署名 - 非商业性使用 - 禁止演绎 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh),欢迎转载,尊重版权。您还可以前往 [NGTE Books](https://ng-tech.icu/books-gallery/) 主页浏览包含知识体系、编程语言、软件工程、模式与架构、Web 与大前端、服务端开发实践与工程架构、分布式基础架构、人工智能与深度学习、产品运营与创业等多类目的书籍列表: 66 | 67 | [![NGTE Books](https://s2.ax1x.com/2020/01/18/19uXtI.png)](https://ng-tech.icu/books-gallery/) 68 | 69 | 70 | 71 | 72 | [contributors-shield]: https://img.shields.io/github/contributors/wx-chevalier/repo.svg?style=flat-square 73 | [contributors-url]: https://github.com/wx-chevalier/repo/graphs/contributors 74 | [forks-shield]: https://img.shields.io/github/forks/wx-chevalier/repo.svg?style=flat-square 75 | [forks-url]: https://github.com/wx-chevalier/repo/network/members 76 | [stars-shield]: https://img.shields.io/github/stars/wx-chevalier/repo.svg?style=flat-square 77 | [stars-url]: https://github.com/wx-chevalier/repo/stargazers 78 | [issues-shield]: https://img.shields.io/github/issues/wx-chevalier/repo.svg?style=flat-square 79 | [issues-url]: https://github.com/wx-chevalier/repo/issues 80 | [license-shield]: https://img.shields.io/github/license/wx-chevalier/repo.svg?style=flat-square 81 | [license-url]: https://github.com/wx-chevalier/repo/blob/master/LICENSE.txt 82 | -------------------------------------------------------------------------------- /03~Kafka/消息代理/日志文件.md: -------------------------------------------------------------------------------- 1 | # 消息存储 2 | 3 | # 日志文件格式 4 | 5 | 一个 topic 可以认为一个一类消息,每个 topic 将被分成多个 partition,每个 partition 在存储层面是 append log 文件。 6 | 7 | ![Producer Topic 与 Partition](https://pic.imgdb.cn/item/607705e18322e6675c2f8ade.jpg) 8 | 9 | 在 Kafka 文件存储中,同一个 topic 下有多个不同 partition,每个 partition 为一个目录,partiton 命名规则为 topic 名称+有序序号,第一个 partiton 序号从 0 开始,序号最大值为 partitions 数量减 1。 10 | 11 | ![文件分区与分割](https://pic.imgdb.cn/item/607706098322e6675c2fe553.jpg) 12 | 13 | - 每个 partion(目录)相当于一个巨型文件被平均分配到多个大小相等 segment(段)数据文件中。但每个段 segment file 消息数量不一定相等,这种特性方便 old segment file 快速被删除。 14 | - 每个 partiton 只需要支持顺序读写就行了,segment 文件生命周期由服务端配置参数决定。 15 | 16 | 这样做的好处就是能快速删除无用文件,有效提高磁盘利用率。 17 | 18 | - segment file 组成:由 2 大部分组成,分别为 index file 和 data file,此 2 个文件一一对应,成对出现,后缀".index"和“.log”分别表示为 segment 索引文件、数据文件. 19 | - segment 文件命名规则:partion 全局的第一个 segment 从 0 开始,后续每个 segment 文件名为上一个 segment 文件最后一条消息的 offset 值。数值最大为 64 位 long 大小,20 位数字字符长度,没有数字用 0 填充。 20 | 21 | ![Segment 文件格式](https://pic.imgdb.cn/item/607706968322e6675c3140cd.jpg) 22 | 23 | ## Segment 索引与 Offset 查找 24 | 25 | 由于 Kafka 消息数据太大,如果全部建立索引,即占了空间又增加了耗时,所以 Kafka 选择了稀疏索引的方式,这样的话索引可以直接进入内存,加快偏查询速度。,通过 mmap 可以直接内存操作,稀疏索引为数据文件的每个对应 message 设置一个元数据指针,它比稠密索引节省了更多的存储空间,但查找起来需要消耗更多的时间。Segment 中 index 与 data file 对应关系物理结构如下: 26 | 27 | ![Segment 中 index 与 data file 对应](https://pic.imgdb.cn/item/607706b98322e6675c3193c6.jpg) 28 | 29 | 上图中索引文件存储大量元数据,数据文件存储大量消息,索引文件中元数据指向对应数据文件中 message 的物理偏移地址。其中以索引文件中元数据 3,497 为例,依次在数据文件中表示第 3 个 message(在全局 partiton 表示 offset 为 368772 的 message),以及该消息的物理偏移地址为 497。 30 | 31 | 查找某个 offset 的消息,先二分法找出消息所在的 segment 文件(因为每个 segment 的命名都是以该文件中消息 offset 最小的值命名);然后,加载对应的.index 索引文件到内存,同样二分法找出小于等于给定 offset 的最大的那个 offset 记录(相对 offset,position);最后,根据 position 到.log 文件中,顺序查找出 offset 等于给定 offset 值的消息。例如读取 offset=368776 的 message,需要通过下面 2 个步骤查找: 32 | 33 | - 第一步查找 segment file,其中 00000000000000000000.index 表示最开始的文件,起始偏移量(offset)为 0。第二个文件 00000000000000368769.index 的消息量起始偏移量为 368770 = 368769 + 1.同样,第三个文件 00000000000000737337.index 的起始偏移量为 737338=737337 + 1,其他后续文件依次类推,以起始偏移量命名并排序这些文件,只要根据 offset **二分查找**文件列表,就可以快速定位到具体文件。当 offset=368776 时定位到 00000000000000368769.index|log 34 | 35 | - 第二步通过 segment file 查找 message 通过第一步定位到 segment file,当 offset=368776 时,依次定位到 00000000000000368769.index 的元数据物理位置和 00000000000000368769.log 的物理偏移地址,然后再通过 00000000000000368769.log 顺序查找直到 offset=368776 为止。 36 | 37 | 上面讲的是如果要找某个 offset 的流程,但是我们大多数时候并不需要查找某个 offset,只需要按照顺序读即可,而在顺序读中,操作系统会对内存和磁盘之间添加 page cahe,也就是我们平常见到的预读操作,所以我们的顺序读操作时速度很快。但是 kafka 有个问题,如果分区过多,那么日志分段也会很多,写的时候由于是批量写,其实就会变成随机写了,随机 I/O 这个时候对性能影响很大。所以一般来说 Kafka 不能有太多的 partition。针对这一点,RocketMQ 把所有的日志都写在一个文件里面,就能变成顺序写,通过一定优化,读也能接近于顺序读。 38 | 39 | ## Message 物理结构 40 | 41 | 了解到 segment data file 由许多 message 组成,下面详细说明 message 物理结构如下: 42 | 43 | ![message 物理结构](https://pic.imgdb.cn/item/607707668322e6675c333389.jpg) 44 | 45 | 参数说明如下: 46 | 47 | | 关键字 | 解释说明 | 48 | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 49 | | 8 byte offset | 在 parition(分区)内的每条消息都有一个有序的 id 号,这个 id 号被称为偏移(offset),它可以唯一确定每条消息在 parition(分区)内的位置。即 offset 表示 partiion 的第多少 message | 50 | | 4 byte message size | message 大小 | 51 | | 4 byte CRC32 | 用 crc32 校验 message | 52 | | 1 byte “magic" | 表示本次发布 Kafka 服务程序协议版本号 | 53 | | 1 byte “attributes" | 表示为独立版本、或标识压缩类型、或编码类型。 | 54 | | 4 byte key length | 表示 key 的长度,当 key 为-1 时,K byte key 字段不填 | 55 | | K byte key | 可选 | 56 | | value bytes payload | 表示实际消息数据。 | 57 | -------------------------------------------------------------------------------- /03~Kafka/消息代理/副本.md: -------------------------------------------------------------------------------- 1 | # 副本(replication)策略 2 | 3 | Kafka 在 0.8 版本前没有提供 Partition 的 Replication 机制,一旦 Broker 宕机,其上的所有 Partition 就都无法提供服务,而 Partition 又没有备份数据,数据的可用性就大大降低了。所以 0.8 后提供了 Replication 机制来保证 Broker 的 failover。 4 | 5 | ![副本选择新的 Leader](https://pic.imgdb.cn/item/607709288322e6675c3740f0.jpg) 6 | 7 | Kafka 的高可靠性的保障来源于其健壮的副本(replication)策略,即多个服务端节点对其他节点的主题分区的日志进行复制。当集群中的某个节点出现故障,访问故障节点的请求会被转移到其他正常节点(这一过程通常叫 Reblance)。Kafka 的 replica 副本单元是 topic 的 partition,一个 partition 的 replica 数量不能超过 broker 的数量,因为一个 broker 最多只会存储这个 partition 的一个副本。所有消息生产、消费请求都是由 partition 的 leader replica 来处理,其他 follower replica 负责从 leader 复制数据进行备份,保持和 Leader 的数据同步。如果没有 Leader 副本,那就需要所有的副本都同时负责读/写请求处理,同时还得保证这些副本之间数据的一致性,假设有 n 个副本则需要有 n×n 条通路来同步数据,这样数据的一致性和有序性就很难保证。 8 | 9 | ## ISR 10 | 11 | 在 Kafka 中并不是所有的 Follower 都能被拿来替代 Leader,所以在 Kafka 的 Leader 节点中维护着一个 ISR(In sync Replicas)集合,翻译过来也叫正在同步中集合,在这个集合中的需要满足两个条件: 12 | 13 | - 一是它必须维护与 ZooKeeper 的 session(这个通过 ZooKeeper 的 Heartbeat 机制来实现)。 14 | - 二是 Follower 必须能够及时将 Leader 的消息复制过来,不能“落后太多”。 15 | 16 | Leader 会跟踪与其保持同步的 Replica 列表,如果一个 Follower 宕机,或者落后太多,Leader 将把它从 ISR 中移除。这里所描述的“落后太多”指 Follower 复制的消息落后于 Leader 后的条数超过预定值或者 Follower 超过一定时间未向 Leader 发送 fetch 请求。 17 | 18 | ## 数据可靠性 19 | 20 | Producer 在发布消息到某个 Partition 时,先通过 ZooKeeper 找到该 Partition 的 Leader,然后无论该 Topic 的 Replication Factor 为多少,Producer 只将该消息发送到该 Partition 的 Leader。Leader 会将该消息写入其本地 Log。每个 Follower 都从 Leader 拉取数据: 21 | 22 | - 消息所在 partition 的 ISR replicas 会定时异步从 leader 上批量复制数据 log 23 | - 当所有 ISR replica 都返回 ack,告诉 leader 该消息已经写 log 成功后,leader 认为该消息 committed,并告诉 Producer 生产成功。这里和以上”alive”条件的第二点是不矛盾的,因为 leader 有超时机制,leader 等 ISR 的 follower 复制数据,如果一定时间不返回 ack(可能数据复制进度落后太多),则 leader 将该 follower replica 从 ISR 中剔除。 24 | - 一旦 Leader 收到了 ISR 中的所有 Replica 的 ACK,该消息就被认为已经 commit 了,Leader 将增加 HW 并且向 Producer 发送 ACK。HW(高水位)是 Consumer 能够看到的此 partition 的位置,LEO 是每个 partition 的 log 最后一条 Message 的位置。HW 能保证 leader 所在的 broker 失效,该消息仍然可以从新选举的 leader 中获取,不会造成消息丢失。 25 | 26 | Kafka Replication 的数据流如下图所示: 27 | 28 | ![同步策略](https://pic.imgdb.cn/item/60779f188322e6675cf6ac9d.png) 29 | 30 | ISR 机制下的数据复制,既不是完全的同步复制,也不是单纯的异步复制,这是 Kafka 高吞吐很重要的机制。同步复制要求所有能工作的 follower 都复制完,这条消息才会被认为 committed,这种复制方式极大的影响了吞吐量。而异步复制方式下,follower 异步的从 leader 复制数据,数据只要被 leader 写入 log 就被认为已经 committed,这种情况下如果 follower 都复制完都落后于 leader,而如果 leader 突然宕机,则会丢失数据。而 Kafka 的这种使用 ISR 的方式则很好的均衡了确保数据不丢失以及吞吐量,follower 可以批量的从 leader 复制数据,数据复制到内存即返回 ack,这样极大的提高复制性能,当然数据仍然是有丢失风险的。 31 | 32 | 对于 Producer 而言,它可以选择是否等待消息 commit。这种机制确保了只要 ISR 有一个或以上的 Follower,一条被 commit 的消息就不会丢失。当 Producer 向 Leader 发送数据时,可以通过 request.required.acks 参数来设置数据可靠性的级别: 33 | 34 | - 1(默认):这意味着 producer 在 ISR 中的 leader 已成功收到的数据并得到确认后发送下一条 message。如果 leader 宕机了,则会丢失数据。 35 | - 0:这意味着 producer 无需等待来自 broker 的确认而继续发送下一批消息。这种情况下数据传输效率最高,但是数据可靠性确是最低的。 36 | - -1:producer 需要等待 ISR 中的所有 follower 都确认接收到数据后才算一次发送完成,可靠性最高。但是这样也不能保证数据不丢失,比如当 ISR 中只有 leader 时(其他节点都和 zk 断开连接,或者都没追上),这样就变成了 acks=1 的情况。 37 | 38 | ## 副本放置策略 39 | 40 | 为了更好的做负载均衡,Kafka 尽量将所有的 Partition 均匀分配到整个集群上。Kafka 分配 Replica 的算法如下: 41 | 42 | - 将所有存活的 N 个 Brokers 和待分配的 Partition 排序 43 | - 将第 i 个 Partition 分配到第(i mod n)个 Broker 上,这个 Partition 的第一个 Replica 存在于这个分配的 Broker 上,并且会作为 partition 的优先副本 44 | - 将第 i 个 Partition 的第 j 个 Replica 分配到第((i + j) mod n)个 Broker 上 45 | 46 | 假设集群一共有 4 个 brokers,一个 topic 有 4 个 partition,每个 Partition 有 3 个副本。下图是每个 Broker 上的副本分配情况。 47 | 48 | ![Broker 副本分布情况](https://pic.imgdb.cn/item/60779e818322e6675cf5b084.jpg) 49 | 50 | # 服务可用性 51 | 52 | ## Leader 选举 53 | 54 | Kafka 所有收发消息请求都由 Leader 节点处理,由以上数据可靠性设计可知,当 ISR 的 follower replica 故障后,leader 会及时地从 ISR 列表中把它剔除掉,并不影响服务可用性。而如果 Leader 发生了故障,则 Kafka 会重新选举 Leader: 55 | 56 | - Kafka 在 Zookeeper 存储 partition 的 ISR 信息,并且能动态调整 ISR 列表的成员,只有 ISR 里的成员 replica 才会被选为 leader,并且 ISR 所有的 replica 都有可能成为 leader; 57 | - Leader 节点宕机后,Zookeeper 能监控发现,并由 broker 的 controller 节点从 ISR 中选举出新的 leader,并通知 ISR 内的所有 broker 节点。 58 | 59 | Leader 选举本质上是一个分布式锁,有两种方式实现基于 ZooKeeper 的分布式锁: 60 | 61 | - 节点名称唯一性:多个客户端创建一个节点,只有成功创建节点的客户端才能获得锁 62 | - 临时顺序节点:所有客户端在某个目录下创建自己的临时顺序节点,只有序号最小的才获得锁 63 | 64 | ## 容灾和数据一致性 65 | 66 | 分布式系统的容灾能力,跟其本身针对数据一致性考虑所选择的算法有关,例如,Zookeeper 的 Zab 算法,raft 算法等。Kafka 的 ISR 机制和这些 Majority Vote 算法对比如下: 67 | 68 | - ISR 机制能容忍更多的节点失败。假如 replica 节点有 2f+1 个,每个 partition 最多能容忍 2f 个失败,且不丢失消息数据;但相对 Majority Vote 选举算法,只能最多容忍 f 个失败。 69 | - 在消息 committed 持久化上,ISR 需要等 2f 个节点返回 ack,但 Majority Vote 只需等 f+1 个节点返回 ack,且不依赖处理最慢的 follower 节点,因此 Majority Vote 有优势 70 | - ISR 机制能节省更多 replica 节点数。例如,要保证 f 个节点可用,ISR 方式至少要 f 个节点,而 Majority Vote 至少需要 2f+1 个节点。 71 | 72 | 如果所有 replica 都宕机了,有两种方式恢复服务: 73 | 74 | - 等 ISR 任一节点恢复,并选举为 leader; 75 | - 选择第一个恢复的节点(不一定是 ISR 中的节点)为 leader 76 | 77 | 第一种方式消息不会丢失(只能说这种方式最有可能不丢而已),第二种方式可能会丢消息,但能尽快恢复服务可用。这是可用性和一致性场景的两种考虑,Kafka 默认选择第二种,用户也可以自主配置。大部分考虑 CP 的分布式系统(假设 2f+1 个节点),为了保证数据一致性,最多只能容忍 f 个节点的失败,而 Kafka 为了兼顾可用性,允许最多 2f 个节点失败,因此是无法保证数据强一致的。 78 | 79 | ![ISR 容灾](https://pic.imgdb.cn/item/6077f2fa8322e6675ca8d556.jpg) 80 | 81 | 如图所示,一开始 ISR 数量等于 3,正常同步数据,红色部分开始,leader 发现其他两个 follower 复制进度太慢或者其他原因(网络分区、节点故障等),将其从 ISR 剔除后,leader 单节点存储数据;然后,leader 宕机,触发重新选举第二节点为 leader,重新开始同步数据,但红色部分的数据在新 leader 上是没有的;最后原 leader 节点恢复服务后,重新从新 leader 上复制数据,而红色部分的数据已经消费不到了。 82 | 83 | 因此,为了减少数据丢失的概率,可以设置 Kafka 的 ISR 最小 replica 数,低于该值后直接返回不可用,当然是以牺牲一定可用性和吞吐量为前提了。 84 | -------------------------------------------------------------------------------- /03~Kafka/集群部署/参数配置.md: -------------------------------------------------------------------------------- 1 | # Kafka 参数配置分析 2 | 3 | # 生产者参数 4 | 5 | - **key.serializer**:用于 key 键的序列化,它实现了 `org.apache.kafka.common.serialization.Serializer` 接口 6 | 7 | - **value.serializer**:用于 value 值的序列化,实现了 `org.apache.kafka.common.serialization.Serializer` 接口 8 | 9 | - **acks**:acks 参数指定了要有多少个分区副本接收消息,生产者才认为消息是写入成功的。此参数对消息丢失的影响较大 10 | 11 | - 如果 acks = 0,就表示生产者也不知道自己产生的消息是否被服务器接收了,它才知道它写成功了。如果发送的途中产生了错误,生产者也不知道,它也比较懵逼,因为没有返回任何消息。这就类似于 UDP 的运输层协议,只管发,服务器接受不接受它也不关心。 12 | - 如果 acks = 1,只要集群的 Leader 接收到消息,就会给生产者返回一条消息,告诉它写入成功。如果发送途中造成了网络异常或者 Leader 还没选举出来等其他情况导致消息写入失败,生产者会受到错误消息,这时候生产者往往会再次重发数据。因为消息的发送也分为 `同步` 和 `异步`,Kafka 为了保证消息的高效传输会决定是同步发送还是异步发送。如果让客户端等待服务器的响应(通过调用 `Future` 中的 `get()` 方法),显然会增加延迟,如果客户端使用回调,就会解决这个问题。 13 | - 如果 acks = all,这种情况下是只有当所有参与复制的节点都收到消息时,生产者才会接收到一个来自服务器的消息。不过,它的延迟比 acks =1 时更高,因为我们要等待不只一个服务器节点接收消息。 14 | 15 | - **buffer.memory**:此参数用来设置生产者内存缓冲区的大小,生产者用它缓冲要发送到服务器的消息。如果应用程序发送消息的速度超过发送到服务器的速度,会导致生产者空间不足。这个时候,send() 方法调用要么被阻塞,要么抛出异常,具体取决于 `block.on.buffer.null` 参数的设置。 16 | 17 | - **compression.type**:此参数来表示生产者启用何种压缩算法,默认情况下,消息发送时不会被压缩。该参数可以设置为 snappy、gzip 和 lz4,它指定了消息发送给 broker 之前使用哪一种压缩算法进行压缩。下面是各压缩算法的对比 18 | 19 | ![img](https://pic.imgdb.cn/item/6076fc828322e6675c1a1f29.jpg) 20 | 21 | ![img](https://pic.imgdb.cn/item/6076fc968322e6675c1a5330.jpg) 22 | 23 | - **retries**:生产者从服务器收到的错误有可能是临时性的错误(比如分区找不到首领),在这种情况下,`reteis` 参数的值决定了生产者可以重发的消息次数,如果达到这个次数,生产者会放弃重试并返回错误。默认情况下,生产者在每次重试之间等待 100ms,这个等待参数可以通过 `retry.backoff.ms` 进行修改。 24 | 25 | - **batch.size**:当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。当批次被填满,批次里的所有消息会被发送出去。不过生产者井不一定都会等到批次被填满才发送,任意条数的消息都可能被发送。 26 | 27 | - **client.id**:此参数可以是任意的字符串,服务器会用它来识别消息的来源,一般配置在日志里 28 | 29 | - **max.in.flight.requests.per.connection**:此参数指定了生产者在收到服务器响应之前可以发送多少消息,它的值越高,就会占用越多的内存,不过也会提高吞吐量。把它设为 1 可以保证消息是按照发送的顺序写入服务器。 30 | 31 | - **timeout.ms、request.timeout.ms 和 metadata.fetch.timeout.ms**:request.timeout.ms 指定了生产者在发送数据时等待服务器返回的响应时间,metadata.fetch.timeout.ms 指定了生产者在获取元数据(比如目标分区的首领是谁)时等待服务器返回响应的时间。如果等待时间超时,生产者要么重试发送数据,要么返回一个错误。timeout.ms 指定了 broker 等待同步副本返回消息确认的时间,与 asks 的配置相匹配----如果在指定时间内没有收到同步副本的确认,那么 broker 就会返回一个错误。 32 | 33 | - **max.block.ms**:此参数指定了在调用 send() 方法或使用 partitionFor() 方法获取元数据时生产者的阻塞时间当生产者的发送缓冲区已捕,或者没有可用的元数据时,这些方法就会阻塞。在阻塞时间达到 max.block.ms 时,生产者会抛出超时异常。 34 | 35 | - **max.request.size**:该参数用于控制生产者发送的请求大小。它可以指能发送的单个消息的最大值,也可以指单个请求里所有消息的总大小。 36 | 37 | - **receive.buffer.bytes 和 send.buffer.bytes**:Kafka 是基于 TCP 实现的,为了保证可靠的消息传输,这两个参数分别指定了 TCP Socket 接收和发送数据包的缓冲区的大小。如果它们被设置为 -1,就使用操作系统的默认值。如果生产者或消费者与 broker 处于不同的数据中心,那么可以适当增大这些值。 38 | 39 | # 消费者配置 40 | 41 | 到目前为止,我们学习了如何使用消费者 API,不过只介绍了几个最基本的属性,Kafka 文档列出了所有与消费者相关的配置说明。大部分参数都有合理的默认值,一般不需要修改它们,下面我们就来介绍一下这些参数。 42 | 43 | - fetch.min.bytes:该属性指定了消费者从服务器获取记录的最小字节数。broker 在收到消费者的数据请求时,如果可用的数据量小于 `fetch.min.bytes` 指定的大小,那么它会等到有足够的可用数据时才把它返回给消费者。这样可以降低消费者和 broker 的工作负载,因为它们在主题使用频率不是很高的时候就不用来回处理消息。如果没有很多可用数据,但消费者的 CPU 使用率很高,那么就需要把该属性的值设得比默认值大。如果消费者的数量比较多,把该属性的值调大可以降低 broker 的工作负载。 44 | 45 | - fetch.max.wait.ms:我们通过上面的 **fetch.min.bytes** 告诉 Kafka,等到有足够的数据时才会把它返回给消费者。而 **fetch.max.wait.ms** 则用于指定 broker 的等待时间,默认是 500 毫秒。如果没有足够的数据流入 kafka 的话,消费者获取的最小数据量要求就得不到满足,最终导致 500 毫秒的延迟。如果要降低潜在的延迟,就可以把参数值设置的小一些。如果 fetch.max.wait.ms 被设置为 100 毫秒的延迟,而 fetch.min.bytes 的值设置为 1MB,那么 Kafka 在收到消费者请求后,要么返回 1MB 的数据,要么在 100 ms 后返回所有可用的数据。就看哪个条件首先被满足。 46 | 47 | - max.partition.fetch.bytes:该属性指定了服务器从每个分区里返回给消费者的`最大字节数`。它的默认值时 1MB,也就是说,`KafkaConsumer.poll()` 方法从每个分区里返回的记录最多不超过 max.partition.fetch.bytes 指定的字节。如果一个主题有 20 个分区和 5 个消费者,那么每个消费者需要`至少`4 MB 的可用内存来接收记录。在为消费者分配内存时,可以给它们多分配一些,因为如果群组里有消费者发生崩溃,剩下的消费者需要处理更多的分区。max.partition.fetch.bytes 的值必须比 broker 能够接收的最大消息的字节数(通过 max.message.size 属性配置大),**否则消费者可能无法读取这些消息,导致消费者一直挂起重试**。在设置该属性时,另外一个考量的因素是消费者处理数据的时间。消费者需要频繁的调用 poll() 方法来避免会话过期和发生分区再平衡,如果单次调用 poll() 返回的数据太多,消费者需要更多的时间进行处理,可能无法及时进行下一个轮询来避免会话过期。如果出现这种情况,可以把 max.partition.fetch.bytes 值改小,或者延长会话过期时间。 48 | 49 | - session.timeout.ms:这个属性指定了消费者在被认为死亡之前可以与服务器断开连接的时间,默认是 3s。如果消费者没有在 **session.timeout.ms** 指定的时间内发送心跳给群组协调器,就会被认定为死亡,协调器就会触发重平衡。把它的分区分配给消费者群组中的其它消费者,此属性与 `heartbeat.interval.ms` 紧密相关。heartbeat.interval.ms 指定了 poll() 方法向群组协调器发送心跳的频率,session.timeout.ms 则指定了消费者可以多久不发送心跳。所以,这两个属性一般需要同时修改,heartbeat.interval.ms 必须比 session.timeout.ms 小,一般是 session.timeout.ms 的三分之一。如果 session.timeout.ms 是 3s,那么 heartbeat.interval.ms 应该是 1s。把 session.timeout.ms 值设置的比默认值小,可以更快地检测和恢复崩愤的节点,不过长时间的轮询或垃圾收集可能导致非预期的重平衡。把该属性的值设置得大一些,可以减少意外的重平衡,不过检测节点崩溃需要更长的时间。 50 | 51 | - auto.offset.reset:该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下的该如何处理。它的默认值是 `latest`,意思指的是,在偏移量无效的情况下,消费者将从最新的记录开始读取数据。另一个值是 `earliest`,意思指的是在偏移量无效的情况下,消费者将从起始位置处开始读取分区的记录。 52 | 53 | - enable.auto.commit:我们稍后将介绍几种不同的提交偏移量的方式。该属性指定了消费者是否自动提交偏移量,默认值是 true,为了尽量避免出现重复数据和数据丢失,可以把它设置为 false,由自己控制何时提交偏移量。如果把它设置为 true,还可以通过 **auto.commit.interval.ms** 属性来控制提交的频率 54 | 55 | - partition.assignment.strategy:我们知道,分区会分配给群组中的消费者。`PartitionAssignor` 会根据给定的消费者和主题,决定哪些分区应该被分配给哪个消费者,Kafka 有两个默认的分配策略`Range` 和 `RoundRobin` 56 | 57 | - client.id:该属性可以是任意字符串,broker 用他来标识从客户端发送过来的消息,通常被用在日志、度量指标和配额中 58 | 59 | - max.poll.records:该属性用于控制单次调用 call() 方法能够返回的记录数量,可以帮你控制在轮询中需要处理的数据量。 60 | 61 | - receive.buffer.bytes 和 send.buffer.bytes:socket 在读写数据时用到的 TCP 缓冲区也可以设置大小。如果它们被设置为 -1,就使用操作系统默认值。如果生产者或消费者与 broker 处于不同的数据中心内,可以适当增大这些值,因为跨数据中心的网络一般都有比较高的延迟和比较低的带宽。 62 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Documents 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 34 | 41 |
42 | 59 | 92 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 144 | 145 | 146 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /03~Kafka/架构概念.md: -------------------------------------------------------------------------------- 1 | # Kafka 架构概念 2 | 3 | Kafka 作为一个高度可扩展可容错的消息系统,一个典型的 kafka 集群中包含若干 producer,若干 broker,若干 consumer,以及一个 Zookeeper 集群。Kafka 通过 Zookeeper 管理集群配置,选举 leader,以及在 consumer group 发生变化时进行 rebalance。producer 使用 push 模式将消息发布到 broker,consumer 使用 pull 模式从 broker 订阅并消费消息: 4 | 5 | ![Kafka 架构概览图](https://pic.imgdb.cn/item/6076bc658322e6675c8d0ed0.png) 6 | 7 | Kafka 专用术语: 8 | 9 | - Broker:消息中间件处理结点,一个 Kafka 节点就是一个 broker,多个 broker 可以组成一个 Kafka 集群。 10 | - Topic:一类消息,Kafka 集群能够同时负责多个 topic 的分发。 11 | - Partition:topic 物理上的分组,一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列。 12 | - Segment:partition 物理上由多个 segment 组成。 13 | - offset:每个 partition 都由一系列有序的、不可变的消息组成,这些消息被连续的追加到 partition 中。partition 中的每个消息都有一个连续的序列号叫做 offset,用于 partition 唯一标识一条消息。 14 | - Producer:负责发布消息到 Kafka broker。 15 | - Consumer:消息消费者,向 Kafka broker 读取消息的客户端。 16 | - Consumer Group:每个 Consumer 属于一个特定的 Consumer Group。 17 | 18 | Kafka 实现了零拷贝原理来快速移动数据,避免了内核之间的切换。Kafka 可以将数据记录分批发送,从生产者到文件系统(Kafka 主题日志)到消费者,可以端到端的查看这些批次的数据。批处理能够进行更有效的数据压缩并减少 I/O 延迟,Kafka 采取顺序写入磁盘的方式,避免了随机磁盘寻址的浪费,总结一下其实就是四个要点: 19 | 20 | - 顺序读写:因为硬盘是机械结构,每次读写都会寻址,写入,其中寻址是一个“机械动作”,它是最耗时的。所以硬盘最“讨厌”随机 I/O,最喜欢顺序 I/O。为了提高读写硬盘的速度,Kafka 就是使用顺序 I/O。 21 | - 零拷贝:在 Linux Kernal 2.2 之后出现了一种叫做“零拷贝(zero-copy)”系统调用机制,就是跳过“用户缓冲区”的拷贝,建立一个磁盘空间和内存空间的直接映射,数据不再复制到“用户态缓冲区”系统上下文切换减少 2 次,可以提升一倍性能。 22 | - 消息压缩:消息都是经过压缩传递、存储的,降低网络与磁盘的负担。 23 | - 分批发送:批量处理是一种非常有效的提升系统吞吐量的方法,在 Kafka 内部,消息都是以“批”为单位处理的。 24 | 25 | # 消息与主题 26 | 27 | 消息是 Kafka 通信的基本单位,由一个固定长度的消息头和一个可变长度的消息体构成。在老版本中,每一条消息称为 Message;在由 Java 重新实现的客户端中,每一条消息称为 Record。为了提高效率,消息会分批次写入 Kafka,批次就代指的是一组消息。Kafka 将一组消息抽象归纳为一个主题(Topic),也就是说,一个主题就是对消息的一个分类。生产者将消息发送到特定主题,消费者订阅主题或主题的某些分区进行消费。 28 | 29 | Kafka 消息传递的事务特点如下: 30 | 31 | - at most once:最多一次,这个和 JMS 中"非持久化"消息类似,发送一次,无论成败,将不会重发。消费者 fetch 消息,然后保存 offset,然后处理消息;当 client 保存 offset 之后,但是在消息处理过程中出现了异常,导致部分消息未能继续处理。那么此后"未处理"的消息将不能被 fetch 到,这就是"at most once"。 32 | - at least once:消息至少发送一次,如果消息未能接受成功,可能会重发,直到接收成功。消费者 fetch 消息,然后处理消息,然后保存 offset。如果消息处理成功之后,但是在保存 offset 阶段 zookeeper 异常导致保存操作未能执行成功,这就导致接下来再次 fetch 时可能获得上次已经处理过的消息,这就是"at least once",原因 offset 没有及时的提交给 zookeeper,zookeeper 恢复正常还是之前 offset 状态。 33 | - exactly once:消息只会发送一次。kafka 中并没有严格的去实现(基于 2 阶段提交),我们认为这种策略在 kafka 中是没有必要的。 34 | 35 | # 生产者(Producer) 36 | 37 | 生产者(Producer)负责将消息发送给代理,也就是向 Kafka 代理发送消息的客户端。生产者会将某 topic 的消息发布到相应的 partition 中。生产者在默认情况下把消息均衡地分布到主题的所有分区上,而并不关心特定消息会被写到哪个分区。不过,在某些情况下,生产者会把消息直接写到指定的分区。 38 | 39 | ![生产者设计](https://pic.imgdb.cn/item/6077eb408322e6675c92ad73.jpg) 40 | 41 | Kafka 中的生产者设计主要考虑了以下方面: 42 | 43 | - 负载均衡:由于消息 topic 由多个 partition 组成,且 partition 会均衡分布到不同 broker 上,因此,为了有效利用 broker 集群的性能,提高消息的吞吐量,producer 可以通过随机或者 hash 等方式,将消息平均发送到多个 partition 上,以实现负载均衡。 44 | - 批量发送:是提高消息吞吐量重要的方式,Producer 端可以在内存中合并多条消息后,以一次请求的方式发送了批量的消息给 broker,从而大大减少 broker 存储消息的 IO 操作次数。但也一定程度上影响了消息的实时性,相当于以时延代价,换取更好的吞吐量。 45 | 46 | # 消费者和消费组 47 | 48 | consumer 是 kafka 当中的消费者,主要用于消费 kafka 当中的数据;在 Kafka 中每一个消费者都属于一个特定消费组(ConsumerGroup),我们可以为每个消费者指定一个消费组,以 groupId 代表消费组名称,通过 group.id 配置设置。如果不指定消费组,则该消费者属于默认消费组 test-consumer-group。 49 | 50 | 同时,每个消费者也有一个全局唯一的 id,通过配置项 client.id 指定,如果客户端没有指定消费者的 id,Kafka 会自动为该消费者生成一个全局唯一的 id,格式为 `${groupId}-${hostName}-${timestamp}-${UUID 前 8 位字符}`。同一个主题的一条消息只能被同一个消费组下某一个消费者消费,但不同消费组的消费者可同时消费该消息。消费组是 Kafka 用来实现对一个主题消息进行广播和单播的手段,实现消息广播只需指定各消费者均属于不同的消费组,消息单播则只需让各消费者属于同一个消费组。 51 | 52 | ![Consumer Group读取消息](https://pic.imgdb.cn/item/6077f4ba8322e6675cac97ae.png) 53 | 54 | 总结而言,Kafka 中的消费者与消费组具备以下属性: 55 | 56 | - 任何 Consumer 必须属于一个 Consumer Group 57 | - 同一 Consumer Group 中的多个 Consumer 实例,不同时消费同一个 partition,等效于队列模式。如图,Consumer Group 1 的三个 Consumer 实例分别消费不同的 partition 的消息,即,TopicA-part0、TopicA-part1、TopicA-part2。 58 | - 不同 Consumer Group 的 Consumer 实例可以同时消费同一个 partition,等效于发布订阅模式。如图,Consumer Group 1 的 Consumer1 和 Consumer Group 2 的 Consumer4,同时消费 TopicA-part0 的消息。 59 | - Partition 内消息是有序的,Consumer 通过 pull 方式消费消息。 60 | - Kafka 不删除已消费的消息 61 | 62 | # 分区 63 | 64 | Kafka 将一组消息归纳为一个主题,而每个主题又被分成一个或多个分区(Partition)。每个分区由一系列有序、不可变的消息组成,是一个有序队列。同一个主题中的分区可以不在一个机器上,有可能会部署在多个机器上,由此来实现 kafka 的伸缩性;单一主题中的分区有序,但是无法保证主题中所有的分区有序。 65 | 66 | ![分区写入示意](https://pic.imgdb.cn/item/6076b72d8322e6675c8343fd.jpg) 67 | 68 | 每个分区在物理上对应为一个文件夹,分区的命名规则为主题名称后接“—”连接符,之后再接分区编号,分区编号从 0 开始,编号最大值为分区的总数减 1。每个分区又有一至多个副本(Replica),分区的副本分布在集群的不同代理上,以提高可用性。从存储角度上分析,分区的每个副本在逻辑上抽象为一个日志(Log)对象,即分区的副本与日志对象是一一对应的。每个主题对应的分区数可以在 Kafka 启动时所加载的配置文件中配置,也可以在创建主题时指定。当然,客户端还可以在主题创建后修改主题的分区数。 69 | 70 | ![消费者群组读取消息](https://pic.imgdb.cn/item/6076b8008322e6675c84dfc3.jpg) 71 | 72 | 分区使得 Kafka 在并发处理上变得更加容易,分区数量决定了每个 Consumer Group 中并发消费者的最大数量。理论上来说,分区数越多吞吐量越高,但这要根据集群实际环境及业务场景而定。某一个主题下的分区数,对于消费该主题的同一个消费组下的消费者数量,应该小于等于该主题下的分区数。某一个主题有 4 个分区,那么消费组中的消费者应该小于等于 4,而且最好与分区数成整数倍 1 2 4 这样。同一个分区下的数据,在同一时刻,不能同一个消费组的不同消费者消费。 73 | 74 | ![不同分区、消费者数目](https://pic.imgdb.cn/item/6077f5cb8322e6675caed3b3.png) 75 | 76 | 同时,分区也是 Kafka 保证消息被顺序消费以及对消息进行负载均衡的基础。Kafka 只能保证一个分区之内消息的有序性,并不能保证跨分区消息的有序性。每条消息被追加到相应的分区中,是顺序写磁盘,因此效率非常高,这是 Kafka 高吞吐率的一个重要保证。同时与传统消息系统不同的是,Kafka 并不会立即删除已被消费的消息,由于磁盘的限制消息也不会一直被存储,因此 Kafka 提供两种删除老数据的策略,一是基于消息已存储的时间长度,二是基于分区的大小。这两种策略都能通过配置文件进行配置。 77 | 78 | # 偏移量 79 | 80 | 任何发布到分区的消息会被直接追加到日志文件(分区目录下以“.log”为文件名后缀的数据文件)的尾部,而每条消息在日志文件中的位置都会对应一个按序递增的偏移量。偏移量是一个分区下严格有序的逻辑值,它并不表示消息在磁盘上的物理位置。由于 Kafka 几乎不允许对消息进行随机读写,因此 Kafka 并没有提供额外索引机制到存储偏移量,也就是说并不会给偏移量再提供索引。 81 | 82 | 消费者可以通过控制消息偏移量来对消息进行消费,如消费者可以指定消费的起始偏移量。为了保证消息被顺序消费,消费者已消费的消息对应的偏移量也需要保存。需要说明的是,消费者对消息偏移量的操作并不会影响消息本身的偏移量。旧版消费者将消费偏移量保存到 ZooKeeper 当中,而新版消费者是将消费偏移量保存到 Kafka 内部一个主题当中。当然,消费者也可以自己在外部系统保存消费偏移量,而无需保存到 Kafka 中。 83 | 84 | # 日志段(Segment) 85 | 86 | 一个日志又被划分为多个日志段(LogSegment),日志段是 Kafka 日志对象分片的最小单位。与日志对象一样,日志段也是一个逻辑概念,一个日志段对应磁盘上一个具体日志文件和两个索引文件。日志文件是以“.log”为文件名后缀的数据文件,用于保存消息实际数据。两个索引文件分别以“.index”和“.timeindex”作为文件名后缀,分别表示消息偏移量索引文件和消息时间戳索引文件。查找某个 offset 的消息,先二分法找出消息所在的 segment 文件(因为每个 segment 的命名都是以该文件中消息 offset 最小的值命名);然后,加载对应的.index 索引文件到内存,同样二分法找出小于等于给定 offset 的最大的那个 offset 记录(相对 offset,position);最后,根据 position 到.log 文件中,顺序查找出 offset 等于给定 offset 值的消息。 87 | 88 | 由于消息在 partition 的 segment 数据文件中是顺序读写的,且消息消费后不会删除(删除策略是针对过期的 segment 文件),这种顺序磁盘 IO 存储设计是 Kafka 高性能很重要的原因。 89 | 90 | # 代理(Broker) 91 | 92 | Kafka 集群包含一个或多个服务器,我们将每一个 Kafka 实例称为代理(Broker),通常也称代理为 Kafka 服务器(KafkaServer)。broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。broker 为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘上的消息。 93 | 94 | 在生产环境中 Kafka 集群一般包括一台或多台服务器,我们可以在一台服务器上配置一个或多个代理。每一个代理都有唯一的标识 id,这个 id 是一个非负整数。在一个 Kafka 集群中,每增加一个代理就需要为这个代理配置一个与该集群中其他代理不同的 id,id 值可以选择任意非负整数即可,只要保证它在整个 Kafka 集群中唯一,这个 id 就是代理的名字,也就是在启动代理时配置的 broker.id 对应的值,因此在本文中有时我们也称为 brokerId。由于给每个代理分配了不同的 brokerId,这样对代理进行迁移就变得更方便,从而对消费者来说是透明的,不会影响消费者对消息的消费。 95 | 96 | 每个集群中都会有一个 broker 同时充当了 集群控制器(Leader)的角色,它是由集群中的活跃成员选举出来的。每个集群中的成员都有可能充当 Leader,Leader 负责管理工作,包括将分区分配给 broker 和监控 broker。集群中,一个分区从属于一个 Leader,但是一个分区可以分配给多个 broker(非 Leader),这时候会发生分区复制。这种复制的机制为分区提供了消息冗余,如果一个 broker 失效,那么其他活跃用户会重新选举一个 Leader 接管。 97 | 98 | ![分区复制示意图](https://pic.imgdb.cn/item/607701288322e6675c249fa8.jpg) 99 | 100 | # ISR 101 | 102 | Kafka 在 ZooKeeper 中动态维护了一个 ISR(In-sync Replica),即保存同步的副本列表,该列表中保存的是与 Leader 副本保持消息同步的所有副本对应的代理节点 id。如果一个 Follower 副本宕机(本书用宕机来特指某个代理失效的情景,包括但不限于代理被关闭,如代理被人为关闭或是发生物理故障、心跳检测过期、网络延迟、进程崩溃等)或是落后太多,则该 Follower 副本节点将从 ISR 列表中移除。对于 Kafka 节点,判断是”alive”有以下两个条件: 103 | 104 | - 节点必须和 Zookeeper 保持心跳连接 105 | - 如果节点是 follower,必须从 leader 节点上复制数据来备份,而且备份的数据相比 leader 而言,不能落后太多。 106 | 107 | # 副本 108 | 109 | ![Kafka 副本示意图](https://pic.imgdb.cn/item/6077f5e78322e6675caf0fe5.png) 110 | 111 | 由于 Kafka 副本的存在,就需要保证一个分区的多个副本之间数据的一致性,Kafka 会选择该分区的一个副本作为 Leader 副本,而该分区其他副本即为 Follower 副本,只有 Leader 副本才负责处理客户端读/写请求,Follower 副本从 Leader 副本同步数据。引入 Leader 副本后客户端只需与 Leader 副本进行交互,这样数据一致性及顺序性就有了保证。Follower 副本从 Leader 副本同步消息,对于 n 个副本只需 n−1 条通路即可,这样就使得系统更加简单而高效。副本 Follower 与 Leader 的角色并不是固定不变的,如果 Leader 失效,通过相应的选举算法将从其他 Follower 副本中选出新的 Leader 副本。 112 | 113 | Kafka 解决了 fail/recover,一条消息只有被 ISR 里的所有 Follower 都从 Leader 复制过去才会被认为已提交。这样就避免了部分数据被写进了 Leader,还没来得及被任何 Follower 复制就宕机了,而造成数据丢失(Consumer 无法消费这些数据)。 114 | 115 | # ZooKeeper 116 | 117 | Kafka 利用 ZooKeeper 保存相应元数据信息,Kafka 元数据信息包括如代理节点信息、Kafka 集群信息、旧版消费者信息及其消费偏移量信息、主题信息、分区状态信息、分区副本分配方案信息、动态配置信息等。Kafka 在启动或运行过程当中会在 ZooKeeper 上创建相应节点来保存元数据信息,Kafka 通过监听机制在这些节点注册相应监听器来监听节点元数据的变化,从而由 ZooKeeper 负责管理维护 Kafka 集群,同时通过 ZooKeeper 我们能够很方便地对 Kafka 集群进行水平扩展及数据迁移。 118 | 119 | 不过 2021 年3 月 30 日,Kafka 背后的企业 Confluent 发布博客表示,在即将发布的 2.8 版本里,用户可在完全不需要 ZooKeeper 的情况下运行 Kafka,该版本将依赖于 ZooKeeper 的控制器改造成了基于 Kafka Raft 的 Quorm 控制器。在之前的版本中,如果没有 ZooKeeper,Kafka 将无法运行。但管理部署两个不同的系统不仅让运维复杂度翻倍,还让 Kafka 变得沉重,进而限制了 Kafka 在轻量环境下的应用,同时 ZooKeeper 的分区特性也限制了 Kafka 的承载能力。 -------------------------------------------------------------------------------- /03~Kafka/消息生产与消费/消费者.md: -------------------------------------------------------------------------------- 1 | # Kafka 消费者 2 | 3 | 消息由生产者发送到 kafka 集群后,会被消费者消费。一般来说我们的消费模型有两种:推送模型(push)和拉取模型(pull)基于推送模型的消息系统,由消息代理记录消费状态。消息代理将消息推送到消费者后,标记这条消息为已经被消费,但是这种方式无法很好地保证消费的处理语义。比如当我们把已经把消息发送给消费者之后,由于消费进程挂掉或者由于网络原因没有收到这条消息,如果我们在消费代理将其标记为已消费,这个消息就永久丢失了。如果我们利用生产者收到消息后回复这种方法,消息代理需要记录消费状态,这种不可取。如果采用 push,消息消费的速率就完全由消费代理控制,一旦消费者发生阻塞,就会出现问题。Kafka 采取拉取模型(pull),由自己控制消费速度,以及消费的进度,消费者可以按照任意的偏移量进行消费。比如消费者可以消费已经消费过的消息进行重新处理,或者消费最近的消息等等。 4 | 5 | 消费者组(Consumer Group)是由一个或多个消费者实例(Consumer Instance)组成的群组,具有可扩展性和可容错性的一种机制。消费者组内的消费者共享一个消费者组 ID,这个 ID 也叫做 Group ID,组内的消费者共同对一个主题进行订阅和消费,同一个组中的消费者只能消费一个分区的消息,多余的消费者会闲置,派不上用场。换言之,同一主题的一条消息只能被同一个消费者组内的一个消费者消费,但多个消费者组可同时消费这一消息。 6 | 7 | ![消费者组与分区](https://pic.imgdb.cn/item/6077ebd78322e6675c948e1c.jpg) 8 | 9 | 这是 Kafka 用来实现一个 Topic 消息的广播(发给所有的 Consumer)和单播(发给某一个 Consumer)的手段。一个 Topic 可以对应多个 Consumer Group。如果需要实现广播,只要每个 Consumer 有一个独立的 Group 就可以了。要实现单播只要所有的 Consumer 在同一个 Group 里。用 Consumer Group 还可以将 Consumer 进行自由的分组而不需要多次发送消息到不同的 Topic。 10 | 11 | # 队列与订阅 12 | 13 | Kafka 的消息队列一般分为两种模式:点对点队列模式和发布订阅模式。如果一个生产者生产的消息由一个消费者进行消费的话,那么这种模式就是点对点模式: 14 | 15 | ![点对点模式的消息队列](https://pic.imgdb.cn/item/6076e6038322e6675ce7f67a.jpg) 16 | 17 | 如果一个生产者或者多个生产者产生的消息能够被多个消费者同时消费的情况,这样的消息队列成为发布订阅模式的消息队列: 18 | 19 | ![发布-订阅模式的消息队列](https://pic.imgdb.cn/item/6076e6628322e6675ce8ca89.jpg) 20 | 21 | ## 队列模式 22 | 23 | 队列模式,指每条消息只会有一个 Consumer 消费到。Kafka 保证同一 Consumer Group 中只有一个 Consumer 会消费某条消息。 24 | 25 | - 在 Consumer Group 稳定状态下,每一个 Consumer 实例只会消费某一个或多个特定 partition 的数据,而某个 partition 的数据只会被某一个特定的 Consumer 实例所消费,也就是说 Kafka 对消息的分配是以 partition 为单位分配的,而非以每一条消息作为分配单元; 26 | - 同一 Consumer Group 中,如果 Consumer 实例数量少于 partition 数量,则至少有一个 Consumer 会消费多个 partition 的数据;如果 Consumer 的数量与 partition 数量相同,则正好一个 Consumer 消费一个 partition 的数据;而如果 Consumer 的数量多于 partition 的数量时,会有部分 Consumer 无法消费该 Topic 下任何一条消息; 27 | 28 | Kafka 消费者从属于消费者群组。一个群组中的消费者订阅的都是相同的主题,每个消费者接收主题一部分分区的消息。下面是一个 Kafka 分区消费示意图: 29 | 30 | ![1 个消费者消费 4 个分区消息](https://pic.imgdb.cn/item/6076fd108322e6675c1b6ec0.jpg) 31 | 32 | 上图中的主题 T1 有四个分区,分别是分区 0、分区 1、分区 2、分区 3,我们创建一个消费者群组 1,消费者群组中只有一个消费者,它订阅主题 T1,接收到 T1 中的全部消息。由于一个消费者处理四个生产者发送到分区的消息,压力有些大,需要帮手来帮忙分担任务,于是就演变为下图: 33 | 34 | ![2 个消费者消费 4 个分区消息](https://pic.imgdb.cn/item/6076fd3b8322e6675c1bcd6a.jpg) 35 | 36 | 这样一来,消费者的消费能力就大大提高了,但是在某些环境下比如用户产生消息特别多的时候,生产者产生的消息仍旧让消费者吃不消,那就继续增加消费者。 37 | 38 | ![4 个消费者消费 4 个分区消息](https://pic.imgdb.cn/item/6076fd558322e6675c1c070e.jpg) 39 | 40 | 如上图所示,每个分区所产生的消息能够被每个消费者群组中的消费者消费,如果向消费者群组中增加更多的消费者,那么多余的消费者将会闲置,如下图所示 41 | 42 | ![5 个消费者消费 4 个分区消息](https://pic.imgdb.cn/item/6076fd7a8322e6675c1c57cc.jpg) 43 | 44 | 向群组中增加消费者是横向伸缩消费能力的主要方式。总而言之,我们可以通过增加消费组的消费者来进行水平扩展提升消费能力。这也是为什么建议创建主题时使用比较多的分区数,这样可以在消费负载高的情况下增加消费者来提升性能。另外,消费者的数量不应该比分区数多,因为多出来的消费者是空闲的,没有任何帮助。 45 | 46 | ## 发布订阅模式 47 | 48 | 发布订阅模式,又指广播模式,Kafka 保证 topic 的每条消息会被所有 Consumer Group 消费到,而对于同一个 Consumer Group,还是保证只有一个 Consumer 实例消费到这条消息。Kafka 一个很重要的特性就是,只需写入一次消息,可以支持任意多的应用读取这个消息。换句话说,每个应用都可以读到全量的消息。为了使得每个应用都能读到全量消息,应用需要有不同的消费组。对于上面的例子,假如我们新增了一个新的消费组 G2,而这个消费组有两个消费者,那么就演变为下图这样: 49 | 50 | ![两个消费者群组消费一个主题的消息](https://pic.imgdb.cn/item/6076fdb58322e6675c1cdade.jpg) 51 | 52 | 在这个场景中,消费组 G1 和消费组 G2 都能收到 T1 主题的全量消息,在逻辑意义上来说它们属于不同的应用。总结起来就是如果应用需要读取全量消息,那么请为该应用设置一个消费组;如果该应用消费能力不足,那么可以考虑在这个消费组里增加消费者。 53 | 54 | # 分区重平衡 55 | 56 | 我们从上面的消费者演变图中可以知道这么一个过程:最初是一个消费者订阅一个主题并消费其全部分区的消息,后来有一个消费者加入群组,随后又有更多的消费者加入群组,而新加入的消费者实例分摊了最初消费者的部分消息,这种把分区的所有权通过一个消费者转到其他消费者的行为称为重平衡,英文名也叫做 Rebalance。如下图所示: 57 | 58 | ![Kafka 分区重平衡](https://pic.imgdb.cn/item/6076fe3f8322e6675c1e2560.jpg) 59 | 60 | 重平衡非常重要,它为消费者群组带来了高可用性和伸缩性,我们可以放心的添加消费者或移除消费者,不过在正常情况下我们并不希望发生这样的行为。在重平衡期间,消费者无法读取消息,造成整个消费者组在重平衡的期间都不可用。另外,当分区被重新分配给另一个消费者时,消息当前的读取状态会丢失,它有可能还需要去刷新缓存,在它重新恢复状态之前会拖慢应用程序。 61 | 62 | 重平衡也是一把双刃剑,它为消费者群组带来高可用性和伸缩性的同时,还有有一些明显的缺点(bug),而这些 bug 到现在社区还无法修改。重平衡的过程对消费者组有极大的影响。因为每次重平衡过程中都会导致万物静止,参考 JVM 中的垃圾回收机制,也就是 Stop The World .也就是说,在重平衡期间,消费者组中的消费者实例都会停止消费,等待重平衡的完成。而且重平衡这个过程很慢。 63 | 64 | ## 重平衡算法 65 | 66 | ![重平衡算法](https://pic.imgdb.cn/item/6077ef3e8322e6675c9f8a21.jpg) 67 | 68 | ## 重平衡流程 69 | 70 | 消费者通过向组织协调者(Kafka Broker)发送心跳来维护自己是消费者组的一员并确认其拥有的分区。对于不同不的消费群体来说,其组织协调者可以是不同的。只要消费者定期发送心跳,就会认为消费者是存活的并处理其分区中的消息。当消费者检索记录或者提交它所消费的记录时就会发送心跳。如果过了一段时间 Kafka 停止发送心跳了,会话(Session)就会过期,组织协调者就会认为这个 Consumer 已经死亡,就会触发一次重平衡。如果消费者宕机并且停止发送消息,组织协调者会等待几秒钟,确认它死亡了才会触发重平衡。在这段时间里,死亡的消费者将不处理任何消息。在清理消费者时,消费者将通知协调者它要离开群组,组织协调者会触发一次重平衡,尽量降低处理停顿。 71 | 72 | - 对于每个 Consumer Group,选举出一个 Broker 作为 Coordinator(0.9 版本以上),由它 Watch Zookeeper,从而监控判断是否有 partition 或者 Consumer 的增减,然后生成 Rebalance 命令,按照以上算法重新分配。 73 | - 当 Consumer Group 第一次被初始化时,Consumer 通常会读取每个 partition 的最早或最近的 offset(Zookeeper 记录),然后顺序地读取每个 partition log 的消息,在 Consumer 读取过程中,它会提交已经成功处理的消息的 offsets(由 Zookeeper 记录)。 74 | - 当一个 partition 被重新分配给 Consumer Group 中的其他 Consumer,新的 Consumer 消费的初始位置会设置为(原来 Consumer)最近提交的 offset。 75 | 76 | # 创建消费者 77 | 78 | 在读取消息之前,需要先创建一个 KafkaConsumer 对象。创建 KafkaConsumer 对象与创建 KafkaProducer 对象十分相似 --- 把需要传递给消费者的属性放在 properties 对象中,后面我们会着重讨论 Kafka 的一些配置,这里我们先简单的创建一下,使用 3 个属性就足矣,分别是 bootstrap.server,key.deserializer,value.deserializer 。 79 | 80 | 还有一个属性是 group.id 这个属性不是必须的,它指定了 KafkaConsumer 是属于哪个消费者群组。创建不属于任何一个群组的消费者也是可以的: 81 | 82 | ```java 83 | Properties properties = new Properties(); 84 | properties.put("bootstrap.server", "192.168.1.9:9092"); 85 | properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); 86 | properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); 87 | KafkaConsumer < String, String > consumer = new KafkaConsumer < > (properties); 88 | ``` 89 | 90 | ## 主题订阅 91 | 92 | 创建好消费者之后,下一步就开始订阅主题了。subscribe() 方法接受一个主题列表作为参数,使用起来比较简单: 93 | 94 | ```java 95 | consumer.subscribe(Collections.singletonList("customerTopic")); 96 | ``` 97 | 98 | 为了简单我们只订阅了一个主题 customerTopic,参数传入的是一个正则表达式,正则表达式可以匹配多个主题,如果有人创建了新的主题,并且主题的名字与正则表达式相匹配,那么会立即触发一次重平衡,消费者就可以读取新的主题。 99 | 100 | 要订阅所有与 test 相关的主题,可以这样做: 101 | 102 | ```java 103 | consumer.subscribe("test.*"); 104 | ``` 105 | 106 | ## 轮询 107 | 108 | 我们知道,Kafka 是支持订阅/发布模式的,生产者发送数据给 Kafka Broker,那么消费者是如何知道生产者发送了数据呢?其实生产者产生的数据消费者是不知道的,KafkaConsumer 采用轮询的方式定期去 Kafka Broker 中进行数据的检索,如果有数据就用来消费,如果没有就再继续轮询等待,下面是轮询等待的具体实现: 109 | 110 | ```java 111 | try { 112 | while (true) { 113 | ConsumerRecords < String, String > records = consumer.poll(Duration.ofSeconds(100)); 114 | for (ConsumerRecord < String, String > record: records) { 115 | int updateCount = 1; 116 | if (map.containsKey(record.value())) { 117 | updateCount = (int) map.get(record.value() + 1); 118 | } 119 | map.put(record.value(), updateCount); 120 | } 121 | } 122 | } finally { 123 | consumer.close(); 124 | } 125 | ``` 126 | 127 | - 这是一个无限循环。消费者实际上是一个长期运行的应用程序,它通过轮询的方式向 Kafka 请求数据。 128 | - 第三行代码非常重要,Kafka 必须定期循环请求数据,否则就会认为该 Consumer 已经挂了,会触发重平衡,它的分区会移交给群组中的其它消费者。传给 `poll()` 方法的是一个超市时间,用 `java.time.Duration` 类来表示,如果该参数被设置为 0 ,poll() 方法会立刻返回,否则就会在指定的毫秒数内一直等待 broker 返回数据。 129 | - poll() 方法会返回一个记录列表。每条记录都包含了记录所属主题的信息,记录所在分区的信息、记录在分区中的偏移量,以及记录的键值对。我们一般会遍历这个列表,逐条处理每条记录。 130 | - 在退出应用程序之前使用 `close()` 方法关闭消费者。网络连接和 socket 也会随之关闭,并立即触发一次重平衡,而不是等待群组协调器发现它不再发送心跳并认定它已经死亡。 131 | 132 | # 提交和偏移量的概念 133 | 134 | ![The Consumer's Position in the Log](https://pic.imgdb.cn/item/6077eedb8322e6675c9e4be9.jpg) 135 | 136 | 如图,Last Commited Offset 指 Consumer 最近一次提交的消费记录 offset,Current Position 是当前消费的位置,High Watermark 是成功拷贝到 log 的所有副本节点(partition 的所有 ISR 节点,下文介绍)的最近消息的 offset,Log End Offset 是写入 log 中最后一条消息的 offset+1。 137 | 138 | 从 Consumer 的角度来看,最多只能读取到 High watermark 的位置,后面的消息对消费者不可见,因为未完全复制的数据还没可靠存储,有丢失可能。 139 | 140 | ## 特殊偏移 141 | 142 | 我们上面提到,消费者在每次调用 poll() 方法进行定时轮询的时候,会返回由生产者写入 Kafka 但是还没有被消费者消费的记录,因此我们可以追踪到哪些记录是被群组里的哪个消费者读取的。消费者可以使用 Kafka 来追踪消息在分区中的位置(偏移量) 143 | 144 | 消费者会向一个叫做 `_consumer_offset` 的特殊主题中发送消息,这个主题会保存每次所发送消息中的分区偏移量,这个主题的主要作用就是消费者触发重平衡后记录偏移使用的,消费者每次向这个主题发送消息,正常情况下不触发重平衡,这个主题是不起作用的,当触发重平衡后,消费者停止工作,每个消费者可能会分到对应的分区,这个主题就是让消费者能够继续处理消息所设置的。 145 | 146 | 如果提交的偏移量小于客户端最后一次处理的偏移量,那么位于两个偏移量之间的消息就会被重复处理: 147 | 148 | ![重平衡后,消息被重复消费](https://pic.imgdb.cn/item/6076fffa8322e6675c21f594.jpg) 149 | 150 | 如果提交的偏移量大于最后一次消费时的偏移量,那么处于两个偏移量中间的消息将会丢失,既然`_consumer_offset` 如此重要,那么它的提交方式是怎样的呢?下面我们就来说一下提交方式,KafkaConsumer API 提供了多种方式来提交偏移量。 151 | 152 | ### 自动提交 153 | 154 | 最简单的方式就是让消费者自动提交偏移量。如果 enable.auto.commit 被设置为 true,那么每过 5s,消费者会自动把从 poll() 方法轮询到的最大偏移量提交上去。提交时间间隔由 auto.commit.interval.ms 控制,默认是 5s。与消费者里的其他东西一样,自动提交也是在轮询中进行的。消费者在每次轮询中会检查是否提交该偏移量了,如果是,那么就会提交从上一次轮询中返回的偏移量。 155 | 156 | ### 提交当前偏移量 157 | 158 | 把 auto.commit.offset 设置为 false,可以让应用程序决定何时提交偏移量。使用 commitSync() 提交偏移量。这个 API 会提交由 poll() 方法返回的最新偏移量,提交成功后马上返回,如果提交失败就抛出异常。 159 | 160 | commitSync() 将会提交由 poll() 返回的最新偏移量,如果处理完所有记录后要确保调用了 commitSync(),否则还是会有丢失消息的风险,如果发生了在均衡,从最近一批消息到发生在均衡之间的所有消息都将被重复处理。 161 | 162 | ### 异步提交 163 | 164 | 异步提交 commitAsync() 与同步提交 commitSync() 最大的区别在于异步提交不会进行重试,同步提交会一致进行重试。 165 | 166 | ### 同步和异步组合提交 167 | 168 | 一般情况下,针对偶尔出现的提交失败,不进行重试不会有太大的问题,因为如果提交失败是因为临时问题导致的,那么后续的提交总会有成功的。但是如果在关闭消费者或再均衡前的最后一次提交,就要确保提交成功。 169 | 170 | 因此,在消费者关闭之前一般会组合使用 commitAsync 和 commitSync 提交偏移量。 171 | 172 | ### 提交特定的偏移量 173 | 174 | 消费者 API 允许调用 commitSync() 和 commitAsync() 方法时传入希望提交的 partition 和 offset 的 map,即提交特定的偏移量。 175 | -------------------------------------------------------------------------------- /03~Kafka/消息生产与消费/生产者.md: -------------------------------------------------------------------------------- 1 | # Kafka Producer 2 | 3 | 在 Kafka 中,我们把产生消息的那一方称为生产者,比如登录电商网站的时候,你的登陆信息,登陆次数都会作为消息传输到 Kafka 后台,当你浏览购物的时候,你的浏览信息,你的搜索指数,你的购物爱好都会作为一个个消息传递给 Kafka 后台,然后系统会根据你的爱好做智能推荐。 4 | 5 | ![生产者设计](https://pic.imgdb.cn/item/6077eb408322e6675c92ad73.jpg) 6 | 7 | Kafka 中的生产者设计主要考虑了以下方面: 8 | 9 | - 负载均衡:由于消息 topic 由多个 partition 组成,且 partition 会均衡分布到不同 broker 上,因此,为了有效利用 broker 集群的性能,提高消息的吞吐量,producer 可以通过随机或者 hash 等方式,将消息平均发送到多个 partition 上,以实现负载均衡。 10 | 11 | - 批量发送:是提高消息吞吐量重要的方式,Producer 端可以在内存中合并多条消息后,以一次请求的方式发送了批量的消息给 broker,从而大大减少 broker 存储消息的 IO 操作次数。但也一定程度上影响了消息的实时性,相当于以时延代价,换取更好的吞吐量。 12 | 13 | # 创建 Kafka 生产者 14 | 15 | 要向 Kafka 写入消息,首先需要创建一个生产者对象,并设置一些属性。Kafka 生产者有 3 个必选的属性: 16 | 17 | - bootstrap.servers:该属性指定 broker 的地址清单,地址的格式为 host:port。清单里不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找到其他的 broker 信息。不过建议至少要提供两个 broker 信息,一旦其中一个宕机,生产者仍然能够连接到集群上。 18 | 19 | - key.serializer:broker 需要接收到序列化之后的 key/value 值,所以生产者发送的消息需要经过序列化之后才传递给 Kafka Broker。生产者需要知道采用何种方式把 Java 对象转换为字节数组。key.serializer 必须被设置为一个实现了 org.apache.kafka.common.serialization.Serializer 接口的类,生产者会使用这个类把键对象序列化为字节数组。这里拓展一下 Serializer 类,Serializer 是一个接口,它表示类将会采用何种方式序列化,它的作用是把对象转换为字节,实现了 Serializer 接口的类主要有 ByteArraySerializer、StringSerializer、IntegerSerializer ,其中 ByteArraySerialize 是 Kafka 默认使用的序列化器,其他的序列化器还有很多,你可以通过 这里 查看其他序列化器。要注意的一点:key.serializer 是必须要设置的,即使你打算只发送值的内容。 20 | 21 | - value.serializer:与 key.serializer 一样,value.serializer 指定的类会将值序列化。 22 | 23 | 下面代码演示了如何创建一个 Kafka 生产者,这里只指定了必要的属性,其他使用默认的配置 24 | 25 | ```java 26 | private Properties properties = new Properties(); 27 | properties.put("bootstrap.servers","broker1:9092,broker2:9092"); 28 | properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer"); 29 | properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer"); 30 | properties = new KafkaProducer(properties); 31 | ``` 32 | 33 | # Kafka 消息发送 34 | 35 | 实例化生产者对象后,接下来就可以开始发送消息了,我们从创建一个 ProducerRecord 对象开始,ProducerRecord 是 Kafka 中的一个核心类,它代表了一组 Kafka 需要发送的 key/value 键值对,它由记录要发送到的主题名称(Topic Name),可选的分区号(Partition Number)以及可选的键值对构成。在发送 ProducerRecord 时,我们需要将键值对对象由序列化器转换为字节数组,这样它们才能够在网络上传输。然后消息到达了分区器。 36 | 37 | ![Producer 发送流程](https://pic.imgdb.cn/item/6076ebe28322e6675cf42160.jpg) 38 | 39 | 如果发送过程中指定了有效的分区号,那么在发送记录时将使用该分区。如果发送过程中未指定分区,则将使用 key 的 hash 函数映射指定一个分区。如果发送的过程中既没有分区号也没有,则将以循环的方式分配一个分区。选好分区后,生产者就知道向哪个主题和分区发送数据了。ProducerRecord 还有关联的时间戳,如果用户没有提供时间戳,那么生产者将会在记录中使用当前的时间作为时间戳。Kafka 最终使用的时间戳取决于 topic 主题配置的时间戳类型。 40 | 41 | - 如果将主题配置为使用 CreateTime,则生产者记录中的时间戳将由 broker 使用。 42 | - 如果将主题配置为使用 LogAppendTime,则生产者记录中的时间戳在将消息添加到其日志中时,将由 broker 重写。 43 | 44 | 然后,这条消息被存放在一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上。由一个独立的线程负责把它们发到 Kafka Broker 上。Kafka Broker 在收到消息时会返回一个响应,如果写入成功,会返回一个 RecordMetaData 对象,它包含了主题和分区信息,以及记录在分区里的偏移量,上面两种的时间戳类型也会返回给用户。如果写入失败,会返回一个错误。生产者在收到错误之后会尝试重新发送消息,几次之后如果还是失败的话,就返回错误消息。 45 | 46 | ## 简单消息发送 47 | 48 | Kafka 最简单的消息发送如下: 49 | 50 | ```java 51 | ProducerRecord record = new ProducerRecord("CustomerCountry","West","France"); 52 | producer.send(record); 53 | ``` 54 | 55 | 代码中生产者(producer)的 send() 方法需要把 ProducerRecord 的对象作为参数进行发送,ProducerRecord 有很多构造函数,这个我们下面讨论,这里调用的是: 56 | 57 | ```java 58 | public ProducerRecord(String topic, K key, V value) {} 59 | ``` 60 | 61 | 这个构造函数,需要传递的是 topic 主题,key 和 value。把对应的参数传递完成后,生产者调用 send() 方法发送消息(ProducerRecord 对象)。我们可以从生产者的架构图中看出,消息是先被写入分区中的缓冲区中,然后分批次发送给 Kafka Broker。 62 | 63 | ![不同的主题与批次](https://pic.imgdb.cn/item/6076f9438322e6675c12a5d0.jpg) 64 | 65 | 发送成功后,send() 方法会返回一个 Future(java.util.concurrent) 对象,Future 对象的类型是 RecordMetadata 类型,我们上面这段代码没有考虑返回值,所以没有生成对应的 Future 对象,所以没有办法知道消息是否发送成功。如果不是很重要的信息或者对结果不会产生影响的信息,可以使用这种方式进行发送。我们可以忽略发送消息时可能发生的错误或者在服务器端可能发生的错误,但在消息发送之前,生产者还可能发生其他的异常。这些异常有可能是 SerializationException(序列化失败),BufferedExhaustedException 或 TimeoutException(说明缓冲区已满),又或是 InterruptedException(说明发送线程被中断)。 66 | 67 | ## 同步发送消息 68 | 69 | 第二种消息发送机制如下所示: 70 | 71 | ```java 72 | ProducerRecord record = new ProducerRecord("CustomerCountry","West","France"); 73 | try { 74 | RecordMetadata recordMetadata = producer.send(record).get(); 75 | }catch(Exception e){ 76 | e.printStackTrace(); 77 | } 78 | ``` 79 | 80 | 这种发送消息的方式较上面的发送方式有了改进,首先调用 send() 方法,然后再调用 get() 方法等待 Kafka 响应。如果服务器返回错误,get() 方法会抛出异常,如果没有发生错误,我们会得到 RecordMetadata 对象,可以用它来查看消息记录。 81 | 82 | 生产者(KafkaProducer)在发送的过程中会出现两类错误:其中一类是重试错误,这类错误可以通过重发消息来解决。比如连接的错误,可以通过再次建立连接来解决;无主错误则可以通过重新为分区选举首领来解决。KafkaProducer 被配置为自动重试,如果多次重试后仍无法解决问题,则会抛出重试异常。另一类错误是无法通过重试来解决的,比如消息过大对于这类错误,KafkaProducer 不会进行重试,直接抛出异常。 83 | 84 | ## 异步发送消息 85 | 86 | 同步发送消息都有个问题,那就是同一时间只能有一个消息在发送,这会造成许多消息无法直接发送,造成消息滞后,无法发挥效益最大化。比如消息在应用程序和 Kafka 集群之间一个来回需要 10ms。如果发送完每个消息后都等待响应的话,那么发送 100 个消息需要 1 秒,但是如果是异步方式的话,发送 100 条消息所需要的时间就会少很多很多。大多数时候,虽然 Kafka 会返回 RecordMetadata 消息,但是我们并不需要等待响应。 87 | 88 | 为了在异步发送消息的同时能够对异常情况进行处理,生产者提供了回掉支持。下面是回调的一个例子: 89 | 90 | ```java 91 | ProducerRecord < String, String > producerRecord = new ProducerRecord < String, String > ("CustomerCountry", "Huston", "America"); 92 | producer.send(producerRecord, new DemoProducerCallBack()); 93 | 94 | class DemoProducerCallBack implements Callback { 95 | public void onCompletion(RecordMetadata metadata, Exception exception) { 96 | if (exception != null) { 97 | exception.printStackTrace();; 98 | } 99 | } 100 | } 101 | ``` 102 | 103 | 首先实现回调需要定义一个实现了 org.apache.kafka.clients.producer.Callback 的类,这个接口只有一个 onCompletion 方法。如果 kafka 返回一个错误,onCompletion 方法会抛出一个非空(non null)异常,这里我们只是简单的把它打印出来,如果是生产环境需要更详细的处理,然后在 send() 方法发送的时候传递一个 Callback 回调的对象。 104 | 105 | # 生产者分区机制 106 | 107 | Kafka 对于数据的读写是以分区为粒度的,分区可以分布在多个主机(Broker)中,这样每个节点能够实现独立的数据写入和读取,并且能够通过增加新的节点来增加 Kafka 集群的吞吐量,通过分区部署在多个 Broker 来实现负载均衡的效果。上面我们介绍了生产者的发送方式有三种:不管结果如何直接发送、发送并返回结果、发送并回调。由于消息是存在主题(topic)的分区(partition)中的,所以当 Producer 生产者发送产生一条消息发给 topic 的时候,你如何判断这条消息会存在哪个分区中呢? 108 | 109 | 这其实就设计到 Kafka 的分区机制了。 110 | 111 | ## 分区策略 112 | 113 | Kafka 的分区策略指的就是将生产者发送到哪个分区的算法。Kafka 为我们提供了默认的分区策略,同时它也支持你自定义分区策略。如果要自定义分区策略的话,你需要显示配置生产者端的参数 Partitioner.class,我们可以看一下这个类它位于 org.apache.kafka.clients.producer 包下: 114 | 115 | ```java 116 | public interface Partitioner extends Configurable, Closeable { 117 | public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster); 118 | public void close(); 119 | default public void onNewBatch(String topic, Cluster cluster, int prevPartition) {} 120 | } 121 | ``` 122 | 123 | Partitioner 类有三个方法,分别来解释一下 124 | 125 | - partition(): 这个类有几个参数: topic,表示需要传递的主题;key 表示消息中的键值;keyBytes 表示分区中序列化过后的 key,byte 数组的形式传递;value 表示消息的 value 值;valueBytes 表示分区中序列化后的值数组;cluster 表示当前集群的原数据。Kafka 给你这么多信息,就是希望让你能够充分地利用这些信息对消息进行分区,计算出它要被发送到哪个分区中。 126 | - close() : 继承了 Closeable 接口能够实现 close() 方法,在分区关闭时调用。 127 | - onNewBatch(): 表示通知分区程序用来创建新的批次 128 | 129 | 其中与分区策略息息相关的就是 partition() 方法了。 130 | 131 | ## 顺序轮询 132 | 133 | 顺序分配,消息是均匀的分配给每个 partition,即每个分区存储一次消息。 134 | 135 | ![分区顺序分配](https://pic.imgdb.cn/item/6076faa18322e6675c15d490.jpg) 136 | 137 | 上图表示的就是轮询策略,轮训策略是 Kafka Producer 提供的默认策略,如果你不使用指定的轮训策略的话,Kafka 默认会使用顺序轮训策略的方式。 138 | 139 | ## 随机轮询 140 | 141 | 随机轮询简而言之就是随机的向 partition 中保存消息,如下图所示。 142 | 143 | ![随机分配示意图](https://pic.imgdb.cn/item/6076fafc8322e6675c169361.jpg) 144 | 145 | 实现随机分配的代码只需要两行,如下: 146 | 147 | ```java 148 | List < PartitionInfo > partitions = cluster.partitionsForTopic(topic); 149 | return ThreadLocalRandom.current().nextInt(partitions.size()); 150 | ``` 151 | 152 | 先计算出该主题总的分区数,然后随机地返回一个小于它的正整数。本质上看随机策略也是力求将数据均匀地打散到各个分区,但从实际表现来看,它要逊于轮询策略,所以如果追求数据的均匀分布,还是使用轮询策略比较好。事实上,随机策略是老版本生产者使用的分区策略,在新版本中已经改为轮询了。 153 | 154 | ## 按照 key 进行消息保存 155 | 156 | 这个策略也叫做 key-ordering 策略,Kafka 中每条消息都会有自己的 key,一旦消息被定义了 Key,那么你就可以保证同一个 Key 的所有消息都进入到相同的分区里面,由于每个分区下的消息处理都是有顺序的,故这个策略被称为按消息键保序策略,如下图所示。 157 | 158 | ![key-ordering 分配示意图](https://pic.imgdb.cn/item/6076fb448322e6675c17362b.jpg) 159 | 160 | 实现这个策略的 partition 方法同样简单,只需要下面两行代码即可: 161 | 162 | ```java 163 | List < PartitionInfo > partitions = cluster.partitionsForTopic(topic); 164 | return Math.abs(key.hashCode()) % partitions.size(); 165 | ``` 166 | 167 | 上面这几种分区策略都是比较基础的策略,除此之外,你还可以自定义分区策略。 168 | 169 | # 生产者压缩机制 170 | 171 | 压缩一词简单来讲就是一种互换思想,它是一种经典的用 CPU 时间去换磁盘空间或者 I/O 传输量的思想,希望以较小的 CPU 开销带来更少的磁盘占用或更少的网络 I/O 传输。Kafka 的消息分为两层:消息集合 和 消息。一个消息集合中包含若干条日志项,而日志项才是真正封装消息的地方。Kafka 底层的消息日志由一系列消息集合日志项组成。Kafka 通常不会直接操作具体的一条条消息,它总是在消息集合这个层面上进行写入操作。 172 | 173 | 在 Kafka 中,压缩会发生在两个地方:Kafka Producer 和 Kafka Consumer,为什么启用压缩?说白了就是消息太大,需要变小一点 来使消息发的更快一些。Kafka Producer 中使用 compression.type 来开启压缩 174 | 175 | ```java 176 | private Properties properties = new Properties(); 177 | properties.put("bootstrap.servers", "192.168.1.9:9092"); 178 | properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); 179 | properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); 180 | properties.put("compression.type", "gzip"); 181 | Producer < String, String > producer = new KafkaProducer < String, String > (properties); 182 | ProducerRecord < String, String > record = new ProducerRecord < String, String > ("CustomerCountry", "Precision Products", "France"); 183 | ``` 184 | 185 | 上面代码表明该 Producer 的压缩算法使用的是 GZIP 186 | -------------------------------------------------------------------------------- /header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 101 | 366 |
367 |

368 | MessageQueue Series 369 |

370 |

371 | 深入浅出消息队列与消息中间件 372 |

373 |
374 | 375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 | 391 | 392 |
393 |
394 |
-------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 2 | Public License 3 | 4 | wx-chevalier 王下邀月熊 王下邀月熊 5 | 6 | By exercising the Licensed Rights (defined below), You accept and agree 7 | to be bound by the terms and conditions of this Creative Commons 8 | Attribution-NonCommercial-ShareAlike 4.0 International Public License 9 | ("Public License"). To the extent this Public License may be 10 | interpreted as a contract, You are granted the Licensed Rights in 11 | consideration of Your acceptance of these terms and conditions, and the 12 | Licensor grants You such rights in consideration of benefits the 13 | Licensor receives from making the Licensed Material available under 14 | these terms and conditions. 15 | 16 | 17 | Section 1 -- Definitions. 18 | 19 | a. Adapted Material means material subject to Copyright and Similar 20 | Rights that is derived from or based upon the Licensed Material 21 | and in which the Licensed Material is translated, altered, 22 | arranged, transformed, or otherwise modified in a manner requiring 23 | permission under the Copyright and Similar Rights held by the 24 | Licensor. For purposes of this Public License, where the Licensed 25 | Material is a musical work, performance, or sound recording, 26 | Adapted Material is always produced where the Licensed Material is 27 | synched in timed relation with a moving image. 28 | 29 | b. Adapter's License means the license You apply to Your Copyright 30 | and Similar Rights in Your contributions to Adapted Material in 31 | accordance with the terms and conditions of this Public License. 32 | 33 | c. BY-NC-SA Compatible License means a license listed at 34 | creativecommons.org/compatiblelicenses, approved by Creative 35 | Commons as essentially the equivalent of this Public License. 36 | 37 | d. Copyright and Similar Rights means copyright and/or similar rights 38 | closely related to copyright including, without limitation, 39 | performance, broadcast, sound recording, and Sui Generis Database 40 | Rights, without regard to how the rights are labeled or 41 | categorized. For purposes of this Public License, the rights 42 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 43 | Rights. 44 | 45 | e. Effective Technological Measures means those measures that, in the 46 | absence of proper authority, may not be circumvented under laws 47 | fulfilling obligations under Article 11 of the WIPO Copyright 48 | Treaty adopted on December 20, 1996, and/or similar international 49 | agreements. 50 | 51 | f. Exceptions and Limitations means fair use, fair dealing, and/or 52 | any other exception or limitation to Copyright and Similar Rights 53 | that applies to Your use of the Licensed Material. 54 | 55 | g. License Elements means the license attributes listed in the name 56 | of a Creative Commons Public License. The License Elements of this 57 | Public License are Attribution, NonCommercial, and ShareAlike. 58 | 59 | h. Licensed Material means the artistic or literary work, database, 60 | or other material to which the Licensor applied this Public 61 | License. 62 | 63 | i. Licensed Rights means the rights granted to You subject to the 64 | terms and conditions of this Public License, which are limited to 65 | all Copyright and Similar Rights that apply to Your use of the 66 | Licensed Material and that the Licensor has authority to license. 67 | 68 | j. Licensor means the individual(s) or entity(ies) granting rights 69 | under this Public License. 70 | 71 | k. NonCommercial means not primarily intended for or directed towards 72 | commercial advantage or monetary compensation. For purposes of 73 | this Public License, the exchange of the Licensed Material for 74 | other material subject to Copyright and Similar Rights by digital 75 | file-sharing or similar means is NonCommercial provided there is 76 | no payment of monetary compensation in connection with the 77 | exchange. 78 | 79 | l. Share means to provide material to the public by any means or 80 | process that requires permission under the Licensed Rights, such 81 | as reproduction, public display, public performance, distribution, 82 | dissemination, communication, or importation, and to make material 83 | available to the public including in ways that members of the 84 | public may access the material from a place and at a time 85 | individually chosen by them. 86 | 87 | m. Sui Generis Database Rights means rights other than copyright 88 | resulting from Directive 96/9/EC of the European Parliament and of 89 | the Council of 11 March 1996 on the legal protection of databases, 90 | as amended and/or succeeded, as well as other essentially 91 | equivalent rights anywhere in the world. 92 | 93 | n. You means the individual or entity exercising the Licensed Rights 94 | under this Public License. Your has a corresponding meaning. 95 | 96 | 97 | Section 2 -- Scope. 98 | 99 | a. License grant. 100 | 101 | 1. Subject to the terms and conditions of this Public License, 102 | the Licensor hereby grants You a worldwide, royalty-free, 103 | non-sublicensable, non-exclusive, irrevocable license to 104 | exercise the Licensed Rights in the Licensed Material to: 105 | 106 | a. reproduce and Share the Licensed Material, in whole or 107 | in part, for NonCommercial purposes only; and 108 | 109 | b. produce, reproduce, and Share Adapted Material for 110 | NonCommercial purposes only. 111 | 112 | 2. Exceptions and Limitations. For the avoidance of doubt, where 113 | Exceptions and Limitations apply to Your use, this Public 114 | License does not apply, and You do not need to comply with 115 | its terms and conditions. 116 | 117 | 3. Term. The term of this Public License is specified in Section 118 | 6(a). 119 | 120 | 4. Media and formats; technical modifications allowed. The 121 | Licensor authorizes You to exercise the Licensed Rights in 122 | all media and formats whether now known or hereafter created, 123 | and to make technical modifications necessary to do so. The 124 | Licensor waives and/or agrees not to assert any right or 125 | authority to forbid You from making technical modifications 126 | necessary to exercise the Licensed Rights, including 127 | technical modifications necessary to circumvent Effective 128 | Technological Measures. For purposes of this Public License, 129 | simply making modifications authorized by this Section 2(a) 130 | (4) never produces Adapted Material. 131 | 132 | 5. Downstream recipients. 133 | 134 | a. Offer from the Licensor -- Licensed Material. Every 135 | recipient of the Licensed Material automatically 136 | receives an offer from the Licensor to exercise the 137 | Licensed Rights under the terms and conditions of this 138 | Public License. 139 | 140 | b. Additional offer from the Licensor -- Adapted Material. 141 | Every recipient of Adapted Material from You 142 | automatically receives an offer from the Licensor to 143 | exercise the Licensed Rights in the Adapted Material 144 | under the conditions of the Adapter's License You apply. 145 | 146 | c. No downstream restrictions. You may not offer or impose 147 | any additional or different terms or conditions on, or 148 | apply any Effective Technological Measures to, the 149 | Licensed Material if doing so restricts exercise of the 150 | Licensed Rights by any recipient of the Licensed 151 | Material. 152 | 153 | 6. No endorsement. Nothing in this Public License constitutes or 154 | may be construed as permission to assert or imply that You 155 | are, or that Your use of the Licensed Material is, connected 156 | with, or sponsored, endorsed, or granted official status by, 157 | the Licensor or others designated to receive attribution as 158 | provided in Section 3(a)(1)(A)(i). 159 | 160 | b. Other rights. 161 | 162 | 1. Moral rights, such as the right of integrity, are not 163 | licensed under this Public License, nor are publicity, 164 | privacy, and/or other similar personality rights; however, to 165 | the extent possible, the Licensor waives and/or agrees not to 166 | assert any such rights held by the Licensor to the limited 167 | extent necessary to allow You to exercise the Licensed 168 | Rights, but not otherwise. 169 | 170 | 2. Patent and trademark rights are not licensed under this 171 | Public License. 172 | 173 | 3. To the extent possible, the Licensor waives any right to 174 | collect royalties from You for the exercise of the Licensed 175 | Rights, whether directly or through a collecting society 176 | under any voluntary or waivable statutory or compulsory 177 | licensing scheme. In all other cases the Licensor expressly 178 | reserves any right to collect such royalties, including when 179 | the Licensed Material is used other than for NonCommercial 180 | purposes. 181 | 182 | 183 | Section 3 -- License Conditions. 184 | 185 | Your exercise of the Licensed Rights is expressly made subject to the 186 | following conditions. 187 | 188 | a. Attribution. 189 | 190 | 1. If You Share the Licensed Material (including in modified 191 | form), You must: 192 | 193 | a. retain the following if it is supplied by the Licensor 194 | with the Licensed Material: 195 | 196 | i. identification of the creator(s) of the Licensed 197 | Material and any others designated to receive 198 | attribution, in any reasonable manner requested by 199 | the Licensor (including by pseudonym if 200 | designated); 201 | 202 | ii. a copyright notice; 203 | 204 | iii. a notice that refers to this Public License; 205 | 206 | iv. a notice that refers to the disclaimer of 207 | warranties; 208 | 209 | v. a URI or hyperlink to the Licensed Material to the 210 | extent reasonably practicable; 211 | 212 | b. indicate if You modified the Licensed Material and 213 | retain an indication of any previous modifications; and 214 | 215 | c. indicate the Licensed Material is licensed under this 216 | Public License, and include the text of, or the URI or 217 | hyperlink to, this Public License. 218 | 219 | 2. You may satisfy the conditions in Section 3(a)(1) in any 220 | reasonable manner based on the medium, means, and context in 221 | which You Share the Licensed Material. For example, it may be 222 | reasonable to satisfy the conditions by providing a URI or 223 | hyperlink to a resource that includes the required 224 | information. 225 | 3. If requested by the Licensor, You must remove any of the 226 | information required by Section 3(a)(1)(A) to the extent 227 | reasonably practicable. 228 | 229 | b. ShareAlike. 230 | 231 | In addition to the conditions in Section 3(a), if You Share 232 | Adapted Material You produce, the following conditions also apply. 233 | 234 | 1. The Adapter's License You apply must be a Creative Commons 235 | license with the same License Elements, this version or 236 | later, or a BY-NC-SA Compatible License. 237 | 238 | 2. You must include the text of, or the URI or hyperlink to, the 239 | Adapter's License You apply. You may satisfy this condition 240 | in any reasonable manner based on the medium, means, and 241 | context in which You Share Adapted Material. 242 | 243 | 3. You may not offer or impose any additional or different terms 244 | or conditions on, or apply any Effective Technological 245 | Measures to, Adapted Material that restrict exercise of the 246 | rights granted under the Adapter's License You apply. 247 | 248 | 249 | Section 4 -- Sui Generis Database Rights. 250 | 251 | Where the Licensed Rights include Sui Generis Database Rights that 252 | apply to Your use of the Licensed Material: 253 | 254 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 255 | to extract, reuse, reproduce, and Share all or a substantial 256 | portion of the contents of the database for NonCommercial purposes 257 | only; 258 | 259 | b. if You include all or a substantial portion of the database 260 | contents in a database in which You have Sui Generis Database 261 | Rights, then the database in which You have Sui Generis Database 262 | Rights (but not its individual contents) is Adapted Material, 263 | including for purposes of Section 3(b); and 264 | 265 | c. You must comply with the conditions in Section 3(a) if You Share 266 | all or a substantial portion of the contents of the database. 267 | 268 | For the avoidance of doubt, this Section 4 supplements and does not 269 | replace Your obligations under this Public License where the Licensed 270 | Rights include other Copyright and Similar Rights. 271 | 272 | 273 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 274 | 275 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 276 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 277 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 278 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 279 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 280 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 281 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 282 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 283 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 284 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 285 | 286 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 287 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 288 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 289 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 290 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 291 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 292 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 293 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 294 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 295 | 296 | c. The disclaimer of warranties and limitation of liability provided 297 | above shall be interpreted in a manner that, to the extent 298 | possible, most closely approximates an absolute disclaimer and 299 | waiver of all liability. 300 | 301 | 302 | Section 6 -- Term and Termination. 303 | 304 | a. This Public License applies for the term of the Copyright and 305 | Similar Rights licensed here. However, if You fail to comply with 306 | this Public License, then Your rights under this Public License 307 | terminate automatically. 308 | 309 | b. Where Your right to use the Licensed Material has terminated under 310 | Section 6(a), it reinstates: 311 | 312 | 1. automatically as of the date the violation is cured, provided 313 | it is cured within 30 days of Your discovery of the 314 | violation; or 315 | 316 | 2. upon express reinstatement by the Licensor. 317 | 318 | For the avoidance of doubt, this Section 6(b) does not affect any 319 | right the Licensor may have to seek remedies for Your violations 320 | of this Public License. 321 | 322 | c. For the avoidance of doubt, the Licensor may also offer the 323 | Licensed Material under separate terms or conditions or stop 324 | distributing the Licensed Material at any time; however, doing so 325 | will not terminate this Public License. 326 | 327 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 328 | License. 329 | 330 | 331 | Section 7 -- Other Terms and Conditions. 332 | 333 | a. The Licensor shall not be bound by any additional or different 334 | terms or conditions communicated by You unless expressly agreed. 335 | 336 | b. Any arrangements, understandings, or agreements regarding the 337 | Licensed Material not stated herein are separate from and 338 | independent of the terms and conditions of this Public License. 339 | 340 | 341 | Section 8 -- Interpretation. 342 | 343 | a. For the avoidance of doubt, this Public License does not, and 344 | shall not be interpreted to, reduce, limit, restrict, or impose 345 | conditions on any use of the Licensed Material that could lawfully 346 | be made without permission under this Public License. 347 | 348 | b. To the extent possible, if any provision of this Public License is 349 | deemed unenforceable, it shall be automatically reformed to the 350 | minimum extent necessary to make it enforceable. If the provision 351 | cannot be reformed, it shall be severed from this Public License 352 | without affecting the enforceability of the remaining terms and 353 | conditions. 354 | 355 | c. No term or condition of this Public License will be waived and no 356 | failure to comply consented to unless expressly agreed to by the 357 | Licensor. 358 | 359 | d. Nothing in this Public License constitutes or may be interpreted 360 | as a limitation upon, or waiver of, any privileges and immunities 361 | that apply to the Licensor or You, including from the legal 362 | processes of any jurisdiction or authority. --------------------------------------------------------------------------------