Go操作kafka | 青训营

186 阅读6分钟

Kafka是Apache下的一个开源消息队列中间件,它常用于数据分析领域,并且它是用Java编写的。现在,我们来学习一个kafka的使用同时用go操作kafka完成数据的生成和消费,并进一步地去理解kafka的内存模型。

安装

通过官网,我们得知可以通过下载kafka的压缩包,然后解压,运行脚本进行安装,但是需要主机上安装有Java虚拟机,对于Go开发者来说,虚拟机不是必须的。因此,我们尝试使用Docker进行kafka服务的安装。

我们找到了一个最近更新且下载了最多的名叫bitnami/kafka的镜像,根据它的镜像介绍,启动一个kafka服务的命令是下面这样:

docker run -d --name kafka-server --hostname kafka-server \
    --network app-tier \
    -e KAFKA_CFG_NODE_ID=0 \
    -e KAFKA_CFG_PROCESS_ROLES=controller,broker \
    -e KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 \
    -e KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT \
    -e KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka-server:9093 \
    -e KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER \
    bitnami/kafka:latest

里面有一堆的环境变量需要指定,我们先不着急启动,先搞懂这些参数的大致作用是什么。

首先,KAFKA_CFG_NODE_ID很好理解,因为kafka要实现分布式集群,集群中每一台服务都需要一个完全独立的ID,所以这个参数是指定服务的ID,根据官网的介绍,官网是通过一个UUID作为节点的ID。

然后是KAFKA_CFG_PROCESS_ROLES参数,后面的值是controller和broker,这个对应的是每个节点的角色,我们查找一下资料,可以指定kafka集群中的角色大致是以下几类:

  • Controller:就是主节点,需要借助Zookeeper进行维护。有一个中心节点服务器,负责集群的管理,分区分配,Topic管理,Leader选举和集群协调等事务,可以将Broker理解成干活的,Controller就是管理人员。
  • Broker:中文名的意思是协调,中间人的意思,类似与一个中间代理,一般一个kafka服务就是一个Broker,负责将生产者的消息存储在对应的Topic分区,并将其分发到对应的消费组进行消费。
  • Poducer:就是消息的生产者,消息从这里产生,一般是一些其他的服务利用kafka的客户端sdk完成消息的生产。
  • Comsumer Group:消费者,是一系列消费者,一般通过指定消费者的组ID可以确定哪些消费者是一个组的,消费组内的娥消费者共同消费同一个消息。

另外我们还需要了解下Topic和Partition的概念,Topic也就是主题是对消息的一种分类,Partition是对Topic的物理分片,相当于一个Topic存在多个分区,Partition的作用是多个消费者可以对多个分区进行并行消费,这样可以提高消息消费的性能。

另外就是Leader和Follower了,一般同样的Topic可能存在于多个Broker中,那么这些相同的Topic就有Leader和Follwer这两种身份,类似于Redis的主从复制,Leader节点负责写,Follwer节点主要负责读,并且Follwer节点会拷贝Leader节点的消息,进行同步。

一个简单的消息生成到消费的过程就是,消息由生成者产生,然后发送到Broker进行集中管理,其中消息根据其Topic会存入Broker的队友Topic的Partition分区中,最后再经由Broker,消息从Topic的各个Partition分区中被对应消费组的多个消费者消费。中间的Broker可能有多个,自然一个Topic也可能存在于多个Broker中。

然后我们看接下来的参数KAFKA_CFG_LISTENERS指定监听者,不太懂什么意思,但根据其后面的值PLAINTEXT://:9092,CONTROLLER://:9093我们大概猜测,它应该是说监听9092和909这两个端口,然后这两个监听者命名为PLAINTEXT和CONTROLLER,因为是对应前面的两种角色,Broker和Controller.

KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT这个环境变量大概是指定监听者使用的编码,都是PLAINTEXT,说明都是普通文本类型,不进行加密。

KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka-server:9093似乎是指定投票的数量,和对应的服务器。

那么我们先来尝试创建一个docker容器并启动它吧,但在这里我们不适用docker的网络,因此我们需要修改一下run命令,同时暴露对应的端口,方便我们的go客户端连接。

