暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

在没有协调器的情况下实施分布式MongoDB备份

原创 Pogrebnoi 2020-01-15
1038

在1.0版之前,Percona Backup for MongoDB(PBM)具有一个单独的协调程序守护程序,作为其体系结构的关键部分。协调员正在处理与备份代理和控制程序的所有通信。它拥有PBM配置和备份列表,决定了哪些代理应该执行备份或还原,并确保所有分片上的备份和还原点都一致。

拥有专用的单一真相来源可以简化沟通和决策。但是,它却以其他方式给系统带来了额外的复杂性(系统的另一部分需要与通信协议一起维护,等等)。更重要的是,它成为单点故障。因此,我们决定放弃1.0版本中的协调器。

从v1.0开始,我们一直将MongoDB本身用作通信中心以及配置和其他与备份相关的数据存储。它从系统中删除了一层额外的通信(在我们的例子中是gRPC),并为我们提供了持久的分布式存储。因此,我们不再需要维护那些东西。

没有协调者的备份和还原?

没有协调者,我们如何进行备份和还原?谁来决定应在副本集中的哪个节点上运行备份?谁来确保群集中所有分片上的备份点一致?

看来我们还是需要一个领导者。让我们仔细看看。

实际上,我们可以将此问题分为两个级别。首先,我们必须确定副本集中的哪个节点应负责操作(备份,还原等)。第二,副本集的领导者将承担额外的责任,以确保在分片群集的情况下群集范围内的一致性。

第二个比较容易。由于分片群集无论如何都必须设置一个唯一的Config Server副本,因此我们可以同意,它的成员之一应该始终负责群集范围的操作。

因此,一个问题解决了,还有一个问题要解决。

如何选择副本集的代理?显而易见的答案是在副本集节点之间进行一些领导者选举,然后让领导者决定进一步的步骤。为此,我们可以使用某些软件,例如Apache ZooKeeper,但它给系统带来了沉重的组件和外部依赖性,因此我们希望避免这种情况。或者我们可以使用一些分布式共识算法,例如Paxos或Raft。可以提前选出领导者,并负责操作(备份,还原等)。在这种情况下,我们必须能够正确处理领导者失败时的情况-检测到它,重新选举新领导者,等等。或者我们可以针对每个操作请求运行选举过程。但这意味着开始操作之前会花费额外的例程和时间(考虑到备份/还原操作的通常频率,这并不是什么大不了的事情,与备份/还原操作本身相比,几次额外的网络往返似乎都不算什么)。

但是,我们可以完全避免举行领导人选举吗?是的,这就是我们的工作。当备份控制程序发出操作命令时,该命令将传递给每个备份代理。然后,代理检查其连接到的节点是否适合该作业(具有可接受的复制滞后,首选备份作为备份等),如果是,则代理尝试为此操作获取锁定。如果碰巧成功,它将继续工作并负责完成此工作的副本集。所有其他未获得锁定的代理都将跳过此工作。实际上,我们必须用一块石头杀死两只鸟,因为我们必须采用某种机制来防止同时运行两个或多个备份和/或还原。

最后要考虑的是我们实际上如何进行锁定。我们使用MongoDB唯一索引。

首先,在新集群上启动PBM时,它将自动创建内部集合。其中之一是admin.pbmLock具有针对“ replest”字段的唯一索引约束。因此,以后,当代表副本集的代理尝试获取锁时,只有一个可以成功。

以下是PBM的简化代码(我们使用官方的MongoDB Go Driver)。

创建具有唯一索引的新集合:

err := mongoclient.Database("admin").RunCommand(
    context.Background(),
    bson.D{{"create", "pbmLock"}}, 
).Err()
if err != nil {
    return err
}

// create index for Locks
c := mongoclient.Database("admin").Collection("pbmLock")
_, err = c.Indexes().CreateOne(
    context.Background(),
    mongo.IndexModel{
        Keys: bson.D{{"replset", 1}},
        Options: options.Index().
            SetUnique(true),
    },
)
if err != nil {
    return errors.Wrap(err, "ensure lock index")
}

Acquiring a lock:
// LockHeader describes the lock. This data will be serialised into the mongo document.
type LockHeader struct {
	Type       Command `bson:"type,omitempty"`
	Replset    string  `bson:"replset,omitempty"`
	Node       string  `bson:"node,omitempty"`
	BackupName string  `bson:"backup,omitempty"`
}

// Acquire tries to acquire a lock with the given header and returns true if it succeeds
func Acquire(lock LockHeader) (bool, error) {
	var err error
	l.Heartbeat, err = l.p.ClusterTime()
	if err != nil {
		return false, errors.Wrap(err, "read cluster time")
	}

	_, err = l.c.InsertOne(l.p.Context(), lock)
	if err != nil && !strings.Contains(err.Error(), "E11000 duplicate key error") {
		return false, errors.Wrap(err, "acquire lock")
	}

	// if there is no duplicate key error, we got the lock
	if err == nil {
		return true, nil
	}

	return false, nil
}

综上所述

我们正在寻求以简单但有效的方式解决复杂问题的解决方案。降低复杂性可简化支持和开发,并减少错误的余地。尽管我们面临着分布式备份和协调挑战的复杂性,但我相信我们提出了一个简单而有效的解决方案。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论