Ceph Monitor Leader Elect

June 1, 2016
Author:Eric
Source:http://blog.wjin.org/posts/ceph-monitor-leader-elect.html
Declaration: this work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License. Creative Commons License

Introduction

monitor运行过程中,需要选举leader,然后所有更新的操作都是通过leader发出paxos propose完成, 如果非leader收到更新请求,会将请求转发到leader节点,让leader代为执行。

leader本身的选举,并不是paxos算法,ceph本身实现的比较简单高效,因为利用了节点在monmap中的rank值,人为的造成各个节点不平等, rank值最小的获胜,简单快速的达到选举目的。

When Start Elect

一个节点发起leader选举是通过函数Monitor::start_election()完成,这个函数会在以下几种情况被调用:

  1. 节点调用bootstrap函数引导启动,接着会probing,查询其他monitor信息(有可能需要同步数据),完成后发起选举

  2. 节点收到选举消息MMonElection,如果节点自己已经处于quorum或自己的编号更小,也会重新发起选举

  3. 节点收到quorum enter/exit命令

bootstrap

对于第一种情况,bootstrap被调用的地方非常频繁:

monitor提供的paxos算法内部采用lease协议,保证副本数据在一定时间范围内可读,leader节点会不停的发送lease消息,延长各peon的时间。 如果peon节点down掉,leader节点不会收到lease_ack消息,超时后就会重新选举。如果leader节点down掉, 所有的peon节点收不到来自leader的更新消息,也会重新选举。

// leader调用
void Paxos::lease_ack_timeout()
{
  assert(mon->is_leader());
  assert(is_active());
  lease_ack_timeout_event = 0;
  mon->bootstrap(); // 重启
}

// peon调用
void Paxos::lease_timeout()
{
  assert(mon->is_peon());
  lease_timeout_event = 0;
  mon->bootstrap(); // 重启
}

MMonElection

对于第二种情况,应该说这是第一种情况间接导致的,当某个节点发出选举消息后,其他节点收到消息会做相应的处理。 常见的case是其他节点已经形成一个quorum,并且有leader存在,此时收到选举消息后,发现是来自quorum外的节点, 表明有新节点加入,需要选举:

void Elector::handle_propose(MMonElection *m)
{
  ......
  } else if (m->epoch < epoch) {
    // got an "old" propose,
    if (epoch % 2 == 0 &&    // in a non-election cycle
	  mon->quorum.count(from) == 0) {  // from someone outside the quorum
      mon->start_election(); // 本节点已经形成quorum,有节点重新启动
	}
  }

  if (mon->rank < from) {
	mon->start_election(); // 自己编号更小
  } else {
	  ......
  }
  
  m->put();
}

quorum enter/exit

第三种情况,这个其实是一个命令,感觉主要用于运维测试。原理是设置一个bool值,然后调用选举函数,这样就会让此monitor加入或者退出quorum。 针对某个特定的monitor,可以通过如下命令验证:

$ceph –admin-daemon=path-to-admin-socket quorum enter

$ceph –admin-daemon=path-to-admin-socket quorum exit

Elector中的处理就是设置标志:

void Elector::start_participating()
{
  if (!participating) {
    participating = true; // 参与选举
    call_election();
  }
}

void stop_participating() { participating = false; } // 不参与

How Elect

整个选举的流程,完全是在Elector类中完成。此类中维护了一个election_epoch,为偶数,表示已经加入quorum且处于稳定状态,为奇数,表示正在选举。 选举的时候,永远只选举rank值为最小的为leader。下表格中的quorum集合只列出了选举成功后的quroum,在选举过程中,会有一个outside quorum表示新加入集群的节点。

以下图演示选举变化过程,第一行中的0,1,2表示三个monitor的rank值:

