一口气说出Kafka为啥这么快?( 三 )

  • 在 read() 方法返回之前 , 将数据从内核缓冲区复制到用户空间缓冲区 。 此时 , 我们的应用程序可以读取文件的内容了 。
  • 随后的 send() 方法将切回到内核态 , 将数据从用户空间缓冲区复制到内核地址空间——这一次是将数据复制到与目标套接字相关联的另一个缓冲区中 。 在后台 , 由 DMA 引擎接管 , 异步地将数据从内核缓冲区复制到协议栈 。 send() 方法在返回之前不会等待这个操作完成 。
  • send() 方法调用返回 , 切回用户态 。
  • 尽管用户态与内核态之间的上下文切换效率很低 , 而且还需要进行额外的复制 , 但在许多情况下 , 它可以提高性能 。
    它可以充当预读缓存 , 异步预读取 , 从而提前运行来自应用程序的请求 。 但是 , 当请求的数据量远远大于内核缓冲区的大小时 , 内核缓冲区就成为了性能瓶颈 。
    不同于直接复制数据 , 而是迫使系统在用户态和内核态之间频繁切换 , 直到所有数据都被传输 。
    相比之下 , 零拷贝方法是在单个操作中处理的 。 前面例子中的代码可以改写为一行代码:
    fileDesc.transferTo(offset, len, socket); 下面详细解释说明是零拷贝:
    一口气说出Kafka为啥这么快?文章插图
    在这个模型中 , 上下文切换的数量减少到一个 。 具体来说 , transferTo() 方法指示块设备通过 DMA 引擎将数据读入读缓冲区 。
    然后 , 将数据从读缓冲区复制到套接字缓冲区 。 最后 , 通过 DMA 将数据从套接字缓冲区复制到 NIC 缓冲区 。
    一口气说出Kafka为啥这么快?文章插图
    因此 , 我们将复制的数量从 4 个减少到 3 个 , 并且其中只有一个复制操作涉及到 CPU 。 我们还将上下文切换的数量从 4 个减少到 2 个 。
    这是一个巨大的改进 , 但还不是查询零拷贝 。 在运行 Linux 内核 2.4 或更高版本时 , 以及在支持 gather 操作的网卡上 , 可以进一步优化 。
    如下图所示:
    一口气说出Kafka为啥这么快?文章插图
    按照前面的示例 , 调用 transferTo() 方法会导致设备通过 DMA 引擎将数据读入内核缓冲区 。
    但是 , 对于 gather 操作 , 读缓冲区和套接字缓冲区之间不存在复制 。 相反 , NIC被赋予一个指向读缓冲区的指针 , 连同偏移量和长度 。 在任何情况下 , CPU 都不涉及复制缓冲区 。
    文件大小从几 MB 到 1GB 的范围内 , 传统拷贝和零拷贝相比 , 结果显示零拷贝的性能提高了两到三倍 。
    但更令人印象深刻的是 , Kafka 使用纯 JVM 实现了这一点 , 没有本地库或 JNI 代码 。
    避免垃圾回收
    大量使用通道、缓冲区和页面缓存还有一个额外的好处——减少垃圾收集器的工作负载 。
    例如 , 在 32 GB RAM 的机器上运行 Kafka 将产生 28-30 GB 的页面缓存可用空间 , 完全超出了垃圾收集器的范围 。
    吞吐量的差异非常小(大约几个百分点) , 但是经过正确调优的垃圾收集器的吞吐量可能非常高 , 特别是在处理短生存期对象时 。 真正的收益在于减少抖动 。
    通过避免垃圾回收 , 服务端不太可能遇到因垃圾回收引起的程序暂停 , 从而影响客户端 , 加大记录的通信延迟 。
    与初期的 Kafka 相比 , 现在避免垃圾回收已经不是什么问题了 。 像 Shenandoah 和 ZGC 这样的现代垃圾收集器可以扩展到巨大的、多 TB 级的堆 , 在最坏的情况下 , 并且可以自动调整垃圾收集的暂停时间 , 降到几毫秒 。
    现在 , 可以看见大量的基于 Java 虚拟机的应用程序使用堆缓存 , 而不是堆外缓存 。
    流处理的并行性
    日志的 I/O 效率是性能的一个重要方面 , 主要的性能影响在于写 。 Kafka 对主题结构和消费生态系统中的并行性处理是其读性能的基础 。
    这种组合产生了整体非常高的端到端消息吞吐量 。 将并发性深入到分区方案和使用者组的操作中 , 这实际上是 Kafka 中的一种负载均衡机制——将分区平均地分配到各个消费者中 。
    将此与传统的消息队列进行比较:在 RabbitMQ 的设置中 , 多个并发的消费者可以以轮询的方式从队列中读取数据 , 但这样做会丧失消息的有序性 。
    分区机制有利于 Kafka 服务端的水平扩展 。 每个分区都有一个专门的领导者 。 因此 , 任何重要的多分区的主题都可以利用整个服务端集群进行写操作 。
    这是 Kafka 和传统消息队列的另一个区别 。 当后者利用集群来提高可用性时 , Kafka 通过负载均衡来提高可用性、持久性和吞吐量 。