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...")  
}
相关推荐
云上艺旅2 小时前
K8S学习之基础十五:k8s中Deployment扩容缩容
学习·docker·云原生·kubernetes·k8s
forever234 小时前
自定义go日志接口的实现
go
DemonAvenger4 小时前
深入Go并发编程:Goroutine性能调优与实战技巧全解析
设计模式·架构·go
颜淡慕潇5 小时前
【K8S系列】Kubernetes中查看日志常见问题&解决方案
后端·云原生·容器·kubernetes
道法自然,人法天5 小时前
微服务的认识与拆分
微服务·云原生·架构
蝴蝶不愿意7 小时前
微服务拆分-拆分购物车服务
笔记·学习·微服务·云原生·架构
计算机软件程序设计8 小时前
Windows下安装kafka
windows·分布式·kafka
java技术小馆8 小时前
Kafka 消息传递模型
分布式·kafka
考虑考虑8 小时前
Golang 使用定时任务(robfig/cron/v3)
后端·程序员·go
LaughingZhu9 小时前
Vercel Serverless
云原生·serverless