本无意写这篇文章,但是之前在分析replset时有一个线程一直没弄明白其作用,后来偶然间在阅读tags时搞明白了notifierthread的作用,tags的实现过程很隐晦.不仔细阅读,很难弄明白,所以这里专门写一篇文章来分析replset tags的实现.首先来看看一份带tags的replset config.
这里的dc可以理解为datacenter或者任何自己觉得好理解的词,dc后的ny sf cloud可以理解为不同的服务器,settings下面的veryImportant中dc:3,表示在调用getLastError调用如:
db.runCommand({getLastError:1,w:"veryImportant"})
这个调用表示getLastError命令要等带上一个操作完成veryImportant指定的将上一个操作更改到3个不同的datacenter中才会返回,这里对应ny sf cloud.那么这里是怎么实现的呢,这就是这篇文章探讨的内容.下面先介绍下本文将会遇到的一些内容.
collection: local.me: 这个collection只保留一个OID,这个OID类型和mongodb中每一条数据中的_id是一样的,是自动生成的.生成了后将其保存到local.me中,以后就不需要再次生成了,读取就行了.这个OID就是来标识这台服务器的,在secondary同步另外一台服务器数据时notifierThread将作为握手协议将这个值发送给对方,用来标识自己.
collection: local.slaves: 用来保存同步自己的服务器当前同步到的操作日志时间戳.
thread: notifierThread: 告诉同步服务器自己当前同步到的操作日志时间戳.
下面简单描述一下tags的执行流程.
1. 在设置replset配置时首先会解析settings中的getLastErrorMode,和members中的tags,将其分组.
2. secondary的producethread选取一个服务器(这里姑且叫做A)同步数据,向其发出连接请求,其连接请求是不需要握手的,当这里produceThread选取了同步对象A后,notifierThread也将去连接A,并且需要握手,就是将自己的表示OID发送给对方,对方拿到自己的OID以及members中的_id将建立一个结构记录和更新这台服务器(姑且叫做B)当天同步到的时间点.
3. 当secondary向服务器请求操作日志并replay后将会更新本地的lastwriten时间,这时notifierThread就将向服务器发送请求,同样是请求数据,但是因为其之前带了握手协议,在服务端处理该请求的连接线程中保存了OID,这里服务端就会认为是notifierThread发过来的消息,表示B服务器已经同步到了notifierThread上一次请求操作日志时最后一条操作日志的时间戳.
4. 这里就可以根据OID以及_id和这个时间戳修改这台服务器同步到的时间戳,然后根据1中该服务器的tags中的分组情况,更新该服务器所在的组中的同步时间戳.
5. 调用getLastError时首先就可以得到本机的上一条操作的时间戳(这里姑且叫做H1),找到getLastError中的w,得到其中的值,根据其值得到replset config中settings.getLastErrorModes中对应的规则,然后处理其中的规则,找出其中的每一条规则(这里叫做C1....Cn)如C1,找到C1规则需要满足的分组中的时间戳(H2),找出所有的规则中的时间戳取其最小值,表示当前规则同步到的时间戳(H3),将H1与H3比较,H1<=H3表示操作已经按照要求完成(举例中已经写到了三个datacenter的服务器上),否则表示规则没满足,则getLastError命令睡眠等待,一段时间醒来检查一下,直到满足要求才返回(getLastError命令可以设置超时).
下面进入代码分析,先来看看replset config的处理,其处理代码为repl\rs_config.cpp的parseRules.
void ReplSetConfig::parseRules(const BSONObj& modes) {
map<string,TagClause> tagMap;
_populateTagMap(tagMap);//将members中的tags归类
for (BSONObj::iterator i = modes.begin(); i.more(); ) {
unsigned int primaryOnly = 0;
// ruleName : {dc : 2, m : 3}
BSONElement rule = i.next();
TagRule* r = new TagRule();
BSONObj clauseObj = rule.Obj();
for (BSONObj::iterator c = clauseObj.begin(); c.more(); ) {
BSONElement clauseElem = c.next();
// get the clause, e.g., "x.y" : 3
const char *criteria = clauseElem.fieldName();//得到每一条规则中的子句
int value = clauseElem.numberInt();
TagClause* node = new TagClause(tagMap[criteria]);
int numGroups = node->subgroups.size();//当前存在的subgroup数要大于规则中指定的数目,要不无法满足要求
uassert(14831, str::stream() << "mode " << clauseObj << " requires "
<< value << " tagged with " << criteria << ", but only "
<< numGroups << " with this tag were found", numGroups >= value);
node->name = criteria;
node->target = value;
// if any subgroups contain "me", we can decrease the target
node->actualTarget = node->target;
// then we want to add pointers between clause & subgroup
for (map<string,TagSubgroup*>::iterator sgs = node->subgroups.begin();
sgs != node->subgroups.end(); sgs++) {
bool foundMe = false;
(*sgs).second->clauses.push_back(node);//记录每一个subgroup上的规则,当subgroup时间戳更新后能够找到相应规则
// if this subgroup contains the primary, it's automatically always up-to-date
for( set<MemberCfg*>::const_iterator cfg = (*sgs).second->m.begin();
cfg != (*sgs).second->m.end();
cfg++)
{
if ((*cfg)->h.isSelf()) {
node->actualTarget--;
foundMe = true;
}
}
scoped_lock lk(groupMx);//foundme为true,表示这个subgroup已经更新了,需要检查其他
for (set<MemberCfg *>::iterator cfg = (*sgs).second->m.begin();//group是否更新,所以这里在memcfg中插入
!foundMe && cfg != (*sgs).second->m.end(); cfg++) {//TagSubgroup时是在!foundme的情况下
(*cfg)->groupsw().insert((*sgs).second);
}
}
// if all of the members of this clause involve the primary, it's always up-to-date
if (node->actualTarget == 0) {
node->last = OpTime(INT_MAX, INT_MAX);
primaryOnly++;
}
// this is a valid clause, so we want to add it to its rule
node->rule = r;
r->clauses.push_back(node);
}
// if all of the clauses are satisfied by the primary, this rule is trivially true
if (primaryOnly == r->clauses.size()) {
r->last = OpTime(INT_MAX, INT_MAX);
}
rules[rule.fieldName()] = r;
}
}
parseRules->_populateTagMap
void ReplSetConfig::_populateTagMap(map<string,TagClause> &tagMap) {
// create subgroups for each server corresponding to each of
// its tags. E.g.:
// A is tagged with {"server" : "A", "dc" : "ny"}
// B is tagged with {"server" : "B", "dc" : "ny"}
// At the end of this step, tagMap will contain:
// "server" => {"A" : [A], "B" : [B]}
// "dc" => {"ny" : [A,B]}
for (unsigned i=0; i<members.size(); i++) {
MemberCfg member = members[i];
for (map<string,string>::iterator tag = member.tags.begin(); tag != member.tags.end(); tag++) {
string label = (*tag).first;
string value = (*tag).second;
TagClause& clause = tagMap[label];
clause.name = label;
TagSubgroup* subgroup;
// search for "ny" in "dc"'s clause
if (clause.subgroups.find(value) == clause.subgroups.end()) {//有同一个值如dc:ny,这里ny表示其对应的服务器在
clause.subgroups[value] = subgroup = new TagSubgroup(value);//同一个subgroup中,subgroup中一台服务器时间戳更新
}//表明这个subgroup时间戳更新.
else {
subgroup = clause.subgroups[value];
}
subgroup->m.insert(&members[i]);//记录subgroup中的成员.
}
}
}
下面来看看notiferThread:
void BackgroundSync::notifierThread() {//只干一件事情,从服务端读出操作时间戳,读到lastOpTimeWritten后就进入等待状态
Client::initThread("rsSyncNotifier");
while (!inShutdown()) {
bool clearTarget = false;
MemberState state = theReplSet->state();
if (state.primary() || state.fatal() || state.startup()) {
sleepsecs(5);
continue;
}
try {
{
boost::unique_lock<boost::mutex> lock(_lastOpMutex);
while (_consumedOpTime == theReplSet->lastOpTimeWritten) {
_lastOpCond.wait(lock);
}
}
markOplog();
}
}
cc().shutdown();
}
继续看markOplog
void BackgroundSync::markOplog() {
if (!hasCursor()) {//连接服务器,得到游标
sleepsecs(1);
return;
}
if (!_oplogMarker.moreInCurrentBatch()) {
_oplogMarker.more();
}
if (!_oplogMarker.more()) {
_oplogMarker.tailCheck();
sleepsecs(1);
return;
}
// if this member has written the op at optime T, we want to nextSafe up to and including T
while (_consumedOpTime < theReplSet->lastOpTimeWritten && _oplogMarker.more()) {//更新_consumedOpTime时间直到lastOpTimeWritten
BSONObj temp = _oplogMarker.nextSafe();
_consumedOpTime = temp["ts"]._opTime();
}
// call more() to signal the sync target that we've synced T
_oplogMarker.more();
}
markOplog->hasCursor
bool BackgroundSync::hasCursor() {
{
// prevent writers from blocking readers during fsync
SimpleMutex::scoped_lock fsynclk(filesLockedFsync);
// we don't need the local write lock yet, but it's needed by OplogReader::connect
// so we take it preemptively to avoid deadlocking.
Lock::DBWrite lk("local");
boost::unique_lock<boost::mutex> lock(_mutex);
if (!_oplogMarkerTarget || _currentSyncTarget != _oplogMarkerTarget) {
if (!_currentSyncTarget) {
return false;
}
_oplogMarkerTarget = _currentSyncTarget;//服务端和sync时读取的服务端一样
_oplogMarker.resetConnection();
if (!_oplogMarker.connect(_oplogMarkerTarget->fullName())) {
_oplogMarkerTarget = NULL;
return false;
}
}
}
if (!_oplogMarker.haveCursor()) {//只查询其操作日志时间戳
BSONObj fields = BSON("ts" << 1);
_oplogMarker.tailingQueryGTE(rsoplog, theReplSet->lastOpTimeWritten, &fields);
}
return _oplogMarker.haveCursor();
}
到这里需要注意_oplogMarker的连接过程.
BackgroundSync::BackgroundSync() : _buffer(256*1024*1024, &getSize),
_lastOpTimeFetched(0, 0),
_lastH(0),
_pause(true),
_currentSyncTarget(NULL),
_oplogMarkerTarget(NULL),
_oplogMarker(true /* doHandshake */),这notifierThread连接sync端是需要握手的
_consumedOpTime(0, 0) {
}
void BackgroundSync::produce() {//这里produceThread从sync端读取数据连接时是不需要握手的
OplogReader r(false /* doHandshake */);
getOplogReader(r);
}
bool OplogReader::connect(string hostName) {
if ( ! commonConnect(hostName) )
return false;
if ( _doHandshake && ! replHandshake(_conn.get() ) )//这里是握手部分.
return false;
return true;
}
bool replHandshake(DBClientConnection *conn) {
string myname = getHostName();
BSONObj me;
{
Lock::DBWrite l("local");
// local.me is an identifier for a server for getLastError w:2+
if ( ! Helpers::getSingleton( "local.me" , me ) ||//读取数据失败则清空collection,然后自动生成一个OID,将其存入local.me中
! me.hasField("host") ||
me["host"].String() != myname ) {
// clean out local.me
Helpers::emptyCollection("local.me");
BSONObjBuilder b;
b.appendOID( "_id" , 0 , true );
b.append( "host", myname );
me = b.obj();
Helpers::putSingleton( "local.me" , me );
}
}
BSONObjBuilder cmd;//握手将自己的member id,OID和host地址发送给同步端.
cmd.appendAs( me["_id"] , "handshake" );
if (theReplSet) {
cmd.append("member", theReplSet->selfId());
}
BSONObj res;
bool ok = conn->runCommand( "admin" , cmd.obj() , res );
return true;
}
这些需要注意的是notifierThread是会向sync端发送OID, member id,host的.这个信息后面会用到,而produceThread在发送信息时是不会有握手过程.继续来看notifierThread中:
while (_consumedOpTime == theReplSet->lastOpTimeWritten) {
_lastOpCond.wait(lock);
while (_consumedOpTime < theReplSet->lastOpTimeWritten && _oplogMarker.more()) {
BSONObj temp = _oplogMarker.nextSafe();
_consumedOpTime = temp["ts"]._opTime();
}
再来看看_lastOpCond这个变量:
void BackgroundSync::notify() {//在notify中将通知上面的等待过程.
boost::unique_lock<boost::mutex> lock(s_mutex);
boost::unique_lock<boost::mutex> opLock(s_instance->_lastOpMutex);
s_instance->_lastOpCond.notify_all();
}
其调用过程在,删除了和这里讲的不相关的内容.这个函数是同步操作日志后将其记录到本地时调用的,可以认为到这里时之前的操作日志已经正确的同步到本地了.
void _logOpObjRS(const BSONObj& op) {
if( theReplSet ) {
theReplSet->lastOpTimeWritten = ts;
theReplSet->lastH = h;
ctx.getClient()->setLastOp( ts );
replset::BackgroundSync::notify();
}
}
这里通知notifier当前同步的时间戳位置,当notifierThread中没有数据时,那么其将通过getMore向sync端请求数据,这时sync端可以认为secondary端当前同步到的操作时间戳至少是notifierThread上一次请求数据时最后一个数据的时间戳.下面我们继续来看服务端的Handshake部分内容.handshake命令的执行如下:
virtual bool run(const string& , BSONObj& cmdObj, int, string& errmsg, BSONObjBuilder& result, bool fromRepl) {
Client& c = cc();
c.gotHandshake( cmdObj );//注意的是一个连接表示一个线程,这里的Client是这个线程独有的.
return 1;
}
void Client::gotHandshake( const BSONObj& o ) {
BSONObjIterator i(o);
BSONElement id = i.next();
_remoteId = id.wrap( "_id" );//这个线程独有的OID,保存到这里
BSONObjBuilder b;
while ( i.more() )
b.append( i.next() );
b.appendElementsUnique( _handshake );//得到和之前handshake独特的数据
_handshake = b.obj();//保存handshake数据.
if (theReplSet && o.hasField("member")) {//建立结构
theReplSet->ghost->associateSlave(_remoteId, o["member"].Int());
}
}
继续看associateSlave.
void GhostSync::associateSlave(const BSONObj& id, const int memberId) {
const OID rid = id["_id"].OID();//得到独特的OID
rwlock lk( _lock , true );
shared_ptr<GhostSlave> &g = _ghostCache[rid];
if( g.get() == 0 ) {
g.reset( new GhostSlave() );//新建一个GhostSlave结构.
}
GhostSlave &slave = *g;
slave.slave = (Member*)rs->findById(memberId);//根据memberId将OID结构与member关联
if (slave.slave != 0)
slave.init = true;
}
下面来看GetMore数据的请求,其请求数据流程为receivedGetMore->processGetMore,在processGetMore函数中有这么一条语句:
if ( pass == 0 )//这就是我们这里关注的地点.pass目前2.2的代码一直为0
cc->updateSlaveLocation( curop );
void ClientCursor::updateSlaveLocation( CurOp& curop ) {
if ( _slaveReadTill.isNull() )
return;
mongo::updateSlaveLocation( curop , _ns.c_str() , _slaveReadTill );
}
下面来看看_slaveReadTill的首次更新点,这里省略了函数的参数与和这部分内容无关的代码,可以明确这是查询流程中的函数,首次slaveReadTill保存的就是这一次请求最后一条数据的时间戳
string queryWithQueryOptimizer() {
OpTime slaveReadTill;
// Note slave's position in the oplog.
if ( pq.hasOption( QueryOption_OplogReplay ) ) {
BSONObj current = cursor->current();
BSONElement e = current["ts"];
if ( e.type() == Date || e.type() == Timestamp ) {
slaveReadTill = e._opTime();
}
}
// Save slave's position in the oplog.
if ( pq.hasOption( QueryOption_OplogReplay ) && !slaveReadTill.isNull() ) {
ccPointer->slaveReadTill( slaveReadTill );
}
}
我们接着看updateSlaveLocation函数.
void updateSlaveLocation( CurOp& curop, const char * ns , OpTime lastOp ) {
if ( lastOp.isNull() )
return;
Client * c = curop.getClient();
BSONObj rid = c->getRemoteID();//将自己看着primary端,则这里读取local.oplog.rs可以当作
if ( rid.isEmpty() )//是secondary在读取操作日志然后replay
return;
//这里的lastOp是上次getMore操作结束后最后一条记录的时间,
//而该函数的调用发生在这一次getmore操作的开始,表示服务
//secondary已经将数据正确的写入了自己的数据库中
slaveTracking.update( rid , curop.getRemoteString( false ) , ns , lastOp );//这个线程保存的OID,来自slave端
if (theReplSet && !theReplSet->isPrimary()) {
// we don't know the slave's port, so we make the replica set keep
// a map of rids to slaves
theReplSet->ghost->send( boost::bind(&GhostSync::percolate, theReplSet->ghost, rid, lastOp) );
}
}
继续这里的slaveTracking.update
void update( const BSONObj& rid , const string& host , const string& ns , OpTime last ) {
Ident ident(rid,host,ns);
scoped_lock mylk(_mutex);
_slaves[ident] = last;
_dirty = true;
if (theReplSet && theReplSet->isPrimary()) {//primary端更新secondary端同步的时间戳
theReplSet->ghost->updateSlave(ident.obj["_id"].OID(), last);
}
if ( ! _started ) {//启动线程,将每一个slave同步到的时间记录到local.slaves中
// start background thread here since we definitely need it
_started = true;
go();
}
_threadsWaitingForReplication.notify_all();
}
继续updateSlave
void GhostSync::updateSlave(const mongo::OID& rid, const OpTime& last) {
rwlock lk( _lock , false );
MAP::iterator i = _ghostCache.find( rid );//握手时保留的OID
GhostSlave& slave = *(i->second);//通过OID找到GhostSlave,找到member,然后再找到membercfg,然后membercfg与tags关联
((ReplSetConfig::MemberCfg)slave.slave->config()).updateGroups(last);
}
继续这里的updateGroups函数.
void updateGroups(const OpTime& last) {//membercfg中的tagSubgroup是在parseRules中插入的.
scoped_lock lk(ReplSetConfig::groupMx);//当前secondary更新则更新其所在的所有tagSubGroup的时间戳
for (set<TagSubgroup*>::const_iterator it = groups().begin(); it != groups().end(); it++)
(*it)->updateLast(last);
}
void ReplSetConfig::TagSubgroup::updateLast(const OpTime& op) {
if (last < op) {//这个group的时间更新到这个时间点上
last = op;//这个group的时间戳更新则需要更新所有加在这个tagGroup上的TagClause上的时间戳.
for (vector<TagClause*>::iterator it = clauses.begin(); it < clauses.end(); it++)
(*it)->updateLast(op);
}
}
void ReplSetConfig::TagClause::updateLast(const OpTime& op) {
if (last >= op) {
return;
}
// check at least n subgroups greater than clause.last
int count = 0;
map<string,TagSubgroup*>::iterator it;//满足同一个时间点的group数大于预设置的group数目,则需要更新rule的时间戳
for (it = subgroups.begin(); it != subgroups.end(); it++) {
if ((*it).second->last >= op) {
count++;
}
}
if (count >= actualTarget) {//满足这条clause要求的target数目了
last = op;
rule->updateLast(op);
}
}
void ReplSetConfig::TagRule::updateLast(const OpTime& op) {
OpTime *earliest = (OpTime*)&op;
vector<TagClause*>::iterator it;
for (it = clauses.begin(); it < clauses.end(); it++) {
if ((*it)->last < *earliest) {//一条规则多条clause需满足,只能选取其中时间最早的
earliest = &(*it)->last;//作为这条rule的时间
}
}
// rules are simply and-ed clauses, so whatever the most-behind
// clause is at is what the rule is at
last = *earliest;
}
到这里我们清楚了一条rule的时间戳是怎么更新的了,下面来看看getLastError函数.
bool run(const string& dbname, BSONObj& _cmdObj, int, string& errmsg, BSONObjBuilder& result, bool fromRepl) {
BSONElement e = cmdObj["w"];
if ( e.ok() ) {
int timeout = cmdObj["wtimeout"].numberInt();
Timer t;
long long passes = 0;
char buf[32];
while ( 1 ) {
OpTime op(c.getLastOp());//得到上一个操作的时间戳,getLastError当然只针对上一个操作了
if ( op.isNull() ) {
if ( anyReplEnabled() ) {//只有在开启replset模式时才w才算有用
result.append( "wnote" , "no write has been done on this connection" );
}
else if ( e.isNumber() && e.numberInt() <= 1 ) {
// don't do anything
// w=1 and no repl, so this is fine
}
else {
// w=2 and no repl
result.append( "wnote" , "no replication has been enabled, so w=2+ won't work" );
result.append( "err", "norepl" );
return true;
}
break;
}
// check this first for w=0 or w=1
if ( opReplicatedEnough( op, e ) ) {//这个是我们真正关心的函数
break;
}
// if replication isn't enabled (e.g., config servers)
if ( ! anyReplEnabled() ) {
result.append( "err", "norepl" );
return true;
}//超时返回
if ( timeout > 0 && t.millis() >= timeout ) {
result.append( "wtimeout" , true );
errmsg = "timed out waiting for slaves";
result.append( "waited" , t.millis() );
result.append( "err" , "timeout" );
return true;
}
sleepmillis(1);
}
result.appendNumber( "wtime" , t.millis() );
}
result.appendNull( "err" );
return true;
}
继续这里的opReplicateEnough函数,其调用的是slaveTracking的opReplicatedEnough函数,直接进入该函数.
bool opReplicatedEnough( OpTime op , BSONElement w ) {
string wStr = w.String();
if (wStr == "majority") {//默认目标,要求超过半数的服务器同步到这个时间点了
// use the entire set, including arbiters, to prevent writing
// to a majority of the set but not a majority of voters
return replicatedToNum(op, theReplSet->config().getMajority());
}//根据名字查找对应的rule,比照上一条操作记录的时间戳与这条rule的当前时间戳,小于表示rule满足要求,已经同步到这个时间点了,否则表示还没有同步到这个时间点
map<string,ReplSetConfig::TagRule*>::const_iterator it = theReplSet->config().rules.find(wStr);
return op <= (*it).second->last;
}
最后来看看updateSlaveLocation部分非primary服务器的流程.非primary部分调用percolate更新slave同步时间点.
void updateSlaveLocation( CurOp& curop, const char * ns , OpTime lastOp ) {
if (theReplSet && !theReplSet->isPrimary()) {
// we don't know the slave's port, so we make the replica set keep
// a map of rids to slaves
theReplSet->ghost->send( boost::bind(&GhostSync::percolate, theReplSet->ghost, rid, lastOp) );
}
}
void GhostSync::percolate(const BSONObj& id, const OpTime& last) {
const OID rid = id["_id"].OID();
GhostSlave* slave;
{
rwlock lk( _lock , false );
MAP::iterator i = _ghostCache.find( rid );
if ( i == _ghostCache.end() ) {
return;
}
slave = i->second.get();
if (!slave->init) {
return;
}
}
const Member *target = replset::BackgroundSync::get()->getSyncTarget();
if (!target || rs->box.getState().primary()
// we are currently syncing from someone who's syncing from us
// the target might end up with a new Member, but s.slave never
// changes so we'll compare the names
|| target == slave->slave || target->fullName() == slave->slave->fullName()) {
return;
}
{
if (!slave->reader.haveCursor()) {//连接slave端读取其操作日志时间戳,因为是slave端,可以确定slave的最后一条记录的时间戳就是该slave的时间戳,到这里就更新了其时间戳.
if (!slave->reader.connect(id, slave->slave->id(), target->fullName())) {
// error message logged in OplogReader::connect
return;
}
slave->reader.ghostQueryGTE(rsoplog, last);
}
if (slave->last > last) {
return;
}
while (slave->last <= last) {
if (!slave->reader.more()) {
// we'll be back
return;
}
BSONObj o = slave->reader.nextSafe();
slave->last = o["ts"]._opTime();
}
}
}
到这里所有关于tags的流程部分分析完毕,因为tags是个很小的功能,之前没注意,并且其实现确实相当的隐蔽,个人感觉不仔细阅读很可能分析不出这部分的流程,notifierthread也就无从得知其作用.所以这里将其流程记录下来. 到这里关于mongodb的服务端,客户端分析完毕,后面的文章将继续分析mongodb的shard的实现.
原文链接:mongodb源码分析(十八)replication replset tags
作者: yhjj0108,杨浩