面试题答案
一键面试设计思路
- 心跳检测:
- 每个节点定期向其他节点发送心跳消息。这可以通过定时任务实现,例如使用Go语言的
time.Ticker
。节点通过接收心跳消息来判断其他节点是否存活。 - 如果在一定时间内(如心跳间隔的数倍)未收到某个节点的心跳,则标记该节点可能故障。
- 每个节点定期向其他节点发送心跳消息。这可以通过定时任务实现,例如使用Go语言的
- 多副本与数据冗余:
- 选举相关的数据,如当前领导者信息、投票记录等,存储在多个节点上。可以使用分布式键值存储(如etcd)来实现数据的多副本存储。
- 当某个节点故障时,其他节点可以从副本中恢复关键数据,继续进行选举流程。
- 领导者租约:
- 领导者被选举出来后,会持有一个租约(Lease)。租约有一个有效期,领导者需要在租约到期前不断续约。
- 其他节点通过检查领导者的租约状态来判断领导者是否仍然有效。如果租约过期,且领导者未续约,则触发新一轮选举。
- 网络分区处理:
- 在网络分区发生时,可能会出现多个“分区内领导者”。为解决这个问题,可以采用基于多数(Quorum)的算法。只有当超过半数节点认可某个领导者时,该领导者才被认为是有效的。
- 在网络分区恢复后,重新进行选举协调,确保只有一个全局领导者。
实现方法
- 心跳检测实现:
package main
import (
"fmt"
"net"
"time"
)
func sendHeartbeat(nodeAddr string, stop chan struct{}) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
conn, err := net.Dial("tcp", nodeAddr)
if err != nil {
fmt.Printf("Heartbeat to %s failed: %v\n", nodeAddr, err)
} else {
fmt.Printf("Heartbeat sent to %s\n", nodeAddr)
conn.Close()
}
case <-stop:
return
}
}
}
- 使用etcd实现多副本与数据冗余:
package main
import (
"context"
"fmt"
"go.etcd.io/etcd/clientv3"
)
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
panic(err)
}
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err = cli.Put(ctx, "election - data", "current - leader - info")
cancel()
if err != nil {
fmt.Printf("Put failed: %v\n", err)
}
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
resp, err := cli.Get(ctx, "election - data")
cancel()
if err != nil {
fmt.Printf("Get failed: %v\n", err)
}
for _, ev := range resp.Kvs {
fmt.Printf("%s : %s\n", ev.Key, ev.Value)
}
}
- 领导者租约实现:
package main
import (
"context"
"fmt"
"time"
"go.etcd.io/etcd/clientv3"
)
func leaderLease(cli *clientv3.Client) {
lease := clientv3.NewLease(cli)
leaseResp, err := lease.Grant(context.TODO(), 10)
if err != nil {
panic(err)
}
leaseID := leaseResp.ID
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err = cli.Put(ctx, "leader - key", "leader - value", clientv3.WithLease(leaseID))
cancel()
if err != nil {
fmt.Printf("Put with lease failed: %v\n", err)
}
keepAlive, err := lease.KeepAlive(context.TODO(), leaseID)
if err != nil {
fmt.Printf("KeepAlive failed: %v\n", err)
}
go func() {
for {
ka, ok := <-keepAlive
if!ok {
fmt.Println("Lease keep - alive channel closed")
break
}
fmt.Printf("Lease renewed: TTL = %d\n", ka.TTL)
}
}()
}
- 网络分区处理(基于多数算法的简单示意):
package main
import (
"fmt"
)
func isQuorumReached(totalNodes, votedNodes int) bool {
return votedNodes > totalNodes/2
}
通过上述设计思路和实现方法,可以在Go语言实现的分布式领导选举中构建一定的容错机制,应对节点故障和网络分区等常见问题。