青训营项目设计-卡夫卡初探 | 青训营笔记

183 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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 来运行容器:

  1. 编写 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
    
  2. shell里输入 docker-compose up -d 启动容器并且后台运行