读构建可扩展分布式系统:方法与实践13可扩展的事件驱动处理
1. 可扩展的事件驱动处理
1.1. 使用消息传递系统进行通信,你可以创建松耦合的架构
-
1.1.1. 消息生产者只是将消息存储在队列中,而不用关心消费者如何处理消息
-
1.1.2. 有一个或多个消费者,并且生产者和消费者的集合可以随着时间的推移而改变
-
1.1.3. 有助于提高服务响应能力、通过缓存消除请求到达峰值以及在面对不可用的消费者时保持系统处理能力
-
1.1.4. 传统上,用于实现异步系统的消息代理技术侧重于消息传输
-
1.1.5. RabbitMQ或ActiveMQ等代理平台支持队列集合用于基于FIFO的内存或基于磁盘的临时存储
-
1.1.6. 当消费者访问队列中的消息时,该消息将从代理中删除
-
1.1.6.1. 称为破坏性消费者语义
-
1.1.6.2. 如果使用发布-订阅消息传递,代理会实现机制来维护队列中的每条消息,直到所有活动订阅者都消费了这条消息,新的订阅者看不到旧的消息
-
1.1.6.3. 释放了代理资源,但也破坏了事件的任何显式记录
-
1.1.7. 代理通常还实现一些额外功能用于消息过滤和路由
1.2. 从事件驱动架构的视角重新审视异步系统
- 1.2.1. Kafka的设计旨在支持大规模的事件驱动系统,利用简单的持久化消息日志数据结构和非破坏性消费者语义
1.3. 事件驱动的架构适用于现代业务环境中的许多用例
-
1.3.1. 可以使用事件来捕获外部活动,并将其流式传输到分析系统中,实时洞察用户和系统的行为
-
1.3.2. 可以使用事件来描述所发布状态的变更,以支持跨不同系统或多个微服务的集成
1.4. 事件处理系统需要一个可靠、健壮和可扩展的平台来捕获和传递事件
1.5. Kafka将事件持久化在主题中,这些主题由消费者以非破坏性的方式处理
- 1.5.1. 可以对主题进行分区和复制,以提供更好的可扩展性和可用性
2. 事件驱动架构
2.1. 事件表示在应用程序的上下文场景中发生了一些有趣的事情
-
2.1.1. 系统捕获到的外部事件
-
2.1.2. 由于某些状态变更而在内部生成的事件
2.2. 事件通常会发布给消息传递系统
-
2.2.1. 事件源只是发出事件,并不关心系统中的其他组件如何处理这些事件
-
2.2.2. 重要的是事件源不会注意到事件生成所触发的操作
-
2.2.3. 产生的架构是松耦合的,并为合并新的事件消费者提供了高度的灵活性
2.3. 事实证明,在简单的日志数据结构中保存一份不可变事件的永久记录是非常有用的
-
2.3.1. 与大多数消息代理管理的FIFO队列相比,事件日志是一种只允许追加的数据结构
-
2.3.2. 记录附加到日志的末尾,每个日志条目都有一个唯一的条目号
-
2.3.3. 条目号表示捕获系统中事件的顺序
-
2.3.4. 具有较低序号的事件被定义为发生在具有较高序号的事件之前
-
2.3.5. 此顺序在分布式系统中特别有用,可以用来分析和洞察应用程序的行为
2.4. 日志是关于每个包裹在任意时刻(曾经)所在位置的唯一真实来源
2.5. 基于事件的系统的另一个常见用例是保持复制数据在微服务之间同步
- 2.5.1. 事件日志本质上是用于跨微服务的数据复制,以便实现状态传递
2.6. 事件日志的持久性的关键优势
-
2.6.1. 可以随时引入新的事件消费者
-
2.6.1.1. 日志存储的是永久的、不可变的事件记录,新的消费者可以访问完整的事件历史记录
-
2.6.1.2. 现有的和新的事件都可以处理
-
2.6.2. 可以修改现有的事件处理逻辑,以添加新功能或修复错误
-
2.6.2.1. 可以在完整的日志上执行新逻辑以丰富结果或修复错误
-
2.6.3. 如果发生服务器或磁盘故障,你可以从日志中恢复最后已知的状态并重播事件以恢复数据集
-
2.6.3.1. 类似于事务日志在数据库系统中的作用
2.7. 缺点
-
2.7.1. 从日志中删除事件
-
2.7.1.1. 有一些用例需要删除日志条目
-
2.7.1.2. 仅允许追加的不可变日志不是为删除条目而设计的,可能会使删除条目出现问题
2.8. 两种主要的日志条目删除机制
-
2.8.1. 生存时间
-
2.8.1.1. 在默认的两周后删除日志条目
-
2.8.1.2. 可以调整它来满足你对日志条目保留和删除的要求
-
2.8.2. 压缩主题
-
2.8.2.1. 主题可以配置为仅保留给定事件键的最新条目
-
2.8.2.2. Kafka会将较旧的条目标记为删除
-
2.8.2.3. 事件实际上在压缩主题中被标记为删除,并在稍后某个时间段(日志压缩任务运行时)被删除
-
2.8.2.4. 此任务的频率是可配置的
3. Apache Kafka
3.1. Kafka的核心是一个分布式的持久日志存储
-
3.1.1. Kafka起源于LinkedIn,旨在简化其系统集成的工作
-
3.1.2. 2012年被转化为一个Apache项目
-
3.1.3. 笨代理/聪明消费者的架构
-
3.1.3.1. 产生的架构已被证明具有令人难以置信的可扩展性,并能提供非常高的吞吐量
-
3.1.4. 日志条目被持久存储,可以被多个消费者多次读取
-
3.1.4.1. 消费者只需指定他们希望读取条目的日志偏移量或索引
-
3.1.5. 代理的主要功能是有效地将新事件追加到持久日志、将事件传递给消费者以及管理日志分区和复制来实现可扩展性和可用性
-
3.1.5.1. 代理从维护与消费者相关的复杂状态中解放出来
3.2. Kafka连接
-
3.2.1. 一个设计用于构建连接器以将外部数据系统链接到Kafka代理的框架
-
3.2.2. 使用该框架构建高性能连接器,从你自己的系统生成或消费Kafka消息
3.3. Kafka流
-
3.3.1. 一个轻量级的客户端库
-
3.3.2. 用于从存储在Kafka代理中的事件构建流应用程序
-
3.3.3. 数据流表示无限的、不断更新的数据集
-
3.3.4. 流应用程序通过处理批量或某时间窗口的数据来提供有用的实时洞察力
-
3.3.5. Kafka支持高度分布式集群部署,其中由代理通信来分发和复制事件日志
3.4. Kafka将元数据管理委托给Apache ZooKeeper
-
3.4.1. 元数据实质上指定了多个事件日志在集群中的位置,以及集群状态的各种其他元素
-
3.4.2. ZooKeeper是一种高可用性服务,许多分布式平台用它来管理配置信息和支持组协调(group coordination)
-
3.4.3. ZooKeeper提供了一个类似于普通文件系统的分层命名空间,Kafka用此来在外部维护集群状态,使其对所有代理可用
-
3.4.4. 意味着你必须创建一个ZooKeeper集群(为了可用性)并使Kafka集群中的代理可以访问它
-
3.4.5. Kafka对ZooKeeper的使用对你的应用程序来说是透明的
3.5. 主题
-
3.5.1. Kafka的主题相当于一般消息传递技术中的队列
-
3.5.2. 主题由代理管理,并且始终是持久的
-
3.5.3. 主题由仅允许追加的日志实现,这意味着新事件总是写入日志的尾部
-
3.5.3.1. 从主题中读取事件是非破坏性的
-
3.5.3.2. 每个主题都会保留所有事件,直到特定主题所配置的事件保留时间到期
-
3.5.3.3. 当事件的存储时间超过此保留时间时,它们会自动从主题中删除
-
3.5.4. 消费者通过指定其希望访问的主题名称以及其想要读取的消息的索引或偏移量来读取事件
-
3.5.5. 代理利用日志的仅追加特性来充分发挥磁盘的线性读写性能
-
3.5.5.1. 操作系统针对这些数据访问模式进行了大量优化,使用数据预取和缓存等技术,使Kafka能够提供恒定的访问时间,无论主题中存储的事件数量如何
3.6. 生产者和消费者
-
3.6.1. Kafka为生产者提供API来写入事件,同时为消费者提供API来从主题中读取事件
-
3.6.2. 一个事件拥有一个应用程序定义的键和一个相关联的值,以及一个发布者提供的时间戳
-
3.6.3. 批量累积事件可以减少Kafka传递事件过程中到代理的网络往返次数
-
3.6.3.1. 使代理在将事件批次追加到主题时执行更少次数、更大量的写入
-
3.6.3.2. 共同促成Kafka系统实现高吞吐量的大部分因素
-
3.6.4. 生产者上的缓冲事件允许你权衡为提升系统吞吐量而批量累积事件时所产生的额外延迟(linger.ms值)
-
3.6.5. Kafka通过acks配置参数为生产者提供不同的事件传递确认机制
-
3.6.5.1. 值为0表示不提供传递确认,这是一个“即发即弃”的选项——事件可能会丢失
-
3.6.5.2. 值为1意味着一旦事件被持久化到目标主题,代理就会确认该事件
-
3.6.5.3. 短暂的网络故障可能会导致生产者重试失败的事件,从而导致重复
> 3.6.5.3.1. 如果不能接受重复事件,可以将enable-idempotence配置参数设置为true
> 3.6.5.3.2. 此设置让代理过滤掉重复事件并提供严格一次(exactly-once)的传递机制
-
3.6.6. Kafka消费者利用拉取模型从主题中批量检索事件
-
3.6.6.1. 如果消费者在批处理事件时失败,则这批事件将不会重新投递
-
3.6.6.2. 在批处理完所有事件后调用commitSync(),为消费者提供至少一次投递的保证
-
3.6.6.3. 如果消费者在处理一批事件时崩溃,则不会提交偏移量,当消费者重新启动时,事件将被重新投递
-
3.6.7. Kafka的消费者API不是线程安全的
-
3.6.7.1. 与代理的所有网络交互都发生在检索事件的同一客户端线程中
-
3.6.7.2. 要并发处理事件,消费者需要自己实现线程方案
-
3.6.7.3. 一种常见的方法是每个消费者一个线程(thread-per-consumer)的模型,它提供了一个简单的解决方案,代价是要在代理端管理更多的TCP连接和获取请求
-
3.6.7.4. 另一种选择是使用单个线程获取事件,并将事件处理放到处理线程池中
> 3.6.7.4.1. 这可能会提供更大的可扩展性,但会使手动提交事件变得更复杂,因为线程需要以某种方式进行协调,以确保在提交之前主题的所有事件都已处理完成
3.7. 可扩展性
-
3.7.1. 可扩展性机制是主题分区
-
3.7.2. 当创建一个主题时,要指定存储事件需要使用的分区数量,Kafka将分区分布在集群中的代理上
-
3.7.3. 生产者和消费者可以分别并行地写入和读取不同的分区,提供了水平可扩展性
-
3.7.4. 根据Kafka实现的“笨代理”架构,由生产者而不是代理负责选择将事件分配到哪个分区
-
3.7.4.1. 使得代理能够专注于接收、存储和传递事件等主要目的
-
3.7.5. 当指定事件的键时,分区程序会使用键值的哈希函数来选择分区
-
3.7.5.1. 会将具有相同键值的事件定向到相同的分区,这对于聚合处理事件的消费者来说非常有用
-
3.7.6. 对主题进行分区会影响事件的排序
-
3.7.6.1. 会按照生产者生成事件的顺序将事件写入单个分区,事件将按照写入的顺序从分区中消费
-
3.7.6.2. 意味着每个分区中的事件都是按时间来排序的,并且提供事件流的部分排序
-
3.7.6.3. 分区之间没有事件的总顺序
-
3.7.7. 分区还可以将事件并发传递给多个消费者
-
3.7.7.1. Kafka为一个主题引入了消费者组的概念
-
3.7.7.2. 一个主题的消费者组可以包含一个或多个消费者,最多可达到为主题配置的分区数
-
3.7.7.3. 如果组内的消费者数等于分区数,则Kafka会将组中的每个消费者分配到一个分区
-
3.7.7.4. 如果组内的消费者数小于分区数,则部分消费者会被分配来自多个分区的消息
-
3.7.7.5. 如果组内的消费者数大于分区数,部分消费者将不会被分配到分区并保持空闲状态
-
3.7.7.6. 为了实现重新平衡,Kafka会从消费者组中选择一个消费者作为组长
-
3.7.7.7. 对于未在消费者之间移动的分区,事件处理可以继续进行,不会停机
-
3.7.7.8. 只需添加分配给消费者的新分区
-
3.7.7.9. 对于任意未出现在新分配中的现有消费者分区,消费者完成当前批次消息的处理,提交偏移量,并放弃其订阅
-
3.7.7.10. 一旦消费者放弃订阅,该分区便会被标记为未分配
3.8. 可用性
-
3.8.1. 在Kafka中创建主题时,可以指定一个复制因子N
-
3.8.1.1. N会促使Kafka使用领导者-追随者架构将主题中的每个分区复制N次
-
3.8.2. 如果领导者发生故障,Kafka可以自动进行故障转移,切换到其中一个追随者,以确保分区保持可用
4. 案例
4.1. Kafka作为底层消息传递组件,被广泛部署在跨多个垂直业务的事件处理系统中
4.2. Big Fish Games是领先的消费型游戏制作商
-
4.2.1. 使用Kafka来捕获游戏运行所产生的高吞吐量事件
-
4.2.1.1. 游戏遥测
-
4.2.1.2. 捕获的数据包括各种事件,例如游戏设备和会话信息,应用内购买和对营销活动的响应,以及特定于游戏的事件
-
4.2.1.3. 该事件流被输入一系列下游分析应用中,为Big Fish提供对游戏功能使用和用户行为模式的实时监测
4.3. Slack利用Kafka从其Web客户端捕获那些因处理成本太高而无法同步处理的事件