HDFS 中的块扫描器、目录扫描器和磁盘检查器

本文主要取材并翻译自HDFS DataNode Scanners and Disk Checker Explained,并结合了一些个人理解。本文假设读者对于HDFS有基本的了解。

背景

在HDFS中,NameNode存储元数据,DataNode存储实际数据内容。每个 DataNode 通常由多个磁盘(在 HDFS 的术语中为“卷”)组成。 HDFS 中的一个文件包含一个或多个块。根据配置的复制因子,块具有一个或多个副本。副本存储在DataNode的一个卷上,同一块的不同副本存储在不同的DataNode上。当创建新文件块或打开现有文件进行追加时,HDFS 写入操作会创建 DataNode 管道来接收和存储副本。块的详细状态通过块报告从每个 DataNode 发送到 NameNode。

最终,一个块会被最终确定并存储在DataNode磁盘上,形成一个块文件和一个本地文件系统上的元文件。块文件存储文件数据,而元文件存储文件数据的校验和,用于验证块文件的完整性。元文件以块文件命名,并额外包含生成标记。

为了区分 NameNode 上下文中的块和 DataNode 上下文中的块,将前者称为“块” ,后者称为“副本”。

副本的状态

DataNode 上下文中的副本可以处于以下状态之一:

  • FINALIZED:副本的写入操作完成,数据已冻结(长度已确定),除非重新打开副本进行追加。具有相同生成标记(Generation Stamp,GS)的块的所有最终副本应具有相同的数据。最终副本的GS可能会因恢复而增加。
  • RBW(Replica Being Written):正在写入中的副本,无论文件是为写入而创建还是为追加而重新打开。 RBW 副本始终是打开文件的最后一个块。数据仍在写入副本中,尚未最终确定。 RBW 副本的数据(不一定是全部)对读取客户端可见。如果发生任何故障,将尝试将数据保留在 RBW 副本中。
  • RWR(Replica Waiting to be Recovered):如果一个DataNode死亡并重新启动,其所有RBW副本将更改为RWR状态。 RWR 副本将变得过时并因此被丢弃,或者将参与租约恢复。
  • RUR(Replica Under Recovery):非TEMPORARY副本在参与租约恢复时将变为RUR状态。
  • TEMPORARY:出于块复制的目的创建临时副本(通过复制监视器或集群平衡器)。它类似于 RBW 副本,只是它的数据对所有读取客户端都是不可见的。如果块复制失败,临时副本将被删除。

块的状态

NameNode 上下文中的块可能处于以下状态之一:

  • UNDER_CONSTRUCTION:在写入时的状态。这种块是打开文件的最后一个块,其长度和生成标记仍可变,部分数据对读取程序可见。NameNode 中的 UNDER_CONSTRUCTION 块跟踪写入管道(有效 RBW 副本的位置)及其 RWR 副本的位置。
  • UNDER_RECOVERY:当文件的最后一个块在客户端租约过期时处于 UNDER_CONSTRUCTION 状态时,块恢复开始时将其改为 UNDER_RECOVERY 状态。
  • COMMITTED:块的数据和生成标记不再可变(除非重新打开进行追加),且报告相同 GS/长度 的 FINALIZED 副本的 DataNode 数量少于最小复制数量。为了服务读请求,COMMITTED 块必须跟踪 RBW 副本的位置、FINALIZED 副本的 GS 和长度。当客户端要求 NameNode 添加新块或关闭文件时,UNDER_CONSTRUCTION 块将转换为 COMMITTED。如果文件的最后一个或倒数第二个块处于 COMMITTED 状态,则文件无法关闭,客户端必须重试。
  • COMPLETE:当 NameNode 看到匹配 GS/长度 的 FINALIZED 副本的最小复制数量时,COMMITTED 块转换为 COMPLETE。只有当文件的所有块都完成时,文件才能关闭。即使块没有最小复制数量的副本,也可能被强制进入 COMPLETE 状态,例如,当客户端请求新块,且前一个块尚未完成时。

