首页 > 其他分享 >Apache BookKeeper Insights Part 2 — Closing Ledgers Safely

Apache BookKeeper Insights Part 2 — Closing Ledgers Safely

时间:2022-12-10 10:11:20浏览次数:68  
标签:recovery bookie ledger 条目 Part Ledgers BookKeeper 客户端

标题:Apache BookKeeper Insights Part 2 — Closing Ledgers Safely
原文:https://medium.com/splunk-maas/apache-bookkeeper-insights-part-2-closing-ledgers-safely-386a399d0524
时间:2021-11-25

在上一篇文章中,我们了解了BookKeeper复制不是由集成的有状态节点执行的,而是由无状态客户端执行的。在本文中,我们将了解该协议如何处理客户端故障,避免出现脑裂的情况,以及一些微妙的不易察觉的领域。

我建议先阅读本文中对协议的高层描述。在本文中,我们将深入研究协议的具体方面。

协议的某些方面是必需的,因为设计BookKeeper的目的是提供分布式日志段(ledger),而不是无界日志(如Raft/Kafka)。BookKeeper ledger可以不必这样设计,但出于各种原因,选择了这些设计。与一切事物一样,设计也是一种权衡。

在我们研究有趣而棘手的部分之前,我们需要了解ledger的生命周期,以及ledger是如何链接在一起形成无界日志的。

Ledgers Are Log Segments

每个ledger都有一个生命周期。任何客户端都可以创建ledger,但只有创建ledger的客户端才能写入。因此正常情况下,客户端A会创建ledger L1,将条目写入其中,一段时间后关闭它。

一种异常情况是,客户端A创建了一个ledger L1,然后写入它,客户端突然消失,使ledger处于打开状态。为了使分段日志能够继续写入数据,必须有另一个客户端进入并关闭ledger,然后创建一个新的ledger,以便继续写入日志。

在Apache Pulsar的背景下,broker使用ZooKeeper就哪些broker拥有哪些主题达成一致。任何给定的主题只能由一个broker拥有,该broker使用BookKeeper客户端创建、写入和关闭其拥有的每个主题对应的ledger。

图1 主题是由BookKeeper ledger构建的分段日志

但是如果一个broker挂了,那么他的主题就会被转移到其他broker那里。每个拥有给定主题所有权的broker都会创建一个BookKeeper客户端,以关闭该主题的最后一个分类账(由另一个broker的BookKeeper客户端创建),创建一个新的ledger,并继续为该主题提供写服务。

图2 broker失败和主题分段

更令人不快的是,最初的客户端没有死,只是有一段时间无法访问,可能是长GC或网络分区。那么,如果一个客户端正在关闭一个ledger,而原始客户端仍在试图写入该ledger,会发生什么呢?这就是所谓的脑裂,会导致数据丢失。

因此,我们需要一种安全的方式让一个客户端关闭另一个客户端的ledger,这叫做ledger recovery。每当一个客户端关闭另一个客户端的ledger时,他们都会通过这个机制来完成。

Ledger Recovery

当客户端关闭自己的ledger时,它会将状态设置为CLOSED,并将Last Entry Id设置为Last Add Confirmated(LAC),这是客户提交的最新条目。在这个关闭的ledger的剩余生命周期中,对ledger执行读取的其他客户端将永远不会读取超过Last Entry Id的条目,Last Entry Id是ledger的结尾。

当一个客户端关闭另一个客户端的ledger时,它还必须设置Last Entry Id,而现在原始客户端已经不在了,找到LAC的唯一方法就是询问存储节点(bookies)。

图3 Ledger状态

Recovery开始时,客户端会将ledger元数据中的状态从OPEN更改为IN_RECOVERY。下一步,它会向最后一个fragment所在所有bookie询问该ledger的LAC。每个条目中存储的LAC通常落后于真实的LAC,因此在所有bookie中找出最高的LAC是recovery过程的起点。可能有后续提交的条目,因此它们需要被发现。

