Windows環境におけるS3へのファイルコピー比較 ~AWS CLI vs AWS Tools for Windows PowerShell~

こんにちは。katoです。

今回はWindows環境におけるS3へのファイルコピーの性能比較を比較していきたいと思います。

Windows Server上からPowerShellを実行し、APIを利用したS3へのファイルコピーを実行した際、AWS CLIAWS 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 PowerShellPythonのプロセスを利用しないため、AWS CLIと比べてCPU負荷は低くなり、平均してみるとある程度余裕のあるCPUの使用率となりました(子プロセスが起動した直後のみ99%付近まで上昇)。

ただし、AWS Tools for PowerShellの場合はメモリを消費するため、マルチスレッドにて実行する場合には、メモリの枯渇に注意が必要となります。

また、どちらのAPIであっても、スレッド数を増やしたからといって処理速度が必ず向上するといった結果にはなりませんでした。

スレッド数を増やしてもCPU不足などが原因でかえって処理に時間がかかる結果となりました。

これだけみると、AWS CLIAWS 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}"
}

 

 

 

このブログの著者

 

 

アプリケーション開発バナー