kakfa生产者消费者实践

背景介绍

最近在工作中项目使用了新的一些开发思路,利用mq来解耦耗时较长的业务流程; 例如以下的场景:

  • 删除某些资源的操作,可能是 服务A => 服务B => xxx 中间的过程是同步的,但可能某一步的业务逻辑需要删除的资源很多,如果是同步操作,给用于的体验就是一直等待,直到删除完毕,交互效果很不友好;
  • 创建/更新资源的联动,用户可能创建一条资源,但是要满足整体的资源联动,则可能需要创建和更新其他的资源,也直接影响了用户的体验性;
  • 每个模块进行完全独立的业务逻辑处理

有个很有意思的想法,就是利用mq的持久性,进行消息传播,让每个业务服务专注于处理自己的业务逻辑,不过这个想法也有一些很明显的优缺点:

  • 优点
    • 每个服务接收到mq后,直接进行最原始的CRUD操作,不需要进行考虑其他问题
    • 可以隐藏后台的耗时操作,让用户无感知
    • 不用考虑跨服务调用失败,mq失败了不会提交offset
  • 缺点
    • 可能存在循环调用的问题,导致msg永久存在,可能需要在业务上考虑如何避免
    • 每个服务都需要有一个模块专门接受和处理消息,消息是否本服务处理、处理完是否需要notify其他服务
    • 不好排查错误,每个服务都是独立的处理逻辑,可以通过增加业务ID trace 进行跟踪

生产者

  • 自定义分区器
    • 模拟业务key进行分区
  • 同步生产者
go 复制代码
package main  
  
import (  
   "fmt"  
   "hash/crc32"   "log"  
   "github.com/IBM/sarama"      "HelloGo/Kafka"  
)  
  
type CustomPartitioner struct{}  
  
func (p *CustomPartitioner) Partition(msg *sarama.ProducerMessage, numPartitions int32) (int32, error) {  
   keyBytes, _ := msg.Key.Encode()  
   hash := crc32.ChecksumIEEE(keyBytes)  
   return int32(hash % uint32(numPartitions)), nil  
}  
  
func (p *CustomPartitioner) RequiresConsistency() bool {  
   return true  
}  
  
func NewMyPartitioner(topic string) sarama.Partitioner {  
   return &CustomPartitioner{}  
}  
  
type BusinessKey struct {  
   UserID string  
}  
  
func (b BusinessKey) Encode() ([]byte, error) {  
   return []byte(b.UserID), nil  
}  
  
func (b BusinessKey) Length() int {  
   return len(b.UserID)  
}  
  
func main() {  
   Producer(Kafka.HappyChanTopic, 60000)  
}  
  
func Producer(topic string, limit int) {  
   config := sarama.NewConfig()  
   //config.Producer.Compression = sarama.CompressionGZIP  
   //config.Producer.CompressionLevel = gzip.BestCompression   config.Producer.Return.Successes = true  
   config.Producer.Return.Errors = true // 这个默认值就是 true 可以不用手动 赋值  
   config.Producer.Partitioner = NewMyPartitioner  
  
   producer, err := sarama.NewSyncProducer([]string{Kafka.Brokers}, config)  
   if err != nil {  
      log.Fatal("NewSyncProducer err:", err)  
   }  
   defer producer.Close()  
   userIds := []string{"user1", "user2", "user3", "user4"}  
   for i := 0; i < limit; i++ {  
      // 生成不同的UserID(示例:时间戳+随机数)  
      bs := BusinessKey{  
         UserID: userIds[i%4],  
      }  
      // 自定义Value(示例:添加时间戳)  
      value := fmt.Sprintf("data_%d", i)  
      msg := &sarama.ProducerMessage{Topic: topic, Key: bs, Value: sarama.StringEncoder(value)}  
      partition, offset, err := producer.SendMessage(msg)  
      if err != nil {  
         log.Println("SendMessage err: ", err)  
         return  
      }  
      log.Printf("[Producer] partitionid: %d; offset:%d\n", partition, offset)  
   }  
}

