こんにちは。katoです。
今回はWindows環境におけるS3へのファイルコピーの性能比較を比較していきたいと思います。
Windows Server上からPowerShellを実行し、APIを利用したS3へのファイルコピーを実行した際、AWS CLIとAWS Tools for PowerShellで処理速度に大きな差が出たので簡単な比較をしたいと思い、本記事にまとめました。
性能比較
今回はPowerShellにて下記のパターンに分けてS3へのコピーを実施致しました。
・aws s3 copy(シングルスレッド) ・aws s3 copy(マルチスレッド) ・aws s3 copy (exclude/include、マルチスレッド) ・write-s3object(マルチスレッド) ・write-s3object(マルチスレッド、配列ループ)
なお、今回の検証は全てファイル単位でのコピーとなります。 最終更新日等の検索条件にヒットした不特定多数のファイルをS3にコピーするといった内容を想定しております。
検証を行った環境は以下の通りとなります。
OS: windows server 2016 InstanceType: t3.medium(T2/T3無制限は無効) EBS: 200GB(gp2) コピー対象:1MB×1000
本検証にて利用したスクリプトは最後に記載致します。
測定結果は以下の通りとなりました。
API | 所要時間 | CPU | メモリ |
aws s3 copy(シングルスレッド) | 43分23秒 | 40~50% | 30% |
aws s3 copy(5スレッド) | 67分45秒 | 99% | 35% |
aws s3 copy (exclude/include、5スレッド) | 4分17秒 | 99% | 40% |
aws s3 copy (exclude/include、10スレッド) | 5分6秒 | 99% | 45% |
aws s3 copy (exclude/include、20スレッド) | 5分8秒 | 99% | 50~55% |
write-s3object(5スレッド) | 175分3秒 | 99% | 35~55% |
write-s3object(配列ループ、5スレッド) | 1分41秒 | 40~99% | 50~60% |
write-s3object(配列ループ、10スレッド) | 2分36秒 | 80~99% | 65~80% |
write-s3object(配列ループ、20スレッド) | 5分7秒 | 99% | 75~99% |
ファイルを一個ずつコピーするような処理ではどちらもかなり時間を要しました。
また、マルチスレッドであっても子プロセスにて一個ずつファイルをコピーするような方法では、子プロセスの起動が連続して走るため、かえって処理に時間がかかり、負荷がかなり高まりました。
AWS CLIの場合は、exclude/includeオプションを利用し、子プロセスにて複数ファイルをまとめてコピーする方法で処理速度が向上致しました。
AWS Tools for PowerShellのwrite-s3objectでは、exclude/includeの様に複数ファイルをまとめてコピーするオプションが存在しないため、APIリクエスト当たり一個のファイルコピーとなります。
子プロセスの起動に時間とリソースが必要となるため、コピー対象をまとめた配列を引数として子プロセスに渡すことで、AWS Tools for PowerShellの場合でも処理速度を向上させることが可能となりました。
また、CPUやメモリの使用状況を確認してみると、AWS CLIの場合はPythonプロセスにかなりのCPUを利用するため、マルチスレッドで複数のプロセスが走るように設定すると、CPUはほぼ99%に張り付くような状態となりました。
AWS Tools for PowerShellはPythonのプロセスを利用しないため、AWS CLIと比べてCPU負荷は低くなり、平均してみるとある程度余裕のあるCPUの使用率となりました(子プロセスが起動した直後のみ99%付近まで上昇)。
ただし、AWS Tools for PowerShellの場合はメモリを消費するため、マルチスレッドにて実行する場合には、メモリの枯渇に注意が必要となります。
また、どちらのAPIであっても、スレッド数を増やしたからといって処理速度が必ず向上するといった結果にはなりませんでした。
スレッド数を増やしてもCPU不足などが原因でかえって処理に時間がかかる結果となりました。
これだけみると、AWS CLIもAWS Tools for PowerShellも処理速度にはそこまでの差は無いように見えます。
次にインスタンスタイプをm5.largeに変更して測定を実施致しました。
API | 所要時間 | CPU | メモリ |
aws s3 copy (exclude/include、10スレッド) | 1分6秒 | 99% | 25% |
write-s3object(配列ループ、10スレッド) | 1分11秒 | 80~99% | 30~45% |
処理速度自体に違いはほぼありませんでしたが、AWS Tools for PowerShellの方はまだリソース的に若干の余裕がありそうでしたので、スレッドをもう少し増やせそうでした。
次にインスタンスタイプはm5.largeのまま、コピー対象を1KB×10000に変更して測定を実施致しました。
API | 所要時間 | CPU | メモリ |
aws s3 copy (exclude/include、10スレッド) | 61分56秒 | 99% | 25% |
write-s3object(配列ループ、10スレッド) | 9分16秒 | 35~99% | 35~50% |
処理速度に大幅な差が出ました。
細かいファイルが大量に存在するようなケースにはAWS CLIは向かないようです。
AWS Tools for PowerShellではリソースも余裕があり、スレッド数を増やすことでさらに処理速度の向上が見込めそうです。
今回のまとめと致しまして、Windows環境におけるS3へのファイルコピー時には、AWS Tools for PowerShellの利用をお勧めいたします。
ただし、インスタンスタイプやファイルサイズ、ファイル数などによって結果が異なってきますので、用途や環境に応じて適切な手法を選択していただければと思います。
スクリプト(おまけ)
最後に今回の検証にて利用したPowershellスクリプトを記載させていただきます。
基本的にはAPIを叩くだけなので、細かい説明は省略させていただきます。
なお、今回の検証ではソースフォルダ内のすべてのファイルをコピー対象としています。
実際にご利用される場合には、Where-ObjectでLastWriteTimeなどを指定して、特定条件に一致したファイルを対象にするよう変更してください。
マルチスレッドのパターンに関しましては、2つのスクリプトから成り立っています。
親スクリプトから子スクリプトを複数同時に実行するような形となります。
aws s3 copy(シングルスレッド)
Set-Item env:tz -Value jst $source = "E:\" $s3bucket = "s3-bucket-name" $logfile = "D:\script\logs\copy_single.log" (get-date -uformat "%Y-%m-%d %H:%M:%S") + (echo " start") >> $logfile $files = Get-ChildItem ${source} -Recurse | Where-Object {! $_.PSIsContainer} foreach ($file in $files.fullname){ $s3path = $file.Replace('E:\', 'E/') $s3path = $s3path.Replace('\', '/') $uploadpath = "s3://${s3bucket}/${s3path}" aws s3 cp $file $uploadpath } (get-date -uformat "%Y-%m-%d %H:%M:%S") + (echo " finish") >> $logfile
aws s3 copy(マルチスレッド)
Set-Item env:tz -Value jst $source = "E:\" $logfile = "D:\script\logs\copy_multi.log" (get-date -uformat "%Y-%m-%d %H:%M:%S") + (echo " start") >> $logfile $files = Get-ChildItem ${source} -Recurse | Where-Object {! $_.PSIsContainer} foreach ($file in $files.fullname){ while ((get-process -ErrorAction 0 -Name powershell).count -gt 5){ Start-Sleep -m 300 } Start-Process powershell.exe -ArgumentList "-file D:\script\copy_multi_child.ps1 ${file}" -WindowStyle Hidden } (get-date -uformat "%Y-%m-%d %H:%M:%S") + (echo " finish") >> $logfile
Set-Item env:tz -Value jst $file = $args[0] $s3bucket = "s3-bucket-name" $s3path = $file.Replace('E:\', 'E/') $s3path = $s3path.Replace('\', '/') $uploadpath = "s3://${s3bucket}/${s3path}" aws s3 cp $file $uploadpath
aws s3 copy (exclude/include、マルチスレッド)
親スクリプト 若干ごちゃついてます。
親スクリプト内で子スクリプトに実行させたいコマンドを生成しています。
対象ファイルの数だけincludeの指定を追加していき、50を超えると子プロセスでコマンドを実行します(子プロセスで50個のファイルを処理させる形)。
また、exclude/includeを指定する場合には、ドライブ以外のフォルダを指定する必要があります(たぶん)。
なのでTOP階層のフォルダが変更されたら子プロセスを実行するような形式をとっております。
指定のプロセス数を超えた場合にはsleepを入れて待ち時間を設けています(無限ループ対策の追加を推奨)。
子プロセスの引数として実行コマンドを指定していますが、スペースで引数が区切られないよう、いろいろ囲ったりして対応しています。
Set-Item env:tz -Value jst $source = "E:\" $logfile = "D:\script\logs\copy_multi_include.log" $s3bucket = "s3://s3-bucket-name" (get-date -uformat "%Y-%m-%d %H:%M:%S") + (echo " start") >> $logfile $files = Get-ChildItem ${source} -Recurse | Where-Object {! $_.PSIsContainer} $option = "--recursive --exclude `"`'*`'`" " $folderpath = "" $filecount = 0 foreach ($file in $files.fullname){ if ($filecount -gt 50){ while ((get-process -ErrorAction 0 -Name powershell).count -gt 10){ Start-Sleep -m 300 } $cpoption = "`"${cpoption}`"" Start-Process powershell.exe -ArgumentList "-file D:\script\copy_multi_include_child.ps1 ${cpoption}" -WindowStyle Hidden $filecount = 0 $cpoption = "aws s3 cp E:\" + $folderpath + " " + $s3bucket + "/E/" + $folderpath + " " + $option } $paths = $file.split("\") if (($folderpath -ne $paths[1]) -and ($folderpath -eq "")){ $folderpath = $paths[1] $cpoption = "aws s3 cp E:\" + $folderpath + " " + $s3bucket + "/E/" + $folderpath + " " + $option } elseif ($folderpath -ne $paths[1]){ while ((get-process -ErrorAction 0 -Name powershell).count -gt 10){ Start-Sleep -m 300 } $cpoption = "`"${cpoption}`"" Start-Process powershell.exe -ArgumentList "-file D:\script\copy_multi_include_child.ps1 ${cpoption}" -WindowStyle Hidden $filecount = 0 $folderpath = $paths[1] $cpoption = "aws s3 cp E:\" + $folderpath + " " + $s3bucket + "/E/" + $folderpath + " " + $option } $contentpath = $file.trim() $cpoption = $cpoption + "--include `"`'${contentpath}`'`" " $filecount++; } if ($filecount -ne 0) { while ((get-process -ErrorAction 0 -Name powershell).count -gt 10){ Start-Sleep -m 300 } $cpoption = "`"${cpoption}`"" Start-Process powershell.exe -ArgumentList "-file D:\script\copy_multi_include_child.ps1 ${cpoption}" -WindowStyle Hidden } while ((get-process -ErrorAction 0 -Name powershell).count -ne 1){ Start-Sleep -s 5 } (get-date -uformat "%Y-%m-%d %H:%M:%S") + (echo " finish") >> $logfile
Set-Item env:tz -Value jst Invoke-Expression $Args[0]
write-s3object(マルチスレッド)
Set-Item env:tz -Value jst $source = "E:\" $logfile = "D:\script\logs\write-s3object_multi.log" (get-date -uformat "%Y-%m-%d %H:%M:%S") + (echo " start") >> $logfile $files = Get-ChildItem ${source} -Recurse | Where-Object {! $_.PSIsContainer} foreach ($file in $files.fullname){ while ((get-process -ErrorAction 0 -Name powershell).count -gt 5){ Start-Sleep -m 300 } Start-Process powershell.exe -ArgumentList "-file D:\script\write-s3object_multi_child.ps1 ${file}" -WindowStyle Hidden } (get-date -uformat "%Y-%m-%d %H:%M:%S") + (echo " finish") >> $logfile
Set-Item env:tz -Value jst $file = $args[0] $s3bucket = "s3-bucket-name" $s3path = $file.Replace('E:\', 'E/') $s3path = $s3path.Replace('\', '/') write-s3object -bucketname $s3bucket -file "${file}" -key "${s3path}"
write-s3object(配列ループ、マルチスレッド)
50個のファイルを配列としてまとめて、子プロセスに引数として渡しています(子プロセスにて配列をループ処理させる形)。
Set-Item env:tz -Value jst $source = "E:\" $logfile = "D:\script\logs\write-s3object_multi_array.log" (get-date -uformat "%Y-%m-%d %H:%M:%S") + (echo " start") >> $logfile $files = Get-ChildItem ${source} -Recurse | Where-Object {! $_.PSIsContainer} $array = @() $filecount = 0 foreach ($file in $files.fullname){ if ($filecount -gt 50){ while ((get-process -ErrorAction 0 -Name powershell).count -gt 10){ Start-Sleep -m 300 } Start-Process powershell.exe -ArgumentList "-file D:\script\write-s3object_multi_child_array.ps1 ${array}" -WindowStyle Hidden $filecount = 0 $array = @() } $contentpath = $file.trim() $array += "`"${contentpath}`"" $filecount++; } if ($filecount -ne 0) { while ((get-process -ErrorAction 0 -Name powershell).count -gt 10){ Start-Sleep -m 300 } Start-Process powershell.exe -ArgumentList "-file D:\script\write-s3object_multi_child_array.ps1 ${array}" -WindowStyle Hidden } while ((get-process -ErrorAction 0 -Name powershell).count -ne 1){ Start-Sleep -s 5 } (get-date -uformat "%Y-%m-%d %H:%M:%S") + (echo " finish") >> $logfile
Set-Item env:tz -Value jst $s3bucket = "s3-bucket-name" foreach ($item in $args){ $filepath = $item $key = $item.Replace('E:\', 'E/') $key = $key.Replace('\', '/') write-s3object -bucketname $s3bucket -file "${filepath}" -key "${key}" }