Kratos-databus/group

308 阅读1分钟

解决的问题

kafka单个消费者多线程顺序消费

组成

Config

// Config the config is the base configuration for initiating a new group.
type Config struct {
   // Size merge size
   Size int
   // Num merge goroutine num
   Num int
   // Ticker duration of submit merges when no new message
   Ticker time.Duration
   // Chan size of merge chan and done chan
   Chan int
}

配置信息: Size: 每一个 worker 接收的 chan 队列容量, 默认为1024

Num: worker 的数量

Ticker: 定时器,定时提交已处理完的消息, 默认5s

Chan: 已完成的或已合并的chan 容量,默认与Size一样大小

message

type message struct {
   next   *message
   data   *databus.Message
   object interface{}
   done   bool
}

消费的消息,为kafka接收到消息的一层封装

next: 下一条消息, 在merge的时候会将新的消息放到他的后面,并置为last

data: kafka中的消息

object: 结构体定义,用于umarsal msg

done: 是否已处理

Group

// Group group.
type Group struct {
   c          *Config
   head, last *message
   state      int
   mu         sync.Mutex

   mc  []chan *message // merge chan
   dc  chan []*message // done chan
   qc  chan struct{}   // quit chan
   msg <-chan *databus.Message

   New   func(msg *databus.Message) (interface{}, error)
   Split func(msg *databus.Message, data interface{}) int
   Do    func(msgs []interface{})

   pool *sync.Pool
}

可以理解为一个消费者组

c: 上面的Config配置,用于初始化

head, last: 已处理到的消息,用于最后的commit

state: group 的状态,开启,关闭

mc: worker num 个chan, 每个chan为Size的容量,用于给worker发送消息

dc: 已经处理完的消息队列

qc: 用于通知各个goruntine退出

msg: 主要的用于kafka的chan

New: func 用于返回实现了message interface的对象

Split: 再次 sharding 的函数

Do: func 具体处理消息的逻辑

pool: message pool

核心方法

consumeproc 预处理

func (g *Group) consumeproc() {
   var (
      ok  bool
      err error
      msg *databus.Message
   )
   for {
      select {
      case <-g.qc:
         return
      case msg, ok = <-g.msg:
         if !ok {
            g.Close()
            return
         }
      }
      // marked head to first commit
      m := g.message()
      m.data = msg
      if m.object, err = g.New(msg); err != nil {
         g.freeMessage(m)
         continue
      }
      g.mu.Lock()
      if g.head == nil {
         g.head = m
         g.last = m
      } else {
         g.last.next = m
         g.last = m
      }
      g.mu.Unlock()
      g.mc[g.Split(m.data, m.object)%g.c.Num] <- m
   }
}

此处并不会开始处理消息的业务逻辑,而是去更新group的处理链表,将kafka的消息进行封装后,推送到各个worker的chan中

mergeproc 真实的处理消息业务

func (g *Group) mergeproc(mc <-chan *message) {
   ticker := time.NewTicker(time.Duration(g.c.Ticker))
   msgs := make([]interface{}, 0, g.c.Size)
   marks := make([]*message, 0, g.c.Size)
   for {
      select {
      case <-g.qc:
         return
      case msg := <-mc:
         msgs = append(msgs, msg.object)
         marks = append(marks, msg)
         if len(msgs) < g.c.Size {
            continue
         }
      case <-ticker.C:
      }
      if len(msgs) > 0 {
         g.Do(msgs)
         msgs = make([]interface{}, 0, g.c.Size)
      }
      if len(marks) > 0 {
         g.dc <- marks
         marks = make([]*message, 0, g.c.Size)
      }
   }
}

此处起了一个定时器,5s 去处理消息的业务逻辑,并推送到done队列中

如果堆积的消息达到阀值,则会开始处理消息的业务,并推送到done队列中

commit 提交最小offset

func (g *Group) commitproc() {
   commits := make(map[int32]*databus.Message)
   for {
      select {
      case <-g.qc:
         return
      case done := <-g.dc:
         // merge partitions to commit offset
         for _, d := range done {
            d.done = true
         }
         g.mu.Lock()
         for g.head != nil && g.head.done {
            cur := g.head
            commits[cur.data.Partition] = cur.data
            g.head = cur.next
            g.freeMessage(cur)
         }
         g.mu.Unlock()
         for k, m := range commits {
            m.Commit()
            delete(commits, k)
         }
      }
   }
}

从done队列中取出message, 并将其标记为done

遍历group的消息链表,并且为done的记录,直到最后一条,这一条即为最小的offset, 进行commit提交

源码: gitee.com/miko201728/…