在 direct 中分隔存储空间分配

适用于:Windows Server 2022、Windows Server 2019

Windows Server 2019 引入了一个选项,用于手动分隔 存储空间 Direct 中的卷分配。 在某些情况下,这样做可以显著提高容错能力,但会增加一些管理注意事项和复杂性。 本主题介绍了它的工作原理,并提供了 PowerShell 中的示例。

重要

此功能是 Windows Server 2019 中的新增功能。 它不适用于 Windows Server 2016。

先决条件

Green checkmark icon. 如果:

  • 群集有六台或六台以上服务器;和
  • 群集仅使用 三向镜像 复原能力

Red X icon. 如果:

了解

查看:常规分配

使用常规的三向镜像时,卷分为多个小型"板",这些小"板"复制三次,并均匀分布在群集中每个服务器的每一个驱动器上。 有关更多详细信息,请阅读 此深入探讨博客

Diagram showing the volume being divided into three stacks of slabs and distributed evenly across every server.

此默认分配可最大程度地提高并行读取和写入,从而提高性能,并且简单明了:每个服务器同样繁忙,每个驱动器同样已满,所有卷保持联机或一起脱机。 每个卷保证最多能保留两次并发故障,如 这些示例所示

但是,通过此分配,卷无法幸存三次并发故障。 如果三台服务器一次发生故障,或者三台服务器中的驱动器一次发生故障,则卷将变为无法访问,因为至少有一些 (,且将) 分配给确切的三个驱动器或发生故障的服务器。

在下面的示例中,服务器 1、3 和 5 同时失败。 尽管许多实验室都有幸存的副本,但有些则没有:

Diagram showing three of six servers highlighted in red, and the overall volume is red.

卷脱机,在服务器恢复之前无法访问。

新增:分隔的分配

使用带分隔符的分配,可以指定一部分服务器, (至少四) 。 卷分为三次复制的板(与之前一样)但不要跨每个服务器分配,而是仅将这些板分配给指定的 服务器的子集

例如,如果有一个 8 节点群集 (节点 1 到 8) ,可以指定卷仅位于节点 1、2、3、4 中的磁盘上。

优点

使用示例分配时,卷可能会在三次并发故障中幸存。 如果节点 1、2 和 6 关闭,则只有 2 个保存卷的 3 个数据副本的节点关闭,卷将保持联机状态。

生存概率取决于服务器数量和其他因素 - 有关详细信息, 请参阅分析。

缺点

分隔分配增加了一些管理注意事项和复杂性:

  1. 管理员负责分隔每个卷的分配,以平衡服务器之间的存储利用率并维持高生存概率,如最佳做法 部分 中所述。

  2. 使用带分隔符的分配,保留每个服务器驱动器的等效容量 (,没有最大 ) 。 这超过了已发布的 常规 分配建议,该建议最多四个容量驱动器。

  3. 如果服务器发生故障并需要更换,如删除服务器及其驱动器中所述,管理员将负责通过添加新服务器并删除发生故障的服务器来更新受影响卷的运行情况 - 示例如下。

PowerShell 中的用法

可以使用 New-Volume cmdlet 在 存储空间 Direct。

例如,若要创建常规的三向镜像卷:

New-Volume -FriendlyName "MyRegularVolume" -Size 100GB

创建卷并分隔其分配

创建三向镜像卷并分隔其分配:

  1. 首先将群集中的服务器分配到变量 $Servers

    $Servers = Get-StorageFaultDomain -Type StorageScaleUnit | Sort FriendlyName
    

    提示

    在 存储空间 Direct 中,术语"存储缩放单元"是指附加到一台服务器的所有原始存储,包括直接连接的驱动器和带驱动器的直接连接外部机箱。 在此上下文中,它与"server"相同。

  2. 指定要与新参数一起使用 -StorageFaultDomainsToUse 的服务器,并按 索引。 $Servers 例如,若要将分配分隔到第一个、第二个、第三个和第四 (索引 0、1、2 和 3) :

    New-Volume -FriendlyName "MyVolume" -Size 100GB -StorageFaultDomainsToUse $Servers[0,1,2,3]
    

请参阅分隔的分配

若要了解如何分配 MyVolume, 请使用附录 中的 脚本

PS C:\> .\Get-VirtualDiskFootprintBySSU.ps1

VirtualDiskFriendlyName TotalFootprint Server1 Server2 Server3 Server4 Server5 Server6
----------------------- -------------- ------- ------- ------- ------- ------- -------
MyVolume                300 GB         100 GB  100 GB  100 GB  100 GB  0       0

请注意,只有 Server1、Server2、Server3 和 Server4 包含 MyVolume 的板

更改分隔的分配

使用 new Add-StorageFaultDomainRemove-StorageFaultDomain cmdlet 更改分配分隔方式。

