AWSCLI、Python(boto3)などからS3フォルダ間のコピーしてみる(第2版)

AWSCLI、Python(boto3)などからS3フォルダ間のコピーしてみる(第2版)

Clock Icon2024.10.13

AWS事業本部コンサルティング部の石川です。今でもよくご覧いただいているブログ AWSCLI、Python(boto3)などからS3フォルダ間のコピーしてみるを6年ぶりに改定しました。以前のPython(boto3)の実装よりもスッキリわかり易く再実装しました。また、並列処理版もサンプルを追加しています。

https://dev.classmethod.jp/articles/20180629-how-to-copy-s3folder/

検証用データ

以下のような階層のデータを事前に作成しました。

% 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フォルダ」なんて言ってましたが、フォルダなんてないものはないことは承知しています。

(都元さん、いつも、いつまでも、ありがとう。)

Amazon S3における「フォルダ」という幻想をぶち壊し、その実体を明らかにする

合わせて読みたい

https://dev.classmethod.jp/articles/20241014-s3-folder-delete/

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.