通过mysql实现leader election
当我们将同一个服务部署到多个节点(或者多个容器),以保证服务的高可用,但我们希望在同一时间,只有一个服务是active状态,其他服务处于inactive状态。或者说,要在这些服务中选出一个主节点。
Why not use zookeeper or etcd
zk和etcd确实能解决上述需求。这也是etcd和zk的主要用法之一。但有时候,我们已经依赖了mysql,而不想仅仅为了选主而引入新的组件。可以直接通过mysql来实现选主机制了。
Why not use GET_LOCK()
首先,要提一点,该方案不是基于锁来实现的。mysql的GET_LOCK()
语句可以获取一把自定义名字的锁,只要连接不断开,该锁就会一直保持。同一把锁只能被一个连接持有。
SELECT GET_LOCK("leader_lock1", 0);
不使用锁来实现的主要原因是:
- 你必须在程序内保持一个单独的mysql连接,并时刻维持其连接。这对于ORM或者mysql连接池是不太友好的。
- 在Mysql 5.7之前,你无法查知当前是哪个连接持有这把锁。
- 如果你的程序hang住了,这个锁仍然被持有,但此时服务是不可用的。
本方案的特性
- 在多节点中,选出一个作为leader节点
- leader节点定时主动的重申自己的地位
- 基于超时的重选举机制,当旧leader节点超过x秒没有重申自己的地位,则开启重选举过程
- 能够强制指定某节点为leader
- 能够强制开始重选举过程
- 某节点可以判断自己是否是leader
- 任何人都可以查询当前leader是哪个节点
SQL方案
这个方案包含一个table,和一系列sql语句来实现上述的特性。
表
service_id
标识一个包含了多个节点的服务,leader_id
标识当前的leader是哪个节点。不同节点的sql语句中,service_id
必须保持一致,leader_id
则为能标识节点身份的值。一种简单的方法是,不同node将leader_id
设置成自己 机器hostname 加 进程pid 。
CREATE TABLE leader_election (
service_id varchar(128) NOT NULL,
leader_id varchar(128) NOT NULL,
last_seen_active timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (service_id)
) ENGINE=InnoDB
选举&重申
所有的节点,需要周期性执行下面的语句。比如每秒执行一次。
insert ignore into leader_election (
service_id, leader_id, last_seen_active
) values (
'controller', 'node1_1234', now()
) on duplicate key update
leader_id = if(last_seen_active < now() - interval 20 second, values(leader_id), leader_id),
last_seen_active = if(leader_id = values(leader_id), values(last_seen_active), last_seen_active)
;
- 使用时,需要把
controller
替换成你的service_id
,把10.17.0.12_1234
替换成每个节点的hostname_pid
。 - 该语句假设重选举的超时时间为20s
- insert ignore的用法,参考这里
- on duplicate key update的用法,参考这里
强制指定某节点为leader
replace into leader_election (
service_id, leader_id, last_seen_active
) values (
'controller', 'node2_2333', now()
)
;
强制开始重选举
delete from leader_election;
判断自己是否是leader
select count(*) as is_leader from leader_election where service_id='controller' and leader_id='node1_1234';
如果结果等于0,那么自己不是leader,否则,代表自己是leader。
查询当前leader是谁
select leader_id as leader from leader_election where service_id='controller';