51.Go操作kafka示例(kafka-go库)

代码地址:https://gitee.com/lymgoforIT/golang-trick/tree/master/31-kafka-go

一、简介

之前已经介绍过一个操作kafka的go库了,28.windows安装kafka,Go操作kafka示例(sarama库) ,但是这个库比较老了,当前比较流行的库是github.com/segmentio/kafka-go,所以本次我们就使用一下它。

我们在GitHub直接输入kafka并带上language标签为Go时,可以看到当前get github.com/segmentio/kafka-go库是最流行的。
在这里插入图片描述

首先启动kafka的服务器,然后在项目中go get github.com/segmentio/kafka-go

接着我们就可以创建生产者和消费者了,注意:在实际工作中,一般是一个服务为生产者,另一个服务作为消费者,但是本案例中不涉及微服务,就是演示一下生成和消费的示例代码,因此写到了一个服务当中。代码文件组织如下:
在这里插入图片描述
user.go :用于测试发送和消费结构体字符串消息

package model

type User struct {
	Id       int64  `json:"id"`
	UserName string `json:"user_name"`
	Age      int64  `json:"age"`
}

二、生产者

启动zookeeperkafka,并创建名为testtopic,步骤可以参考:28.windows安装kafka,Go操作kafka示例(sarama库)

producer.go

package producer

import (
	"context"
	"encoding/json"
	"fmt"
	"golang-trick/31-kafka-go/model"
	"time"

	"github.com/segmentio/kafka-go"
)

var (
	topic    = "user"
	Producer *kafka.Writer
)

func init() {
	Producer = &kafka.Writer{
		Addr:                   kafka.TCP("localhost:9092"), //TCP函数参数为不定长参数,可以传多个地址组成集群
		Topic:                  topic,
		Balancer:               &kafka.Hash{}, // 用于对key进行hash,决定消息发送到哪个分区
		MaxAttempts:            0,
		WriteBackoffMin:        0,
		WriteBackoffMax:        0,
		BatchSize:              0,
		BatchBytes:             0,
		BatchTimeout:           0,
		ReadTimeout:            0,
		WriteTimeout:           time.Second,       // kafka有时候可能负载很高,写不进去,那么超时后可以放弃写入,用于可以丢消息的场景
		RequiredAcks:           kafka.RequireNone, // 不需要任何节点确认就返回
		Async:                  false,
		Completion:             nil,
		Compression:            0,
		Logger:                 nil,
		ErrorLogger:            nil,
		Transport:              nil,
		AllowAutoTopicCreation: false, // 第一次发消息的时候,如果topic不存在,就自动创建topic,工作中禁止使用
	}
}

// 生产消息,发送user信息
func SendMessage(ctx context.Context, user *model.User) {
	msgContent, err := json.Marshal(user)
	if err != nil {
		fmt.Println(fmt.Sprintf("json marshal user err,user:%v,err:%v", user, err))
	}
	msg := kafka.Message{
		Topic:         "",
		Partition:     0,
		Offset:        0,
		HighWaterMark: 0,
		Key:           []byte(fmt.Sprintf("%d", user.Id)),
		Value:         msgContent,
		Headers:       nil,
		WriterData:    nil,
		Time:          time.Time{},
	}

	err = Producer.WriteMessages(ctx, msg)
	if err != nil {
		fmt.Println(fmt.Sprintf("写入kafka失败,user:%v,err:%v", user, err))
	}
}

main.go: 测试消息发送

package main

import (
	"context"
	"fmt"
	"golang-trick/31-kafka-go/model"
	"golang-trick/31-kafka-go/producer"
)

func main() {
	ctx := context.Background()
	for i := 0; i < 5; i++ {
		user := &model.User{
			Id:       int64(i + 1),
			UserName: fmt.Sprintf("lym:%d", i),
			Age:      18,
		}
		producer.SendMessage(ctx, user)
	}
	producer.Producer.Close() // 消息发送完毕后,关闭生产者
}

可以看到五条消息都发送成功
在这里插入图片描述

三、消费者

consumer.go

package consumer

import (
	"context"
	"encoding/json"
	"fmt"
	"golang-trick/24-gin-learning/class08/model"
	"time"

	"github.com/segmentio/kafka-go"
)

var (
	topic    = "user"
	Consumer *kafka.Reader
)