例如,将 MyVolume 移到一台服务器上:

  1. 指定第五台服务器 可以 存储 MyVolume 的板

    Get-VirtualDisk MyVolume | Add-StorageFaultDomain -StorageFaultDomains $Servers[4]
    
  2. 指定第一台服务器 不能 存储 MyVolume 的板

    Get-VirtualDisk MyVolume | Remove-StorageFaultDomain -StorageFaultDomains $Servers[0]
    
  3. 重新平衡存储池,使更改生效:

    Get-StoragePool S2D* | Optimize-StoragePool
    

可以使用 监视重新平衡的进度 Get-StorageJob

完成后,再次运行 验证 MyVolume 是否 移动。

PS C:\> .\Get-VirtualDiskFootprintBySSU.ps1

VirtualDiskFriendlyName TotalFootprint Server1 Server2 Server3 Server4 Server5 Server6
----------------------- -------------- ------- ------- ------- ------- ------- -------
MyVolume                300 GB         0       100 GB  100 GB  100 GB  100 GB  0

请注意,Server1 不再包含 MyVolume 的版块 - 相反,Server5 会包含。

最佳实践

下面是使用分隔卷分配时应遵循的最佳实践:

选择四台服务器

将每个三向镜像卷分隔到四台服务器,而不是更多服务器。

均衡存储

平衡分配给每个服务器的存储量,并计算卷大小。

错开分隔的分配卷

若要最大程度地提高容错能力,请使每个卷的分配唯一,这意味着它不会与另一个卷共享其所有服务器, (重叠的情况) 。

例如,在八节点系统中:卷 1:服务器 1、2、3、4 卷 2:服务器 5、6、7、8 卷 3:服务器 3、4、5、6 卷 4:服务器 1、2、7、8

分析

本部分派生了卷保持联机和可访问的数学概率 (或等效于总体存储中保持联机和可访问状态的预期部分) 这是故障次数和群集大小的函数。

注意

本部分是可选阅读内容。 如果有兴趣查看数学,请继续阅读! 但如果没有,请不要担心:成功实现分隔分配只需使用PowerShell和最佳做法。

最多两次失败始终正常

无论三向镜像卷的分配如何,每个三向镜像卷可同时保留最多两个故障。 如果两个驱动器发生故障,或者两台服务器发生故障,或每个服务器各有一个,则即使定期分配,每个三向镜像卷也保持联机和可访问状态。

超过一半群集发生故障永远没有问题

相反,在群集中超过一半的服务器或驱动器同时发生故障的极端情况下,仲裁会丢失,并且每个三向镜像卷都会脱机,并且无法访问,而不管其分配如何。

介于两者之间呢?

如果一次发生三次或三次以上故障,但至少有一半的服务器和驱动器仍处于启动状态,则具有分隔分配的卷可能会保持联机并可访问,具体取决于哪些服务器出现故障。

常见问题

能否分隔某些卷,但不能分隔其他卷?

是的。 可以选择是否按卷分隔分配。

分隔的分配是否更改了驱动器更换的工作原理?

否,它与常规分配相同。

其他参考

附录

此脚本可帮助你了解如何分配卷。

若要如上所述使用它,请复制/粘贴并将其另存为 Get-VirtualDiskFootprintBySSU.ps1

