踩坑记:临界区内要小心


这周对一个服务进行了升级,结果踩了一个不大不小的坑。

先介绍下这个服务的背景:

这是一个数据接收的服务,通过http协议接收到json数据之后进行解析,然后落地到本地文件;
之后再由其他服务读取这些文件,进行后续的处理。

最开始的实现是会把接收到的数据保存在一个成员变量里,然后等积攒到一定数据量后,写入一个文件内。

这样做最简单,但是有这么一个问题:

如果服务器重启,那么当前hold在内存中的这些数据,就永久的丢失了。  

所以这一版呢,我就把它改成了实时append写入文件的方式,写够一定的条目数,就切换一个文件来写。

既然要实时落地,那么这里在接受数据的线程和落地文件的线程之间,就需要一个数据队列来作为缓冲。

针对这个数据队列的操作,则应该加锁避免竞态出现。

那么接受数据的伪代码如下:

1
2
3
4
5
6
// Thread receiver
ConstructContentFromJson(json_value, &content_elem);
{
MutexLocker locker(&mutex_);
contents_.emplace(std::move(content_elem));
}

而落地文件的伪代码则是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Thread writer
while(!quit_) {
TryWriteContent();
}

//...

int TryWriteContent() {
ContentElem content_elem;
{
MutexLocker locker(&mutex_);
if (contents_.empty) {
usleep(1000);
return ERROR_NO_ELEM;
}
content_elem = contents_.pop_front();
}
return DoWriteContent(content_elem);
}

看上去一切都很合理:

  • 接受到数据,就在锁的保护下塞到队列中;
  • 写数据时,则在锁的保护下拿出一个数据来拷贝到局部变量,然后就可以放心写入文件了;
  • 如果队列中没数据可获取了,那就原地等待1ms。

临界区也基本控制的很小,应该也没有性能问题。

DUANG!!!

但是,当我升级完后台服务器群中的一台后,看监控,却发现请求量在更新服务器之后,暴跌了90%。

但是问题是,我这才只更新了一台服务器啊。还有好几台没更新,跑的也是旧版本程序,它们上面接收到的请求也暴跌了。

那一定不是我的锅!

于是我向前去查了nginx log,发现确实nginx接收到的请求数量就少了很多。

难道是服务的调用方也同时发了新版本?

于是赶紧电话联系千里之外的网友同事,查出来的结果是:

由于服务超时严重,所以调用方主动限流了。

那看来还是我的锅……是你的,总是逃不掉。毕竟调用方看来,nginx代理屏蔽了后台所有服务器细节,所以后台一台服务器超时严重,调用方就会认为整个服务超时严重。

再回头去看上面的核心代码,问题很快就浮出水面了:

usleep放在了锁的临界区范围内!

这会导致写文件线程在没数据时,一直占据着锁。虽然这个线程在sleep,但是它却占据着锁,其他线程也没办法往队列里填充数据,等于这部分时间服务啥也干不了。多来几次,就大面积超时了。

问题找到了,解决方法也很简单,把usleep挪到锁外即可。

总结

  • 锁的临界区内,一定不能出现sleep这种阻塞操作(包括但不限于文件IO、网络IO等)。
  • 需要sleep时,可以考虑主动将线程的控制权让出,从而避免使用sleep。



推荐阅读:
使用双buffer无锁化
不要拷贝
读写锁的性能一定更好吗

转载请注明出处: http://blog.guoyb.com/2018/07/28/mutex-sleep/

欢迎使用微信扫描下方二维码,关注我的微信公众号TechTalking,技术·生活·思考:
后端技术小黑屋

Comments