【问题标题】:Fastest way to copy files (but not the entire directory) from one location to another将文件(但不是整个目录)从一个位置复制到另一个位置的最快方法
【发布时间】:2020-02-20 23:38:00
【问题描述】:

总结

  • 我目前的任务是将大约 6TB 的数据迁移到云服务器,并且正在尝试优化迁移的速度。
  • 我通常会使用标准的 Robocopy 来执行此操作,但要求我只传输 SQL 文件表中存在的文件,而不是整个目录(因为这些文件夹中有很多垃圾我们不想迁移)。

我的尝试

将单个文件从数组输入 Robocopy 速度非常慢,因为每个文件都按顺序启动 Robocopy 实例,因此我尝试通过两种方式加快此过程。

  1. 如果只传输一个文件,将 /MT 设置为 1 以上是没有意义的,因此我尝试模拟多线程功能。我通过使用 PowerShell 7.0 中的新 ForEach-Object –Parallel 功能来做到这一点,并将油门限制设置为 4。这样,我能够传入数组并并行运行 4 个 Robocopy 作业(仍然为每个文件启动和停止) ,这提高了一点速度。

  2. 其次,我将数组拆分为 4 个相等的数组,并将上述函数作为作业在每个数组中运行,这再次将速度提高了很多。为清楚起见,我将相等的 4 个数组馈送到 4 个运行 4 个 Robocopy 实例的 ForEach-Object -Parallel 代码块,因此一次总共有 16 个 Robocopy 实例。

问题

我遇到了一些问题。

  1. 我对 多线程功能的模拟的行为与 /MT 标志在 Robocopy 中的工作方式不同。在检查正在运行的进程时,我的代码一次执行 16 个 Robocopy 实例,而 Robocopy 的正常 /MT:16 标志只会启动一个 Robocopy 实例(但仍然是多线程)。

  2. 其次,代码会导致内存泄漏。内存使用量在作业时开始增加并随着时间的推移累积,直到大部分内存被使用。作业完成后,内存使用率仍然很高,直到我关闭 PowerShell 并释放内存。普通的 Robocopy 没有这样做。

  3. 最后,我决定比较一下我的方法所用的时间,然后是整个测试目录的标准 Robocopy,而普通的 Robocopy 仍然快 10 倍以上,并且有一个更高的成功率(很多文件没有用我的代码复制,而且很多时候我收到错误消息指出文件当前正在使用并且无法被机器复制,大概是因为它们正在处理中被机器复制)。

有没有更快的替代方案,或者有没有办法手动创建一个多线程的 robocopy 实例,其性能类似于标准 robocopy 的 /MT 标志?我很欣赏任何看待这个问题的洞察力/替代方法。谢谢!

#Item(0) is the Source excluding the filename, Item(2) is the Destination, Item(1) is the filename
$robocopy0 = $tables.Tables[0].Rows
$robocopy1 = $tables.Tables[1].Rows
$robocopy0 | ForEach-Object -Parallel {robocopy $_.Item(0) $_.Item(2) $_.Item(1) /e /w:1 /r:1 /tee /NP /xo /mt:1 /njh /njs /ns 
                                      } -ThrottleLimit 4 -AsJob
$robocopy1 | ForEach-Object -Parallel {robocopy $_.Item(0) $_.Item(2) $_.Item(1) /e /w:1 /r:1 /tee /NP /xo /mt:1 /njh /njs /ns 
                                      } -ThrottleLimit 4 -AsJob
#*8 for 8 arrays