func init() {
	Consumer = kafka.NewReader(kafka.ReaderConfig{
		Brokers:                []string{"localhost:9092"}, // broker地址 数组
		// 消费者组id,每个消费者组可以消费kafka的完整数据,但是同一个消费者组中的消费者根据设置的分区消费策略共同消费kafka中的数据
		// 多个服务都启动了对于某个Topic的消费者时,如果消费者组名字一样,那么他们就会共同组成消费者组中的消费者,按照一定策略,共同消费Topic的各个分区
		// 注:一个分区只能被一个消费者组中的一个消费者消费,但是一个消费者可以消费多个分区
		GroupID:                "test",                     
		GroupTopics:            nil,
		Topic:                  topic, // 消费哪个topic
		Partition:              0,
		Dialer:                 nil,
		QueueCapacity:          0,
		MinBytes:               0,
		MaxBytes:               0,
		MaxWait:                0,
		ReadBatchTimeout:       0,
		ReadLagInterval:        0,
		GroupBalancers:         nil,
		HeartbeatInterval:      0,
		CommitInterval:         time.Second, // offset 上报间隔
		PartitionWatchInterval: 0,
		WatchPartitionChanges:  false,
		SessionTimeout:         0,
		RebalanceTimeout:       0,
		JoinGroupBackoff:       0,
		RetentionTime:          0,
		StartOffset:            kafka.FirstOffset, // 仅对新创建的消费者组生效,从头开始消费,工作中可能更常用从最新的开始消费kafka.LastOffset
		ReadBackoffMin:         0,
		ReadBackoffMax:         0,
		Logger:                 nil,
		ErrorLogger:            nil,
		IsolationLevel:         0,
		MaxAttempts:            0,
		OffsetOutOfRangeError:  false,
	})
}

// 消费消息
func ReadMessage(ctx context.Context) {
	// 消费者应该通过协程一直开着,一直消费
	for {
		if msg, err := Consumer.ReadMessage(ctx); err != nil {
			fmt.Println(fmt.Sprintf("读kafka失败,err:%v", err))
			continue // 当前消息读取失败时,并不退出for终止所有后续消费,而是跳过该消息即可
		} else {
			user := &model.User{}
			err := json.Unmarshal(msg.Value, user)
			if err != nil {
				fmt.Println(fmt.Sprintf("json unmarshal msg value err,msg:%v,err:%v", user, err))
				continue // 当前消息处理失败时,并不退出for终止所有后续消费,而是跳过该消息即可
			}

			fmt.Println(fmt.Sprintf("topic=%s,partition=%d,offset=%d,key=%s,user=%v", msg.Topic, msg.Partition, msg.Offset, msg.Key, user))
		}
	}
}

main.go: 测试接收消息

package main

import (
	"context"
	"fmt"
	"golang-trick/31-kafka-go/consumer"
	"os"
	"os/signal"
	"syscall"
)

// 需要监听信息2和15,在程序退出时,关闭Consumer
func listenSignal() {
	c := make(chan os.Signal, 1)
	signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
	sig := <-c
	fmt.Printf("收到信号 %s ", sig.String())
	if consumer.Consumer != nil {
		consumer.Consumer.Close()
	}
	os.Exit(0)
}

func main() {
	ctx := context.Background()
	//for i := 0; i < 5; i++ {
	//	user := &model.User{
	//		Id:       int64(i + 1),
	//		UserName: fmt.Sprintf("lym:%d", i),
	//		Age:      18,
	//	}
	//	producer.SendMessage(ctx, user)
	//}
	//producer.Producer.Close()

	go consumer.ReadMessage(ctx)
	listenSignal()
}

启动后,因为我们设置的从头开始消费,所以原有的五条消息消费成功,然后在等待着队列中有消息时继续消费
在这里插入图片描述
我们可以通过kafka客户端发两条消息,看看我们的消费者程序是否能消费到

在这里插入图片描述
最后关闭服务停止消费
在这里插入图片描述