docker run -d --name kafka-server \ 
    -e KAFKA_CFG_NODE_ID=0 \
    -e KAFKA_CFG_PROCESS_ROLES=controller,broker \
    -e KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 \
    -e KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT \
    -e KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@localhost:9093 \
    -e KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER \
    -p 9092:9092 \
    -p 9093:9093 \
    bitnami/kafka:latest

我们现在已经成功启动了一个kafka服务,接下来让我们用go连接它吧。

Go操作kafka

由于官方似乎没有提供go的kafka sdk,因此我们使用一些第三方的go客户端库,比如kafka-go,sarama,在这里我们使用sarama进行连接。

package main

import (
    "fmt"
    "log"
    "os"
    "os/signal"
    "sync"

    "github.com/IBM/sarama"
)

func main() {
    // 设置Kafka配置
    config := sarama.NewConfig()
    config.Producer.RequiredAcks = sarama.WaitForAll
    config.Producer.Retry.Max = 5
    config.Producer.Return.Successes = true
    config.Consumer.Offsets.Initial = sarama.OffsetOldest

    // 创建Kafka生产者
    producer, err := sarama.NewSyncProducer([]string{"localhost:9092"}, config)
    if err != nil {
       log.Fatalln("Failed to create producer:", err)
    }
    defer producer.Close()

    // 创建Kafka消费者
    consumer, err := sarama.NewConsumer([]string{"localhost:9092"}, config)
    if err != nil {
       log.Fatalln("Failed to create consumer:", err)
    }
    defer consumer.Close()

    // 定义Topic和Partition
    topic := "test-topic"
    partition := int32(0)

    // 发送消息
    message := &sarama.ProducerMessage{
       Topic:     topic,
       Partition: partition,
       Value:     sarama.StringEncoder("Hello, Kafka!"),
    }
    _, _, err = producer.SendMessage(message)
    if err != nil {
       log.Fatalln("Failed to send message:", err)
    }
    fmt.Println("Message sent successfully!")

    // 消费消息
    partitionConsumer, err := consumer.ConsumePartition(topic, partition, sarama.OffsetNewest)
    if err != nil {
       log.Fatalln("Failed to create partition consumer:", err)
    }
    defer partitionConsumer.Close()

    signals := make(chan os.Signal, 1)
    signal.Notify(signals, os.Interrupt)

    wg := &sync.WaitGroup{}
    wg.Add(1)

    go func() {
       defer wg.Done()
       for {
          select {
          case msg := <-partitionConsumer.Messages():
             fmt.Println("Received message:", string(msg.Value))
          case <-signals:
             return
          }
       }
    }()

    wg.Wait()
}

运行一下,发现报错了:

查了下资料说,lookup是kafka内部给Broker分配的hostname,一般在/etc/hosts/中指定lookup对应的ip地址就可以了。这说明go操作docker中的kafka服务器,可能需要一个内网环境。当我们在run中指定--hostname="kafka-server"时,这个lookup后面就会变成kafka-server,而如果我们将我们的应用包含在kafka创建的network中时,就能正常解析kafka-server这个hostname了,因为hostname被注册的是docker的/etc/hosts而不是本地的hosts文件中,我们打开容器内部的/etc/hosts文件,看一下:

127.0.0.1   localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2  be07c2a4d2ca

发现最后面果然被添加了一个be07c2a4d2ca的主机,这可能在容器内部发生了一个负载均衡或者转发的过程。我们连接的是localhost:9092,但是最后被转发给了172.17.0.2,但是我们查了下本机的ip发现不是172.17.0.2这,说明这个地址可能只是一个容器的内网地址。

那没办法,要么通过官网的方式在本地安装jvm,然后安装kafka,妖魔使用docker-compose将我们的go程序连接进kafka容器的内网中。

这里我们决定使用docker-compose

version: '2'

networks:
  app-tier:
    driver: bridge

services:
  kafka:
    image: 'bitnami/kafka:latest'
    networks:
      - app-tier
    environment:
      - KAFKA_CFG_NODE_ID=0
      - KAFKA_CFG_PROCESS_ROLES=controller,broker
      - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
      - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
      - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:9093
      - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
  kafka-test:
    build:./cmd/kafka
    networks:
      - app-tier

build指向我们的Dockerfile文件的位置,Dockerfile文件将我们的项目构建成一个新的镜像。同时需要修改我们的go代码中的localhost位kafka。

我们运行docker-compose文件,并在容器中再运行一下main.go文件,可以看到我们的消息已经发送了。