Kafka

Kafka 是由 Linkedin 公司开发的,它是一个分布式的,支持多分区、多副本,基于 Zookeeper 的分布式消息流平台,它同时也是一款开源的基于发布订阅模式的消息引擎系统

一 消息队列介绍

1. Kafka 的基本术语

  • 消息:Kafka 中的数据单元被称为消息,也被称为记录,可以把它看作数据库表中某一行的记录。
  • 批次:为了提高效率, 消息会分批次写入 Kafka,批次就代指的是一组消息。
  • 主题:消息的种类称为 主题(Topic),可以说一个主题代表了一类消息。相当于是对消息进行分类。主题就像是数据库中的表。
  • 分区:主题可以被分为若干个分区(partition),同一个主题中的分区可以不在一个机器上,有可能会部署在多个机器上,由此来实现 kafka 的伸缩性,单一主题中的分区有序,但是无法保证主题中所有的分区有序
  • 偏移量:偏移量(Consumer Offset)是一种元数据,它是一个不断递增的整数值,用来记录消费者发生重平衡时的位置,以便用来恢复数据。
  • broker: 一个独立的 Kafka 服务器就被称为 broker,broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。
  • broker 集群:broker 是集群 的组成部分,broker 集群由一个或多个 broker 组成,每个集群都有一个 broker 同时充当了集群控制器的角色(自动从集群的活跃成员中选举出来)。
  • 副本:Kafka 中消息的备份又叫做 副本(Replica),副本的数量是可以配置的,Kafka 定义了两类副本:领导者副本(Leader Replica) 和 追随者副本(Follower Replica),前者对外提供服务,后者只是被动跟随。
  • 重平衡:Rebalance。消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。Rebalance 是 Kafka 消费者端实现高可用的重要手段。

2. Kafka 的特性(设计原则)

  • 高吞吐、低延迟:kakfa 最大的特点就是收发消息非常快,kafka 每秒可以处理几十万条消息,它的最低延迟只有几毫秒。
  • 高伸缩性:每个主题(topic) 包含多个分区(partition),主题中的分区可以分布在不同的主机(broker)中。
  • 持久性、可靠性:Kafka 能够允许数据的持久化存储,消息被持久化到磁盘,并支持数据备份防止数据丢失,Kafka 底层的数据存储是基于 Zookeeper 存储的,Zookeeper 我们知道它的数据能够持久存储。
  • 容错性:允许集群中的节点失败,某个节点宕机,Kafka 集群能够正常工作
  • 高并发:支持数千个客户端同时读写

3. Kafka 的使用场景

  • 活动跟踪:Kafka 可以用来跟踪用户行为,比如我们经常回去淘宝购物,你打开淘宝的那一刻,你的登陆信息,登陆次数都会作为消息传输到 Kafka ,当你浏览购物的时候,你的浏览信息,你的搜索指数,你的购物爱好都会作为一个个消息传递给 Kafka ,这样就可以生成报告,可以做智能推荐,购买喜好等。
  • 传递消息:Kafka 另外一个基本用途是传递消息,应用程序向用户发送通知就是通过传递消息来实现的,这些应用组件可以生成消息,而不需要关心消息的格式,也不需要关心消息是如何发送的。
  • 度量指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。
  • 日志记录:Kafka 的基本概念来源于提交日志,比如我们可以把数据库的更新发送到 Kafka 上,用来记录数据库的更新时间,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、Hbase、Solr等。
  • 流式处理:流式处理是有一个能够提供多种应用程序的领域。
  • 限流削峰:Kafka 多用于互联网领域某一时刻请求特别多的情况下,可以把请求写入Kafka 中,避免直接请求后端程序导致服务崩溃。

4. Kafka 系统架构

img

5. 核心 API