【问题讨论】:

    标签: powershell data-migration robocopy


    【解决方案1】:

    RunspaceFactory 多线程可能最适合这种类型的工作——有一个巨大的警告。网上有不少关于它的文章。本质上,您创建一个脚本块,该脚本块为要复制的源文件和要写入的目标文件提供参数,并使用这些参数对其执行 robocopy。您创建单独的 PowerShell 实例来执行脚本块的每个变体并将其附加到 RunspaceFactory。 RunspaceFactory 会将作业排队并一次处理可能数百万个作业 X 编号,其中 X 等于您为工厂分配的线程数。

    警告:首先,相对于 6TB 中可能拥有的数百万个文件,要排队数百万个作业,您可能需要大量内存。假设源和目标的平均路径长度为 40 个字符(可能非常大)* 5000 万个文件的 WAG 本身在内存中接近 4GB,这不包括对象结构开销、PowerShell 实例等。您可以克服这要么将工作分解成更小的块,要么使用具有 128GB RAM 或更好的服务器。此外,如果您在处理完作业后不终止它们,您还会遇到看似内存泄漏的情况,但只是您的作业产生的信息在完成时不会关闭。

    这是我最近将文件从旧域 NAS 迁移到新域 NAS 的一个项目示例——我使用的是 Quest SecureCopy 而不是 RoboCopy,但您应该能够轻松替换这些位:

    ## MaxThreads is an arbitrary number I use relative to the hardware I have available to run jobs I'm working on.
    $FileRSpace_MaxThreads = 15
    $FileRSpace = [runspacefactory]::CreateRunspacePool(1, $FileRSpace_MaxThreads, ([System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()), $Host)
    $FileRSpace.ApartmentState = 'MTA'
    $FileRSpace.Open()
    
    ## The scriptblock that does the actual work.
    $sb = {
    param(
        $sp,
        $dp
    )
    
        ## This is my output object I'll emit through STDOUT so I can consume the status of the job in the main thread after each instance is completed.
        $n = [pscustomobject]@{
            'source'  = $sp
            'dest'    = $dp
            'status'  = $null
            'sdtm'    = [datetime]::Now
            'edtm'    = $null
            'elapsed' = $null
        }
    
        ## Remove the Import-Module and SecureCopy cmdlet and replace it with the RoboCopy version
        try {
            Import-Module "C:\Program Files\Quest\Secure Copy 7\SCYPowerShellCore.dll" -ErrorAction Stop
            Start-SecureCopyJob -Database "C:\Program Files\Quest\Secure Copy 7\SecureCopy.ssd" -JobName "Default" -Source $sp -Target $dp -CopySubFolders $true -Quiet $true -ErrorAction Stop | Out-Null
            $n.status = $true
        } catch {
            $n.status = $_
        }
        $n.edtm    = [datetime]::Now
        $n.elapsed = ("{0:N2} minutes" -f (($n.edtm - $n.sdtm).TotalMinutes))
        $n        
    }
    
    ## The array to hold the individual runspaces and ulitimately iterate over to watch for completion.
    $FileWorkers = @()
    
    $js = [datetime]::now
    log "Job starting at $js"
    
    ## $peers is a [pscustomobject] I precreate that just contains every source (property 's') and the destination (property 'd') -- modify to suit your needs as necessary
    foreach ($c in $peers) {
    
        try {
    
            log "Configuring migration job for '$($c.s)' and '$($c.d)'"
            $runspace = [powershell]::Create()
            [void]$runspace.AddScript($sb)
    
            [void]$runspace.AddArgument($c.s)
            [void]$runspace.AddArgument($c.d)
    
            $runspace.RunspacePool = $FileRSpace
            $FileWorkers += [pscustomobject]@{
                'Pipe'   = $runspace
                'Async'  = $runspace.BeginInvoke()
            }
    
            log "Successfully created a multi-threading job for '$($c.s)' and '$($c.d)'"
        } catch {
            log "An error occurred creating a multi-threading job for '$($c.s)' and '$($c.d)'"
        }
    }
    
    while ($FileWorkers.Async.IsCompleted -contains $false) {
        $Completed = $FileWorkers | ? { $_.Async.IsCompleted -eq $true }
        [pscustomobject]@{
            'Numbers' = ("{0}/{1}" -f $Completed.Count, $FileWorkers.Count)
            'PercComplete' = ("{0:P2}" -f ($Completed.Count / $FileWorkers.Count))
            'ElapsedMins' = ("{0:N2}" -f ([datetime]::Now - $js).TotalMinutes)
        }
    
        $Completed | % { $_.Pipe.EndInvoke($_.Async) } | Export-Csv -NoTypeInformation ".\$($DtmStamp)_SecureCopy_Results.csv"
    
        Start-Sleep -Seconds 15
    }
    
    ## This is to handle a race-condition where the final job(s) aren't completed before the sleep but do when the while is re-eval'd
    $FileWorkers | % { $_.Pipe.EndInvoke($_.Async) } | Export-Csv -NoTypeInformation ".\$($DtmStamp)_SecureCopy_Results.csv"
    

    如果您没有强大的服务器来同时将所有作业排队,建议的策略是将文件批量处理为静态大小的块(例如 100,000 或您的硬件可以采用的任何内容),或者您可以将文件组合在一起发送到每个脚本块(例如,每个脚本块 100 个文件),这将最大限度地减少在运行空间工厂中排队的作业数量(但需要更改一些代码)。

    HTH

    编辑 1:构建我正在使用的输入对象的地址

    $destRoot = '\\destinationserver.com\share'
    
    $peers = @()
    
    $children = @()
    $children += (get-childitem '\\sourceserver\share' -Force) | Select -ExpandProperty FullName 
    
    foreach ($c in $children) {
    
        $peers += [pscustomobject]@{
            's' = $c
            'd' = "$($destRoot)\$($c.Split('\')[3])\$($c | Split-Path -Leaf)"
        }
    }
    

    在我的例子中,我从 \server1\share1\subfolder1 中取出一些东西并将其移动到 \server2\share1\subfolder1\subfolder2 之类的地方。所以本质上,'$peers' 数组所做的就是构造一个对象,该对象采用源目标的全名并构造相应的目标路径(因为源/目标服务器名称不同,并且可能也共享名称)。

    您不必这样做,您可以动态构建目标并循环访问源文件夹。我执行了这个额外的步骤,因为现在我有一个两个属性数组,我可以验证它是准确预先构建的,并执行测试以确保事物存在并且可以访问。

    我的脚本中有很多额外的膨胀,因为自定义对象意味着我可以从放入多线程的每个线程中获得输出,因此我可以看到每次复制尝试的状态——跟踪诸如文件夹之类的东西成功与否,执行该单独复制需要多长时间等。如果您使用 robocopy 并将结果转储到文本文件,您可能不需要这个。如果您希望我将脚本与其准系统组件配对以实现多线程,如果您愿意,我可以这样做。

    【讨论】:

    • 您好,非常感谢您的回复!抱歉,我对这个角色比较陌生,无法真正理解你的剧本。另外,我想我可能是模棱两可的。大约有 6TB,但我们将在迁移项目数据时逐个文件夹处理这个问题。此外,我已根据文件名将文件拆分为单独的作业。你的解决方案还能用吗?我很困惑如何将单独的数组输入 $sp/$dp(源路径/目标路径?)
    • 嘿,谢谢。幸运的是,我发现了 robocopy 的 /if 标志(与 /xf 相对),我可以在其中指定要包含的文件。花了很长时间让 robocopy 读取文件(必须在 powershell 中将文件创建为数组),但这看到速度与普通 robocopy 一样快!
    • 不用担心 - 很高兴您找到了解决方案。这就是它的全部意义所在。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-08-01
    • 2015-01-30
    • 2013-05-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多