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 系统架构
5. 核心 API
Kafka 有四个核心API,它们分别是
- Producer API,它允许应用程序向一个或多个 topics 上发送消息记录
- Consumer API,允许应用程序订阅一个或多个 topics 并处理为其生成的记录流
- Streams API,它允许应用程序作为流处理器,从一个或多个主题中消费输入流并为其生成输出流,有效的将输入流转换为输出流。
- Connector API,它允许构建和运行将 Kafka 主题连接到现有应用程序或数据系统的可用生产者和消费者。例如,关系数据库的连接器可能会捕获对表的所有更改
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客户端中,支持以下几种压缩配置:
- sarama.CompressionNone:不使用压缩。
- sarama.CompressionGZIP:使用 GZIP 压缩.
- sarama.CompressionSnappy:使用 Snappy 压缩。
- sarama.CompressionLZ4:使用 LZ4 压缩。
- sarama.CompressionZSTD:使用 ZSTD 压缩。
要在 Sarama Kafka Go 客户端中使用压缩消息,需要在创建生产者时设置 config.Producer.Compression 参数。例如,要使用 LZ4 压缩算法,可以将config.Producer.Compression 设置为 sarama.CompressionLZ4 ,虽然压缩消息的压缩和解压缩,发生客户端,是一种用计算换带宽的优化方式,但是由于Broker 针对压缩消息存在校验行为会付出额外的计算成本,尤其是 gzip 压缩,Broker 对其校验计算成本会比较大,在某种程度上可能会出现得不偿失的情况,反而因为计算的增加导致Broker消息处理能力偏低,导致带宽吞吐更低。在低吞吐或者低规格服务下,不建议使用压缩消息。如果还是需要压缩消息,这种情况建议可以使用如下方式进行使用:
-
在 Producer 端对消息数据独立压缩,生成压缩包数据:messageCompression,同时在消息的 key 存储压缩方式:
{"Compression","CompressionLZ4"} -
在Producer端将messageCompression当成正常消息发送。
-
在 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> / (<单个线程每秒消费的条数> * <消费线程的个数>)的值。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!