当客户端向每台bookie索要LAC时,它还会在请求上设置fencing标志,使每台bookie对ledger做fence操作。Fencing是一种局部单向幂等操作。一旦一台bookie关闭了一个ledger,该bookie将不再接受任何对该ledger的写入。这将防止原始客户端在其实际上仍然处于活动状态时继续执行写入操作。如果没有fencing,我们可能会出现脑裂的情况,两个客户端认为他们都拥有该topic,并且都在同一ledger上取得执行写入,这可能会导致最终的数据丢失。

图4 Recovery client fence了足够多的bookie,这样原始客户端如果还活着就无法继续写入

注意:在TLA+中验证BookKeeper协议的系列文章中,你可以了解更多关于fencing和一些边缘案例的信息。

图5 Recovery client从bookie集群中发现了最后一个fragment中已知的最高LAC,在本例中,最高值为1(滞后于原始客户端的LAC)

上图中C1写入的E0、E1、E2得到了Ack Quorum个bookie的确认,因此LAC变为2,而E3还没有得到Ack Quorum个bookie的确认,因此E3还未提交。

获取最高的LAC只是第一步,现在客户端必须找出是否存在超过最高LAC的其他条目。客户端开始为超出最高LAC的条目发送读取请求(也带有fencing标志)。对于每个条目,客户端必须决定该条目是否可恢复。如果确定某个条目不可恢复,客户端将在该点停止读取,并将前一条目视为Last Entry Id。客户端将继续读取,直到达到其认为的最后一条可恢复条目。

对于成功读取的每个条目,它都会再次将其写回bookie集合。写入条目是幂等的,不会导致重复或排序问题。我们将在另一篇文章中看到为什么在恢复期间重写条目需要保证正确性。如果所有写入都成功(达到AQ),则最后一个操作是通过将ledger状态设置为CLOSED来关闭ledger,并将Last Entry Id设置为找到的最后一个提交的条目。

上图中的Last Entry Id最终是2还是3?

Ledger Truncation (Bad!)

当执行恢复的客户端将Last Entry Id设置得太低,就会发生ledger截断,导致提交的条目无法读取。这些条目至少在AQ个bookie的磁盘上,因此它们是安全的,但由于这些条目现在超过了Last Entry Id,它们无法读取,因此基本上认为是丢失了。

图6 客户端C2在条目1处关闭分类账,但条目2在之前已提交

Ldger截断是需要警惕的事情。接下来,我们将介绍它可能发生的一些方式以及如何避免。理解ledger截断是如何发生的,关键在于理解ledger recovery过程怎样决定哪些条目是可恢复的,哪些是不可恢复的。

Deciding when an entry is recoverable

在ledger recovery期间,对于客户端收到的每个recovery read响应,都需要决定该条目是可恢复的、不可恢复的还是需要更多响应来才能做决策。

对于任何给定的条目,客户端都迫不及待地等待集合中每一台bookie的回复。如果真是这样的话,那就意味着,如果一个bookie宕机,ledger的恢复就无法完成。但是,如果没有bookie回应,我们不能立即关闭ledger,因为这可能会导致ledger截断。该条目可能存在于所有bookie中,但如果此时无法访问,我们只需要中止ledger recovery,而不是关闭ledger。

我们希望避免ledger截断,但我们不希望被速度慢或不可用的bookie阻塞——恢复过程需要快速进行,因为只要恢复过程还在继续,分段日志就无法正常提供服务。因此,在ledger recovery快速完成和安全完成之间存在一些棘手的关系。

BookKeeper采取的方法是宽松地处理positive响应,而对于可能导致关闭ledger的非positive响应则很严格。也就是说,一次positive recovery read可能会使客户端决定一个条目是可恢复的。但是,对于不可恢复的条目(并在该点关闭ledger),判断标准设置得很高(也就说需要很严格的判断才能确定条目不可恢复),因为我们不想截断ledger。

