MongoDB源码概述——日志
本篇文章主要介绍MongoDB的日志模块以及数据持久化存储模块的代码实现方式。大家也许会惊讶,为什么日志模块和持久化存储模块会放到一篇文章来总结。嘿嘿,在别的系统,可能这两个模块联系不是特别大,可是这MongoDB ,这两个模块还真不能分开来讲。这是怎么回事呢?请听我娓娓道来…通常说来,MongoDB具有三个日志模块,
Log
Journal
Oplog
Log: 位于log.h,它主要负责用户日志文件,这和我们普通系统的日志系统没有什么区别,作用也就是记录系统的一些重要流程,然后持久化到log文件。这个log文件可以通过系统启动参数"--logpath".
Journal: 位于dur.h,通过启动参数"--dur"启动该模块功能。主要用于解决因系统宕机时,内存中的数据未写入磁盘而造成的数据丢失(为什么数据会被放到内存做存储而不是直接对外存上的文件进行操作呢?这一点与MongoDB的存储机制有关,稍后会讲到)。其机制主要是通过log方式定时将操作日志(对数据库有更改的操作,查询不在记录范围之类)记录到dbpath的命名为journal文件夹下,这样当系统再次重启时从该文件夹下恢复丢失的数据。
Oplog :当部署应用于生产的健壮的服务器时,需要对服务器进行同步备份,MongoDB为解决这一问题提出了复制集(Replica sets)模式,而Oplog 的作用则主要是负责记录写服务器(一个复制集内只有一台服务器可写,多台备份服务器可读)上所有对数据的更改(查询等对数据库不产生更改的操作不会被记录),这样,复制集内的其他读扩展(即用于备份的机器和分散读压力的服务器)的服务器通过获取Oplog 就可以进行差异同步了。
本文主要是介绍日志和持久化存储,以及他们之间的关系。所以本文就不对Oplog做过多的说明,后续文章讲到复制集模块时,我一定会写上。本文的主要重点还是分析Journal以及持久化的实现,所以,对于Log模块,我也就只是简单的概括一下了。
Log模块:
当我们启动MongoDB,对Log模块的调用流程如下:
Main(...)->addWindowsOptions(...)->initLogging(...)->loggingManager.start(...);
之后会调用这样的代码来设置stdout的输出目标
FILE* tmp = freopen(_path.c_str(), (_append ? "a" : "w"), stdout);
又因为static的logfile指针指向stdout
FILE* Logstream::logfile = stdout;
所以在Logstream内最后数据会被flush到stdout,即系统所指定的目的地.
在log.h下有如下定义:
1 enum LogLevel { LL_DEBUG , LL_INFO , LL_NOTICE , LL_WARNING , LL_ERROR , LL_SEVERE };
2 inline Nullstream& log( int level ) {
3 if ( level > logLevel )
4 return nullstream;
5 return Logstream::get().prolog();
6 }
7
8 inline Nullstream& log() {
9 return Logstream::get().prolog();
10 }
又因为Logstream重载了一些基本的流符号:
Logstream& operator<<(const char *x) { ss << x; return *this; }
Logstream& operator<<(const string& x) { ss << x; return *this; }
Logstream& operator<<(const StringData& x) { ss << x.data(); return *this; }
Logstream& operator<<(char *x) { ss << x; return *this; }
…
Logstream& operator<< (ostream& ( *_endl )(ostream&)) {
ss << '\n';
flush(0);
return *this;
}
Logstream& operator<< (ios_base& (*_hex)(ios_base&)) {
ss << _hex;
return *this;
}
所以,我们可以轻松的使用下面的操作来记录我们的日志。
log() << "WARNING: alloc() failed after allocating new extent. lenWHdr: "<<endl;
我不知道大家是否喜欢我这样的分析模式,我是挺喜欢的,节奏快,很直接,很靠谱!
这里做一下简要说明<<重载运算符方法将要输出的日志放入stringstream的,接着调用<<endl时,触发上面列出来的Logstream& operator<< (ostream& ( *_endl )(ostream&))方法,间接调用flush(0)
void Logstream::flush(Tee *t)方法的职责就是将stringstream内缓存的所有日志进行持久化到logfile.因为flush这部分的代码也非常的简洁易懂,所以这里就不贴了。至此用户日志也被写到了外存上,基本功能已经完成。
在flush中另外值得注意是与Tee相关的代码
if( t ) t->write(logLevel,out);
if ( globalTees ) {
for ( unsigned i=0; i<globalTees->size(); i++ )
(*globalTees)[i]->write(logLevel,out);
}
关于Tee的定义:
class Tee {
public:
virtual ~Tee() {}
virtual void write(LogLevel level , const string& str) = 0;
};
所以我们可以很清晰的认识到,实际上Tee的职责是订阅日志信息(观察者设计模式),任何Tee的派生类都可以实现在不影响现有日志的情景下将日志额外的记录到其他任何地方。例如远程日志.或者在服务器很多的情况下,收集各个服务器的用户日志放入数据库,以供管理员查看.
Journal模块:
实际上在MongoDB中,Journal\Durability是一个很大的模块,牵扯到的东西也是非常之多,他的设计初衷是为了使用日志的方式来提高单机数据的可靠性,在1.7版本的最新分支上首次出现了这个部分.具体他的职责可以用一句话来概括:
通过log方式定时将操作日志(对数据库有更改的操作,查询不在记录范围之类)记录到dbpath的命名为journal文件夹下,这样当系统再次重启时从该文件夹下恢复丢失的数据。
根据其完成的功能,我们可以将这个部分的实现概括为以下几个问题:
何时调用
如何记录用户操作
如何序列化用户操作并持久化
如何根据现有Journal日志恢复数据
下面我们来一一根据源码分析其重要步骤:
一.何时调用
当我们需要更改数据库时,需要记录下用户的操作以及用户更改后的数据,这些记录的数据将是进行恢复时的数据源。举一个例子,我们向数据库插入一条记录的时候,我们需要记录用户的操作以及操作的数据,我截取了这部分代码,如下:
r = (Record*) getDur().writingPtr(r, lenWHdr);//持久化插入记录信息
...
if( obuf )
memcpy(r->data, obuf, len);//直接拷贝数据到记录字段
我们来看DurableImpl内的几个重要方法:
//告诉系统我正在往x位置写入数据(更改或插入时丢会调用)
void* writingPtr(void *x, unsigned len);
//告诉系统我创建了一个文件
void createdFile(string filename, unsigned long long len);
其实调用上两个函数的潜台词就是,我现在干了什么事,你给我把他完整的记录下来,如果我这件事没有最终被保存到磁盘的话,我就需要你这个负责记录的模块拿出原来所做的记录,我来进行恢复操作,以确保数据万无一失!
二.如何记录用户操作
在这个模块,用户的操作类型实际上可以归类为两种,一种是基本写操作,一种是非基本写操作。对数据的新增和修改等都可以认为是写操作,而类似与创建文件(FileCreatedOp),删除数据库(DropDbOp)操作都是非基本写操作,这类操作建模为DurOp.最终这两种操作都会在CommitJob:: note()与CommitJob::noteOp()进行内存存储。
基本写操作会被D结构体封装,我们来看下他的结构:
struct D {
void *p;//用户更改的数据源首地址
unsigned len;//用户更改的数据长度
static void go(const D& d);
};
基本写记录会被存储到Writes类在CommitJob类的实例_wi,继而存储到TaskQueue<D>在Writes类的实例_deferred.
非基本写记录会被存储到Writes类的vector< shared_ptr<DurOp> > _ops;
这个流程有很多个类参与,下面用两张顺序图来总结这一流程。
调用getDur().writingPtr的时序图
调用g