Kafka 有四个核心API,它们分别是

  • Producer API,它允许应用程序向一个或多个 topics 上发送消息记录
  • Consumer API,允许应用程序订阅一个或多个 topics 并处理为其生成的记录流
  • Streams API,它允许应用程序作为流处理器,从一个或多个主题中消费输入流并为其生成输出流,有效的将输入流转换为输出流。
  • Connector API,它允许构建和运行将 Kafka 主题连接到现有应用程序或数据系统的可用生产者和消费者。例如,关系数据库的连接器可能会捕获对表的所有更改

img

6. Kafka 为何如此之快

Kafka 实现了零拷贝原理来快速移动数据,避免了内核之间的切换。Kafka 可以将数据记录分批发送,从生产者到文件系统(Kafka 主题日志)到消费者,可以端到端的查看这些批次的数据。

批处理能够进行更有效的数据压缩并减少 I/O 延迟,Kafka 采取顺序写入磁盘的方式,避免了随机磁盘寻址的浪费,更多关于磁盘寻址的了解,请参阅 程序员需要了解的硬核知识之磁盘

总结一下其实就是四个要点

  • 顺序读写
  • 零拷贝
  • 消息压缩
  • 分批发送

二. 安装 Kafka

2.1 docker安装

dockercompose.yaml

services:
kafka:
image: 'bitnami/kafka:3.6.0'
ports:
- '9092:9092'
- '9094:9094'
environment:
- KAFKA_CFG_NODE_ID=0
# - 允许自动创建 topic,线上不要开启
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true
- KAFKA_CFG_PROCESS_ROLES=controller,broker
- KAFKA_CFG_LISTENERS=PLAINTEXT://0.0.0.0:9092,CONTROLLER://:9093,EXTERNAL://0.0.0.0:9094
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,EXTERNAL://localhost:9094
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:9093
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER

三. go操作kafka

3.1 安装sarama

go get github.com/Shopify/sarama

3.2 生产者

3.2.1 同步

package main
import (
"fmt"
"github.com/Shopify/sarama"
)
// 基于sarama第三方库开发的kafka client
func main() {
config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForAll // 发送完数据需要leader和follow都确认
config.Producer.Partitioner = sarama.NewRandomPartitioner // 新选出一个partition
config.Producer.Return.Successes = true // 成功交付的消息将在success channel返回
// 构造一个消息
msg := &sarama.ProducerMessage{}
msg.Topic = "web_log"
msg.Value = sarama.StringEncoder("this is a test log")
// 连接kafka
client, err := sarama.NewSyncProducer([]string{"127.0.0.1:9092"}, config)
if err != nil {
fmt.Println("producer closed, err:", err)
return
}
defer client.Close()
// 发送消息
pid, offset, err := client.SendMessage(msg)
if err != nil {
fmt.Println("send msg failed, err:", err)
return
}
fmt.Printf("pid:%v offset:%v\n", pid, offset)
}

3.2.1 异步

package kafka
import (
"github.com/IBM/sarama"
"github.com/stretchr/testify/assert"
"fmt"
)
func main() {
cfg := sarama.NewConfig()
cfg.Producer.RequiredAcks = sarama.NoResponse
cfg.Producer.Return.Successes = true
producer, err := sarama.NewAsyncProducer([]string{"localhost:9092"}, cfg)
messages := producer.Input()
messages <- &sarama.ProducerMessage{
Topic: "test",
Value: sarama.StringEncoder("this is producer send message"),
}
select {
case <-producer.Successes():
fmt.Printf("消息发送成功")
case <-producer.Errors():
fmt.Printf("发送消息失败")
default:
fmt.Printf("panic")
}
}

3.3 消费者

3.3.1 自动提交位点

自动提交位点:消费者在拉取消息后会自动提交位点,无需手动操作。这种方式的优点是简单易用,但是可能会导致消息重复消费或丢失。