Function ConvertTo-PrettyCapacity {
    Param (
        [Parameter(
            Mandatory = $True,
            ValueFromPipeline = $True
            )
        ]
    [Int64]$Bytes,
    [Int64]$RoundTo = 0
    )
    If ($Bytes -Gt 0) {
        $Base = 1024
        $Labels = ("bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
        $Order = [Math]::Floor( [Math]::Log($Bytes, $Base) )
        $Rounded = [Math]::Round($Bytes/( [Math]::Pow($Base, $Order) ), $RoundTo)
        [String]($Rounded) + " " + $Labels[$Order]
    }
    Else {
        "0"
    }
    Return
}

Function Get-VirtualDiskFootprintByStorageFaultDomain {

    ################################################
    ### Step 1: Gather Configuration Information ###
    ################################################

    Write-Progress -Activity "Get-VirtualDiskFootprintByStorageFaultDomain" -CurrentOperation "Gathering configuration information..." -Status "Step 1/4" -PercentComplete 00

    $ErrorCannotGetCluster = "Cannot proceed because 'Get-Cluster' failed."
    $ErrorNotS2DEnabled = "Cannot proceed because the cluster is not running Storage Spaces Direct."
    $ErrorCannotGetClusterNode = "Cannot proceed because 'Get-ClusterNode' failed."
    $ErrorClusterNodeDown = "Cannot proceed because one or more cluster nodes is not Up."
    $ErrorCannotGetStoragePool = "Cannot proceed because 'Get-StoragePool' failed."
    $ErrorPhysicalDiskFaultDomainAwareness = "Cannot proceed because the storage pool is set to 'PhysicalDisk' fault domain awareness. This cmdlet only supports 'StorageScaleUnit', 'StorageChassis', or 'StorageRack' fault domain awareness."

    Try  {
        $GetCluster = Get-Cluster -ErrorAction Stop
    }
    Catch {
        throw $ErrorCannotGetCluster
    }

    If ($GetCluster.S2DEnabled -Ne 1) {
        throw $ErrorNotS2DEnabled
    }

    Try  {
        $GetClusterNode = Get-ClusterNode -ErrorAction Stop
    }
    Catch {
        throw $ErrorCannotGetClusterNode
    }

    If ($GetClusterNode | Where State -Ne Up) {
        throw $ErrorClusterNodeDown
    }

    Try {
        $GetStoragePool = Get-StoragePool -IsPrimordial $False -ErrorAction Stop
    }
    Catch {
        throw $ErrorCannotGetStoragePool
    }

    If ($GetStoragePool.FaultDomainAwarenessDefault -Eq "PhysicalDisk") {
        throw $ErrorPhysicalDiskFaultDomainAwareness
    }

    ###########################################################
    ### Step 2: Create SfdList[] and PhysicalDiskToSfdMap{} ###
    ###########################################################

    Write-Progress -Activity "Get-VirtualDiskFootprintByStorageFaultDomain" -CurrentOperation "Analyzing physical disk information..." -Status "Step 2/4" -PercentComplete 25

    $SfdList = Get-StorageFaultDomain -Type ($GetStoragePool.FaultDomainAwarenessDefault) | Sort FriendlyName # StorageScaleUnit, StorageChassis, or StorageRack

    $PhysicalDiskToSfdMap = @{} # Map of PhysicalDisk.UniqueId -> StorageFaultDomain.FriendlyName
    $SfdList | ForEach {
        $StorageFaultDomain = $_
        $_ | Get-StorageFaultDomain -Type PhysicalDisk | ForEach {
            $PhysicalDiskToSfdMap[$_.UniqueId] = $StorageFaultDomain.FriendlyName
        }
    }

    ##################################################################################################
    ### Step 3: Create VirtualDisk.FriendlyName -> { StorageFaultDomain.FriendlyName -> Size } Map ###
    ##################################################################################################

    Write-Progress -Activity "Get-VirtualDiskFootprintByStorageFaultDomain" -CurrentOperation "Analyzing virtual disk information..." -Status "Step 3/4" -PercentComplete 50

    $GetVirtualDisk = Get-VirtualDisk | Sort FriendlyName

    $VirtualDiskMap = @{}

    $GetVirtualDisk | ForEach {
        # Map of PhysicalDisk.UniqueId -> Size for THIS virtual disk
        $PhysicalDiskToSizeMap = @{}
        $_ | Get-PhysicalExtent | ForEach {
            $PhysicalDiskToSizeMap[$_.PhysicalDiskUniqueId] += $_.Size
        }
        # Map of StorageFaultDomain.FriendlyName -> Size for THIS virtual disk
        $SfdToSizeMap = @{}
        $PhysicalDiskToSizeMap.keys | ForEach {
            $SfdToSizeMap[$PhysicalDiskToSfdMap[$_]] += $PhysicalDiskToSizeMap[$_]
        }
        # Store
        $VirtualDiskMap[$_.FriendlyName] = $SfdToSizeMap
    }

    #########################
    ### Step 4: Write-Out ###
    #########################

    Write-Progress -Activity "Get-VirtualDiskFootprintByStorageFaultDomain" -CurrentOperation "Formatting output..." -Status "Step 4/4" -PercentComplete 75

    $Output = $GetVirtualDisk | ForEach {
        $Row = [PsCustomObject]@{}

        $VirtualDiskFriendlyName = $_.FriendlyName
        $Row | Add-Member -MemberType NoteProperty "VirtualDiskFriendlyName" $VirtualDiskFriendlyName

        $TotalFootprint = $_.FootprintOnPool | ConvertTo-PrettyCapacity
        $Row | Add-Member -MemberType NoteProperty "TotalFootprint" $TotalFootprint

        $SfdList | ForEach {
            $Size = $VirtualDiskMap[$VirtualDiskFriendlyName][$_.FriendlyName] | ConvertTo-PrettyCapacity
            $Row | Add-Member -MemberType NoteProperty $_.FriendlyName $Size
        }

        $Row
    }

    # Calculate width, in characters, required to Format-Table
    $RequiredWindowWidth = ("TotalFootprint").length + 1 + ("VirtualDiskFriendlyName").length + 1
    $SfdList | ForEach {
        $RequiredWindowWidth += $_.FriendlyName.Length + 1
    }

    $ActualWindowWidth = (Get-Host).UI.RawUI.WindowSize.Width

    If (!($ActualWindowWidth)) {
        # Cannot get window width, probably ISE, Format-List
        Write-Warning "Could not determine window width. For the best experience, use a Powershell window instead of ISE"
        $Output | Format-Table
    }
    ElseIf ($ActualWindowWidth -Lt $RequiredWindowWidth) {
        # Narrower window, Format-List
        Write-Warning "For the best experience, try making your PowerShell window at least $RequiredWindowWidth characters wide. Current width is $ActualWindowWidth characters."
        $Output | Format-List
    }
    Else {
        # Wider window, Format-Table
        $Output | Format-Table
    }
}

Get-VirtualDiskFootprintByStorageFaultDomain