AWSCLI、Python(boto3)などからS3フォルダ間のコピーしてみる(第2版)
AWS事業本部コンサルティング部の石川です。今でもよくご覧いただいているブログ AWSCLI、Python(boto3)などからS3フォルダ間のコピーしてみるを6年ぶりに改定しました。以前のPython(boto3)の実装よりもスッキリわかり易く再実装しました。また、並列処理版もサンプルを追加しています。
検証用データ
以下のような階層のデータを事前に作成しました。
% aws s3 ls s3://cm-test-20241013/source-folder/ --recursive
2024-10-13 22:58:45 21 source-folder/file_0000.txt
2024-10-13 22:58:45 21 source-folder/file_0001.txt
2024-10-13 22:58:45 21 source-folder/file_0002.txt
2024-10-13 22:58:45 21 source-folder/file_0003.txt
2024-10-13 22:58:46 21 source-folder/file_0004.txt
:
:
AWSCLIでS3フォルダのコピーする
方法1: サブコマンドcpによるS3フォルダのコピー
S3フォルダ名を指定してコピーするには、--recursive
を指定します。S3フォルダごとコピーされたことが確認できます。
$ aws s3 cp s3://cm-test-20241013/source-folder s3://cm-test-20241013/target-folder --recursive
copy: s3://cm-test-20241013/source-folder/file_0001.txt to s3://cm-test-20241013/target-folder/file_0001.txt
:
:
copy: s3://cm-test-20241013/source-folder/file_2998.txt to s3://cm-test-20241013/target-folder/file_2998.txt
copy: s3://cm-test-20241013/source-folder/file_2996.txt to s3://cm-test-20241013/target-folder/file_2996.txt
copy: s3://cm-test-20241013/source-folder/file_2993.txt to s3://cm-test-20241013/target-folder/file_2993.txt
2999ファイルのコピー時間を計測すると、約32秒程度でした。
7.21s user 0.84s system 26% cpu 30.907 total
方法2: サブコマンドsyncによるS3フォルダのコピー
syncは、2つのフォルダ間のオブジェクトを同期します。同じく、2999ファイルのコピー時間を計測すると、約32秒程度でした。速度はサブコマンドcpと殆ど差がありません。
$ time aws s3 sync s3://cm-test-20241013/source-folder s3://cm-test-20241013/target-folder
copy: s3://cm-test-20241013/source-folder/file_0001.txt to s3://cm-test-20241013/target-folder/file_0001.txt
:
:
copy: s3://cm-test-20241013/source-folder/file_2991.txt to s3://cm-test-20241013/target-folder/file_2991.txt
copy: s3://cm-test-20241013/source-folder/file_2989.txt to s3://cm-test-20241013/target-folder/file_2989.txt
copy: s3://cm-test-20241013/source-folder/file_2994.txt to s3://cm-test-20241013/target-folder/file_2994.txt
7.24s user 0.85s system 26% cpu 30.413 total
もう一度、サブコマンドsyncを実行するとすでにコピー済みなのでコピーしません。コピー時間を計測すると、約4秒程度でした。
サブコマンドcpは実行するたびに同じファイルをコピーしますが、サブコマンドsyncはすでにコピー済みであるかのチェックがおこり、無駄なコピーが不要になり、結果として処理が速く終わります。
$ time aws s3 sync s3://cm-test-20241013/source-folder s3://cm-test-20241013/target-folder
0.79s user 0.13s system 31% cpu 2.885 total
また、サブコマンドsyncの場合、万が一コピーが中断したとしても、もう一度実行することで続きのコピーができる点で優れています。
補足:オブジェクト数のカウント
なお、オブジェクト数は以下のコマンドで高速に取得できます。下記の方法では、S3基盤でオブジェクト数をカウントし、レスポンスにはオブジェクト数のみが含まれるため、オブジェクトリスト全体を取得するよりは高速に動作します。
$ time aws s3api list-objects-v2 --bucket s3-delete-folder --prefix my_folder/folder_1 --query 'length(Contents)'
2999
real 0m3.156s
user 0m0.640s
sys 0m0.144s
Python(boto3)でS3フォルダ間でコピーする方法
方法3: S3フォルダをまとめてコピーする
paginatorによるページング処理することで、1000以上オブジェクトをコピーできるように実装しました。最も一般的な方法です。
import boto3
def copy_s3_folder(s3, source_bucket, source_prefix, target_bucket, target_prefix, dryrun=False):
paginator = s3.get_paginator('list_objects_v2')
pages = paginator.paginate(Bucket=source_bucket, Prefix=source_prefix)
for page in pages:
if 'Contents' in page:
for obj in page['Contents']:
source_key = obj['Key']
target_key = source_key.replace(source_prefix, target_prefix, 1)
if dryrun:
print(f"Would Copy {source_key} to {target_bucket}/{target_key}")
else:
s3.copy_object(
CopySource={'Bucket': source_bucket, 'Key': source_key},
Bucket=target_bucket,
Key=target_key
)
print(f"Copied {source_key} to {target_bucket}/{target_key}")
# S3クライアントの作成
PROFILE_NAME = 'ishikawa' # Optional
session = boto3.Session()
if PROFILE_NAME in boto3.Session().available_profiles:
session = boto3.Session(profile_name=PROFILE_NAME)
s3 = session.client('s3')
# コピー元
source_bucket = 'cm-test-20241013'
source_prefix = 'source-folder/'
# コピー先
target_bucket = 'cm-test-20241013'
target_prefix = 'target-folder/'
copy_s3_folder(s3, source_bucket, source_prefix, target_bucket, target_prefix)
実際に実行すると、オブジェクトをシリアルに処理します。314秒(約5分14秒)でした。
% time python copy_s3_folder.py
Copied source-folder/file_0000.txt to cm-test-20241013/target-folder/file_0000.txt
Copied source-folder/file_0001.txt to cm-test-20241013/target-folder/file_0001.txt
:
:
Copied source-folder/file_2997.txt to cm-test-20241013/target-folder/file_2997.txt
Copied source-folder/file_2998.txt to cm-test-20241013/target-folder/file_2998.txt
Copied source-folder/file_2999.txt to cm-test-20241013/target-folder/file_2999.txt
16.51s user 1.34s system 5% cpu 5:13.58 total
そこで、気づいたのは、AWSCLI の cp や sync の方が圧倒的に速かったこと。もう一つ、cp や syncは、出力を見ると順番がバラバラでした。つまり、並列処理しているのではないかということです。自分でプログラム書いたほうが遅いなんて、傷つきますよね。
だったら、並列処理してやろじゃない。
方法4: S3フォルダをまとめてコピーする(マルチスレッド版)
ThreadPoolExecutorを使って、並列処理します。
import boto3
from concurrent.futures import ThreadPoolExecutor
import threading
# 並列数
MAX_WORKERS=10
def copy_object(s3, source_bucket, source_key, target_bucket, target_key):
s3.copy_object(
CopySource={'Bucket': source_bucket, 'Key': source_key},
Bucket=target_bucket,
Key=target_key
)
print(f"Copied {source_key} to {target_bucket}/{target_key}")
def copy_s3_folder_multithreads(s3, source_bucket, source_prefix, target_bucket, target_prefix, max_workers=MAX_WORKERS, dryrun=False):
paginator = s3.get_paginator('list_objects_v2')
pages = paginator.paginate(Bucket=source_bucket, Prefix=source_prefix)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
for page in pages:
if 'Contents' in page:
for obj in page['Contents']:
source_key = obj['Key']
target_key = source_key.replace(source_prefix, target_prefix, 1)
if dryrun:
print(f"Would Copy {source_key} to {target_bucket}/{target_key}")
else:
executor.submit(copy_object, s3, source_bucket, source_key, target_bucket, target_key)
# S3クライアントの作成
PROFILE_NAME = 'ishikawa' # Optional
session = boto3.Session()
if PROFILE_NAME in boto3.Session().available_profiles:
session = boto3.Session(profile_name=PROFILE_NAME)
s3 = session.client('s3')
# コピー元
source_bucket = 'cm-test-20241013'
source_prefix = 'source-folder/'
# コピー先
target_bucket = 'cm-test-20241013'
target_prefix = 'target-folder/'
copy_s3_folder_multithreads(s3, source_bucket, source_prefix, target_bucket, target_prefix)
実際に実行すると、並列処理するので圧倒的に速くなりました。並列数を10にすると約32秒程度で、cpやsyncと同レベルになりました。
% time python copy_s3_folder_multithreads.py
Copied source-folder/file_0000.txt to cm-test-20241013/target-folder/file_0000.txt
Copied source-folder/file_0001.txt to cm-test-20241013/target-folder/file_0001.txt
:
:
Copied source-folder/file_2995.txt to cm-test-20241013/target-folder/file_2995.txt
Copied source-folder/file_2994.txt to cm-test-20241013/target-folder/file_2994.txt
6.34s user 0.65s system 19% cpu 36.437 total
並列数を100にすると8秒程度でした。勝った!(大人げない)
検証結果
前回のブログと同じく、フォルダの2999のS3オブジェクトを別のフォルダへコピーする方式と処理時間を計測しました。
方法1と方法2は、出力から類推すると並列処理していると考えられる。方法3では方法1と方法2に習い並列処理(並列数10)するように実装したところ同じ処理時間になった。更に方法3の並列数100ではその1/4まで処理時間を短縮できた。
- 簡単に速くフォルダをコピーするのなら、方法1や方法2
- ファイルを差分で追加するなら方法2がより望ましい
- フォルダ内のファイルをコピーする順番に依存関係があるのなら方法3
- できる限り速くフォルダをコピーするのなら、方法4(並列数100)
方式 | 処理時間(秒) |
---|---|
方法1: AWSCLIのサブコマンドcpによるS3フォルダのコピー | 32 |
方法2: AWSCLIのサブコマンドsyncによるS3フォルダのコピー(初回) | 32 |
方法2: AWSCLIのサブコマンドsyncによるS3フォルダのコピー(2回目以降) | 4 |
方法3: Python(Boto3)でS3フォルダをまとめてコピーする | 314 |
方法4: Python(Boto3)でS3フォルダをまとめてコピーする(並列数10) | 32 |
方法4: Python(Boto3)でS3フォルダをまとめてコピーする(並列数100) | 8 |
最後に
比較的よくご覧頂いてる AWSCLI、Python(boto3)などからS3フォルダ間のコピーしてみるを6年ぶりに改定しました。2018年から、S3のベースラインパフォーマンスの改善やレートリミット上限が緩和され、最新の状況を測定したいと考えていたところでした。当時は、「list_objects」から「list_objects_v2」の移行期でしたので、手厚く解説していましたが、今回は省略しました。しかし、paginatorオブジェクトのインスタンスを作る際には、「list_objects_v2」を指定しています。
今回の第2版では、マルチスレッドでフォルダをコピーする方法についても加筆しています。AWSの各種ログファイルは、小さく大量のファイルが多く存在するため、パフォーマンスを改善したいというユースケースは多いのではないでしょうか。今後はこちらのブログを参照していただけたら幸いです。
なお、何度も「S3フォルダ」なんて言ってましたが、フォルダなんてないものはないことは承知しています。
(都元さん、いつも、いつまでも、ありがとう。)
合わせて読みたい