Closed demiaowu closed 5 years ago
更新一下我们的分析进度:
(1)主要原因:
根据上面core dump和日志,我们发现死锁和新原因是Follower Abraft::LocalSnapshotCopier::copy_file
从session->join();
唤醒之后,如下代码调lck.lock()
,发现此lck
已经被当前pthread lock
过了,并且没有unlock
,那么造成下面调lck.lock()
拿不到锁,这样就死锁了。相当于一个bthread拿到了一把锁,没有释放又去拿这把锁,从而造成死锁,自己把自己给锁死了。
void LocalSnapshotCopier::copy_file(const std::string& filename) {
......
lck.unlock();
session->join();
lck.lock();
.......
这个bthread的调度看上去几乎不能避免这个问题
(2)死锁场景复现分析
结合coredump 和日志,我们给出一种死锁场景复现分析:
(2.1)Follower A下载快照,负责下载快照的bthread等待某个下载文件完成的之后切出去
第1步:I0603 11:26:01.204864 579067 node.cpp:2354] node 4294969292:10.182.26.0:8216:0 received InstallSnapshotRequest last_included_log_index=68275306 last_include_log_term=40 from 10.182.26.1:8217:0 when last_log_id=(index=67858560,term=23) 下载快照,进入copy_file
下载某个文件,session->join();
这个bthread切出去,切出去之后,已经将锁释放,这个时候,没有拿到任何的lock
(2.2)Follower A收到更大term的RequestVote,然后step down,执行打断interrupt_downloading_snapshot
的逻辑,日志如下:
I0603 11:28:00.075150 579082 node.cpp:1946] node 4294969292:10.182.26.0:8216:0 received RequestVote from 10.182.26.2:8206:0 in term 41 current_term 40
第1步:NodeImpl::handle_request_vote_request
处理投票请求,拿到NodeImpl 的mutex
第2步:NodeImpl::step_down
,执行打断当前正在下载的snapshot _snapshot_executor->interrupt_downloading_snapshot(term)
第3步:interrupt_downloading_snapshot拿到SnapshotExecutor的mutex,执行_cur_copier->cancel()
第4步:LocalSnapshotCopier::cancel拿到 LocalSnapshotCopier的mutex,调用_cur_session->cancel()
第5步:Session::cancel拿到 RemoteFileCopier的mutex,然后执行rpc::StartCancel(_rpc_call);取消session那边下载文件的rpc,通过ECANCELED错误和on_finished();,通过_finish_event.signal();唤醒(2.1)的session.join()
问题来,如果第5步,拿到了LocalSnapshotCopier和RemoteFileCopier等的mutex之后,切出去,并且(2.2)和(2.1)运行在同一个worker pthread空间上,那么(2.2)的第5步切换出去,(2.1)切换进来掉lck.lock()
,那么就死锁了。
实际上,从上面的堆栈来看,on_rpc_returned
、'copy_file'、handle_append_entries_request
、handle_election_timeout
就都会拿不到锁,从而最后将整个bthread的theadpool都锁死掉了
我们分析下这个case
分析得比较详细。看起来是这样的,按照bthread的调度,signal之后会唤醒join的bthread前台继续处理,然后自己成为一个background bthread,等待被重新调度。这时候,如果没有可用线程的话,会导致这个background bthread没有机会被调用到,从而lock没有释放。 这个问题在全用 bthread mutex 的时候几率会比较小,bthread mutex冲突的时候会自己排队。 单纯针对这个问题,貌似 signal 的动作需要在异步执行来避免这个问题。其它有 singnal的地方也需要排查下
这里整个流程上嵌套的锁有些多😓
其实也有可能就在brpc::StartCancel(_rpc_call);
里面就切出去了,因为StartCancel里面也有bthread_start_urgent
,这里也会导致切出去
void RemoteFileCopier::Session::cancel() {
BAIDU_SCOPED_LOCK(_mutex);
if (_finished) {
return;
}
brpc::StartCancel(_rpc_call);
if (bthread_timer_del(_timer) == 0) {
// Release reference of the timer task
Release();
}
if (_st.ok()) {
_st.set_error(ECANCELED, "%s", berror(ECANCELED));
}
on_finished();
}
嗯嗯,异步执行可以解决这个问题。不过感觉很多地方都会有这样的坑,特别在并发压力大的时候,还是有可能又类似上面的死锁,或者https://github.com/apache/incubator-brpc/issues/473 上面死锁的问题出现。
不知道你们有没有使用手册之类的,明确的list,告诉哪些操作容易造成死锁供我们参考下~
其实也有可能就在
brpc::StartCancel(_rpc_call);
里面就切出去了,因为StartCancel里面也有bthread_start_urgent
,这里也会导致切出去void RemoteFileCopier::Session::cancel() { BAIDU_SCOPED_LOCK(_mutex); if (_finished) { return; } brpc::StartCancel(_rpc_call); if (bthread_timer_del(_timer) == 0) { // Release reference of the timer task Release(); } if (_st.ok()) { _st.set_error(ECANCELED, "%s", berror(ECANCELED)); } on_finished(); }
嗯,对。这个彻底fix有点儿麻烦,依赖了bthread的调度。建议评估下,是不是直接先把 mutex 切换 bthread,百度内部的业务其实一直用的是 bthread mutex,这个的问题在于死锁的时候定位比较麻烦,所以开源版本默认用了 pthread mutex
主要之前看 https://github.com/brpc/braft/issues/69 讨论,后面评估下考虑将pthread mutex替换成bthread mutex测测~
StartCancel可以考虑移到锁外面,另外不建议在raft的用户接口牵涉到的实现里面直接使用bthread_mutex.
(1) 你们线上业务braft使用的是pthread mutex还是bthread mutex ?
(2)另外不建议在raft的用户接口牵涉到的实现里面直接使用bthread_mutex
,是不推荐braft使用bthread_mutex的意思吗?因为使用bthread mutex有其他的隐患吗 ?
StartCancel可以考虑移到锁外面,另外不建议在raft的用户接口牵涉到的实现里面直接使用bthread_mutex.
仅仅移到RemoteFileCopier的mutex外面还不够,需要移到LocalSnapshotCopier的mutex外面才行,因为现在死锁的地方就是在LocalSnapshotCopier的mutex上面
StartCancel可以考虑移到锁外面,另外不建议在raft的用户接口牵涉到的实现里面直接使用bthread_mutex.
其实其它类似的地方也有这个问题,要这样修改的话,需要整体排查一遍
StartCancel可以考虑移到锁外面,另外不建议在raft的用户接口牵涉到的实现里面直接使用bthread_mutex.
其实其它类似的地方也有这个问题,要这样修改的话,需要整体排查一遍
(1)signal 的动作需要在异步执行来避免这个问题
,这样做有内存的管理的问题,当前Session放在scoped_refptr
中的,一旦异步,那么处理的时候,Session的内存可能被释放掉了
(2)StartCancel可以考虑移到外面去,同时on_finished的里面的signal也需要异步化处理
(1) 你们线上业务braft使用的是pthread mutex还是bthread mutex ? (2)
另外不建议在raft的用户接口牵涉到的实现里面直接使用bthread_mutex
,是不推荐braft使用bthread_mutex的意思吗?因为使用bthread mutex有其他的隐患吗 ?
1) 线上业务用的是 bthread mutex; 2) @chenzhangyi 的意思应该是业务自己应该最好不要去直接去使用bthread mutex吧。pthread bthread 混用的时候比较容易失控,类似这里的这个 case
(1) 你们线上业务braft使用的是pthread mutex还是bthread mutex ? (2)
另外不建议在raft的用户接口牵涉到的实现里面直接使用bthread_mutex
,是不推荐braft使用bthread_mutex的意思吗?因为使用bthread mutex有其他的隐患吗 ?
- 线上业务用的是 bthread mutex;
- @chenzhangyi 的意思应该是业务自己应该最好不要去直接去使用bthread mutex吧。pthread bthread 混用的时候比较容易失控,类似这里的这个 case
这个问题在全用 bthread mutex 的时候几率会比较小
,使用bthread mutex,上面那段有问题的逻辑还是存在,只是不会造成全面的死锁(1) 你们线上业务braft使用的是pthread mutex还是bthread mutex ? (2)
另外不建议在raft的用户接口牵涉到的实现里面直接使用bthread_mutex
,是不推荐braft使用bthread_mutex的意思吗?因为使用bthread mutex有其他的隐患吗 ?
- 线上业务用的是 bthread mutex;
- @chenzhangyi 的意思应该是业务自己应该最好不要去直接去使用bthread mutex吧。pthread bthread 混用的时候比较容易失控,类似这里的这个 case
这个问题在全用 bthread mutex 的时候几率会比较小
,使用bthread mutex,上面那段有问题的逻辑还是存在,只是不会造成全面的死锁- 也就是说你们线上都没有pthread和bthread混用。但是就目前来看,业务使用pthread,raft里面使用bthread mutex,好像也没啥问题,看了下bthread mutex的代码,bthread mutex支持业务pthread直接调用。
分析得比较详细。看起来是这样的,按照bthread的调度,signal之后会唤醒join的bthread前台继续处理,然后自己成为一个background bthread,等待被重新调度。这时候,如果没有可用线程的话,会导致这个background bthread没有机会被调用到,从而lock没有释放。 这个问题在全用 bthread mutex 的时候几率会比较小,bthread mutex冲突的时候会自己排队。 单纯针对这个问题,貌似 signal 的动作需要在异步执行来避免这个问题。其它有 singnal的地方也需要排查下
代码中造成bthread 切换而又不能再次被调度时就会大概率死锁
用bthread mutex或者pthread mutex都会出现死锁,改成pthread_create貌似解决了
代码版本:release 版本 1.0.1 场景:一个进程有上百个落后的copyset,重启后并发恢复出现死锁,gdb堆栈如下:
通过gdb分析发现,确实存在死锁的现象: 现象(1)所有卡在
braft::LocalSnapshotCopier::copy_file
的thread,都在等LocalSnapshotCopier的_mutex,但是gdb发现这个pthread已经持有了这把锁,所以再次进入的时候会死锁,例如上面堆栈的Thead 14、18、21、22、69等 现象(2)所有卡在braft::RemoteFileCopier::Session::on_rpc_returned
的thread,都在等RemoteFileCopier的_mutex,但是gdb显示,这把锁都被卡在braft::LocalSnapshotCopier::copy_file
的线程持有了,类似的线程有Thread 16,它需要的锁已经被Thread 69持有,而Thread 69又死锁了,也就是卡在braft::LocalSnapshotCopier::copy_file
中这里根本的原因就是现象(1)死锁了,导致一连串的死锁,理论上在install snapshot的时候怎么会拿到LocalSnapshotCopier的_mutex之后又重新尝试拿这把锁?这跟bthread调度有关吗?感觉像是bthread拿到了_mutex,又切换出去,然后切换进来的新bthread,又去拿这把_mutex,从而造成了死锁。
不知道你们有遇到类似的问题没?附上部分日志: