Kafka 架构和设计原则

Kafka 架构和设计原则,第1张

Kafka 架构和设计原则

http://notes.stephenholiday.com/Kafka.pdf

首先我们介绍 Kafka 的基本概念。

topic: 把特定类型的消息流称为 topicproducer: 生产者能把消息发到 topicbroker:被发布的消息会保存在一些服务器,这些服务器称为 brokersconsumer: 消费能从 broker 订阅一个或多个 topic,并且能从 broker 拉取并处理消息

消息传送就是这么简单,Kafka API 会反映这一简单性。下面展示了生产者发送消息的伪代码。一则消息被定义为一串字节。用户可以自定义编码方式,将消息转成字节数组然后发送出去。而且支持在一次请求中发送多个消息。

Sample producer code:

 producer = new Producer(…);
 message = new Message(“test message str”.getBytes());
 set = new MessageSet(message);
 producer.send(“topic1”, set);

Sample consumer code:

 streams[] = Consumer.createMessageStreams(“topic1”, 1)
 for (message : streams[0]) {
 bytes = message.payload();
 // do something with the bytes
 }

消费者基于 topic 创建若干个消息流,发送到 topic 的消息最终被到这些消息流。
关于分发的细节将会在 3.2 节介绍。 每个消息流提供了迭代器,消费者通过迭代器来获取并处理每条消息的内容。迭代的过程是不会终止的。如果流里面没有消息,轮询就被阻塞,直到新的消息到达。我们支持集群消费,多个消费者共享一个 topic 的消息,也支持广播消费,每个消费者都消费 topic 的全量消息。
( We support both the point-to-point delivery model in which multiple consumers jointly consume a single copy of all messages in a topic, as well as the publish/subscribe model in which multiple consumers each retrieve its own copy of a topic. )

图一展示了 Kafka 的架构。Kafka 天生是分布式的,一个 kafka 集群拥有多个 brokers 。为了负载均衡,每个 topic 分为多个分区,每个 broker 存储一到多个这些分区。多个 producers 和 consumers 同时发送消息和拉取消息。

在 3.1 节,我们会介绍一个 broker 上的某个分区,分享一些提高分区性能 (partition efficient) 的设计思路。在 3.2 节,我们阐述多个 producers 和 consumers 在分布式环境下如何与 brokers 交互。在 3.3 节,我们讨论 kafka 的消息传递的可靠性。

3.1 单个分区的性能

我们做了一些决定来提高分区的性能。

简化存储(Simple storage):

kafka 使用了非常简单的存储机制。topic 下的每个分区对应一份逻辑日志。一份日志在物理上用多个文件段来实现,每个文件的大小基本相同,比如 1 G。每当生产者发布一条消息到分区,broker 只是把消息追加到最后一个文件段的末尾。为了更好的性能,当消息达到一定数量,或者经过一定时间才会保存到磁盘。一则消息只有当刷盘之后才会暴露给消费者。

与大多数消息系统不同,kafka 的消息没有使用 message id 来标识,而是利用在 log 文件中的逻辑偏移量(offset)来定位。避免维护一个复杂的、随机访问的、用 message id 查找 message 实际地址的映射机制。注意我们的 message id 是递增的但不一定连续。为了计算下一条消息的id,我们需要记录当前消息的长度。从现在开始,我们说的 message id 和 offsets 是等价的。(译者注:kafka 的 offset 相当于 message id,一般说 offset 更多一些)。

一个消费者实例通常从分区顺序消费消息。如果消费者提交了某条消息的 offset(acknowledges a particular message offset),意味着在此 offset 之前的消息都被该消费者收到了。消费者向 brokers 发起拉取请求,获取一份可供消费的数据。每个拉取请求包含消费者当前的消费位移,以及可接受的字节数。每个 broker 在内存维护了一个排序列表,记录每个文件段第一条消息的位移。broker 通过遍历排序列表,在文件段上找到指定的消息,然后返回给消费者。消费者收到消息之后,计算出下一个待消费消息的偏移值,然后用于下一次拉取请求。图2 展示了 Kafka 日志的模式以及内存的排序列表,每个小格显示了消息位移:

高效传输(Efficient transfer):

我们非常关注 Kafka 的数据吞吐量。前文介绍了 producers 能够在一次请求中,提交多条消息。虽然终端的 consumers 一次只遍历一个消息流,但它在一次拉取请求,通常拉取特定大小的消息,一般是几百个kb。

另外一个比较不寻常的决定是,我们不把消息存储在 Kafka 应用层的内存,而是依赖文件系统的 page cache。带来的一个巨大的好处,在于避免维护两份缓存——消息只会存在 page cache。还有另外一个好处在于,即使 Kafka 崩溃重启,page cache 的缓存依然有效。又由于 Kafka 不会在自己的进程中持有消息内容,在基于虚拟机实现的程序上,只需要付出很少的 GC 开销。最后,producers 和 consumers 都是持续不断地访问文件段,通常 consumers 的进度会稍稍落后 producers ,因此 *** 作系统的高速缓存得到了高效利用(只需要写缓存和提前读)。我们看到,即使数据量线性增长到 TB 级别, producers 和 consumers 的表现依然强劲。

此外,我们还优化了 consumers 的网络访问。Kafka 是一个多消费者的系统,并且一条消息可以被不同的消费者分别消费。一个典型的从本地文件发送数据到远程套接字(remote socket)包括如下步骤:

    在 *** 作系统中读取文件到 page cache从 page cache 拷贝数据到应用程序的缓存从应用程序缓存拷贝到另一个内核缓冲区发送内核缓冲区到 socket

这里包括4次数据拷贝和2次系统调用。在 Linux 和其它类 Unix 系统,存在一个 sendfile API ,可以直接从文件通道传输到 socket 通道。这避免了步骤2和3的两次数据拷贝,和一次系统调用。Kafka 利用了这个 API 高效地把日志段的字节数据,从 broker 发送到 consumers。

无状态的 broker(Stateless broker):

与大多数其它消息系统不同,consumer 的消费状态并不是由 broker 维护。这样的设计极大地降低了 broker 的复杂度和负载。然而,这导致删除一条消息是比较难的,因为 broker 不知道它是否所有的 consumer 都消费了。Kafka 制定了一个基于时间的保留策略来解决该问题。如果一则消息保存在 broker 的时间超过一定周期,就会被删除,通常是 7 天。这种办法在实践中很有效。大多数 consumers,包括那些离线系统,都按天、按小时或者实时消费消息。Kafka 的性能不会随着数据量变大而下降,这一特性允许设置一个灵活的消息保留策略。

这个设计还带来另一个附加收益。一个 consumer 可以轻易把 offset 向前重置,并消费老的消息。这个特性似乎与队列的通常概念有所冲突,却是一个对于大多数消费者而言都很重要的特性。举例来说,当 consumer 的消费逻辑内部发生 error,修好 error 之后,它可以重新消费特定的消息。这对数据仓库来说很重要。另一个例子,consumers 可以对消息定期的持久化到磁盘里,或者一个全文搜索引擎。如果 consumers 挂了,未刷到磁盘的数据可能会丢。在这种场景,可以检查未被刷盘的数据的最小的 offset,并在 consumers 重启之后从该 offset 开始消费。我们还注意到,在拉模式下重置消费者,会比在推模式下重置消费者更容易实现。(译者注:consumers是从 broker 拉取消息,而不是 broker 推送)

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5707441.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存