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