DataNode 将副本的状态保存到磁盘,但 NameNode 不会将块状态保存到磁盘。当NameNode重新启动时,它会将任何先前打开的文件的最后一个块的状态更改为UNDER_CONSTRUCTION状态,并将所有其他块的状态更改为COMPLETE。

生成标记(Generation Stamp)

生成标记(GS)是由NameNode持久维护的每个块的单调递增的8字节数字。引入块和副本的GS主要用于以下几个目的:

  • 检测块的陈旧副本:当副本的GS早于块的GS时。当副本以某种方式跳过了追加操作时,可能会发生这种情况。
  • 检测长时间失效的DataNode上的过时副本,并重新加入集群。

需要更新GS的情况包括:

  • 创建新文件。
  • 客户端打开现有文件以进行追加或截断。
  • 客户端在向DataNode写入数据时遇到错误并请求新的GS。
  • NameNode为文件启动租约恢复。

块扫描器(Block Scanner)和卷扫描器(Volume Scanner)

HDFS 的核心假设之一是硬件故障是常态而不是例外。当磁盘发生故障时,块文件和元文件中的一个或两个可能会损坏。 HDFS 有识别和处理这些的机制。

块扫描器的功能是扫描块数据以检测可能的损坏。由于任何 DataNode 上的任何块上的数据损坏都可能随时发生,因此及时识别这些错误非常重要。这样,NameNode 可以删除损坏的块并相应地重新复制,以保持数据完整性并减少客户端错误。另一方面,不应使用太多资源,以便磁盘 I/O 仍然可以服务实际请求。因此,块扫描器需要确保可疑块被相对快速地扫描,而其他块每隔一段时间以相对较低的频率被扫描,而不需要大量的 I/O 使用。

块扫描器与 DataNode 关联,并包含卷扫描器(Volume Scanner)的集合。每个卷扫描器都运行自己的线程,负责扫描DataNode的单个卷。卷扫描器缓慢读取所有块,并验证每个块,称之为定期扫描。请注意这是很慢的操作,因为需要读取整个块来执行检查,这会消耗大量 I/O。

卷扫描器还维护可疑块的列表。这些块在从磁盘读取时会导致抛出特定类型的异常。在扫描过程中,可疑块优先于常规块。此外,每个卷扫描器都会跟踪过去 10 分钟内扫描过的可疑块,以避免重复扫描相同的可疑块。

请注意,块扫描器和卷扫描器是实现细节,它们一起处理块扫描的工作。因此,为了简单起见,将在下文将块扫描器和卷扫描器并称为“块扫描器”。

块扫描器用于决定扫描哪个块的机制如下:

  • 当 DataNode 正在处理来自客户端或另一个 DataNode 的 I/O 请求时,如果捕获到 IOException,并且不是由于网络(即套接字超时、管道损坏或连接重置)引起的,则该块将被标记为可疑并添加到块扫描器的可疑块列表中。
  • 块扫描器循环遍历所有块。在每次迭代时,它都会检查一个块:如果可疑块列表不为空,则弹出一个可疑块进行扫描;否则,扫描正常块。

只有本地(非网络)IOException 才会导致块被标记为可疑,因为希望保持可疑块列表简短并减少误报。这样,故障就会得到优先处理并及时报告。

为了跟踪块之间的扫描位置,为每个卷维护一个块光标。光标定期保存到磁盘(间隔可通过dfs.block.scanner.cursor.save.interval.ms配置,默认值为 10 分钟)。这样,即使DataNode进程重新启动或服务器重新启动,扫描也不必从头开始。

扫描器的另一个问题是 I/O 消耗。无论被扫描的块是可疑块还是正常块,都不能连续地循环扫描它们,因为这可能会产生繁忙的 I/O 并损害正常的 I/O 性能。相反,扫描器以配置的速率运行以进行节流,并在两个扫描周期之间有适当的睡眠间隔。扫描周期是执行整个扫描的时间间隔。当一个块被标记为可疑时,如果卷扫描器正在等待下一个扫描周期,则它会被唤醒。如果在扫描周期内未完成完整扫描,则扫描将继续而不休眠。

