业务背景
公司上半年调度过来一批GPU卡服务于业务,业务侧需要利用这批GPU卡在夜间生成智能化数据 (白天这批GPU卡会服务于其他业务在晚上才会被调度回来)。
方案
方案:消费队列 + 延迟消费
业务大致流程:在需要触发其生成的业务场景(B)的链路上埋点往kafka中写入一条消息,消费方在白天阻塞消息,在晚上11点后解除阻塞进行消费处理。(ps:这里暂不考虑消息量级和限流问题)。
延迟消费控制器代码片段:
// DelayConsumerCtl 延迟消费控制器
type DelayConsumerCtl struct {
State int32 // 状态值: 0: 关闭(阻塞消费) 1: 打开 (不阻塞消费)
C chan int // 信号量
TimeSep TimeSep // 允许时间段
}
// NewDelayConsumerCtl 新建对象
func NewDelayConsumerCtl() *DelayConsumerCtl {
return &DelayConsumerCtl{
State: 0,
C: make(chan int),
}
}
// Open 打开
func (c *DelayConsumerCtl) Open(ctx context.Context) {
if c.inTimeSep(ctx) {
if c.State == 0 {
c.State = 1
c.C <- 1 // 释放信号
}
} else {
c.State = 0
}
}
// Pass 延迟控制能否消费(会阻塞消费
func (c *DelayConsumerCtl) Pass(ctx context.Context) {
log.InfoContext(ctx, "enter DelayConsumerCtl")
for !c.pass(ctx) {
log.InfoContextf(ctx, "pass DelayConsumerCtl")
time.Sleep(5 * time.Second)
}
}
// pass 延迟控制能否消费(会阻塞消费
func (c *DelayConsumerCtl) pass(ctx context.Context) bool {
switch c.State {
case 0: // 关闭
select { // 阻塞
case <-c.C: // 信号量
if c.inTimeSep(ctx) {
return true
}
}
case 1:
if c.inTimeSep(ctx) { // 时间段允许
return true
}
default:
}
// 如果不能访问则需要将状态重置
c.Reset()
return false
}
流程
sequenceDiagram
定时任务->>服务: 晚上11点定时任务触发(广播)调用open方法解除阻塞
服务-->>定时任务:
线上表现
项目上线后值守观察的现状,如下:
| 日期 | 业务表现 | 结果 |
|---|---|---|
| 第一天 | 11后正常消费 | 正常 |
| 第二天 | 11后 重平衡,持续0.5小时,后续正常消费 | 异常 |
| 第三天 | 11后 重平衡,持续1小时,后续正常消费 | 异常 |
| 第四天 | 11后 重平衡,持续0.5小时,后续正常消费 | 异常 |
.....
框架
sarama v1.29.1
其他因素
- 第二天晚上该服务有其他需求发布
思考
为什么一到11点kafka会触发重平衡呢?
kafka重平衡触发条件
-
有新的消费者加入消费者组。
-
当运行的消费者停止运行,离开消费者组。常见的情况如消费者重启,消费者应用崩溃,消费者进程上报的心跳超时等
-
分区数变动的时候(增加或删除)。
问题溯源
从上面kafka触发重平衡条件看 1和3 可以排除,只有2有可能导致重平衡,考虑到是夜晚11点触发任务,消费者没有重启和崩溃,那只能是心跳上报的问题。但之前看过sarama源码它的心跳上报是单独一个协程来处理,阻塞消费应该不会影响心跳上报。(代码片段如下)。第二天联系kafka平台的同事帮忙捞一下日志,发现在11点那个时间段有大量的 类似下面代码段输入的日志
info(s"Member ${member.memberId} in group ${group.groupId} has failed, removing it from the group")
从服务端日志不难看出,还是有对应的机器心跳上报导致的超时。服务端日志和我的理解存在出入,导致我又扒了sarama的源码重新看了一遍,整体下来也没有发现导致重平衡的点。后面从业务逻辑开始分析,刚开始分析一遍没有任何问题,后面不经意的一瞥发现的症结所在,就是无缓冲的chan导致的心跳超时,具体怎么导致的这里流程很复杂。
sarama 心跳上报
newConsumerGroupSession -> sess.heartbeatLoop()
func newConsumerGroupSession(ctx context.Context, parent *consumerGroup, claims map[string][]int32, memberID string, generationID int32, handler ConsumerGroupHandler) (*consumerGroupSession, error) {
// init offset manager
offsets, err := newOffsetManagerFromClient(parent.groupID, memberID, generationID, parent.client)
if err != nil {
return nil, err
}
// init context
ctx, cancel := context.WithCancel(ctx)
// init session
sess := &consumerGroupSession{
parent: parent,
memberID: memberID,
generationID: generationID,
handler: handler,
offsets: offsets,
claims: claims,
ctx: ctx,
cancel: cancel,
hbDying: make(chan none),
hbDead: make(chan none),
}
// start heartbeat loop
go sess.heartbeatLoop()
// create a POM for each claim
for topic, partitions := range claims {
for _, partition := range partitions {
pom, err := offsets.ManagePartition(topic, partition)
if err != nil {
_ = sess.release(false)
return nil, err
}
// handle POM errors
go func(topic string, partition int32) {
for err := range pom.Errors() {
sess.parent.handleError(err, topic, partition)
}
}(topic, partition)
}
}
// perform setup
if err := handler.Setup(sess); err != nil {
_ = sess.release(true)
return nil, err
}
// start consuming
for topic, partitions := range claims {
for _, partition := range partitions {
sess.waitGroup.Add(1)
go func(topic string, partition int32) {
defer sess.waitGroup.Done()
// cancel the as session as soon as the first
// goroutine exits
defer sess.cancel()
// consume a single topic/partition, blocking
sess.consume(topic, partition)
}(topic, partition)
}
}
return sess, nil
}
根因
业务上线时刻
由于业务上线延迟消费都是关闭(没有阻塞)所有服务滚动发布时kafka重平衡可以均衡,每台机器都能分配一个分区。
1day
晚上11点定时任务广播触发,定时任务逻辑会调用open方法,此时会往chan写数据并把状态设置,消费逻辑的阻塞会打开进行正常消息消费。
2day
在晚上11点之前,该服务的其他需求发布打破了kafka平衡状态触发重平衡 且 机器在滚动发布,这就会导致被阻塞的旧机器 在心跳上报的时候发现消费组在重平衡 从而导致心跳上报推出,新机器开始分配分区,可能1台机器分配多个分区。等所有的机器都发布完成的话,发现最终分区都会落在最新的机器上。等到晚上11点时,信号量发送,协程消费被唤醒,开始进行业务逻辑处理,处理完毕发现之前的订阅关系不存在 且 其他分区的消费还在阻塞中(无缓冲chan导致),导致该机器不能进行重平衡,有些机器之前分了一个分区,这时可以参与重平衡会发送joinGroup请求导致现有的消费组进行重平衡。
3day
由于机器都有之前订阅的协程阻塞,所以每次发送信号量就会唤醒之前的协程,如果正常全部唤醒就会重新出发点重平衡,周而复始,后续每天在11点都会出发重平衡。
总结
延迟控制器设计的缺陷,并发场景下设计的欠缺,这里如果改造的可以采用关闭chan的方法解除所有的阻塞。