|
| 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