主从系统的实现

这是GPRC+ETCD服务发现的一种变种实现

在服务端代码的封装上有很大的区别,一个简单的服务发现系统中,服务端之间的状态互不影响

在主从系统中,主需要知道从的IP来向从复制数据(或者反过来从需要知道主的IP来拉取数据),当主挂了,需要选举一台从来切换成主


服务发现的变种实现

在我的实现中,将这种服务发现设计为selecter类,该类是没有grpc代码的,是对ETCD的一个封装

该类有两个作用,首先是不间断的将自己的地址注册在ETCD上,其次是将ETCD的各种节点变化事件变为主从事件,通过管道发送给调用方

1 首先看第一个作用,将自己的地址注册在ETCD上,并且让客户端能够明显的鉴别哪个是主

优雅的实现是利用ETCD的自增节点,将一系列IP注册在自增节点的内容中,哪个自增节点最小,哪个就是主

当主挂了,次小的那个就应该是主了

由于ETCD不像zookeeper封装了自动刷新节点,因此需要自己实现

这部分可以用状态机来实现,分为INIT,REFRESH,OFFLINE三种状态

INIT状态进行节点创建,成功进入REFRESH状态,失败进入OFFLINE状态

REFRESH状态不断刷新节点,失败进入OFFLINE状态

OFFLINE状态进入INIT状态

2 第二个作用,首先需要设计有事件的结构

1
2
3
4
type Event struct {
Status EventStatus
Node []string
}

这是事件的状态码和事件变化的节点

大致有如下几种

1
2
3
4
5
6
7
INIT_MASTER
INIT_SLAVE
CHANGE_TO_MASTER
CHANGE_TO_SLAVE
MASTER_CHANGE
SLAVE_ADD
SLAVE_DEL

这里分为INIT_MASTER和CHANGE_TO_MASTER,是为了考虑到初始化可能要做一些工作

具体实现是建立一个ETCD watcher,任何的节点变化事件都会触发watcher的回调

对所有节点排序来获取新的主和从,来与历史记录的主从进行比较

从而生成主从事件和变化节点列表

这里有个注意点是创建这个watcher前,需要先调用一遍watcher的回调,否则调用方无法初始化主从状态

而这个主动调用,然后创建watcher的过程可以做成一个函数,任何失败发生(例如网络超时),就重新调用这个函数


服务端的实现

服务端我设计为sync_manager类,该类使用设计好的grpc master和grpc slave类,生成master server和slave server实例

master server有对外部开放的读写接口

而slave server只有对内部开放的同步接口

而后启动一个后台进程housekeeper,该进程监听selecter类的各种事件

切换主从状态,当slave新加或者删除,对应建立或者销毁相对的grpc slave client

外部调用master的write接口时,如果是强一致性协议,就需要写从成功后返回

客户端的实现

客户端需要连接一组或者多组服务端

例如服务端将路径注册在/server/0001这样的节点时

客户端就需要监听/server下所有的节点变化事件,当新的一组主从0002加入时,必须有所响应

这部分功能基本没什么难点了,只是封装上可以实现成两种方式

1
2
3
4
s = service.NewClientManager("etcdURL+Path")
//随机获取一个conn来连接
c = master.NewMasterClient(s.GetConn())
c.WriteFile(context.Background(),&master.MasterRequest{})
1
2
3
4
5
6
7
s = service.NewClientManager("etcdURL+Path")
for ;; {
//新加组或者减少组通过管道通知调用方
}
//调用方自己维护需要连接的IP和对应grpc客户端实例
c = this.GetConn()
c.WriteFile(context.Background(),&master.MasterRequest{})

方式一的问题在于如果有个服务注册在ETCD上,但是他有BUG,有可能出错,重复调用还是有可能调用到这个重复的上门

方式二的调用方可以自己管理客户端实例,遍历可用的客户端来进行访问

补充

ETCD的自增节点有个小坑。监听路径进行watcher并且设置Recursive为true

如果用set创建孙子节点,不会触发watcher

而如果用CreateInOrder创建孙子节点,则会触发watcher

所以需要对watcher触发的路径值resp.Node.Key进行判断,如果是孙子节点,那么屏蔽