你提到的 `kafka-broker1`、`kafka-broker2`、`kafka-broker3` 是你为 Kafka 集群中的三台 broker 主机配置的别名(host alias),这些别名是你在本地 `/etc/hosts` 文件中定义的,用于将 IP 地址映射为主机名,方便 Kerberos 认证和 Kafka 配置。 --- ## 🧩 问题:`kafka-broker1` 怎么获取? 你问的 `kafka-broker1` 是你本地定义的主机名(别名),它本身不是“获取”的对象,而是你为 Kafka broker 的 IP 地址 `25.45.48.105` 所起的主机名。它的作用是: - 用于 Kerberos 认证(principal 通常为 `kafka/kafka-broker1@REALM`) - 用于 Kafka 客户端连接 Kafka broker - 用于服务间通信时使用主机名而非 IP --- ## ✅ 如何确保 `kafka-broker1` 可用? ### 1. 确保 `/etc/hosts` 中已配置 在你的客户端机器(运行 Kafka 消费者或生产者的机器)上,确保 `/etc/hosts` 文件中包含如下内容: ```bash 25.45.48.105 kafka-broker1 25.45.48.191 kafka-broker2 25.45.49.19 kafka-broker3 ``` > ✅ **注意:** 如果你是在 Windows 上开发,编辑的是 `C:\Windows\System32\drivers\etc\hosts` 文件。 --- ### 2. 验证 `kafka-broker1` 是否能解析 你可以使用以下命令验证主机名是否解析正确: #### Linux / Mac: ```bash ping kafka-broker1 nslookup kafka-broker1 ``` #### Windows: ```cmd ping kafka-broker1 nslookup kafka-broker1 ``` 如果能成功解析到 `25.45.48.105`,说明配置成功。 --- ## ✅ 如何获取 Kafka broker 的 `kafka-broker1` 的 Kerberos principal? Kafka broker 的 principal 通常形如: ``` kafka/kafka-broker1@HADOOP.COM ``` 你需要从 Kerberos KDC 管理员处获取该 principal 的 keytab 文件,或者使用以下命令在 KDC 上创建: ```bash # 在 KDC 服务器上执行 ktutil add -p sh_sjzt_dwzy@HADOOP.COM -k kafka-broker1.keytab ``` 然后将该 keytab 文件拷贝到 Kafka broker 节点上,并在 Kafka broker 的配置中指定: ```properties sasl.jaas.config=com.sun.security.auth.module.Krb5LoginModule required \ useKeyTab=true \ storeKey=true \ keyTab="/path/to/kafka.keytab" \ principal="kafka/kafka-broker1@HADOOP.COM"; ``` --- ## ✅ Kafka broker 如何配置 `kafka-broker1` 的主机名? 在 Kafka broker 的 `server.properties` 文件中,你需要确保以下配置项使用了主机名而不是 IP: ```properties # Kafka broker 的监听地址 listeners=PLAINTEXT://kafka-broker1:21007 # 对外广播的地址(其他客户端连接时使用) advertised.listeners=PLAINTEXT://kafka-broker1:21007 ``` > ⚠️ 如果你使用 IP 地址而不是主机名,Kerberos 认证将失败。 --- ## ✅ Kafka 客户端如何使用 `kafka-broker1` 在 Kafka 客户端(消费者或生产者)配置中,使用如下方式连接: ```yaml mrs: kafka: bootstrap-servers: kafka-broker1:21007,kafka-broker2:21007,kafka-broker3:21007 security-protocol: SASL_PLAINTEXT sasl-kerberos-service-name: kafka ``` 并在 JAAS 配置中指定你的用户 principal 和 keytab 文件: ```java props.put("sasl.jaas.config", "com.sun.security.auth.module.Krb5LoginModule required " + "useKeyTab=true " + "storeKey=true " + "keyTab=\"/path/to/user.keytab\" " + "principal=\"sh_sjzt_dwzy@HADOOP.COM\";"); ``` --- ## ✅ 示例:完整的 Kafka 消费者 Java 代码(使用主机名) ```java import org.apache.kafka.clients.consumer.*; import org.apache.kafka.common.serialization.StringDeserializer; import java.time.Duration; import java.util.Collections; import java.util.Properties; public class KafkaConsumerExample { public static void main(String[] args) { Properties props = new Properties(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker1:21007"); props.put(ConsumerConfig.GROUP_ID_CONFIG, "test-group"); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); // Kerberos 配置 props.put("security.protocol", "SASL_PLAINTEXT"); props.put("sasl.kerberos.service.name", "kafka"); props.put("sasl.jaas.config", "com.sun.security.auth.module.Krb5LoginModule required " + "useKeyTab=true " + "storeKey=true " + "keyTab=\"/path/to/user.keytab\" " + "principal=\"sh_sjzt_dwzy@HADOOP.COM\";"); Consumer<String, String> consumer = new KafkaConsumer<>(props); consumer.subscribe(Collections.singletonList("xx")); try { while (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100)); for (ConsumerRecord<String, String> record : records) { System.out.printf("Received: %s (Topic: %s, Partition: %d, Offset: %d)%n", record.value(), record.topic(), record.partition(), record.offset()); } } } finally { consumer.close(); } } } ``` --- ## ✅ 总结 | 目标 | 操作 | |------|------| | 使用 `kafka-broker1` | 在 `/etc/hosts` 中配置 IP 到主机名的映射 | | 验证主机名是否可用 | 使用 `ping`, `nslookup`, `dig` | | 获取 Kafka broker 的 principal | 从 KDC 管理员获取 `kafka/kafka-broker1@REALM` 的 keytab | | Kafka broker 配置 | 使用主机名配置 `listeners` 和 `advertised.listeners` | | Kafka 客户端配置 | 使用主机名连接 Kafka,配置 Kerberos JAAS | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值