design\project\kafka(二)

133 阅读11分钟

Kafka 的设计思想

设计动机

我们设计 kafka 为一个统一的平台,来处理全公司所有的所有实时数据。为了做到这一点,我们考虑了很多的使用场景。

  • 它必须具有高吞吐量,才能支持实时日志聚合等大容量事件流
  • 它需要优雅地处理大量数据积压,才能支持系统离线,与定期加载数据
  • 这还意味着系统必须处理低延迟交付,才能处理更传统的消息传递用例。
  • 我们希望这些“源”支持分区、分布式、实时处理,支持优雅的派生新的“源”。这激发了我们的分区和消费者模型(可扩展性
  • 最后,当数据流被送入其他数据系统进行服务时,我们想到系统必须能够在出现机器故障时保证容错
  • 为了支持这些用途,与传统的消息传递系统相比,我们设计了一个独特的系统,它更类似于数据库日志

Kafka 的独特性——持久化

1. 文件系统性能没有大家想的那么可怕

要是有谁问,你们系统性能瓶颈? IO(文件系统)、网络、JVM GC 君荣登榜首

kafka 重度依赖文件系统来存储和缓存消息,人们普遍认为“磁盘速度很慢”,这让人们怀疑使用文件持久化的性能问题。事实上,磁盘的速度,取决于它们的使用方式,一个设计合理的磁盘存储结构通常可以和网络一样快。

在过去十年中,我们对于磁盘性能一直有一个误区:硬盘驱动器的吞吐量不等于磁盘搜索的延迟不同。

因此,在具有六个7200rpm SATA RAID-5阵列的JBOD配置上,线性写入的性能约为600MB/秒,而随机写入的性能仅为100k/秒,相差超过6000X

因为线性读写是所有使用模式中最可预测的,并且受到操作系统的高度优化

  • 现代操作系统提供预读和后写技术
  • 以大数据块倍数预取数据
  • 并将较小的逻辑写入分组为较大的一起写入

在这篇 ACM队列 文章中,他们实际上发现,顺序磁盘访问在某些情况下比随机内存访问更快

现代操作系统大量使用主内存进行磁盘缓存,现代操作系统恨不得将所有可用内存用于磁盘缓存。所有磁盘读写都会使用到这个缓存。

此外,我们的应用构建在 JVM 之上,大家都知道两件事:

  • 对象的内存开销非常高,通常是存储数据大小的两倍(甚至更糟)。
  • 随着堆内数据的增加,Java垃圾收集变得越来越精细和缓慢。

所以,我们使用文件系统(PageCache) 而不是内存,我们存储压缩的字节码而不是对象,释放更多的可用内存。

一个 32G 内存的服务器上,可以产生28~30G 的堆外缓存,这些缓存在系统重启后会自动重建

代码更简单,不会有缓存一致性问题,还支持预读

和对比传统的尽可能将数据加载到内存中,在空间耗尽时将其全部刷新到文件系统相反, 这篇文章 中也提到了这种设计

2. 常量级的时间复杂度

传统的消息队列,其消费者队列的元数据,通常使用 BTree 结构存储。但是这对于 kafka 行不通。 BTree 的 O(logn) 复杂度不适应于磁盘。由于磁盘的物理结构,每次寻道需要 10 ms,且磁盘不支持并行寻址。即使是少量的磁盘搜索也会导致非常高的开销。

所以 Kafka 选用了与日志类似的的队列持久化方案。生产者时在文件后追加,消费者顺序读取。

这种结构的优点是:

  1. 所有操作都是 O(1) 复杂度
  2. 读操作不会阻塞写操作
  3. 读写性能与数据大小完全解耦
  4. 可以充分利用廉价的、低转速的硬盘,价格为 1/3,容量提升 3 倍

同时,这也意味着 Kafka 可以将消息保留相对较长的时间(栗如一周)

Kafka 的独特性——高性能

  • 我们主要考虑的应用场景是处理web活动数据,Web 通常数据量非常大

    • 每个页面视图可能会生成几十次写入。
  • 我们假设消息至少有一位消费者(通常是许多消费者)订阅,因此我们努力使消费尽可能方便。

  • 根据经验,在构建多租户的系统时,高性能是必须的

    • 高性能的核心是,提升各个组件的处理速度,达到全链路优化,没有性能瓶颈
    • 而在整个应用链路中,应用程序最为脆弱,很容易由于负载波动成为瓶颈
    • 因此,我们使用一个处理速度非常快的中心服务,去支持数十个或数百个应用程序
  • 我们在上一节讨论了磁盘效率。除此之外,这种系统效率低下的常见原因有两个:

    • 频繁小I/O操作

      • 原因:客户机和服务器之间过于频繁的通信
      • 原因:及服务器自身过多的持久性操作
      • 为了避免小I/O,协议是围绕一个“message set”(一组消息)抽象构建的。
      • 这允许发送一组消息,而不是一次发送一条消息。
      • 服务器用“message set”批量日志,消费者一次获一整个线性块
      • 更大的网络数据包,顺序磁盘操作,连续的内存块,产生了几个数量级的加速
      • 使 Kafka 能够将突发的随机消息写入转化为流向消费者的线性写入
    • 过多的字节复制

      • 在高负载下,过多的字节复制影响很大
      • Kafka 采用了一种标准化的二进制消息格式,由生产者、代理和消费者共享(因此数据块可以在它们之间进行传输,而无需修改)
      • 现代的 unix 操作系统对 pagecache 到 socket 的数据传输提供了高度优化
      • 一般的发送文件,有两个系统调用,数据在内核和用户态之间来回倒
      • os 把数据从磁盘读到内核
      • 应用程序从内核 ->用户态
      • 应用程序从用户态 -> 内核 socket 缓冲区
      • os 复制到 NIC,通过网络发送
      • 我们希望当多个消费者读取时,数据只复制到 pagecache 一次
    • kafka 通过 pagecache 和 sendfile 实现最小磁盘读取,参考:零拷贝

端到端批量压缩

对于需要通过广域网在数据中心之间发送消息,网络带宽容易成为瓶颈。

  • 而一次压缩一条消息可能会导致非常低的压缩比,因为许多冗余是由于相同类型的消息之间的重复(例如JSON中的字段名或web日志中的用户代理或公共字符串值)
  • 高效压缩需要将多条消息压缩在一起。
  • Kafka 一批消息可以聚集在一起压缩并以这种形式发送到服务器
  • 这批消息将以压缩形式写入,并在日志中保持压缩状态,仅由使用者解压缩
  • Kafka支持 GZIP、Snappy、LZ4 和 ZStandard

Producer

负载均衡

producer 直接将消息发送给 partition 的 leader,不经过任何路由。

  • 所以在任何时间,所有 kafka 节点都能响应 metadata 请求含:

    • 服务器在线情况
    • 指定 partition 的 leader 在哪里

客户端控制将消息发布到哪个分区

  • 分区选择可以是

    • 随机
    • 实现某种负载均衡机制
    • 按分区键分区(栗如:通过用户 ID 分区)

异步推送

批量处理是提高效率的主要因素之一。Kafka 将尝试在内存中积累数据,并在单个请求中发送更多的数据。可配置为:

  • 累积一定量的消息
  • 一定时间内

以此平衡(trade off),客户端更少的 IO 次数,服务端也不用处理大 IO。少量的延迟换取更大的吞吐率。

Consumer

kafka consumer 通过向指定 partition 的 broker 发送 fetch 请求的方式来工作。

  • consumer 每次请求都会指定一个 offset,接收该位置开始的一段日志
  • consumer 可以“倒带”重新消费

Kafka 的消息消费模式( Push vs Pull )

kafka 使用了传统的消费模式,即:生产者将消息推送到 Broker,消费者从从代理中提取(即为 Pull 消费模式)

Scribe 和 Apache Flume 则使用的是 Push 消费模式

  • 基于推送的系统很难处理不同消费的消费能力不同的问题

    • 因为 Broker 来控制数据传输速率
    • 如果有的消费者的消费能力小于推送速率,会轰趴消费者导致消费者拒绝服务
  • 基于 Pull 模式只会消费落后,然后在可能的时候赶上,目标为:最终一致性

  • 基于 Pull 模式的另一个好处是,便于对消息进行批量处理

    • 基于推送的模式,由于不知道消费者是否能够立即处理
    • 推送模式一般是主动延迟发送,用于累积消息
    • 延迟高了消息不及时,延迟低了发送频率过快
    • 而基于 Pull 模式可以总是在当前位置之后拉动所有可用消息(或者拉取配置的最多消息数)
    • 基于 Pull 模式不需要主动延迟
  • 基于 Pull 模式的缺点是,如果代理中没有数据,Pull 请求会一直空跑

    • Kafka 通过在 Pull 请求中设置参数,允许长轮询 “long pull”
    • 长轮询会阻塞,等待数据达到,或等待指定消息数全部到达后返回(批量处理)
  • "store-and-forward" 一种经常被提到的设计方案为存储转发

    • 存储转发模型

      • 信息被发送到保存信息的中间站,并在稍后发送到最终目的地或另一个中间站
      • 在转发消息之前验证消息的完整性
      • 在传输延迟时间长、错误率可变且高的情况下,或者如果没有直接的端到端连接时
    • 但 Kafka 考虑的是有数千个生产者的目标用例

      • 在数千个磁盘上做存储转发会是一个噩梦
    • 所以 Kafka 不做大规模存储转发,而是选择 SLA

      • SLA:即构建生产者和消费者之间高效的,高可用的传输协议,而不是考虑数据冗余

检查点(Consumer Point)

跟踪消费者消费了哪些消息(检查点),是消息传递系统的关键性能点之一。

大多数消息传递系统都使用 Broker 来存储检查点。也就是说,当消息分发时,或者消费者确认收到消息时,Broker 需要在本地持久化检查点。这种做法对于点对点传输没啥问题。对于消费者较多的情况下,大多数消息传递系统的检查点设计的异常简单,甚至可以删除哪些过期的检查点,以此来保证性能。

也许大家没有意识到,检查点的同步问题并不像看起来那么简单直白。为了防止网络原因或者消费者崩溃造成的数据丢失。消息传递系统一般会添加一个 Ack 机制。在收到 ack 消息后,状态由 sent 改为 consumed 。这会产生两个问题:

  • 确认消息丢失导致的消息重复消费问题

  • 性能问题,每条消息都需要写两次状态

    • Broker 首先锁定消息,防止消息重复发出,收到 ack 后标记为已完成,可删除

Kafka 使用了不同的检查点机制

  • 每个主题被划分为一组完全有序的分区

  • 每个分区在任何给定的时间都被每个订阅组的一个客户端使用

  • 即每个分区中,每个客户端的检查点其实使用 整数 (要消费的下一条消息的偏移量)

    • 每个分区只记录一个数字,最简单的实现,可频繁的做定期检查
  • 整个整数使得客户端可以“倒带”

    • 这使得消息不再是按队列依次消费的
    • 但是在实际使用时非常有用
    • 栗如:消费者代码有个 BUG,在消费后才发现,消费者可以修复 BUG 后,倒带重新消费

离线数据加载