不会飞的章鱼

熟能生巧,勤能补拙;念念不忘,必有回响。

由FFmpeg合成视频静帧引发的协程未关闭的解决方案

产生静帧的原因

视频的长度比音频短 ——> 为什么视频短? ——> 1,外部给的数据就少;2,内部合成的问题。

本文主要对7月底出现的一次静帧bug做详细的分析和给出的解决方案。

回顾整个任务流程

  • 开始任务:上层服务通知开始做任务时,我根据数据合成最终视频后,通过Redis数据库告知上层服务我已经做完任务了。
  • 结束任务:上层服务发送结束任务时,我立刻停掉所有正在进行的协程。

问题溯源

首先,我通过看日志发现:上层服务只调用了一次开始任务的请求,但我这里却重复做了任务,也就是一个人做了两次任务。

为什么呢?于是我根据日志查到源头,发现是这个循环检测redis key出了问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var redis_value string

for {

redis_value, _ = redis.String(c.Do("GET", redis_key))

Log.Info(GoID+": get redis value is: ", redis_value)
if len(redis_value) == 0 {
time.Sleep(time.Duration(15) * time.Second)
continue
} else {

var filelist FileList
Unerr := json.Unmarshal([]byte(redis_value), &filelist)
if Unerr != nil {
return
}

Log.Info(GoID+": unmarshal filelist: ", filelist)

break

}

}


然而本地测试后发现没问题,那么问题出在哪里呢?

后来我又看了日志,发现两个任务发的时间间隔很短,会不会是任务重了,导致同一个人开了两个协程,协程1在检测协程2已经生成的视频文件,结果视频还没有生成完,就送去做转场合成,导致最终的视频过短,从而产生静帧?

经过和上层服务的负责人沟通,确认他当时发第一次任务后忘记停止任务了,导致发第二次任务时,第一次任务还在循环检测redis key,最终两个任务一起做了。

于是他改了下代码,重新测试,但是问题还存在,这次主要集中在我这里,因为我发现即使上层服务发送了停止任务的请求,但我这里还是有协程在做检测redis key的活,所以最终问题确认为————发送了停止任务后,我这里没有及时关闭掉协程。

修改代码,使用sync.WaitGroup控制协程的数量:

person.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
wg := sync.WaitGroup{}  //很重要

stopdone := make(chan bool)
StopTaskChannelMapOp(taskid, "set", stopdone)
for _, person := range personredis {
go Task(stopdone,wg)
go func() {
wg.Add(1)
select {

case <-stopdone:
Log.Info("Stop Task", taskid)
StopTaskChannelMapOp(taskid, "delete", stopdone)

wg.Done()

return

}
}()
}

task.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func Task(stop chan bool,wg *sync.WaitGroup) {
wg.Add(1) //很重要
defer wg.Done() //很重要

var redis_value string

t := time.NewTicker(15 * time.Second)

for {

redis_value, _ = redis.String(c.Do("GET", redis_key))

if len(redis_value) == 0 {

select {
case <-stop:
//收到停止任务的请求后,直接结束这个协程
return

case <-t.C:
//睡15s后继续检测redis key
continue
}

continue
} else {
//unmarshal redis value
var filelist FileList
Unerr := json.Unmarshal([]byte(redis_value), &filelist)
if Unerr != nil {
return
}

Log.Info(GoID+": unmarshal filelist: ", filelist)

break

}

}

}

经过测试,收到停止任务后即可结束正在做任务的协程。

个人理解的sync.WaitGroup的用法

专业解释可以看官方文档或文章底部的参考资料,在此仅仅对我目前所用的WaitGroup做简单的理解,如有误差请及时提出来我好补充,感谢。

WaitGroup就像是一个计数器,开一个协程,Add就会让计数器加1,当协程运行完或中间退出时,Done就会减1,但当你开了很多协程,但又不知道什么时候协程会全部结束时,就需要Wait等待所有协程结束。

而对于一些必须要及时退出协程的情况(比如我上述锁描述的情况),就需要用channel通知协程关闭,所以一定要在所有协程可能会退出的情况记得Done,或者使用defer wg.Done,否则WaitGroup这个“计数器”不为1,它就会一直卡住。

总结

  • 遇到问题多想,多测试,多看日志,多分析;
  • 实在找不到问题,可以和相关的负责人员沟通,一起寻找突破口;
  • 一定要找到问题的本质,从根源解决,不要想着用一个应急方案先凑活,不然你会发现你的职业生涯就一直在和这类业务问题做斗争;
  • sync.WaitGroup是让协程优雅退出的一种解决方式,那么是否还有其他让协程退出的方法呢?
  • 所有看似匪夷所思的Bug,背后一定绕不开它的基础原理;
  • 最后感谢部门同事@yangfengyu和@chenhan的帮助,让我有get了一个新技能。

参考资料

简书_Golang并发:并发协程的优雅退出
你真的会用sync.WaitGroup吗
起风了_Golang等待组sync.WaitGroup的用法

------ 本文结束------
如果本篇文章对你有帮助,可以给作者加个鸡腿~(*^__^*),感谢鼓励与支持!