我们将recovery read响应分为三类:

  • Positive(条目在那台bookie上)
  • Negative(条目不在那台bookie上)
  • Unknown(未知错误或者超时)

注:之前犯过的一个错误是将所有非positive响应视为negative响应。这是错误的,可能会导致ledger截断。一些响应可能是超时,或由于bookie过载而拒绝服务,或由于短暂的i/o错误等,但条目实际上可能存在于bookie上。

Quorum Coverage

关闭ledger的标准是根据quorum coverage。我们在TLA+系列教程中描述了quorum coverage。基本上我们将其定义为:

  • 对于任何一个ack quorum集合,至少存在一个bookie满足条件,那么给定的属性就是满足条件的;
  • 所有ack quorum集合中的bookie都不满足条件,那么给定的属性就是不满足条件的。

这两个定义是等价的。

Ledger recovery在两个地方使用到了quorum coverage:

  • Fencing——判断依据为:

    1. 一个给定的bookie已完成fence操作
    2. cohort是当前ensemble集合(在[这篇文章](/Users/oyld/Nutstore Files/我的坚果云/note/Pulsar/文章翻译/01 Understanding How Apache Pulsar Works.md)中,描述的cohort是WQ,猜测这里的假设为Ensemble Size等于Write Quorum)。

    对于ensemble中的任意ack quorum子集,没有一个ack quorum集合中的bookie全都未完成fence操作,那么fencing过程就满足quorum coverage条件了。这等价于对于任意ack quorum集合中,都至少有一个bookie完成了fence操作。如果存在一个ack quorum集合,这个集合中的所有bookie都未完成fence,那么原来的老客户端就还能继续向ledger写入数据(脑裂)。

  • 确定条目不可恢复——判断依据为:

    1. 一个给定的bookie不包含这条entry
    2. cohort为这个entry的写集合WQ(也就是应该保存这条entry的bookie集合)

    那么,如果不存在一个AQ大小的集合,这个集合中的bookie都保存有entry,那么该entry将不可恢复。

Quorum coverage的阈值可以通过下面的公式计算出来:

Bookies that satisfy property = (Cohort size — Ack quorum) + 1

将公式应用于fencing场景,如果(WQ-AQ)+1个bookie满足“已经fence”这个操作,那么就满足了quorum coverage——ledger fence操作成功。

(WQ-AQ)+1个bookie已经fence,意味着至少有一个属于AQ的bookie已经fence,和之前的文字描述是相符的。忽略前面正式化描述带来的复杂性,这里的思想在于如果(WQ-AQ)+1个bookie完成fence,那么老的broker就无法再写入成功了,因为写成功bookie数无法达到AQ。

将公式应用于判断条目不可恢复场景,如果(WQ-AQ)+1个bookie满足“不包含该entry”这个条件,那么就满足了quorum coverage——该条目不可恢复。

(WQ-AQ)+1个bookie不包含一条entry,那么任意AQ集合中至少有一台bookie未保存entry,和之前的文字描述是相符的。

客户端只需要简单的对negative响应进行计数,如果数量达到了quorum coverage阈值,那么entry就是不可恢复的。

+----+----+------------------------+
| WQ | AQ | Neg Responses Required |
+----+----+------------------------+
|  2 |  1 |                      2 |
|  2 |  2 |                      1 |
|  3 |  1 |                      3 |
|  3 |  2 |                      2 |
|  3 |  3 |                      1 |
|  4 |  2 |                      3 |
|  4 |  3 |                      2 |
|  4 |  4 |                      1 |
+----+----+------------------------+

这个原则背后的思想也很简单。从上表可以看出,一旦满足negative列的个数,也就是至少(WQ-AQ)+1个bookie不包含这个条目,那么包含条目的的bookie数量肯定无法达到AQ,那么就认为这个条目是写入不成功的,便将其丢弃。

