1

在公司业务中有场景需要实时订阅Topic,也就是当有新的Topic出现时,需要自动发现、监听、消费

诸多比较之后选择了用户群体最多的 sarama,但是遇到了一个问题,这个包并没有实现像Java的一样的正则匹配策略,不要说正则,连实时刷新机制都没有,所以需要我们自己来实现 Java 客户端 subscribe(Pattern)的通配符模式,废话不多说,直接上代码:


package main

import (
    "context"
    "errors"
    "log"
    "os"
    "os/signal"
    "reflect"
    "sort"
    "strings"
    "sync"
    "syscall"
    "time"

    "github.com/IBM/sarama"
)

// Sarama configuration options
var (
    brokers  = "127.0.0.1:9092"
    version  = "2.8.1" // Note: kafka broker version (not Sarama version)
    group    = "kfk_group_id"
    assignor = "sticky"
)

func main() {
    keepRunning := true

    log.Println("Starting a new Sarama consumer")
    sarama.Logger = log.New(os.Stdout, "[sarama] ", log.LstdFlags)
    version, err := sarama.ParseKafkaVersion(version)
    if err != nil {
        log.Panicf("Error parsing Kafka version: %v", err)
    }

    /**
     * Construct a new Sarama configuration.
     * The Kafka cluster version has to be defined before the consumer/producer is initialized.
     */
    config := sarama.NewConfig()
    config.Version = version
    config.Consumer.Offsets.Initial = sarama.OffsetOldest

    switch assignor {
    case "sticky":
        config.Consumer.Group.Rebalance.GroupStrategies = []sarama.BalanceStrategy{sarama.NewBalanceStrategySticky()}
    case "roundrobin":
        config.Consumer.Group.Rebalance.GroupStrategies = []sarama.BalanceStrategy{sarama.NewBalanceStrategyRoundRobin()}
    case "range":
        config.Consumer.Group.Rebalance.GroupStrategies = []sarama.BalanceStrategy{sarama.NewBalanceStrategyRange()}
    default:
        log.Panicf("Unrecognized consumer group partition assignor: %s", assignor)
    }

    /**
     * Set up a new Sarama consumer group
     */
    consumer := Consumer{
        ready: make(chan bool),
    }

    ctx, cancel := context.WithCancel(context.Background())

    newClient, err := sarama.NewClient(strings.Split(brokers, ","), config)
    consumerGroup, err := sarama.NewConsumerGroupFromClient(group, newClient)
    if err != nil {
        log.Fatalf("Error creating consumer group client: %v", err)
    }

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

    // Get all the Topic
    topics, err := newClient.Topics()

    topics = filterTopics(topics)

    go func() {
        defer wg.Done()
        for {
            // `Consume` should be called inside an infinite loop, when a
            // server-side rebalance happens, the consumer session will need to be
            // recreated to get the new claims
            if err := consumerGroup.Consume(ctx, topics, &consumer); err != nil {
                if errors.Is(err, sarama.ErrClosedConsumerGroup) {
                    return
                }
                log.Panicf("Error from consumer: %v", err)
            }
            // check if context was cancelled, signaling that the consumer should stop
            if ctx.Err() != nil {
                log.Printf("Context err from consumer: %v", ctx.Err())
                return
            }
            consumer.ready = make(chan bool)
        }
    }()

    <-consumer.ready // Await till the consumer has been set up

    log.Println("Sarama consumer up and running!...")

    go refreshTopics(newClient, consumerGroup, topics)

    sigusr1 := make(chan os.Signal, 1)
    signal.Notify(sigusr1, syscall.SIGUSR1)

    sigterm := make(chan os.Signal, 1)
    signal.Notify(sigterm, syscall.SIGINT, syscall.SIGTERM)

    for keepRunning {
        select {
        case <-ctx.Done():
            log.Println("terminating: context cancelled")
            keepRunning = false

        case <-sigterm:
            log.Println("terminating: via signal")
            keepRunning = false

        case <-sigusr1:

        }
    }

    cancel()

    wg.Wait()
    if err = consumerGroup.Close(); err != nil {
        log.Panicf("Error closing client: %v", err)
    }
}

func EqualSlices(s1, s2 []string) bool {
    if len(s1) != len(s2) {
        return false
    }

    m1 := make(map[string]struct{})
    m2 := make(map[string]struct{})

    for _, v := range s1 {
        m1[v] = struct{}{}
    }

    for _, v := range s2 {
        m2[v] = struct{}{}
    }

    return reflect.DeepEqual(m1, m2)
}

