MD 集群

集群 MD 是一个用于集群的共享设备 RAID,它支持两个级别:raid1 和 raid10(有限支持)。

1. 磁盘格式

每个集群节点都使用单独的写意图位图。位图记录了该节点上可能已开始但尚未完成的所有写入操作。磁盘布局如下:

0                    4k                     8k                    12k
-------------------------------------------------------------------
| idle                | md super            | bm super [0] + bits |
| bm bits[0, contd]   | bm super[1] + bits  | bm bits[1, contd]   |
| bm super[2] + bits  | bm bits [2, contd]  | bm super[3] + bits  |
| bm bits [3, contd]  |                     |                     |

在“正常”运行期间,我们假设文件系统确保在任何给定时间只有一个节点写入任何给定块,因此一个写入请求将:

  • 设置相应的位(如果尚未设置)

  • 将写入提交到所有镜像

  • 在超时后安排清除该位。

读取操作正常处理。文件系统负责确保一个节点不会从另一个节点(或同一节点)正在写入的位置读取。

2. 用于管理的 DLM 锁

有三组用于管理设备的锁:

2.1 位图锁资源 (bm_lockres)

bm_lockres 保护各个节点位图。它们命名格式为 node 1 对应 bitmap000,node 2 对应 bitmap001,以此类推。当一个节点加入集群时,它以 PW 模式获取锁,并在该节点作为集群一部分的生命周期内保持此状态。锁资源编号基于 DLM 子系统返回的槽号。由于 DLM 的节点计数从一开始,而位图槽从零开始,因此从 DLM 槽号中减去一以得到位图槽号。

特定节点的位图锁的 LVB (Lock Value Block) 记录了该节点正在重新同步的扇区范围。其他节点不得写入这些扇区。这在有新节点加入集群时使用。

2.2 消息传递锁

每个节点在开始或结束重新同步以及进行元数据超级块更新时都必须与其他节点通信。此通信通过三个锁进行管理:“token”、“message”和“ack”,以及其中一个“message”锁的锁值块 (LVB)。

2.3 新设备管理

一个单独的锁:“no-new-dev”用于协调新设备的添加——这必须在整个阵列中同步。通常,所有节点在此设备上都持有并发读锁。

3. 通信

消息可以广播到所有节点,发送方等待所有其他节点确认消息后才继续。一次只能处理一条消息。

3.1 消息类型

有六种消息类型进行传递:

3.1.1 METADATA_UPDATED

通知其他节点元数据已更新,节点必须重新读取 md 超级块。此操作是同步执行的。它主要用于信号设备故障。

3.1.2 RESYNCING

通知其他节点重新同步已启动或结束,以便每个节点可以暂停或恢复该区域。每个 RESYNCING 消息都标识了发送节点即将重新同步的设备范围。这会覆盖该节点之前的任何通知:每个节点一次只能重新同步一个范围。

3.1.3 NEWDISK

通知其他节点有设备正在添加到阵列。消息包含该设备的标识符。详见下文。

3.1.4 REMOVE

一个失败或备用设备正在从阵列中移除。设备槽号包含在消息中。

3.1.5 RE_ADD

一个失败的设备正在重新激活——假设它已被确定再次工作。

3.1.6 BITMAP_NEEDS_SYNC

如果一个节点在本地停止但位图不干净,则通知另一个节点接管重新同步的所有权。

3.2 通信机制

DLM LVB 用于集群节点内部通信。为此目的使用了三种资源:

3.2.1 token

保护整个通信系统的资源。持有 token 资源的节点被允许进行通信。

3.2.2 message

承载通信数据的锁资源。

3.2.3 ack

获取此资源意味着消息已被集群中所有节点确认。资源的 BAST (Batch Acknowledge State Table) 用于通知接收节点有节点想要通信。

算法如下:

  1. 接收状态 - 所有节点在“ack”上都具有并发读锁

    sender                         receiver                 receiver
    "ack":CR                       "ack":CR                 "ack":CR
    
  2. 发送方在“token”上获取 EX (Exclusive) 锁,发送方在“message”上获取 EX 锁

    sender                        receiver                 receiver
    "token":EX                    "ack":CR                 "ack":CR
    "message":EX
    "ack":CR
    

    发送方检查是否仍需要发送消息。在等待“token”期间收到的消息或其他事件可能已使此消息不适合或多余。

  3. 发送方写入 LVB

    发送方将“message”从 EX 降级为 CW (Convert to Write)

    发送方尝试获取“ack”的 EX 锁

     [ wait until all receivers have *processed* the "message" ]
    
                                      [ triggered by bast of "ack" ]
                                      receiver get CR on "message"
                                      receiver read LVB
                                      receiver processes the message
                                      [ wait finish ]
                                      receiver releases "ack"
                                      receiver tries to get PR on "message"
    
    sender                         receiver                  receiver
    "token":EX                     "message":CR              "message":CR
    "message":CW
    "ack":EX
    
  4. 由“ack”上 EX 锁的授予触发(表示所有接收方都已处理消息)

    发送方将“ack”从 EX 降级为 CR (Convert to Read)

    发送方释放“message”

    发送方释放“token”

                                receiver upconvert to PR on "message"
                                receiver get CR of "ack"
                                receiver release "message"
    
    sender                      receiver                   receiver
    "ack":CR                    "ack":CR                   "ack":CR
    

4. 故障处理

4.1 节点故障