Generous on positive, Strict on close

一个positive响应可以被视为一次成功的recovery read,但为了找到第一个不可恢复的entry从而关闭ledger,我们必须使用quorum coverage。

在收到每个响应后,客户会评估条目是否可恢复、不可恢复或需要等待更多响应。

Examples…

Positive recovery read #1

图7 第二个响应是positive的,这足以将条目视为可恢复的,进一步的响应将被忽略

quorum coverage用于判断条目是否不可恢复,如果在满足quorum coverage条件之前已经判断出条目可以恢复(一次positive响应就可以认为条目可以恢复),那么quorum coverage过程中止,条目直接恢复出来,这就是对positive响应很宽松的含义。

Negative recovery read #1

图8 前两个响应为negative,达到negative阈值

Unknown #1

图9 没有明确的positive或negative响应,这意味着我们不知道条目是否可恢复,中止恢复过程

中止之后再次重试。

Wait for more responses

图10 两个阈值均未达到,等待最后一个响应

Positive recovery read #2

图10a 最后一个反应是positive的,达到了positive阈值

Negative recovery read #2

图10b 最后一个响应到达,结果是negative的,达到了negative阈值

Unknown #2

图10c 最后一个响应未到达两个阈值,AQ台bookie中可能存有条目,所以我们不能把这个响应视为negative的,ledger recovery需要中止后重试

如果ledger recovery可以完成,则所有可恢复的条目都已提交,并且已设置Last Entry Id。如果由于无法判断条目是否可恢复而无法完成,则可以重复,直到恢复成功。一旦完成,分段日志就可以继续写入了。

The Danger of Ack Quorum of 1

使用ack quorum 1不仅是危险的,因为某些条目可能没有冗余,而且如果一台bookie离线,它还可能导致ledger recovery暂停。当ack quorum为1时,只有收到每台bookie的回复,才有可能关闭ledger——超时或离线的bookie可能拥有该条目。恢复过程停滞将会使主题变得不可用。

这句话的意思是,根据(WQ-AQ)+1,QA=1,那么需要所有WQ都回复negative响应才能认为条目不可恢复。如果有一台bookie响应超时,ledger recovery过程都将阻塞。

记住,由于保存ledger的bookie成员是动态分配的,因此可以将WQ设置成和AQ相等。如果你希望复制系数为2,则选择WQ=2和AQ=2。使用AQ=1会面临数据丢失或不可用。

Beware of Ledger Truncation!

我们知道什么是ledger截断,我们知道ledger recovery是如何避免ledger截断的,但是还有其他一些不经意间允许发生ledger截断的场景。

避免ledger截断的基本规则是,决不允许bookie对其之前确认的条目做出NoSuchEntry或NoSuchLedger响应,否则可能会导致经过ledger recovery后的ledger被截断。

例如:假设我们有一个WQ=3,AQ=2的ledger。

  1. 由于ensemble change,条目10只写入b1和b3
  2. 写入将继续,LAC当前为100
  3. 条目10不知怎么在b1上丢失了
  4. 客户端在该ledger上启动恢复,当它恢复到条目10时,它收到的前两个响应是来自b1和b2的NoSuchEntry响应。negative阈值已经满足
  5. 负责恢复的客户端在条目9处关闭ledger,并将其截断(91个条目被丢弃)

上面的例子说明一个错误的NoSuchEntry响应导致了数据的丢失。

但为什么一个已确认的条目会消失?让我们来看一些例子。

Operator Example 1 — Running without the journal on a version prior to 4.15.

注意:在撰写本文时,我的团队正在向BookKeeper提交一个更改,以允许它在没有journal的情况下运行,而不会导致ledger截断,希望这个问题可以在4.15版本修复。

通过使用配置journalWriteData=false,可以在没有journal的情况下运行。写入操作被添加到ledger write cache中,然后向客户端返回确认,从而完全跳过journal。条目将保留在缓存中,直到执行下一次批量刷盘。bookie崩溃可能会导致已确认的条目因为未刷盘而丢失。