消费者

实现的功能:

  • 消费者抽象
    • 工厂模式管理不同Topic的处理器
    • 统一接口实现业务逻辑解耦
    • 自动化的消费者生命周期管理
  • 重试机制
  • 死信队列
  • 监控建议
    • 消息处理成功率
    • 重试队列堆积情况
    • 死信队列数量
    • 消费者组延迟 还可以再优化的地方:
  • 分优先级处理不同消息的重试次数
  • 消费者的config可以通过配置文件、k8s 的 configmap 资源、或者配置中心拉取,进行热加载 等其他方式进行初始化
go 复制代码
package main  
  
import (  
   "context"  
   "fmt"   "log"   "os"   "os/signal"   "syscall"   "time"  
   "github.com/IBM/sarama"   "github.com/jinzhu/copier")  
  
// 配置常量  
const (  
   Brokers            = "localhost:9092"  
   MaxRetries         = 3  
   RetryHeaderKey     = "retry_count"  
   DLQTopicSuffix     = "_dlq"  
   SessionTimeout     = 30 * time.Second  
   HeartbeatInterval  = 3 * time.Second  
   MaxProcessingTime  = 2 * time.Minute  
   AutoCommitInterval = 1 * time.Second  
)  
  
// 扩展MessageHandler接口  
type MessageHandler interface {  
   Handle(ctx context.Context, msg *sarama.ConsumerMessage) error  
   Topic() string  
   ConsumerGroup() string    // 新增:消费者组ID  
   Concurrency() int         // 新增:消费者并发数  
   RetryPolicy() RetryConfig // 新增:重试策略  
}  
  
type RetryConfig struct {  
   MaxAttempts int  
   Backoff     time.Duration  
}  
  
// 示例处理器改造  
type SampleHandler struct {  
   topic       string  
   group       string  
   concurrency int  
   retryConfig RetryConfig  
}  
  
func NewSampleHandler(topic, group string, concurrency int) *SampleHandler {  
   return &SampleHandler{  
      topic:       topic,  
      group:       group,  
      concurrency: concurrency,  
      retryConfig: RetryConfig{MaxAttempts: 3, Backoff: 1 * time.Second},  
   }  
}  
  
// 实现新增方法  
func (h *SampleHandler) ConsumerGroup() string    { return h.group }  
func (h *SampleHandler) Concurrency() int         { return h.concurrency }  
func (h *SampleHandler) RetryPolicy() RetryConfig { return h.retryConfig }  
  
type ConsumerFactory struct {  
   producer  sarama.SyncProducer  
   consumers map[string][]sarama.ConsumerGroup // Key格式:group+topic  
   configs   map[string]*sarama.Config         // 不同消费者组的配置  
   handlers  map[string]MessageHandler         // 不同消费者组的处理器  
}  
  
func NewConsumerFactory() *ConsumerFactory {  
   return &ConsumerFactory{  
      consumers: make(map[string][]sarama.ConsumerGroup),  
      configs:   make(map[string]*sarama.Config),  
      handlers:  make(map[string]MessageHandler),  
   }  
}  
  
// 获取处理器实例  
func (f *ConsumerFactory) getHandlerByKey(groupKey string) MessageHandler {  
   return f.handlers[groupKey] // 从映射表中直接查找  
}  
  
