You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PowerShell SFTP脚本运行中连接中断问题求助

SFTP批量上传脚本连接稳定性优化建议

问题描述

我正在测试一个用于向远程服务器上传文件的PowerShell SFTP脚本,脚本可正常运行,但即便配置了KeepAlive命令,连接仍会在30至40分钟后断开。首次运行需上传超过20万个文件,因此需要维持连接稳定,希望得到能提升连接稳定性的优化建议或最佳实践,目前优先确保脚本稳定运行,代码整理可后续进行。

优化建议

  • 修正KeepAlive配置一致性:脚本中注释标注KeepAlive间隔为2分钟,但实际代码设置为[TimeSpan]::FromMinutes(1),需统一数值;同时可将间隔缩短至30秒,避免长时间无操作导致连接被服务器回收。
  • 增强连接状态检查频率:当前仅在每检查10个文件时发送KeepAlive,且仅在上传前检查连接。建议在每次SFTP操作(如Exists、GetAttributes、UploadFile)前都执行连接状态检测,或设置独立的定时KeepAlive任务(比如用Start-Job后台运行定时发送KeepAlive)。
  • 完善重连逻辑:当前Reconnect-SFTP函数已实现重连,但需确保所有后续操作都使用新的连接对象;同时在Test-SftpConnection失败时直接调用Reconnect-SFTP,而非手动断开再连接,避免重复代码。
  • 调整超时参数:
    • ConnectionInfo.Timeout是连接建立超时,而非会话超时,可保留30分钟;
    • 将OperationTimeout缩短至5分钟,避免单个操作卡住太久占用连接;
    • 确保SftpClient.KeepAliveInterval正确生效,部分环境下需同时设置ConnectionInfo.KeepAliveInterval(Renci.SshNet中两者关联,可同时配置)。
  • 修复文件流重复关闭问题:脚本中上传文件后重复执行$fileStream.Close(),会导致IO错误,需删除其中一行。
  • 为所有SFTP操作添加重试机制:在Exists、GetAttributes、UploadFile等操作外层包裹try-catch,当操作因连接失败报错时,自动触发重连并重试1-2次,再记录错误日志。
  • 轻量化KeepAlive操作:将Send-KeepAlive中的ListDirectory(".")替换为更轻量的GetAttributes("."),减少服务器资源消耗,同时达到保活效果。
  • 避免递归中的连接闲置:在递归遍历目录的循环开头增加连接检查,确保处理每个目录/文件前连接处于活跃状态。
  • 增强日志信息:在连接重连、KeepAlive失败时添加更详细的日志,比如当前操作的文件路径、错误堆栈信息,便于定位问题。

原脚本

# Load the SSH.NET assembly
Add-Type -Path "C:\Scripts\Renci.SshNet.dll"

# Set the connection details
$ftpServer = ""
$ftpUsername = ""
$ftpPassword = ""
$remoteDir = "/"
$localDir = "D:\Data\Transfer"

# Define log file
$logDir = "C:\log"
$logFile = Join-Path -Path $logDir -ChildPath ("log_" + (Get-Date).ToString("yyyyMMdd") + ".txt")

# Create log directory if it doesn't exist
if (-not (Test-Path -Path $logDir)) {
    New-Item -ItemType Directory -Path $logDir | Out-Null
}

# Function to write to log
function Write-Log {
    param (
        [string]$message
    )
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logMessage = "$timestamp - $message"
    Add-Content -Path $logFile -Value $logMessage
}

# Initialize counters
[int]$global:filesChecked = 0
[int]$global:filesCopied = 0

# Function to establish SFTP connection
function Connect-SFTP {
    param (
        [string]$server,
        [string]$username,
        [string]$password
    )
    $sftp = New-Object Renci.SshNet.SftpClient($server, $username, $password)
    try {
        $sftp.ConnectionInfo.Timeout = [TimeSpan]::FromMinutes(30) # Set timeout to 30 minutes
        $sftp.KeepAliveInterval = [TimeSpan]::FromMinutes(1) # Set keep-alive interval to 2 minutes
        $sftp.OperationTimeout = [TimeSpan]::FromMinutes(30) # Set operation timeout to 30 minutes
        $sftp.Connect()
        Write-Log "Connected to SFTP server $server"
    } catch {
        Write-Log "SFTP connect function - Failed to connect to SFTP server"
        throw "Connection failed"
    }
    return $sftp
}

# Function to disconnect SFTP connection
function Disconnect-SFTP {
    param (
        [ref]$sftp
    )
    if ($sftp.Value -and $sftp.Value.IsConnected) {
        $sftp.Value.Disconnect()
        Write-Log "SFTP Disconnect function - Disconnected from SFTP server"
    }
}

# Function to reconnect SFTP connection
function Reconnect-SFTP {
    param (
        [ref]$sftp,
        [string]$server,
        [string]$username,
        [string]$password
    )
    Disconnect-SFTP -sftp $sftp
    $sftp.Value = Connect-SFTP -server $server -username $username -password $password
}

