Skip to content

Commit bb2cc92

Browse files
author
yangjingjing
committed
init
1 parent 146e12c commit bb2cc92

15 files changed

+5039
-2
lines changed

_posts/2018-03-09-Mongo源码Command体系.md

Lines changed: 437 additions & 0 deletions
Large diffs are not rendered by default.

_posts/2018-03-09-Mongo源码主从.md

Lines changed: 314 additions & 0 deletions
Large diffs are not rendered by default.

_posts/2018-03-09-Mongo源码内存文件映射.md

Lines changed: 437 additions & 0 deletions
Large diffs are not rendered by default.

_posts/2018-03-09-Mongo源码分布式锁.md

Lines changed: 370 additions & 0 deletions
Large diffs are not rendered by default.

_posts/2018-03-09-Mongo源码删除记录.md

Lines changed: 425 additions & 0 deletions
Large diffs are not rendered by default.

_posts/2018-03-09-Mongo源码均衡Balancer.md

Lines changed: 429 additions & 0 deletions
Large diffs are not rendered by default.

_posts/2018-03-09-Mongo源码插入记录.md

Lines changed: 883 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
---
2+
layout: post
3+
categories: [Mongodb]
4+
description: none
5+
keywords: MongoDB
6+
---
7+
# Mongo源码日志持久化
8+
mongodb会在系统启动同时,初始化了日志持久化服务,该功能貌似是1.7版本后引入到系统中的,主要用于解决因系统宕机时,内存中的数据未写入磁盘而造成的数据丢失。
9+
10+
## 日志持久化
11+
日志持久化其机制主要是通过log方式定时将操作日志(如cud操作等)记录到db的journal文件夹下,这样当系统再次重启时从该文件夹下恢复丢失的(内存)数据。
12+
13+
也就是在_initAndListen()函数体(db.cpp文件第511行)中下面这一行代码:
14+
```
15+
dur::startup();
16+
```
17+
今天就以这个函数为起点,看一下mongodb的日志持久化的流程,及实现方式。
18+
19+
在Mongodb中,提供持久化的类一般都以dur开头,比如下面几个:
20+
- dur.cpp:封装持久化主要方法和实现,以便外部使用
21+
- dur_commitjob.cpp:持久化任务工作(单元),封装延时队列TaskQueue < D > ,操作集合vector < shared_ptr < DurOp > > 等
22+
- dur_journal.cpp:提供日志文件 / 路径,创建,遍历等操作
23+
- dur_journalformat.h:日志文件格式定义
24+
- dur_preplogbuffer.cpp:构造用于输出的日志buffer
25+
- dur_recover.h:日志恢复类(后台任务方式BackgroupJob)
26+
- dur_stats.h:统计类,包括提交 / 同步数据次数等
27+
- dur_writetodatafiles.cpp:封装写入数据文件mongofile方法
28+
- durop.h:持久化操作类,提供序列化,创建操作(FileCreatedOp),DROP操作(DropDbOp)
29+
30+
首先我们看一下dur::startup()方法实现(dur.cpp),如下:
31+
```
32+
/* * at startup, recover, and then start the journal threads */
33+
void startup() {
34+
if ( ! cmdLine.dur ) /* 判断命令行启动参数是否为持久化 */
35+
return ;
36+
37+
DurableInterface::enableDurability(); // 对持久化变量 _impl 设置为DurableImpl方式
38+
39+
journalMakeDir(); /* 构造日志文件所要存储的路径:dur_journal.cpp */
40+
try {
41+
recover(); /* 从上一次系统crash中恢复数据日志信息:dur_recover.cpp */
42+
}
43+
catch (...) {
44+
log() << " exception during recovery " << endl;
45+
throw ;
46+
}
47+
48+
preallocateFiles();
49+
50+
boost::thread t(durThread);
51+
}
52+
```
53+
注意:上面的DurableInterface,因为mongodb使用类似接口方式,从而约定不同的持久化方式实现,如下:
54+
```
55+
class DurableInterface : boost::noncopyable {
56+
virtual void * writingPtr( void * x, unsigned len) = 0 ;
57+
virtual void createdFile( string filename, unsigned long long len) = 0 ;
58+
virtual void declareWriteIntent( void * x, unsigned len) = 0 ;
59+
virtual void * writingAtOffset( void * buf, unsigned ofs, unsigned len) = 0 ;
60+
....
61+
}
62+
```
63+
接口定义了写文件的方式及方法等等。
64+
65+
并且mongodb包括了两种实现方式,即:
66+
```
67+
class NonDurableImpl : public DurableInterface{ /* 非持久化,基于内存临时存储 */
68+
}
69+
70+
class DurableImpl : public DurableInterface { /* 持久化,支持磁盘存储 */
71+
}
72+
```
73+
再回到startup函数最后一行:boost::thread t(durThread);
74+
75+
该行代码会创建一个线程来运行durThread方法,该方法就是持久化线程,如下:
76+
```
77+
void durThread() {
78+
Client::initThread( " dur " );
79+
const int HowOftenToGroupCommitMs = 90 ; /* 多少时间提交一组信息,单位:毫秒 */
80+
// 注:commitJob对象用于封装并执行提交一组操作
81+
while ( ! inShutdown() ) {
82+
sleepmillis( 10 );
83+
CodeBlock::Within w(durThreadMain); /* 定义代码块锁,该设计很讨巧,接下来会介绍 */
84+
try {
85+
int millis = HowOftenToGroupCommitMs;
86+
{
87+
stats.rotate(); // 统计最新的_lastRotate信息
88+
{
89+
Timer t; /* 声明定时器 */
90+
/* 遍历日志文件夹下的文件并更新文件的“最新更新时间”标志位并移除无效或关闭之前使用的日志文件:dur_journal.cpp */
91+
journalRotate();
92+
millis -= t.millis(); /* 线程睡眠时间为90减去遍历时间 */
93+
assert( millis <= HowOftenToGroupCommitMs );
94+
if ( millis < 5 )
95+
millis = 5 ;
96+
}
97+
98+
// we do this in a couple blocks, which makes it a tiny bit faster (only a little) on throughput,
99+
// but is likely also less spiky on our cpu usage, which is good:
100+
sleepmillis(millis / 2 );
101+
// 从commitJob的defer任务队列中获取任务并执行,详情参见: taskqueue.h的invoke() 和 dur_commitjob.cpp 的
102+
// Writes::D::go(const Writes::D& d)方法(用于非延迟写入信息操作)
103+
commitJob.wi()._deferred.invoke();
104+
105+
sleepmillis(millis / 2 );
106+
// 按mongodb开发者的理解,通过将休眠时间减少一半(millis/2)并紧跟着继续从队列中取任务,
107+
// 以此小幅提升读取队列系统的吞吐量
108+
commitJob.wi()._deferred.invoke();
109+
}
110+
111+
go(); // 执行提交一组信息操作
112+
}
113+
catch (std::exception & e) { /* 服务如果突然crash */
114+
log() << " exception in durThread causing immediate shutdown: " << e.what() << endl;
115+
abort(); // based on myTerminate()
116+
}
117+
}
118+
cc().shutdown(); // 关闭当前线程,Client::initThread("dur")
119+
}
120+
```
121+
122+
下面是go()的实现代码:
123+
```
124+
static void go() {
125+
if ( ! commitJob.hasWritten() ){ /* hasWritten一般在CUD操作时会变为true,后面会加以介绍 */
126+
commitJob.notifyCommitted(); /* 发送信息已存储到磁盘的通知 */
127+
return ;
128+
}
129+
{
130+
readlocktry lk( "" , 1000 ); /* 声明读锁 */
131+
if ( lk.got() ) {
132+
groupCommit(); /* 提交一组操作 */
133+
return ;
134+
}
135+
}
136+
137+
// 当未取到读锁时,可能获取读锁比较慢,则直接使用写锁,不过写锁会用更多的RAM
138+
writelock lk;
139+
groupCommit();
140+
}
141+
```
142+
143+
```
144+
/* * locking: in read lock when called. */
145+
static void _groupCommit() {
146+
stats.curr -> _commits ++ ; /* 提交次数加1 */
147+
148+
......
149+
// 预定义页对齐的日志缓存对象,该对象对会commitJob.ops()的返回值(该返回值类型vector< shared_ptr<DurOp> >)进行对象序列化
150+
// 并保存到commitJob._ab中,供下面方法调用,位于dur_preplogbuffer.cpp-->_PREPLOGBUFFER()方法
151+
PREPLOGBUFFER();
152+
// todo : write to the journal outside locks, as this write can be slow.
153+
// however, be careful then about remapprivateview as that cannot be done
154+
// if new writes are then pending in the private maps.
155+
WRITETOJOURNAL(commitJob._ab); /* 写入journal信息,最终操作位于dur_journal.cpp的 Journal::journal(const AlignedBuilder& b)方法 */
156+
157+
// data is now in the journal, which is sufficient for acknowledging getLastError.
158+
// (ok to crash after that)
159+
commitJob.notifyCommitted();
160+
161+
WRITETODATAFILES(); /* 写信息到mongofile文件中 */
162+
163+
commitJob.reset(); /* 重置当前任务操作 */
164+
165+
// REMAPPRIVATEVIEW
166+
// remapping 私有视图必须在 WRITETODATAFILES 方法之后调用,否则无法读出新写入的数据
167+
DEV assert( ! commitJob.hasWritten() );
168+
if ( ! dbMutex.isWriteLocked() ) {
169+
// this needs done in a write lock (as there is a short window during remapping when each view
170+
// might not exist) thus we do it on the next acquisition of that instead of here (there is no
171+
// rush if you aren't writing anyway -- but it must happen, if it is done, before any uncommitted
172+
// writes occur). If desired, perhpas this can be eliminated on posix as it may be that the remap
173+
// is race-free there.
174+
//
175+
dbMutex._remapPrivateViewRequested = true ;
176+
}
177+
else {
178+
stats.curr -> _commitsInWriteLock ++ ;
179+
// however, if we are already write locked, we must do it now -- up the call tree someone
180+
// may do a write without a new lock acquisition. this can happen when MongoMMF::close() calls
181+
// this method when a file (and its views) is about to go away.
182+
//
183+
REMAPPRIVATEVIEW();
184+
}
185+
}
186+
```
187+
到这里只是知道mongodb会定时从任务队列中获取相应任务并统一写入,写入journal和mongofile文件后再重置任务队列及递增相应统计计数信息(如privateMapBytes用于REMAPPRIVATEVIEW)。
188+
189+
但任务队列中的操作信息又是如何生成的呢?这个比较简单,我们只要看一下相应的cud数据操作时的代码即可,这里以插入(insert)数据为例:
190+
191+
我们找到pdfile.cpp文件的插入记录方法,如下(1467行):
192+
```
193+
DiskLoc DataFileMgr::insert( const char * ns, const void * obuf, int len, bool god, const BSONElement & writeId, bool mayAddIndex) {
194+
......
195+
196+
r = (Record * ) getDur().writingPtr(r, lenWHdr); // 位于1588行
197+
```
198+
该方法用于将客户端提交的数据(信息)写入到持久化队列(defer)中去,如下(按函数调用顺序):
199+
```
200+
void * DurableImpl::writingPtr( void * x, unsigned len) {
201+
void * p = x;
202+
declareWriteIntent(p, len);
203+
return p;
204+
}
205+
206+
void DurableImpl::declareWriteIntent( void * p, unsigned len) {
207+
commitJob.note(p, len);
208+
}
209+
210+
void CommitJob::note( void * p, int len) {
211+
DEV dbMutex.assertWriteLocked();
212+
dassert( cmdLine.dur );
213+
if ( ! _wi._alreadyNoted.checkAndSet(p, len) ) {
214+
MemoryMappedFile::makeWritable(p, len); /* 设置可写入mmap文件的信息 */
215+
216+
if ( ! _hasWritten ) {
217+
assert( ! dbMutex._remapPrivateViewRequested );
218+
219+
// 设置写信息标志位, 用于进行_groupCommit(上面提到)时进行判断
220+
_hasWritten = true ;
221+
}
222+
......
223+
224+
// 向defer任务队列中加入操作信息
225+
_wi.insertWriteIntent(p, len);
226+
wassert( _wi._writes.size() < 2000000 );
227+
assert( _wi._writes.size() < 20000000 );
228+
229+
......
230+
}
231+
```
232+
其中insertWriteIntent方法定义如下:
233+
```
234+
void insertWriteIntent( void * p, int len) {
235+
D d;
236+
d.p = p; /* 操作记录record类型 */
237+
d.len = len; /* 记录长度 */
238+
_deferred.defer(d); /* 延期任务队列:TaskQueue<D>类型 */
239+
}
240+
```
241+
到这里总结一下,mongodb在启动时,专门初始化一个线程不断循环(除非应用crash掉),用于在一定时间周期内来从defer队列中获取要持久化的数据并写入到磁盘的journal(日志)和mongofile(数据)处,当然因为它不是在用户添加记录时就写到磁盘上,所以按mongodb开发者说,它不会造成性能上的损耗,因为看过代码发现,当进行CUD操作时,记录(Record类型)都被放入到defer队列中以供延时批量(groupcommit)提交写入,但相信其中时间周期参数是个要认真考量的参数,系统为90毫秒,如果该值更低的话,可能会造成频繁磁盘操作,过高又会造成系统宕机时数据丢失过多。
242+
243+
最后对文中那个mongodb设置很计巧的代码做一下简要分析,代码如下:
244+
```
245+
CodeBlock::Within w(durThreadMain);
246+
```
247+
248+
它的作为就是一个对多线程访问指定代码块加锁的功能,其类定义如下(位于race.h):
249+
```
250+
class CodeBlock {
251+
volatile int n;
252+
unsigned tid;
253+
void fail() {
254+
log() << " synchronization (race condition) failure " << endl;
255+
printStackTrace();
256+
abort(); /**/
257+
}
258+
void enter() {
259+
if ( ++ n != 1 ) fail(); /* 当已有线程执行该代码块时,则执行fail */
260+
#if defined(_WIN32)
261+
tid = GetCurrentThreadId();
262+
#endif
263+
}
264+
void leave() { /* 只有调用 leave 操作,才会--n,即在线程执行完该代码块时调用 */
265+
if ( -- n != 0 ) fail();
266+
}
267+
public :
268+
CodeBlock() : n( 0 ) { }
269+
270+
class Within {
271+
CodeBlock & _s;
272+
public :
273+
Within(CodeBlock & s) : _s(s) { _s.enter(); }
274+
~ Within() { _s.leave(); }
275+
};
276+
277+
void assertWithin() {
278+
assert( n == 1 );
279+
#if defined(_WIN32)
280+
assert( GetCurrentThreadId() == tid );
281+
#endif
282+
}
283+
};
284+
285+
#else
286+
```
287+
通过其内部类Within的构造函数和析构函数,分别调用了_s.enter,_s.leave()方法,这样只要在一个代码块之前定义一个该类实例,则从下一行开始到codeblock结束之后,该进程内只允许一个线程执行该代码块,呵呵。
288+
289+
290+
291+
292+
293+
294+
295+
296+
297+

0 commit comments

Comments
 (0)