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
相关产品推荐
相关产品推荐

