最近在 B 站实习,mentor 在上线服务时实例出现 OOM 问题。记录一下
OOM
现象:出现 OOM 后,容器会自动重启,内存飙升,然后继续 OOM
因为线上出现问题,降低损耗是第一位。mentor 考虑到刚刚进行一次发布上线,怀疑是新代码内存泄露造成的 OOM,立刻进行回滚
但是回滚后,问题依旧存在,陷入重启、OOM、重启的无限循环
由于该服务主要是消费上游稿件指纹识别结果的 MQ,然后进行一系列侵权规则匹配的处理。所以 mentor 把消息 MQ 的速率进行降级,即消费 MQ 的速率进行限流,服务重启后逐渐恢复到正常的状态。在此之前,为了更好的排查分析问题,对重启后的容器生成内存的火焰图如下:
通过火焰图很明显可以推断出消耗大量内存的正是 loadVideoTortRuleMap
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
type Service struct {
videoTortRuleMap map[int64]*dmm.VideoTortRule
videoTortRulePropertyMap map[int64][]*dmm.VideoTortRuleProperty
}
// 消费每一条 MQ
func (s *Service) Process() {
...
if len(s.videoTortRuleMap) == 0 {
s.loadVideoTortRuleMap(ctx)
}
...
}
func (s *Service) loadVideoTortRuleMap(ctx context.Context) {
// select 读数据库
s.videoTortRuleMap = 数据库读取的数据
videoTortRulePropertyMap = 数据库读取的数据
}
|
其实很容易发现问题,当服务刚刚启动时,if len(s.videoTortRuleMap) == 0
判断为 true,会执行 loadVideoTortRuleMap
进行读取,把数据库加载到本地内存中。而由于服务器启动时,并发消费 MQ 将会导致 if len(s.videoTortRuleMap) == 0
多次判断为 true,并多次执行 loadVideoTortRuleMap
,该方法会扫描把整个表都读取加载到内存,多次加载过程中,导致了 OOM(规则表有 5 k 行,规则资产表有 18 万行)
既然该代码存在很久,为什么现在才会暴露呢?
mentor 说其实之前上线时实例会偶发 OOM,但重启后都没事。这次由于上线时刚好在跑任务,消费速度加快,并发度高,才暴露了这个问题
解决方法
由于本质问题是对并发访问没有做到互斥,所以加个 sync.Once 来达到只执行一次即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
type Service struct {
videoTortRuleMap map[int64]*dmm.VideoTortRule
videoTortRulePropertyMap map[int64][]*dmm.VideoTortRuleProperty
once sync.Once
}
// 消费每一条 MQ
func (s *Service) Process() {
...
if len(s.videoTortRuleMap) == 0 {
s.once.Do(s.loadVideoTortRuleMap(ctx))
}
...
}
|
其实这个是个历史遗留问题,应该在服务启动的时候就执行该方法,而不是消费 MQ 时才懒加载
本质问题:懒加载,解决:锁 + double check
其实在字节实习的时候也因为资源的并发懒加载踩过坑,所以懒加载其实是个坑,一般放在 main
函数进行饿汉式加载即可,才不会出现并发问题。如果真要懒加载,那么该加载函数一定要保证是并发安全的,很经典的 double check 问题
例如:单例模式懒汉式的 double check
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public class DatabaseConn {
private volatile static DatabaseConn databaseConn = null;
private DatabaseConn() {
}
public static DatabaseConn getInstance() {
// 第一次 check
if (databaseConn == null) {
synchronized (DatabaseConn.class) {
// 第二次 check
if (databaseConn == null) {
databaseConn = new DatabaseConn();
}
}
}
return databaseConn;
}
}
|
其实上面解决方法的 sync.Once.Do(func)
其实内部也是用来互斥锁+ double check 来实现快速校验和并发安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
type Once struct {
done uint32
m Mutex
}
func (o *Once) Do(f func()) {
// 第一次 check
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
// 第二次 check
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
|
后续
其实这种配置,一定会用到,应该放在 main 函数中进行加载,而不是懒加载。所以后续是提了一个 mr,把所有的配置加载,都在 main 函数加载一次
总结
End