因此,不要在没有journal的情况下运行BookKeeper。

如果bookie的磁盘出现故障,作为运维人员,您可能会尝试使用相同id的新bookie和新的空磁盘。在这种情况下,BookKeeper cookie会阻止新bookie的启动,但您可以使用CLI工具删除ZooKeeper中的cookie,以允许bookie使用空磁盘启动。你认为这些条目会被复制到新bookie中,这样就没问题了。但你忘记了账本被截断的可能性。

你已经从一个数据丢失可恢复场景变成了一个潜在的数据丢失不可恢复场景。

取而代之的是,使用decommissioning process来安全地移除一个挂掉的bookie,然后让一个空磁盘bookie重新加入集群。

这个场景没看懂。

Operator Example 3— Index file corruption

DbLedgerStorage条目位置信息和ledger索引文件都有可能损坏。幸运的是,如果发生这种情况,有CLI命令可以重建这些索引。您应该在bookie离线或处于只读模式时运行这些重建。

如果在bookie运行时执行重建操作,重建的索引将不会包含重建操作期间添加的条目,这些条目就有可能会被截断。

Contributor Example 1— Storage expansion

这是一个不好的BookKeeper功能,没有考虑到ledger截断。它允许您添加或删除journal和ledger存储目录。BookKeeper支持多个目录,您可以在其中挂载不同的磁盘,从而对单bookie进行扩缩容。

每个journal目录都会获得一个journal实例,同样,每个ledger目录也会获得一个ledger存储实例。ledger读取和写入通过其ledger id路由到journal实例和ledger存储实例。如果更改路由规则,还必须重写所有现有数据,否则后续读取可能会命中错误的ledger存储实例。例如,从1个ldger目录扩展到2个,会使50%的ledger不可读。打开的ledger中不可读条目使该ledger容易被截断。

相反,使用decommissioning process安全地移除bookie,然后将配置好的bookie重新加入集群。

Contributor Example 2— Changing the ledger routing algorithm

如果修改了bookie将读写请求路由到对应journal实例和ledger存储实例的路由算法时,已存在的数据也可能会不可读,增加了被截断的风险。

Contributor Example 3— Returning a NoSuchEntry response due to entry log file corruption

文件损坏可能会导致一个或多个条目在bookie上无法读取。在某些情况下,这会导致返回NoSuchEntry响应。

元数据显示某个条目在某台bookie上存在,但实际又没有,这是没有问题的,这可能发生在ensemble changes场景。但如果bookie上的索引指示条目在本地,但实际有返回了NoSuchEntry,那么就会增加数据被截断的风险。

可能有比上面列出的场景更多的场景,例如以各种方式更改元数据。

Bookie decommissioning process

decommissioning process使用外部恢复机制将数据从一台bookie迁移到另一台。它将把托管在目标bookie上的所有ledger fragment迁移到集群中的其他bookie。对于每个片段,会被复制到新bookie上,然后更新元数据让新bookie生效。

这并不是一个完美的decommissioning机制,因为需要先下掉bookie,然后复制条目使得满足复制因子。更安全的方法是先迁移数据,然后再关闭bookie。

最后,让一台bookie退役最安全的方法就是将其设置为只读,然后等待数据过期后删除。

Summary

BookKeeper协议与Raft或Kafka复制协议等集成协议有很大不同。

因此请注意,如果你为项目贡献了代码,请确保对BookKeeper所做的任何更改都不会导致bookie对之前写入的条目返回NoSuchEntry或NoSuchLedger响应。如果你运行BookKeeper,请确保避免之前写入的条目变得不可读或消失。

标签:recovery,bookie,ledger,条目,Part,Ledgers,BookKeeper,客户端
From: https://www.cnblogs.com/oyld/p/16970832.html

相关文章