func filterTopics(topics []string) []string {
    filteredTopics := make([]string, 0)
    for _, topic := range topics {
        if topic != "__consumer_offsets" {
            filteredTopics = append(filteredTopics, topic)
        }
    }
    return filteredTopics
}

func refreshTopics(client sarama.Client, prevConsumerGroup sarama.ConsumerGroup, topicsOld []string) {
    ticker := time.NewTicker(5 * time.Second)

    for {
        <-ticker.C

        if err := client.RefreshMetadata(); err != nil {
            log.Printf("Error refreshing metadata: %v", err)
            continue
        }

        topics, err := client.Topics()
        if err != nil {
            log.Printf("Error refreshing topics: %v", err)
            continue
        }

        filteredTopics := filterTopics(topics) // filter "__consumer_offsets"
        sort.Strings(filteredTopics)
        log.Printf("All Topics: %v", filteredTopics)

        if !EqualSlices(filteredTopics, topicsOld) {
            topicsOld = filteredTopics

            if prevConsumerGroup != nil {
                err := prevConsumerGroup.Close()
                if err != nil {
                    log.Printf("Error closing prev consumer group: %v", err)
                }
            }

            newConsumer := Consumer{
                ready: make(chan bool),
            }

            newConsumerGroup, err := sarama.NewConsumerGroupFromClient(group, client)
            if err != nil {
                log.Printf("Error creating new consumer group: %v", err)
                return
            }

            defer func(newConsumerGroup sarama.ConsumerGroup) {
                err := newConsumerGroup.Close()
                if err != nil {
                    log.Printf("Error closing new consumer group: %v", err)
                }
            }(newConsumerGroup)

            go func() {
                ctx, cancel := context.WithCancel(context.Background())
                defer cancel()

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

                // start Consume
                go func() {
                    defer wg.Done()
                    if err := newConsumerGroup.Consume(ctx, filteredTopics, &newConsumer); err != nil {
                        log.Printf("Error from consumer: %v", err)
                    }
                }()

                wg.Wait()
            }()
        }
    }
}

// Consumer represents a Sarama consumer group consumer
type Consumer struct {
    ready chan bool
}

// Setup is run at the beginning of a new session, before ConsumeClaim
func (consumer *Consumer) Setup(sarama.ConsumerGroupSession) error {
    // Mark the consumer as ready
    close(consumer.ready)
    return nil
}

// Cleanup is run at the end of a session, once all ConsumeClaim goroutines have exited
func (consumer *Consumer) Cleanup(sarama.ConsumerGroupSession) error {
    return nil
}

// ConsumeClaim must start a consumer loop of ConsumerGroupClaim's Messages().
// Once the Messages() channel is closed, the Handler must finish its processing
// loop and exit.
func (consumer *Consumer) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
    // NOTE:
    // Do not move the code below to a goroutine.
    // The `ConsumeClaim` itself is called within a goroutine, see:
    // https://github.com/IBM/sarama/blob/main/consumer_group.go#L27-L29
    for {
        select {
        case message, ok := <-claim.Messages():
            if !ok {
                log.Printf("message channel was closed")

                return nil
            }

            log.Printf("Message claimed: value = %s, timestamp = %v, topic = %s", string(message.Value), message.Timestamp, message.Topic)

            session.MarkMessage(message, "")
        // Should return when `session.Context()` is done.
        // If not, will raise `ErrRebalanceInProgress` or `read tcp <ip>:<port>: i/o timeout` when kafka rebalance. see:
        // https://github.com/IBM/sarama/issues/1192
        case <-session.Context().Done():
            return nil
        }
    }
}

代码很简单,相信大家都能看懂我是在做什么了,其实就是增加了一个 refreshTopics 来刷新Topic,当检测到新的Topic的时候关掉之前的消费者组、创建新的组、订阅新的主题

注意:此处有一个大坑!当新的Topic加入的时候,refreshTopics 监测到新的消息进行消费,这里如果不进行配置,则只会消费订阅之后的消息,你不要忘记啊,能触发订阅新的主题的条件是有了一条新的消息,那这条触发订阅的消息去哪了呢?首先肯定不会被原消费者组消费,因为他们还没订阅,其次,新启动的消费这组虽然订阅了新的Topic,但是因为你没有配置,它默认没有消费记录的新的Topic会从消息的最后的位置进行消费(有消费记录的Topic从未消费的位置开始消费),所以,根本就不会消费到那条消息!那怎么办呢?你只需要在启动的时候增加一个配置即可:

config.Consumer.Offsets.Initial = sarama.OffsetOldest

问题解决,撒花···


少年吖少年
65 声望2 粉丝