Kafka设计核心架构点解析

107 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

本文适用了解并使用过Kakfa非初学者,主要总结下Kafka架构设计上的一些核心点。

Kafka定位:解耦上下游服务,扩展性更强的分布式消息中间件
上游服务(Producer侧服务)通知下游服务(Consumer侧服务)消息场景,若上游服务程序代码中侵入式调用下游服务接口,上下游耦合大,且后续需要通知更多下游扩展性差,上游代码仍需增加更多下游调用。

有了Kafka中间件后,上游只需通过Kafka Producer将消息发送Kafka Broker,不用负责后续具体投递工作,解耦下游。下游通过Kafka Consumer按需消费,自由扩展。但注意由于通知过程解耦,过程也由同步调用变为异步消息,Kafka不保证消息可靠性,可能会丢失消息。适合高吞吐量大数据,日志处理等离线非可靠业务场景。

1、如何提高消息生产效率和吞吐量?

负载均衡

生产者直接将数据发送到Leader分区所在Broker,没有任何中间的路由层。为了帮助生产者做到这一点,所有Kafka节点都可以回答关于哪些服务器处于活动的状态以及Topic分区的领导者在任何给定时间的位置的元数据请求,以允许生产者任何时间直接请求。

选择向哪个分区发布消息。可以实现一种随机负载平衡,也可以通过一些语义分区功能来完成。我们通过允许用户指定分区键并使用此键Hash到特定分区。例如,如果选择的键是用户ID,那么给定用户的所有数据都将发送到同一个分区,实现分区内消息有序。

异步发送

批处理是效率的主要驱动力之一,为了启用批处理,Kafka生产者将尝试在内存中积累数据,并在单个请求中发送更大的批处理。批处理可以配置为积累不超过固定数量的消息,并且等待时间不超过某些固定延迟限制(例如64k或10毫秒)。这允许积累更多的字节来发送,并且在服务器上进行很少的大型I/O操作。这种缓冲是可配置的,并提供了一种机制来权衡少量额外的延迟以获得更好的吞吐量。

2、如何实现高吞吐且低延时的持久化?

使用内存持久化?Kafka源码是Java写的,程序建立于JVM之上,存储大吞吐消息时会遇到:

  • 对象的内存开销非常高,通常使存储(或更糟)的数据大小增加一倍。
  • 随着内存数据的增加,Java垃圾收集变得越来越烦人和缓慢。

所以思路便扩展到磁盘上,人们普遍认为“磁盘很慢”,但实际上,磁盘既慢,又比人们预期的要快得多。设计正确的磁盘结构通常与网络一样快(这点值得我们学习,有时摒弃常识和偏见,不是别人不好,可能是你打开姿势不对)。我们使用磁盘多是随机写入的性能仅为100k/sec,但如果顺序写性能可达到600mb/sec,差异超过6000倍。因为顺序读取和写入是所有使用模式中最可预测的,所以操作系统会进行了大量优化。 kafka就是顺序写磁盘,充分利用page cache以获得低时延和吞吐。kafka的存储模型为每个partition都由若干segment(物理文件)组成的逻辑文件。

3、如何提高消息消费效率并可水平扩展?

分区是针对Consumer并发且可水平扩展来消费设计的,消息传递系统的关键性能点之一是跟踪已消费过的内容。 大多数消息传递系统保留了在Broker上消费了哪些消息的元数据。也就是说,当消息被传递给消费者时,Broker要么立即在本地记录该事实,要么等待消费者的确认。但是让Broker和消费者就已经消费的内容达成一致不是一个小问题。如果Broker在每次通过网络传递消息时都将消息记录为立即消费,那么如果消费者未能处理消息(比如因为它崩溃或请求超时或其他原因),该消息将丢失。为了解决这个问题,许多消息传递系统添加了确认特征,这意味着消息在发送时只被标记为已发送而不是消费;Broker等待消费者的特定确认来记录消息消费。这种策略解决了丢失消息的问题,但也产生了新的问题。首先,如果消费者处理消息但在发送确认之前失败,那么消息将被消费两次。第二个问题与性能有关,现在Broker必须保持每个消息的多个状态(首先锁定它,这样它就不会第二次发出,然后将其标记为永久消费,这样它就可以被删除)。必须处理棘手的问题,比如如何处理已发送但从未确认的消息。

Kafka以不同的方式处理这个问题。Topic被分成一组完全有序的分区(Partition),这种分布式放置对于可扩展性非常重要,因为它允许客户端(Consumer和Producer)同时对多个Broker读取和写入数据。当一个新事件发布到一个Topic时,它实际上被附加到该Topic的一个分区中。具有相同事件键(例如,客户或车辆ID)的事件被写入同一个分区,Kafka保证给定分区的任何消费者将始终以与写入时完全相同的顺序读取该分区的事件(分区内有序)。

每个分区在任何给定时间都被每个订阅消费者组中的一个消费者消费。这意味着每个分区中消费者的位置只是一个整数,即下一个要消费的消息的偏移量。这使得关于已消费内容的状态非常小,每个分区只有一个数字。这种状态可以定期检查点。这使得相当于消息确认的东西非常便宜。

这种判定还有一个附带的好处。消费者可以故意倒带回旧的偏移量并重新消费数据。例如,如果消费者代码有一个bug,并且在一些消息被消费后被发现,一旦bug被修复,消费者就可以重新消费这些消息。

image.png

4、如何实现系统高可用?

image.png Replication&Cluster:部署多台Broker,消息根据Partition在不同机器之间备份。

复制的单位是Topic分区。在非故障条件下,Kafka中的每个分区都有一个Leader和零个或多个Followers。包括Leader在内的副本总数构成复制因子。所有写入都转到分区的Leader,读取可以转到Leader或分区的Followers。通常,分区比Broker多得多,Leader在Broker中均匀分布。Followers上的日志与Leader的日志相同——都有相同的偏移量和相同顺序的消息(当然,在任何给定时间,Leader可能在其日志末尾有一些尚未复制的消息)。

Followers使用来自Leader的消息,就像普通的Kafka消费者一样,并将它们应用到自己的日志中。让Followers从Leader那里拉取有一个很好的特性,那就是允许Followers自然批量组合即将导入到自己日志中的日志条目。

与大多数分布式系统一样,自动处理故障需要精确定义节点“活着”意味着什么。在Kafka中,一个被称为“控制器”的特殊节点负责管理集群中Brokers的注册。Broker活跃有两个条件:

  • Brokers必须保持与控制器的活跃会话,以便接收定期的元数据更新。
  • Brokers中Followers必须复制Leader的写入,不要“落后太远”。
    “活跃会话”的含义取决于集群配置。对于KRaft集群,活动会话是通过向控制器发送周期性心跳来维护的。如果控制器在broker.session.timeout.ms配置的超时到期前未能接收到心跳,则节点被视为离线。