问题描述

分布式系统的复杂性大多数都和通信有关,因为通信没办法做到完全可靠,所以,当分布式系统中一个节点没有响应时,很难判断到底是通信出问题了,还是该节出问题了。 对一个分布式系统来说,如何快速有效的检测系统中的故障节点,并及时的进行修复,对于提高分布式系统的可用性无疑有非常重要的意义。

解决方案

说到监控,我们很自然的会想到push和pull的方法,简单的说,就是集群中有一个或多个监控的节点,所有的节点周期性的向它们汇报或者它们周期性的去检测其他节点的状态。 这种方法容易理解,也容易实现,但是它们也存在明显的缺陷,具体如下:

  • 一旦不能push或pull,无法判断是网络出问题了,还是对端出问题了;
  • 监控的节点本身也有可能出问题,如果只有一个监控节点,那么整个监控就失效了;如果有多个监控节点,多个监控节点之间数据如何同步,如何保证其一致性,这都是大问题;
  • 另外,对于一些复杂的系统,一个分布式系统内部可能包含多个不同的网络,如何检测各个不同网络的故障呢?

所以,除了上面这类“集中”的监控方法外,我们也需要考虑一些别的方法。

分布式系统中,为了保证一致性,通常的做法是首先通过时钟服务器保证所有节点的时间一致,并通过选举算法选出一个协调者,大家统一听协调者的安排;当然除了选举算法, 还有paxos这种P2P类型的算法来保证一致性。上面两种方法都能实现强一致性,与强一致性相对的是弱一致性,所谓的弱一致性就是说,不是任意一个时刻系统都是一致的,但是 随着时间的推移,系统会逐渐接近或达到一致的状态,也就是说系统最终会收敛到一致的状态。如果系统不要求强一致性,就可以考虑通过弱一致性的算法来达成目标。

gossip协议就是这样一种弱一致性算法的协议。gossip协议是模拟八卦的传播,一开始是某个人得知了一个消息,然后它把这个消息告诉它认识的几个人,其他人听到这个消息后,也会再告诉其他人。 简单的说就是一传十,十传百,最后,所有的人就都知道了。听上去很简单,也容易理解,这个算法要真正的发挥作用,关键是收敛速度,也就是如果有节点出问题了,集群中正常的节点可以在较短的时间内达成共识。 通常情况下,一个分布式系统中,如果超过半数的节点认为某个节点出问题了,那么基本上可以判断该节点出问题了。如果集群所有的节点都运行着gossip协议,在某个时刻某个节点发现它不能和某个节点通信了,它把这个消息发送给其他节点,其他节点收到消息后,也尝试和该节点通信,发现也不能通信,于是在将这个消息发送给其他节点。这样,很快,整个集群中超过半数的节点就都知道哪个节点出问题了。

serf是gossip协议的一个具体实现,它功能相对单一,但是配合gossip协议实现的event系统,就非常强大了。gossip协议的收敛速度还是挺快的,和集群的规模、gossip的间隔、网络丢包率都有关系,基本上可以做到与规模无关。具体可以参考serf模拟器

上面是理论铺垫,下面看看具体的部署和使用方法。

部署serf

首先是到官网上去下载编译好的可执行文件:

1
2
curl -O https://releases.hashicorp.com/serf/0.8.1/serf_0.8.1_linux_amd64.zip
unzip serf_0.8.1_linux_amd64.zip -d /usr/local/bin/

因为serf是用go语言实现的,采用静态编译,所以,安装就这么简单。

启动测试

安装之后,我们可以试试在单机启动三个serf实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
for i in {0..2}
do
    BIND=`expr 2 + $i`
    PORT=`expr 7373 + $i`
    echo Starting Serf agent $i on 127.0.0.$BIND, RPC on port $PORT
    serf agent -node=node$i -rpc-addr=127.0.0.1:$PORT -bind=127.0.0.$BIND -join=127.0.0.2 &
    if [ $i -eq 0 ]
    then
        sleep 1;
    fi
done

启动完成后,我们能看到类似下面这样的信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
2018/02/02 02:47:34 [INFO] agent: Serf agent starting
2018/02/02 02:47:34 [INFO] serf: EventMemberJoin: node2 127.0.0.4
2018/02/02 02:47:34 [INFO] agent: joining: [127.0.0.2] replay: false
2018/02/02 02:47:34 [INFO] serf: EventMemberJoin: node1 127.0.0.3
2018/02/02 02:47:34 [INFO] serf: EventMemberJoin: node0 127.0.0.2
2018/02/02 02:47:34 [INFO] agent: joined: 1 nodes
2018/02/02 02:47:35 [INFO] serf: EventMemberJoin: node2 127.0.0.4
2018/02/02 02:47:35 [INFO] agent: Received event: member-join
2018/02/02 02:47:35 [INFO] agent: Received event: member-join
2018/02/02 02:47:36 [INFO] agent: Received event: member-join

下面我们看看,是不是和我们想的一样,现在有三个节点了。

1
2
3
node0  127.0.0.2:7946  alive
node1  127.0.0.3:7946  alive
node2  127.0.0.4:7946  alive