epoch  |  0  |  1  |  2  | quorum | leader | comment 
------ | --- | --- | --- | ------ | ------ | -------
epoch  |  0  |  0  |  0  |        |        | mon(0,1,2) startup
epoch  |  1  |  1  |  1  |        |        | electing
epoch  |  2  |  2  |  2  | 0,1,2  |   0    |
...
epoch  |  2  |  2  |  2  | 0,1,2  |   0    |
epoch  |  3  |  2  |     |        |        | mon2 down; leader lease_ack timeout -> electing
epoch  |  3  |  3  |     |        |        | electing
epoch  |  4  |  4  |     |  0,1   |   0    |
...
epoch  |  4  |  4  |  2  |  0,1   |   0    | mon2 up
epoch  |  4  |  4  |  3  |  0,1   |   0    | electing
epoch  |  4  |  4  |  4  | 0,1,2  |   0    |
...
epoch  |     |  4  |  5  |        |        | mon0 down; mon2 lease timeout -> electing
epoch  |     |  5  |  5  |        |        | mon1 lease timeout -> electing
epoch  |     |  6  |  6  |  1,2   |   1    | 
...
epoch  |     |  6  |  6  |  1,2   |   1    | 
epoch  |  4  |  6  |  6  |        |        | mon0 up
epoch  |  5  |  6  |  6  |        |        | electing
epoch  |  6  |  6  |  6  | 0,1,2  |   0    |
...

下面简单跟踪一下代码流程:

void Monitor::start_election()
{
  wait_for_paxos_write();
  _reset();
  state = STATE_ELECTING;
  cancel_probe_timeout();

  elector.call_election(); // 开始选举
}

void call_election() {
  start();
}

void Elector::start()
{
  if (!participating) { // 默认值为true,可以通过命令quorum exit修改
    return;
  }

  acked_me.clear();
  classic_mons.clear();
  init();
  
  if (epoch % 2 == 0) 
    bump_epoch(epoch+1);  // 设置election_epoch为奇数,表示正在选举,这个值会存入store中

  start_stamp = ceph_clock_now(g_ceph_context);
  electing_me = true;
  acked_me[mon->rank] = CEPH_FEATURES_ALL;
  leader_acked = -1;

  for (unsigned i=0; i<mon->monmap->size(); ++i) {
    if ((int)i == mon->rank) continue;
    Message *m = new MMonElection(MMonElection::OP_PROPOSE, epoch, mon->monmap); // 发送消息给其他monitor
    mon->messenger->send_message(m, mon->monmap->get_inst(i));
  }
  
  reset_timer();
}

其他节点收到消息后,调用handle_propose处理,如果接受,就会调用defer,发送回接受消息:

void Elector::defer(int who)
{
  ......
  leader_acked = who;
  ack_stamp = ceph_clock_now(g_ceph_context);
  MMonElection *m = new MMonElection(MMonElection::OP_ACK, epoch, mon->monmap); // 发回确认消息
  m->sharing_bl = mon->get_supported_commands_bl();
  mon->messenger->send_message(m, mon->monmap->get_inst(who));
  
  // set a timer
  reset_timer(1.0);  // give the leader some extra time to declare victory
}

待收到所有ACK消息后(注意这里并不是收到大多数, leader必须是所有节点都确认),就会宣告自己胜出:

void Elector::handle_ack(MMonElection *m)
{
	......
    if (acked_me.size() == mon->monmap->size()) {
      victory(); // 选举成功
    }
	......
}

void Elector::victory()
{
  ......

  bump_epoch(epoch+1);     // 设置为偶数

  ......

  for (set<int>::iterator p = quorum.begin();
       p != quorum.end();
       ++p) {
    if (*p == mon->rank) continue;
    MMonElection *m = new MMonElection(MMonElection::OP_VICTORY, epoch, mon->monmap); // 告诉其他monitor
    m->quorum = quorum;
    m->quorum_features = features;
    m->sharing_bl = *cmds_bl;
    mon->messenger->send_message(m, mon->monmap->get_inst(*p));
  }
    
  // tell monitor
  mon->win_election(epoch, quorum, features, cmds, cmdsize, &copy_classic_mons); // 胜出,让monitor做相应的初始化
}

后面流程就是胜出的monitor一方执行win_election,调用函数leader_init初始化leader,失败的一方执行lose_election, 调用函数peon_init初始化peon,整个monitor集群差不多就可以开始稳定工作。

Summary

总结来看,monitor选举的过程是非常简单迅速的,满足条件后向monmap中的所有节点发送消息MMonElection::OP_PROPOSE消息,待收到所有确认消息后就会胜出。

逻辑处理主要是在函数handle_propose中,举例如下,假设monitor A向monitor B发送PROPOSE消息,考虑两种情况:

rank A < rank B

rank A > rank B