当一个节点发生故障时,DLM 会将槽号通知集群。节点启动一个集群恢复线程。集群恢复线程会:

  • 获取故障节点的 bitmap<number> 锁

  • 打开位图

  • 读取故障节点的位图

  • 将已设置的位图复制到本地节点

  • 清除故障节点的位图

  • 释放故障节点的 bitmap<number> 锁

  • 在当前节点上启动位图重新同步。md_check_recovery 在 recover_bitmaps 内部调用,然后 md_check_recovery -> metadata_update_start/finish,它将通过 lock_comm 锁定通信。这意味着当一个节点正在重新同步时,它会阻止所有其他节点向阵列的任何位置写入。

重新同步过程是常规的 md 重新同步。然而,在集群环境中执行重新同步时,需要告知其他节点哪些区域已暂停。在重新同步开始之前,节点发送 RESYNCING 消息,其中包含需要暂停的区域的 (lo,hi) 范围。每个节点维护一个 suspend_list,其中包含当前暂停的范围列表。收到 RESYNCING 消息后,节点将该范围添加到 suspend_list。同样,当执行重新同步的节点完成时,它会向其他节点发送带有空范围的 RESYNCING 消息,其他节点会从 suspend_list 中移除相应的条目。

一个辅助函数 ->area_resyncing() 可用于检查特定 I/O 范围是否应该暂停。

4.2 设备故障

设备故障通过元数据更新例程进行处理和通信。当一个节点检测到设备故障时,它不允许进一步写入该设备,直到所有其他节点都确认了该故障。

5. 添加新设备

为了添加新设备,所有节点都必须“看到”要添加的新设备。为此,使用以下算法:

  1. 节点 1 发出 mdadm --manage /dev/mdX --add /dev/sdYY,该命令发出 ioctl(ADD_NEW_DISK,其中 disc.state 设置为 MD_DISK_CLUSTER_ADD)

  2. 节点 1 发送带有 uuid 和槽号的 NEWDISK 消息

  3. 其他节点发出带有 uuid 和槽号的 kobject_uevent_env(步骤 4、5 可以是一个 udev 规则)

  4. 在用户空间中,节点搜索磁盘,可能使用 blkid -t SUB_UUID=””

  5. 根据是否找到磁盘,其他节点发出以下任一命令:ioctl(ADD_NEW_DISK,其中 disc.state 设置为 MD_DISK_CANDIDATE 且 disc.number 设置为槽号) 或 ioctl(CLUSTERED_DISK_NACK)

  6. 如果找到设备,其他节点释放“no-new-devs”(CR)上的锁

  7. 节点 1 尝试在“no-new-dev”上获取 EX 锁

  8. 如果节点 1 获得锁,它会在将磁盘标记为 SpareLocal 后发送 METADATA_UPDATED

  9. 如果未获得锁(获取“no-new-dev”锁),则操作失败并发送 METADATA_UPDATED。

  10. 其他节点通过后续的 METADATA_UPDATED 获得磁盘是否已添加的信息。

6. 模块接口

md 核心可以向集群模块发出 17 个回调。理解这些可以很好地概述整个过程。

6.1 join(nodes) 和 leave()

这些在带有集群位图的阵列启动和阵列停止时调用。join() 确保集群可用并初始化各种资源。集群中只有前 'nodes' 个节点可以使用该阵列。

6.2 slot_number()

报告集群基础设施建议的槽号。范围从 0 到 nodes-1。

6.3 resync_info_update()

这会更新存储在位图锁中的重新同步范围。起始点随着重新同步的进行而更新。终点始终是阵列的末尾。它不会发送 RESYNCING 消息。

6.4 resync_start(), resync_finish()

这些在重新同步/恢复/重塑开始或停止时调用。它们更新位图锁中的重新同步范围,并发送 RESYNCING 消息。resync_start 报告整个阵列正在重新同步,resync_finish 报告没有重新同步。

resync_finish() 还会发送一个 BITMAP_NEEDS_SYNC 消息,允许其他节点接管。

6.5 metadata_update_start(), metadata_update_finish(), metadata_update_cancel()

metadata_update_start 用于获取元数据的独占访问权限。如果获得访问权限后仍需要更改,metadata_update_finish() 将向所有其他节点发送 METADATA_UPDATE 消息,否则可以使用 metadata_update_cancel() 释放锁。

6.6 area_resyncing()

这结合了两种功能。

首先,它会检查是否有任何节点当前正在给定扇区范围内重新同步任何内容。如果发现任何重新同步,则调用者将避免在该范围内写入或读取平衡。

其次,当节点恢复进行时,它报告所有区域都为 READ 请求而重新同步。这避免了集群文件系统和集群 RAID 处理节点故障时的竞争条件。

6.7 add_new_disk_start(), add_new_disk_finish(), new_disk_ack()

这些用于管理上述新磁盘协议。当添加新设备时,在将其绑定到阵列之前调用 add_new_disk_start(),如果成功,则在设备完全添加后调用 add_new_disk_finish()。

当设备响应先前的请求而添加,或者当设备被声明为“不可用”时,会调用 new_disk_ack()。

6.8 remove_disk()

当备用或失败设备从阵列中移除时调用此函数。它会导致 REMOVE 消息发送到其他节点。

6.9 gather_bitmaps()

这会向所有其他节点发送 RE_ADD 消息,然后从所有位图中收集位图信息。然后使用此组合位图来恢复重新添加的设备。

6.10 lock_all_bitmaps() 和 unlock_all_bitmaps()

当将位图更改为无时,会调用这些函数。如果一个节点计划清除集群 RAID 的位图,它需要确保没有其他节点正在使用该 RAID,这通过锁定集群内的所有位图锁来实现,并且这些锁也会相应地解锁。

7. 不支持的功能

MD 集群目前不支持一些功能:

  • 更改 array_sectors。