// 注册处理器时创建多个消费者实例  
func (f *ConsumerFactory) RegisterHandler(handler MessageHandler, baseConfig *sarama.Config) error {  
   // 生成消费者组唯一标识  
   groupKey := fmt.Sprintf("%s-%s", handler.ConsumerGroup(), handler.Topic())  
   f.handlers[groupKey] = handler  
  
   // 克隆基础配置并设置组参数  
   config := sarama.Config{}  
   err := copier.Copy(&config, &baseConfig)  
   if err != nil {  
      return err  
   }  
   config.Consumer.Group.Session.Timeout = SessionTimeout  
   config.Consumer.Group.Rebalance.Timeout = 60 * time.Second  
  
   client, err := sarama.NewClient([]string{Brokers}, &config)  
   if err != nil {  
      return fmt.Errorf("failed to create client: %v", err)  
   }  
  
   // 创建指定数量的消费者实例  
   var consumers []sarama.ConsumerGroup  
   for i := 0; i < handler.Concurrency(); i++ {  
      consumer, err := sarama.NewConsumerGroupFromClient(handler.ConsumerGroup(), client)  
      if err != nil {  
         return fmt.Errorf("failed to create consumer: %v", err)  
      }  
      consumers = append(consumers, consumer)  
   }  
  
   f.consumers[groupKey] = consumers  
   f.configs[groupKey] = &config  
   return nil  
}  
  
// 消费者处理器  
type ConsumerHandler struct {  
   handler    MessageHandler  
   producer   sarama.SyncProducer  
   dlqTopic   string  
   retryTopic string  
}  
  
func (h *ConsumerHandler) Setup(sess sarama.ConsumerGroupSession) error {  
   log.Printf("Consumer setup for topic %s", h.handler.Topic())  
   return nil  
}  
  
func (h *ConsumerHandler) Cleanup(sess sarama.ConsumerGroupSession) error {  
   log.Printf("Consumer cleanup for topic %s", h.handler.Topic())  
   return nil  
}  
  
func (h *ConsumerHandler) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {  
   for msg := range claim.Messages() {  
      ctx, cancel := context.WithTimeout(context.Background(), MaxProcessingTime)  
  
      retryCount := getRetryCount(msg)  
      if retryCount >= h.handler.RetryPolicy().MaxAttempts {  
         h.sendToDLQ(msg)  
         sess.MarkMessage(msg, "")  
         cancel()  
         continue  
      }  
  
      if err := h.handler.Handle(ctx, msg); err != nil {  
         h.retryWithBackoff(msg, retryCount, h.handler.RetryPolicy().Backoff)  
      }  
      sess.MarkMessage(msg, "")  
      cancel()  
   }  
   return nil  
}  
  
func (h *ConsumerHandler) retryWithBackoff(msg *sarama.ConsumerMessage, attempt int, backoff time.Duration) {  
   select {  
   case <-time.After(backoff * time.Duration(attempt)):  
      h.retryMessage(msg, attempt)  
   case <-context.Background().Done():  
      return  
   }  
}  
  
// 重试逻辑  
func (h *ConsumerHandler) retryMessage(msg *sarama.ConsumerMessage, currentRetry int) {  
   var headers []sarama.RecordHeader  
   for _, h := range msg.Headers {  
      headers = append(headers, *h)  
   }  
   newMsg := &sarama.ProducerMessage{  
      Topic: msg.Topic,  
      Key:   sarama.ByteEncoder(msg.Key),  
      Value: sarama.ByteEncoder(msg.Value),  
      Headers: append(headers, sarama.RecordHeader{  
         Key:   []byte(RetryHeaderKey),  
         Value: []byte{byte(currentRetry + 1)},  
      }),  
   }  
  
   if _, _, err := h.producer.SendMessage(newMsg); err != nil {  
      log.Printf("Failed to retry message: %v", err)  
   }  
}  
  
// 死信队列处理  
func (h *ConsumerHandler) sendToDLQ(msg *sarama.ConsumerMessage) error {  
   var headers []sarama.RecordHeader  
   for _, h := range msg.Headers {  
      headers = append(headers, *h)  
   }  
   dlqMsg := &sarama.ProducerMessage{  
      Topic: h.dlqTopic,  
      Key:   sarama.ByteEncoder(msg.Key),  
      Value: sarama.ByteEncoder(msg.Value),  
      Headers: append(headers, sarama.RecordHeader{  
         Key:   []byte("original_topic"),  
         Value: []byte(msg.Topic),  
      }),  
   }  
  
   _, _, err := h.producer.SendMessage(dlqMsg)  
   return err  
}  
  