相关的重要配置如下:

  • dfs.block.scanner.volume.bytes.per.second: 将扫描带宽限制为可配置的每秒字节数。默认值为 1M。将其设置为 0 将禁用块扫描器。
  • dfs.datanode.scan.period.hours: 配置扫描周期,定义执行整个扫描的频率。出于上述原因,应将其设置为足够长的间隔才能真正生效。默认值为 3 周(504 小时)。将此值设置为 0 将使用默认值。将其设置为负值将禁用块扫描器。

目录扫描器(Directory Scanner)

虽然块扫描器确保存储在磁盘上的块文件处于良好状态,但 DataNode 将块信息缓存在内存中。确保缓存的信息准确无误至关重要。目录扫描器会检查并修复缓存与磁盘上实际文件之间的不一致。它会定期扫描数据目录中的块和元数据文件,并协调磁盘中维护的块信息和内存中的块信息之间的差异。

如果数据块被标记为已损坏,则会通过下一个数据块报告将其报告给 NameNode。然后,NameNode 将安排块以便从良好的副本进行复制。与块扫描器类似,目录扫描器也需要限制。限制是通过限制每个目录扫描器线程仅运行每秒给定的毫秒数来完成的。

相关的重要配置如下:

  • dfs.datanode.directoryscan.throttle.limit.ms.per.sec: 控制线程每秒应运行的毫秒数。请注意,此限制是按线程采用的,而不是所有线程的聚合值。默认值为 1000,表示已禁用限制。只有 1 到 1000 之间的值才有效。设置无效值将导致禁用限制并记录错误消息。
  • dfs.datanode.directoryscan.threads: 控制目录扫描器可以并行具有的最大线程数。默认值为 1。
  • dfs.datanode.directoryscan.interval: 控制目录扫描器线程运行的时间间隔(以秒为单位)。将此值设置为负值将禁用目录扫描器。默认值为 6 小时(21600 秒)。

磁盘检查器(Disk Checker)

DataNode 还可以在后台线程中运行磁盘检查器,以确定卷是否运行状况不佳以及是否将其删除。使用磁盘检查器的原因是,如果在卷级别出现问题,HDFS 应该检测到它并停止尝试写入该卷。另一方面,删除卷并非易事,并且会产生广泛的影响,因为它会使该卷上的所有块都无法访问,并且 HDFS 必须处理由于删除而复制不足的所有块。因此,磁盘检查器执行最基本的检查,并使用非常保守的逻辑来考虑故障。

检查中的逻辑非常简单,它按顺序检查 DataNode 上的以下目录(目录的含义请参考上述的DataNode副本状态枚举):

  • Directory finalized
  • Directory tmp
  • Directory rbw

在检查每一个目录时,磁盘检查器会验证:

  • 该目录及其所有父目录都存在,或者可以通过其他方式创建。
  • 该路径确实是 directory 类型。
  • 该进程对目录具有读取、写入和执行权限。

严格来说,应该递归地对所有这些目录下的所有子目录进行相同的检查。但事实证明,这会产生过多的 I/O,几乎没有什么好处:那些出错的块无论如何都会被添加到块扫描器中的可疑块中,因此它们会相对较快地被扫描/报告。另一方面,HBase 等应用程序对性能非常敏感,并努力确保 SLA,此处过多的 I/O 可能会产生峰值,这是不能容忍的。因此,仅执行上面列出的检查。

虽然块扫描器和目录扫描器在 DataNode 启动时激活并定期扫描,但磁盘检查器仅按需运行,它的线程是懒惰创建的。具体来说,磁盘检查器仅在常规 I/O 操作期间(例如关闭块或元数据文件、目录扫描器报告错误等)在 DataNode 上捕获到 IOException 时运行。此外,磁盘检查器在 5~6 秒内最多只运行一次。此特定时间段是在创建磁盘检查器线程时随机生成的。

此外,如果启动时失败的卷数量大于配置的阈值,则 DataNode 会自行关闭。DataNode 通过比较所有配置的存储位置与尝试将所有存储位置投入使用后实际使用的存储位置之间的差异来执行此初始检查。阈值可通过 dfs.datanode.failed.volumes.tolerated 进行配置。默认值为 0。