package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"sync"
"time"
"github.com/Shopify/sarama"
)
func main() {
config := sarama.NewConfig()
config.Version = sarama.V2_1_0_0
config.Consumer.Offsets.Initial = sarama.OffsetOldest
config.Consumer.Offsets.AutoCommit.Enable = true
config.Consumer.Offsets.AutoCommit.Interval = 1 * time.Second
brokers := []string{"localhost:9092"}
topic := "test-topic"
client, err := sarama.NewConsumerGroup(brokers, "test-group", config)
if err != nil {
log.Fatalf("unable to create kafka consumer group: %v", err)
}
defer client.Close()
ctx, cancel := context.WithCancel(context.Background())
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for {
err := client.Consume(ctx, []string{topic}, &consumerHandler{})
if err != nil {
log.Printf("consume error: %v", err)
}
select {
case <-signals:
cancel()
return
default:
}
}
}()
wg.Wait()
}
type consumerHandler struct{}
func (h *consumerHandler) Setup(sarama.ConsumerGroupSession) error {
return nil
}
func (h *consumerHandler) Cleanup(sarama.ConsumerGroupSession) error {
return nil
}
func (h *consumerHandler) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for msg := range claim.Messages() {
fmt.Printf("Received message: key=%s, value=%s, partition=%d, offset=%d\n", string(msg.Key), string(msg.Value), msg.Partition, msg.Offset)
sess.MarkMessage(msg, "")
}
return nil
}

3.3.2 手动提交位点

手动提交位点:消费者在处理完消息后需要手动提交位点。这种方式的优点是可以精确控制位点的提交,避免消息重复消费或丢失。但是需要注意,手动提交位点如果太频繁会导致 Broker CPU 很高,影响性能,随着消息量增加,CPU 消费会很高,影响正常 Broker 的其他功能,因此建议间隔一定消息提交位点。

package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"sync"
"github.com/Shopify/sarama"
)
func main() {
config := sarama.NewConfig()
config.Version = sarama.V2_1_0_0
config.Consumer.Offsets.Initial = sarama.OffsetOldest
config.Consumer.Offsets.AutoCommit.Enable = false
brokers := []string{"localhost:9092"}
topic := "test-topic"
client, err := sarama.NewConsumerGroup(brokers, "test-group", config)
if err != nil {
log.Fatalf("unable to create kafka consumer group: %v", err)
}
defer client.Close()
ctx, cancel := context.WithCancel(context.Background())
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for {
err := client.Consume(ctx, []string{topic}, &consumerHandler{})
if err != nil {
log.Printf("consume error: %v", err)
}
select {
case <-signals:
cancel()
return
default:
}
}
}()
wg.Wait()
}
type consumerHandler struct{}
func (h *consumerHandler) Setup(sarama.ConsumerGroupSession) error {
return nil
}
func (h *consumerHandler) Cleanup(sarama.ConsumerGroupSession) error {
return nil
}
func (h *consumerHandler) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for msg := range claim.Messages() {
fmt.Printf("Received message: key=%s, value=%s, partition=%d, offset=%d\n", string(msg.Key), string(msg.Value), msg.Partition, msg.Offset)
sess.MarkMessage(msg, "")
sess.Commit()
}
return nil
}

3.4 生产者参数与调优

3.4.1 生产者参数

在使用 Sarama go 客户端写入 kafka 时候,需要配置如下关键参数,相关的参数和默认值如下:

config := sarama.NewConfig()
sarama.MaxRequestSize = 100 * 1024 * 1024 //请求最大大小,默认100MB,可以调整,写入大于100MB的消息会直接报错
sarama.MaxResponseSize = 100 * 1024 * 1024 //响应最大大小,默认100MB,可以调整,获取大于100MB的消息会直接报错
config.Producer.RequiredAcks = sarama.WaitForLocal // 默认值为sarama.WaitForLocal(1)
config.Producer.Retry.Max = 3 // 生产者重试的最大次数,默认为3
config.Producer.Retry.Backoff = 100 * time.Millisecond // 生产者重试之间的等待时间,默认为100毫秒
config.Producer.Return.Successes = false //是否返回成功的消息,默认为false
config.Producer.Return.Errors = true // 返回失败的消息,默认值为true
config.Producer.Compression = CompressionNone //对消息是否压缩后发送,默认CompressionNone不压缩
config.Producer.CompressionLevel = CompressionLevelDefault // 指定压缩等级,在配置了压缩算法后生效
config.Producer.Flush.Frequency = 0 //producer缓存消息的时间, 默认缓存0毫秒
config.Producer.Flush.Bytes = 0 // 达到多少字节时,触发一次broker请求,默认为0,直接发送,存在天然上限值MaxRequestSize,因此默认最大100MB
config.Producer.Flush.Messages = 0 // 达到多少条消息时,强制,触发一次broker请求,这个是上限值,MaxMessages < Messages
config.Producer.Flush.MaxMessages = 0 // 最大缓存多少消息,默认为0,有消息立刻发送,MaxMessages设置大于0时,必须设置 Messages,且需要保证:MaxMessages > Messages
config.Producer.Timeout = 5 * time.Second // 超时时间
config.Producer.Idempotent = false //是否需要幂等,默认false
config.Producer.Transaction.Timeout = 1 * time.Minute // 事务超时时间默认1分钟
config.Producer.Transaction.Retry.Max = 50 //事务重试时间
config.Producer.Transaction.Retry.Backoff = 100 * time.Millisecond
config.Net.MaxOpenRequests = 5 //默认值5,一次发送请求的数量
config.Producer.Transaction.ID = "test" //事务ID
config.ClientID = "your-client-id" // 客户端ID

3.4.2 版本选择

在选择 Sarama 客户端版本时,需要确保所选版本与 Kafka broker 版本兼容。Sarama 库支持多个 Kafka 协议版本,可以通过设置 config.Version 来指定使用的协议版本。

config := sarama.NewConfig()
config.Version = sarama.V2_8_2_0

3.4.3 参数说明调优

3.4.3.1 关于 RequiredAcks 参数优化

RequiredAcks 参数用于控制生产者发送消息时的确认机制。该参数的默认值为 WaitForLocal,表示消息发送给 Leader Broker 后,Leader 确认消息写入后即返回。RequiredAcks 参数还有以下可选值:

  • NoResponse: 不等待任何确认,直接返回。
  • WaitForLocal: 等待 Leader 副本确认写入后返回。
  • WaitForAll: 等待 Leader 副本以及相关的 Follower 副本确认写入后返回。

由上可知,在跨可用区场景,以及副本数较多的 Topic,RequiredAcks 参数的取值会影响消息的可靠性和吞吐量。因此:

在一些在线业务消息的场景下,吞吐量要求不大,可以将 RequiredAcks 参数设置为 WaitForAll,则可以确保消息被所有副本接收和确认后才返回,从而提高消息的可靠性。

在日志采集等大数据或者离线计算的场景下,要求高吞吐(即每秒写入 Kafka 的数据量)的情况下,可以将 RequiredAcks 设置为 WaitForLocal,提高吞吐。

3.4.3.2 关于 Flush 参数优化(缓存)

默认情况下,传输同等数据量的情况下,多次请求和一次请求的网络传输,一次请求传输能有效减少相关计算和网络资源,提高整体写入的吞吐量。因此,可以通过这个参数设置优化客户端发送消息的吞吐能力。在高吞吐场景下,可以配合计算和设置: 其中 Bytes 建议设置为16K,对齐 Kafka 标准 Java SDK 定义, 预估一条消息为1K(1024)个字节,因此得出如下 Messages 和 MaxMessages 的写入参数: 其中 Frequency 的计算方式为:预估流量为 16MB,分区数为16个分区,此时单分区每秒写入流量为:16 * 1024 * 1024 / 16 = 1 * 1024 *** 1024 = 1MB,单个分区每秒 1MB 的流量。假设按照 16K 一个请求,数据量发送,那么在 1s 内要实现 1MB 的流量传输 110241024/16/1024 = 64个请求,因此 Frequency <= 15.62ms(1000/64)。 实际上,由于业务流量不是持续生产的,在低峰期,可能出现即时达到 16ms,也缓存不了太多的数据,因此在高吞吐的情况下,可以将条件简化,以 Bytes 为准,Frequency 可以适当调大,例如能接受 500ms 的延时增加,那么就可以设置为 500ms,因为此时如果命中数据量大于等于 Bytes,会按照 Bytes 的条件发送请求。