// 获取重试次数  
func getRetryCount(msg *sarama.ConsumerMessage) int {  
   for _, hdr := range msg.Headers {  
      if string(hdr.Key) == RetryHeaderKey {  
         return int(hdr.Value[0])  
      }  
   }  
   return 0  
}  
  
func (h *SampleHandler) Handle(ctx context.Context, msg *sarama.ConsumerMessage) error {  
   log.Printf("开始消费分区范围: %+v", msg)  
   log.Printf("处理分区 %d 的消息: msg.key: %+v, msg.value: %s \n", msg.Partition, string(msg.Key), string(msg.Value))  
   log.Print("分区消费结束 ===== ")  
   return nil  
}  
  
func (h *SampleHandler) Topic() string {  
   return h.topic  
}  
  
func main() {  
   // 初始化配置  
   config := sarama.NewConfig()  
   config.Version = sarama.V2_8_0_0  
   config.Consumer.Offsets.Initial = sarama.OffsetOldest  
   config.Producer.Return.Successes = true  
  
   // 初始化生产者  
   producer, err := sarama.NewSyncProducer([]string{Brokers}, config)  
   if err != nil {  
      log.Fatalf("Failed to create producer: %v", err)  
   }  
   defer producer.Close()  
  
   // 创建多个独立消费者组  
   handlers := []MessageHandler{  
      NewSampleHandler("orders", "order-group", 3),     // 订单Topic,3个消费者  
      NewSampleHandler("payments", "payment-group", 2), // 支付Topic,2个消费者  
   }  
  
   // 创建消费者工厂  
   factory := NewConsumerFactory()  
   for _, handler := range handlers {  
      if err := factory.RegisterHandler(handler, config); err != nil {  
         log.Fatal(err)  
      }  
   }  
  
   // 启动所有消费者组  
   for groupKey, consumers := range factory.consumers {  
      for _, consumer := range consumers {  
         go func(c sarama.ConsumerGroup, h MessageHandler) {  
            handler := &ConsumerHandler{  
               handler:    h,  
               producer:   factory.producer,  
               dlqTopic:   h.Topic() + DLQTopicSuffix,  
               retryTopic: h.Topic(),  
            }  
            for {  
               if err := c.Consume(context.Background(), []string{h.Topic()}, handler); err != nil {  
                  log.Printf("[%s] 消费者异常: %v", h.ConsumerGroup(), err)  
               }  
            }  
         }(consumer, factory.getHandlerByKey(groupKey))  
      }  
   }  
  
   // 信号处理  
   sigchan := make(chan os.Signal, 1)  
   signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)  
   <-sigchan  
   log.Println("Shutting down consumers...")  
}
相关推荐
Sweety丶╮7941 小时前
【Ansible】实施 Ansible Playbook知识点
服务器·云原生·ansible
cg.family1 小时前
Doris 消费kafka消息
kafka·doris
一朵筋斗云2 小时前
golang底层原理剖析
go
学历真的很重要2 小时前
Claude Code Windows 原生版安装指南
人工智能·windows·后端·语言模型·面试·go
趴着喝可乐2 小时前
openEuler2403安装部署Kafka
kafka·openeuler
wp90904 小时前
Docker命令大全
docker·云原生·eureka
xiao-xiang4 小时前
kubernetes-lxcfs解决资源可见性问题
云原生·容器·kubernetes
向上的车轮5 小时前
云原生TodoList Demo 项目,验证云原生核心特性
云原生
喂完待续5 小时前
【序列晋升】28 云原生时代的消息驱动架构 Spring Cloud Stream的未来可能性
spring cloud·微服务·云原生·重构·架构·big data·序列晋升
jzzy_hony5 小时前
云原生:微服务与Serverless指南
微服务·云原生·serverless