在存储空间直通中界定卷的分配

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

重要

此功能是 Windows Server 2019 中的新增功能。 该功能在 Windows Server 2016 中不可用。

先决条件

绿色复选标记图标。 如果出现以下情况,请考虑使用此选项:

  • 群集有六个或更多服务器;并且
  • 群集仅使用三向镜像复原

红色 X 图标。 如果出现以下情况,请勿使用此选项:

了解

检查:定期分配

使用常规的三向镜像,卷被划分为多个小“板块”,这些“板块”将复制三次,并平均分布在群集中每个服务器的每个驱动器上。 有关更多详细信息,请阅读此“深入了解”博客

示意图显示卷被划分为三个板块堆栈并在每个服务器上平均分布。

此默认分配可将并行读取和写入数最大化,从而提高性能,并具有简洁化的优点:每个服务器繁忙程度相同,每个驱动器均充分利用,所有卷同时联机或脱机。 每个卷最多可以承受两次并发故障,如这些示例所示。

但是,使用此分配,卷无法承受三次并发故障。 如果三台服务器同时发生故障,或者如果三台服务器中的驱动器同时发生故障,卷将变得不可访问,因为至少会有一些板块(非常有可能)分配给了故障的三个驱动器或服务器。

在下面的示例中,服务器 1、3 和 5 同时故障。 尽管许多板块有用于应对故障的副本,但有些没有:

示意图显示六个服务器中的三个以红色突出显示,整体卷为红色。

卷会脱机、在服务器恢复之前不可访问。

新增:界定的分配

使用界定的分配,可以指定要使用的服务器子集(至少四个)。 该卷与以前一样被划分为复制三次的板块,但这些板块只分配给指定的服务器子集,而不是分配给所有服务器

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

优点

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

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

缺点

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

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

  2. 通过划分分配,为每个服务器预留相当于一个容量驱动器的空间(没有最大容量)。 这超出了针对常规分配量的已发布的建议中所述的量,其中建议最多配置不超过四个容量驱动器。

  3. 如果服务器发生故障且需要替换(如删除服务器及其驱动器中所述),管理员负责通过添加新服务器并删除失败的服务器来更新界定的受影响卷的信息 -示例如下。

PowerShell 中的用法

可以使用 New-Volume cmdlet 在存储空间直通中创建卷。

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

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

创建卷并界定其分配

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

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

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

    提示

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

  2. 通过使用新 -StorageFaultDomainsToUse 参数并索引到 $Servers 来指定要使用的服务器。 例如,若要将分配界定为第一、第二、第三和第四台服务器(索引 0、1、2 和 3):

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

参见界定的分配

若要查看 MyVolume 的分配方式,请使用附录中的 Get-VirtualDiskFootprintBySSU.ps1 脚本:

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 的板块

更改界定的分配

使用新的 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 监视重新均衡的进度。

完成后,再次运行 Get-VirtualDiskFootprintBySSU.ps1,验证 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