如上所示,我们的确有三个serf节点了,他们运行在同一个机器上。这时候,如果我们kill掉一个serf会怎么样呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ pkill -9 -f "bind=127.0.0.2"
2018/02/02 02:56:27 [INFO] memberlist: Suspect node0 has failed, no acks received
2018/02/02 02:56:27 [INFO] memberlist: Suspect node0 has failed, no acks received
2018/02/02 02:56:30 [INFO] memberlist: Suspect node0 has failed, no acks received
2018/02/02 02:56:30 [INFO] memberlist: Suspect node0 has failed, no acks received
2018/02/02 02:56:32 [INFO] memberlist: Marking node0 as failed, suspect timeout reached (0 peer confirmations)
2018/02/02 02:56:32 [INFO] serf: EventMemberFailed: node0 127.0.0.2
2018/02/02 02:56:32 [INFO] memberlist: Marking node0 as failed, suspect timeout reached (0 peer confirmations)
2018/02/02 02:56:32 [INFO] serf: EventMemberFailed: node0 127.0.0.2
2018/02/02 02:56:32 [INFO] memberlist: Suspect node0 has failed, no acks received
2018/02/02 02:56:33 [INFO] memberlist: Suspect node0 has failed, no acks received
2018/02/02 02:56:33 [INFO] agent: Received event: member-failed
2018/02/02 02:56:33 [INFO] agent: Received event: member-failed

如上,我们让其中一个serf异常退出了,很快其他两个serf节点就发现了,并将其标记为failed。 agent也收到member-failed的消息,结合serf的event机制,我们可以在收到事件时执行一些脚本,这点在后面会进行更详细的讲解。

集群部署

在实际的分布式集群中,我们不会把serf部署在一个节点上,也没什么意义。集群部署也非常简单,如下:

1
2
3
$ unzip serf_0.8.1_linux_amd64.zip
$ clush -g demo --copy serf --dest /usr/local/bin
$ clush -g demo serf agent -iface eth0 -discover demo.net0 &

如果机器的服务器有多张网卡组成多个不同的网络,可以为每个网络启动一个serf实例。如下:

1
2
$ clush -g demo serf agent -iface eth0 -discover demo.net0 &
$ clush -g demo serf agent -iface eth1 -discover demo.net1 -rpc-addr=<eth1 ip> &

和单网卡主要的区别是指定不同的RPC监听地址。这么的命令中,会启用mDNS,通过mDNS自动发现 同一网络中的其他serf实例。因为mDNS依赖于多播,在一些云平台上(如:AWS的EC2)可能不支持,如果不支持, 可以给不同的serf实例设置不同的监听地址来解决。更多的配置选项,可以参考官方的配置文档

添加事件处理脚本

集群的监控已经部署好了,但是,它只能报告系统中发生的事件,并不能处理事件,需要我们添加事件处理脚本。

一旦产生事件,gossip会把事件发送给agent,agent会执行事先配置好的脚本。具体的事件,及参数会通过环境变量传递给脚本。下面看一个例子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ cat /etc/serf/event-demo.sh
#!/bin/sh

echo "serf event demo: show all variables."

for env in $(printenv | grep ^SERF)
do
    echo "${env} = $(eval ${env})"
done
$ chmod +x /etc/serf/event-demo.sh

然后在本地启动三个serf实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
for i in {0..2}
do
    BIND=`expr 2 + $i`
    PORT=`expr 7373 + $i`
    echo Starting Serf agent $i on 127.0.0.$BIND, RPC on port $PORT
    serf agent -node=node$i -rpc-addr=127.0.0.1:$PORT -bind=127.0.0.$BIND -join=127.0.0.2 --event-handler=/etc/serf/event-demo.sh &
    if [ $i -eq 0 ]
    then
        sleep 1;
    fi
done

启动完成后,我们触发一个事件看看。

1
2
3
4
5
6
$ serf event hello

    2018/02/02 03:27:08 [INFO] agent.ipc: Accepted client: 127.0.0.1:44446
    2018/02/02 03:27:09 [INFO] agent: Received event: user-event: hello
    2018/02/02 03:27:09 [INFO] agent: Received event: user-event: hello
    2018/02/02 03:27:09 [INFO] agent: Received event: user-event: hello

从上面的输出可以看出,所有的serf实例都执行了事件处理脚本。遗憾的是,都没有返回结果。 如果想要获得输出结果,需要用到serf提供的query功能,query和event很像,但是能返回结果。 需要注意的是,因为结果是通过gossip协议进行传输的,gossip使用的是UDP协议,所以,返回的结果最好不要超过1024个字节。另外一个需要注意的是,如果event需要处理很长时间,官方文档说需要在脚本中关闭stdout和stderr文件描述符。

更多的信息可查考官方的手册

后续工作

从上面的文档中可以看出serf这个工具简单但是很灵活,配合自定义的event和query可以做的很强大。不过,在一个分布式系统中,有的时候情况很复杂,上面这样简单的机制可能会造成误判,比如:因为负载高,导致网络消息没有及时回复,从而被检测程序认为是故障。这种情况下,简单的通过serf来判断就不一定准确了。另外,serf只是检测故障,如何在检测到故障后,对有问题的节点进行隔离,及自动恢复有故障的节点,仍然有很多工作需要做。