config.Producer.Flush.Frequency = 16 //producer缓存消息的时间, 默认缓存100毫秒,如果发送的流量较小,这里可以进一步增加延时时间。
config.Producer.Flush.Bytes = 16*1024 // 达到多少字节时,触发一次broker请求,默认为0,直接发送,存在天然上限值MaxRequestSize,因此默认最大100MB
config.Producer.Flush.Messages = 17 // 达到多少条消息时,强制,触发一次broker请求,这个是上限值,MaxMessages 需要小于 Messages
config.Producer.Flush.MaxMessages = 16 // 16条,实际上因为消息大小不严格1024字节,Messages和MaxMessages 建议配置值更大或者直接使用Int的最大值,
//因为命中Frequency,Bytes,MaxMessages < Messages任何一个条件都会触发flush

3.4.3.3 关于事务参数优化

config.Producer.Idempotent = true //是否需要幂等,在事务场景下需要设置为true
config.Producer.Transaction.Timeout = 1 * time.Minute // 事务超时时间默认1分钟
config.Producer.Transaction.Retry.Max = 50 //事务重试时间
config.Producer.Transaction.Retry.Backoff = 100 * time.Millisecond
config.Net.MaxOpenRequests = 5 //默认值5,一次发送请求的数量
config.Producer.Transaction.ID = "test" //事务ID

需要强调,事务因为要保障消息的 exactly once 语义,因此会额外付出更多的计算资源,所以 config.Net.MaxOpenRequests 的选取必须小于等于5,Broker 端的 ProducerStateManager 实例会缓存每个 PID 在每个 Topic-Partition 上发送的最近 5 个 batch 数据,如果客户在事务的基础上还需要保持一定的吞吐,因此可以设置该值为5,同时适当增加事务超时时间,容忍高负载下一些网络抖动带来的时延问题。

3.4.3.4 关于压缩参数优化

Sarama Go 支持如下压缩参数:

config.Producer.Compression = CompressionNone //对消息是否压缩后发送,默认CompressionNone不压缩
config.Producer.CompressionLevel = CompressionLevelDefault //指定压缩等级,在配置了压缩算法后生效

在Sarama Kafka Go客户端中,支持以下几种压缩配置:

  1. sarama.CompressionNone:不使用压缩。
  2. sarama.CompressionGZIP:使用 GZIP 压缩.
  3. sarama.CompressionSnappy:使用 Snappy 压缩。
  4. sarama.CompressionLZ4:使用 LZ4 压缩。
  5. sarama.CompressionZSTD:使用 ZSTD 压缩。

要在 Sarama Kafka Go 客户端中使用压缩消息,需要在创建生产者时设置 config.Producer.Compression 参数。例如,要使用 LZ4 压缩算法,可以将config.Producer.Compression 设置为 sarama.CompressionLZ4 ,虽然压缩消息的压缩和解压缩,发生客户端,是一种用计算换带宽的优化方式,但是由于Broker 针对压缩消息存在校验行为会付出额外的计算成本,尤其是 gzip 压缩,Broker 对其校验计算成本会比较大,在某种程度上可能会出现得不偿失的情况,反而因为计算的增加导致Broker消息处理能力偏低,导致带宽吞吐更低。在低吞吐或者低规格服务下,不建议使用压缩消息。如果还是需要压缩消息,这种情况建议可以使用如下方式进行使用:

  1. 在 Producer 端对消息数据独立压缩,生成压缩包数据:messageCompression,同时在消息的 key 存储压缩方式:

    {"Compression","CompressionLZ4"}
  2. 在Producer端将messageCompression当成正常消息发送。

  3. 在 Consumer 端读取消息key,获取使用的压缩方式,独立进行解压缩。

