这是我参与「第五届青训营 」伴学笔记创作活动的第 11 天
今天和大家分享在青训营项目实战中对卡夫卡技术的探索笔记。(本篇文章主要参考打卡阅读活动文章和其他优秀kafka学习指南)
Kafka概念
Kafka 是分布式发布-订阅消息系统。它是一个分布式的,可划分的,冗余备份的持久性(存储在磁盘) 的日志服务。它主要用于处理活跃的流式数据。
Kafka特点
- 为发布和订阅提供高吞吐量。据了解,Kafka 每秒可以生产约25万消息(50 MB),每秒处理55万消息(110 MB)。
- 可进行持久化操作。将消息持久化到磁盘,因此可用于批量消费,例如ETL,以及实时应用程序。通过将数据持久化到硬盘以及 replication(复制) 防止数据丢失。
- 分布式系统,易于向外扩展。所有的 producer、broker 和 consumer 都会有多个,均为分布式的。无需停机即可扩展机器。
- 消息被处理的状态是在 consumer 端维护,而不是由server端维护。当失败时能自动平衡。
Kafka架构图
Kafka 体系架构包括若干 Producer(生产者)和Consumer(消费者)、若干 Broker(缓存代理) ,以及一个 ZooKeeper 集群,如图所示。
其中 ZooKeeper 是 Kafka 用来负责集群元数据的管理、控制器 的选举等操作的。Producer,consumer 实现 Kafka 注册的接口,数据从 producer 发送到 broker,broker承担一个中间缓存和分发的作用。broker 分发注册到系统中的 consumer 。broker 的作用类似于缓存,即活跃的数据和离线处理系统之间的缓存。
从设计模式来理解Kafka
对于设计模式来说,我们可以先用用发布订阅模式来解析Kafka:
- Broker 可以当作一个消息中心,用来存储消息;
- Producer 作为发布者,向消息中心发布信息,而此时Broker进行对信息中间缓存和分发注册到Consumer(也可以看作是Consumer的订阅过程);
- Consumer 作为订阅者,在需要使用数据时,根据注册信息,在Broker中拿到信息。
当我们涉及到多个Broker即分布式时,就需要使用 ZooKeeper 集群对多个Broker进行维护。
主题(Topic)和分区(Partition)
Kafka 中的消息以 topic 为单位进行归类,生产者发送到 Kafka 集群中的每一条消息都要指定一个主题,而消费者负责订阅特定的主题并进行消费。
主题是一个逻辑上的概念,实际情况下,消息组的最小单位是一个分区(Partition)。一个主题可以对应多个分区,一个分区只属于单个主题。
当消息进行存储的时候,实际上是根据一个分区的偏移值(offset) 进行存储,每一类消息在存储到主题中时,都是根据这个偏移值追加到对应分区的特定位置当中。同时,我们可以随时修改分区的数量来横向扩展消息储存容量。
消费者和分区的关系
每个消费者都对应一个特定的主题,而对于主题内的分区来说,默认是消费者瓜分分区,互相不影响。但也可以自定义为消费者共享分区,此时就形成了消息广播。
物理存储
主题和分区都是提供给上层用户的抽象,而在 Log 层面才有实际物理上的设计。
为了避免数据冗余和Log过大,在物理存储层面,Kafka采用了以下两个策略:
- 同一分区的多个副本必须分布在不同的broker中(解决数据冗余)
- 引入分段日志(LogSegment),各个段之间通过 offset 来保证有序性(解决单个Log过大)
多副本
上面提到了消息会存在副本,这是分布式的特点。Kafka为分区引入了多副本(Replica)机制,通过增加副本数量可以提升容灾能力。同一分区的不同副本中保存的是相同的消息(在同一时刻,副本之间并非完全一样),副本之间是一主多从的关系,其中 leader 副本负责处理读写请求,follower 副本只负责与 leader 副本的消息同步。
由于多副本的机制和出现某个节点故障时的自动转移,Kafka 可以做到集群中某个 broker 失效时仍然能保证服务可用。
Kafka应用实战
saram
我们使用 go 操作 Kafka,这里使用比较常用的第三方库 sarama 来连接 Kafka 并且实现简单的发送消息和消费操作:
package main
import (
"fmt"
"github.com/Shopify/sarama"
)
var config *sarama.Config
func main() {
SendMsg()
ConsumeMsg()
}
func SendMsg() {
config = sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForAll // 发送完数据需要leader和follow都确认
config.Producer.Partitioner = sarama.NewRandomPartitioner // 随机分配分区
config.Producer.Return.Successes = true // 成功交付的消息将在success channel返回
// 连接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()
// 构造一个消息
msg := &sarama.ProducerMessage{}
msg.Topic = "topic1"
msg.Value = sarama.StringEncoder("this is a test log")
// 发送消息
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)
}
func ConsumeMsg() {
consumer, err := sarama.NewConsumer([]string{"127.0.0.1:9092"}, nil)
if err != nil {
fmt.Printf("fail to start consumer, err:%v\n", err)
return
}
partitionList, err := consumer.Partitions("topic1") // 根据topic取到所有的分区
if err != nil {
fmt.Printf("fail to get list of partition:err%v\n", err)
return
}
fmt.Println(partitionList)
for partition := range partitionList { // 遍历所有的分区
// 针对每个分区创建一个对应的分区消费者
pc, err := consumer.ConsumePartition("topic1", int32(partition), sarama.OffsetNewest)
if err != nil {
fmt.Printf("failed to start consumer for partition %d,err:%v\n", partition, err)
return
}
defer pc.AsyncClose()
// 异步从每个分区消费信息
go func(sarama.PartitionConsumer) {
for msg := range pc.Messages() {
fmt.Printf("Partition:%d Offset:%d Key:%v Value:%v", msg.Partition, msg.Offset, msg.Key, msg.Value)
}
}(pc)
}
}
docker-compose
Kafka 方面,我们使用 docker-compose 来运行容器:
-
编写 docker-compose.yml 文件
version: '3.7' services: zookeeper: image: wurstmeister/zookeeper volumes: - ./data:/data ports: - 2181:2181 kafka: image: wurstmeister/kafka ports: - 9092:9092 environment: KAFKA_BROKER_ID: 0 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://192.168.0.106:9092 #主机host,对外访问地址 KAFKA_CREATE_TOPICS: "topic1:2:0" #kafka启动后初始化一个有2个partition(分区)0个副本名叫kafeidou的topic KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092 volumes: - ./kafka-logs:/kafka depends_on: - zookeeper -
shell里输入
docker-compose up -d启动容器并且后台运行