注意
本文档适用于 Ceph 的开发版本。
防止陈旧读取
我们在向客户端发送 ACK 之前同步写入所有副本,这限制了写入路径中出现不一致的可能性。但是,默认情况下,我们只从一个副本(每个 PG 的主 OSD)提供读取服务,客户端将使用其拥有的任何 OSDMap 来选择从中读取的 OSD。在大多数情况下,这没有问题:要么客户端映射正确,要么我们认为是对象主 OSD 的 OSD 知道它不再是主 OSD,并且可以向客户端提供一个指示更新主 OSD 的新映射。
关键是确保这种情况始终为真。特别是,我们需要确保一个与其对等节点隔离并且没有了解映射更新的 OSD,在允许新主 OSD 进行写入之后的任何时间点,不会继续向同样陈旧的客户端提供读取请求服务。
我们通过一种类似于读取租约的机制来实现这一点。每个池可能都有一个 read_lease_interval 属性,它定义了租约的持续时间,尽管默认情况下我们只是将其设置为 osd_pool_default_read_lease_ratio(默认值:.8)乘以 osd_heartbeat_grace。(这样,当我们将失败的 OSD 标记为 down 时,租约通常已经过期。)
readable_until
主 OSD 和副本 OSD 都跟踪几个值
readable_until 是我们在我们的“租约”过期之前允许提供(读取)请求服务的时间。
readable_until_ub 是作用集(acting set)中任何 OSD 的 readable_until 的上限。
主 OSD 通过向副本发送 pg_lease_t 消息来管理这两个值,这些消息会增加上限。一旦所有作用集中的 OSD 都确认它们看到了更高的上限,主 OSD 就会增加自己的 readable_until 并共享它(在随后的 pg_lease_t 消息中)。由此产生的不变性是,任何作用集中的 OSD 的 readable_until 始终 <= 任何作用集中的 OSD 的 readable_until_ub。
为了避免时钟偏移带来的任何问题,我们在整个过程中使用单调时钟(仅在本地精确且不受时间调整影响)来管理这些租约。对等 OSD 计算 OSD 本地时钟之间增量的上下限,允许主 OSD 基于其本地时钟共享时间戳,而副本将其转换为适合其自身本地时钟的适当边界。
先前的间隔
每当有间隔更改时,我们需要对先前间隔中任何 OSD 的 readable_until 值有一个上限。该间隔中的所有 OSD 都具有此值(readable_until_ub),并在对等期间将其作为 pg_history_t 的一部分共享。
因为对等可能涉及以前没有通信且可能没有时钟增量边界的 OSD,所以 pg_history_t 中的边界作为上限过期前的简单持续时间共享。这意味着由于对等消息的传输时间,边界在时间上向前滑动,但这通常非常短,并且将边界向后移动是安全的,因为它是一个上限。
PG “laggy” 状态
当 PG 处于活动状态时,会定期交换 pg_lease_t 和 pg_lease_ack_t 消息。但是,如果客户端请求进入并且租约已过期(readable_until 已过),PG 将进入 LAGGY 状态,并且请求将被阻塞。一旦租约续订,请求将被重新排队。
PG “wait” 状态
如果对等完成但先前间隔的 OSD 仍可能可读,则 PG 将进入 WAIT 状态,直到足够的时间过去。在此期间,任何 OSD 请求都将被阻塞。恢复可以在此状态下进行,因为对象的逻辑、用户可见内容不会改变。
死 OSD
一般来说,我们需要等到先前间隔的 OSD 知道它们不应再可读。如果已知 OSD 已崩溃(例如,因为进程不再运行,我们可以通过收到 ECONNREFUSED 错误来推断),那么我们可以推断它不可读。
同样,如果一个 OSD 被标记为 down,收到一个告知它的映射更新,然后通知监视器它知道自己被标记为 down,我们可以同样推断它没有仍在为先前间隔提供请求服务。
当 PG 处于 WAIT 状态时,它会观察新映射中的 OSD dead_epoch 值,该值表明它们知道自己已死。如果先前间隔中的所有 down OSD 都如此知晓,我们可以提前退出 WAIT 状态。