# Function to send a keep-alive command
function Send-KeepAlive {
    param (
        [Renci.SshNet.SftpClient]$sftp
    )
    try {
        $sftp.ListDirectory(".") | Out-Null
        Write-Log "Keep-alive command sent"
    } catch {
        Write-Log "Keep-alive command failed: $_"
    }
}


# Capture the start time
$startTime = Get-Date

# Establish SFTP connection
$sftp = Connect-SFTP -server $ftpServer -username $ftpUsername -password $ftpPassword

# Function to check if the SFTP connection is open
function Test-SftpConnection($sftp) {
    try {
        # Attempt to list the root directory to check if the connection is alive
        $sftp.ListDirectory("/") | Out-Null
        return $true
    } catch {
        Write-Log "SFTP connection test function - SFTP connection test failed: $_"
        return $false
    }
}
# Function to upload files and directories
function UploadFilesRecursively {
    param (
        [string]$localPath,
        [string]$remotePath
    )

    # Get all files and directories in the current local directory
    $localItems = Get-ChildItem -Path $localPath

    foreach ($item in $localItems) {
        $localItemPath = $item.FullName
        $remoteItemPath = "$remotePath/$($item.Name)"

        if ($item.PSIsContainer) {
            # Create the remote directory if it doesn't exist
            if (-not $sftp.Exists($remoteItemPath)) {
                $sftp.CreateDirectory($remoteItemPath)
                Write-Log "Created remote directory $remoteItemPath"
            }
            # Recurse into the directory
            UploadFilesRecursively $localItemPath $remoteItemPath
        } else {
            # Increment the files checked counter
            $global:filesChecked++

            # Check if the file exists on the remote server and compare modification dates
            $uploadFile = $true

            if ($sftp.Exists($remoteItemPath)) {
                $remoteFileAttributes = $sftp.GetAttributes($remoteItemPath)
                if ($item.LastWriteTime -le $remoteFileAttributes.LastWriteTime) {
                    $uploadFile = $false
                    #Write-Output "Skip upload of $localItemPath to $remoteItemPath"
                }
            }

            if ($uploadFile) {
            # Check if the connection is still open
                if (-not (Test-SftpConnection $sftp)) {
                    Write-Log "SFTP connection lost. Reconnecting..."
                    $sftp.Disconnect()
                    $sftp.Connect()
                }
                try {
                    Write-Output "Uploading $localItemPath to $remoteItemPath"
                    $fileStream = [System.IO.File]::OpenRead($localItemPath)
                    $sftp.UploadFile($fileStream, $remoteItemPath)
                    $fileStream.Close()
                    $global:filesCopied++
                    Write-Log "Uploaded $($item.Name) to $remoteItemPath"

                    # Preserve the last modified date
                    $attributes = $sftp.GetAttributes($remoteItemPath); 
                    $attributes.LastWriteTime = $item.LastWriteTime; 
                    $sftp.SetAttributes($remoteItemPath, $attributes)
                    $fileStream.Close()
                    Write-Log "Uploaded $($item.Name) to $remoteItemPath"
                } catch {
                    Write-Log "Initial upload catch - Error uploading $($item.Name): $_"
                    # Attempt to reconnect and retry upload
                    Reconnect-SFTP -sftp ([ref]$sftp) -server $ftpServer -username $ftpUsername -password $ftpPassword
                    try {
                        Write-Output "Retrying upload of $localItemPath to $remoteItemPath"
                        $fileStream = [System.IO.File]::OpenRead($localItemPath)
                        $sftp.UploadFile($fileStream, $remoteItemPath)
                        $fileStream.Close()
                        $global:filesCopied++
                        Write-Log "Uploaded $($item.Name) to $remoteItemPath on retry"

                        #check connection & set modified date
                        if (-not (Test-SftpConnection $sftp)) {
                    Write-Log "SFTP connection lost. Reconnecting..."
                    $sftp.Disconnect()
                    $sftp.Connect()
                }
                        Set-RemoteFileLastWriteTime -sftp $sftp -remoteFilePath $remoteItemPath -lastWriteTime $item.LastWriteTime
                    } catch {
                        Write-Log "Nested catch - 2nd Error uploading $($item.Name) on retry: $_"
                    }
                }
            }
        }

        # Periodically send a keep-alive command
        if ($global:filesChecked % 10 -eq 0) {
           Send-KeepAlive -sftp $sftp
        }
    }
}

# Start the upload process from the local root directory
UploadFilesRecursively $localDir $remoteDir

# Write summary to the log
Write-Log "File upload process completed."
Write-Log "Total files checked: $global:filesChecked"
Write-Log "Total files copied: $global:filesCopied"

# Capture the end time
$endTime = Get-Date

# Calculate the duration
$duration = $endTime - $startTime
Write-Log "Script completed in $($duration.ToString())"

# Disconnect the SFTP connection
Disconnect-SFTP -sftp ([ref]$sftp)

内容的提问来源于stack exchange,提问作者DarkW1nter

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 19:52:02