3.5 消费者参数与调优

3.5.1 消费者参数

在使用 Sarama go 客户端消费 kafka 时候,需要配置如下关键参数,相关的参数和默认值如下:

config := sarama.NewConfig()
config.Consumer.Group.Rebalance.Strategy = sarama.NewBalanceStrategyRange //消费者分配分区的默认方式
config.Consumer.Offsets.Initial = sarama.OffsetNewest //在没有提交位点情况下,使用最新的位点还是最老的位点,默认是最新的消息位点
config.Consumer.Offsets.AutoCommit.Enable = true //是否支持自动提交位点,默认支持
config.Consumer.Offsets.AutoCommit.Interval = 1 * time.Second //自动提交位点时间间隔,默认1s
config.Consumer.MaxWaitTime = 250 * time.Millisecond //在没有最新消费消息时候,客户端等待的时间,默认250ms
config.Consumer.MaxProcessingTime = 100 * time.Millisecond
config.Consumer.Fetch.Min = 1 //消费请求中获取的最小消息字节数,Broker将等待至少这么多字节的消息然后返回。默认值为1,不能设置0,因为0会导致在没有消息可用时消费者空转。
config.Consumer.Fetch.Max = 0 //消费请求最大的字节数。默认为0,表示不限制
config.Consumer.Fetch.Default = 1024 * 1024 //消费请求的默认消息字节数(默认为1MB),需要大于实例的大部分消息,否则Broker会花费大量时间计算消费数据是否达到这个值的条件
config.Consumer.Return.Errors = true
config.Consumer.Group.Rebalance.Strategy = sarama.NewBalanceStrategyRange // 设置消费者组在进行rebalance时所使用的策略为NewBalanceStrategyRange,默认NewBalanceStrategyRange
config.Consumer.Group.Rebalance.Timeout = 60 * time.Second // 设置rebalance操作的超时时间,默认60s
config.Consumer.Group.Session.Timeout = 10 * time.Second // 设置消费者组会话的超时时间为,默认为10s
config.Consumer.Group.Heartbeat.Interval = 3 * time.Second // 心跳超时时间,默认为3s
config.Consumer.MaxProcessingTime = 100 * time.Millisecond //消息处理的超时时间,默认100ms,

3.5.2 版本选择

在选择 Sarama 客户端版本时,需要确保所选版本与 Kafka broker 版本兼容。Sarama 库支持多个 Kafka 协议版本,可以通过设置 config.Version 来指定使用的协议版本。

config := sarama.NewConfig()
config.Version = sarama.V2_8_2_0

3.5.3 参数说明与调优

一般消费主要是rebalance时间频繁和消费线程阻塞问题,参考以下说明参数优化:

  • config.Consumer.Group.Session.Timeout:v0.10.2之前的版本可适当提高该参数值,需要大于消费一批数据的时间,但不要超过30s,建议设置为25s;而v0.10.2及其之后的版本,保持默认值10s 即可。
  • config.Consumer.Group.Heartbeat.Interval:默认3s,设置该值 需要小于Consumer.Group.Session.Timeout/3。
  • config.Consumer.Group.Rebalance.Timeout:默认60s,如果分区数和消费者较多,建议适当调大该值。
  • config.Consumer.MaxProcessingTime:该值要大于<max.poll.records> / (<单个线程每秒消费的条数> * <消费线程的个数>)的值。
posted @   Gentry-Yang  阅读(23)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
点击右上角即可分享
微信分享提示