transfer_manager で GCS へのファイルアップロードが高速化できるか確認してみた。

transfer_manager で GCS へのファイルアップロードが高速化できるか確認してみた。

Clock Icon2024.09.26

こんにちは、みかみです。

ミドリフグが飼いたくて探していたのですが、なかなか見つからないので、琉球メダカを飼い始めました。
正式名称は知りませんが、その辺の水辺で普通に獲れるメダカです。オス(多分)はグッピーに近い綺麗な体色で、なかなかかわいいです。

はじめに

1ヶ月以上前のことですが、Google Cloud ブログで気になる記事を見ました。

Google Cloud 環境でバッチ処理を実装する場合、GCS からのファイルのダウンロードやアップロードはよく使う処理ではないかと思います。
特にファイル数が多かったり1ファイルのサイズが大きいケースでは、ファイルのダウンロードやアップロードに時間がかかり、しばしば悩ましい問題に発展することがあります。

ということで。

やりたいこと

  • Google Cloud Storage の Python ライブラリ transfer_manager を使うと、本当に GCS へのファイルアップロード処理が速くなるのか確認してみたい。

前提

google-cloud-storage ライブラリはインストール済みであるものとします。 本エントリでは、Cloud Shell を使用しました。

また、Cloud Storage API の有効化と操作に必要な権限は付与済みです。

テスト用ファイルを準備

アップロード処理の確認に使用する、テスト用ファイルを準備します。

以下のスクリプトを実行して、10MB のファイルを100個作成しました。

#!/bin/bash

FILE_SIZE=10000000
FILE_COUNT=100
FILE_PREFIX="sample_10MB_"
TARGET_DIR="files"

# フォルダが存在しない場合は作成
if [ ! -d "$TARGET_DIR" ]; then
  mkdir -p "$TARGET_DIR"
fi

# ファイル作成
for i in $(seq 1 $FILE_COUNT); do
  FILE_NAME="${FILE_PREFIX}${i}"
  dd if=/dev/urandom bs=$FILE_SIZE count=1 of="$TARGET_DIR/$FILE_NAME" status=none
  echo "create $TARGET_DIR/$FILE_NAME"
done

echo "done."
$ bash ./create_sample_files.sh
create files/sample_10MB_1
create files/sample_10MB_2
create files/sample_10MB_3
(省略)
create files/sample_10MB_100
done.
$ ls -la
total 16
drwxrwxr-x  3 mikami_yuki mikami_yuki 4096 Sep 24 15:20 .
drwxr-xr-x 67 mikami_yuki        1001 4096 Sep 24 15:20 ..
-rw-rw-r--  1 mikami_yuki mikami_yuki  457 Sep 24 15:20 create_sample_files.sh
drwxrwxr-x  2 mikami_yuki mikami_yuki 4096 Sep 24 15:20 files
$ ls -la files/
total 976820
drwxrwxr-x 2 mikami_yuki mikami_yuki     4096 Sep 24 15:20 .
drwxrwxr-x 3 mikami_yuki mikami_yuki     4096 Sep 24 15:20 ..
-rw-rw-r-- 1 mikami_yuki mikami_yuki 10000000 Sep 24 15:20 sample_10MB_1
-rw-rw-r-- 1 mikami_yuki mikami_yuki 10000000 Sep 24 15:20 sample_10MB_10
-rw-rw-r-- 1 mikami_yuki mikami_yuki 10000000 Sep 24 15:20 sample_10MB_100
-rw-rw-r-- 1 mikami_yuki mikami_yuki 10000000 Sep 24 15:20 sample_10MB_11
(省略)
-rw-rw-r-- 1 mikami_yuki mikami_yuki 10000000 Sep 24 15:20 sample_10MB_99

直列処理でテスト用ファイルをアップロード

まずは、google.cloud.storage ライブラリの upload_from_filename メソッドで、直列処理で GCS にアップロードしてみます。
特にパフォーマンスの考慮はしておらず、アップロード前後の時間から、処理時間を計測して print します。

以下の Python コードを実行しました。

from google.cloud import storage
import time
import os

def upload_files(bucket_name, filenames, destination_path, directory):
    storage_client = storage.Client()
    bucket = storage_client.bucket(bucket_name)

    start_time = time.time()  # アップロード開始時刻

    for filename in filenames:
        blob_name = os.path.join(destination_path, filename)
        blob = bucket.blob(blob_name)
        blob.upload_from_filename(os.path.join(directory, filename))

    end_time = time.time()  # アップロード終了時刻
    elapsed_time = end_time - start_time  # 経過時間

    print(f"アップロード時間: {elapsed_time:.2f} 秒")

bucket_name = "test-mikami"
directory = "./files"
destination_path = "upload_files/serial"

# ディレクトリ内のファイル名を取得
filenames = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))]
# 直列アップロード
upload_files(bucket_name, filenames, destination_path, directory)

実行結果は以下です。

$ python serial.py
アップロード時間: 69.10 秒

結構かかりましたね。。
1分10秒ほどかかりました。

transfer_manager を使ってアップロード

同様に、google.cloud.storage.transfer_managerupload_many_from_filenames でアップロードする場合の処理時間を確認します。

以下のコードを実行しました。

from google.cloud import storage
from google.cloud.storage import transfer_manager
import time
import os

def upload_files(bucket_name, filenames, source_directory="", blob_name_prefix="", workers=8):
    storage_client = storage.Client()
    bucket = storage_client.bucket(bucket_name)

    start_time = time.time()  # アップロード開始時刻

    results = transfer_manager.upload_many_from_filenames(
        bucket, filenames, source_directory=source_directory, blob_name_prefix=blob_name_prefix, max_workers=workers
    )

    end_time = time.time()  # アップロード終了時刻
    elapsed_time = end_time - start_time  # 経過時間

    print(f"アップロード時間: {elapsed_time:.2f} 秒")

bucket_name = "test-mikami"
directory = "./files"
destination_path = "upload_files/transfer_manager/"

# ディレクトリ内のファイル名を取得
filenames = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))]
# Transfer Manager でアップロード
upload_files(bucket_name, filenames, source_directory=directory, blob_name_prefix=destination_path)

結果は以下です。

$ python transfer_manager.py
アップロード時間: 10.06 秒

先ほど1分以上かかったアップロード処理が、約10秒で完了しました。

並列処理でアップロード

では、transfer_manager を使わずに、ThreadPoolExecutor を使って並列でアップロードしてみたらどうでしょうか?
transfer_manager での実行と同様、max_workers=8 を指定して、並列でアップロード処理を実行します。

以下のコードを実行しました。

from google.cloud import storage
import time
import os
from concurrent.futures import ThreadPoolExecutor

def upload_files(bucket_name, filenames, destination_path, directory):
    storage_client = storage.Client()
    bucket = storage_client.bucket(bucket_name)

    start_time = time.time()  # アップロード開始時刻

    with ThreadPoolExecutor(max_workers=8) as executor:
        for filename in filenames:
            blob_name = os.path.join(destination_path, filename)
            blob = bucket.blob(blob_name)
            executor.submit(blob.upload_from_filename, os.path.join(directory, filename))

    end_time = time.time()  # アップロード終了
    elapsed_time = end_time - start_time  # 経過時間

    print(f"アップロード時間: {elapsed_time:.2f} 秒")

bucket_name = "test-mikami"
directory = "./files"
destination_path = "upload_files/parallel"

# ディレクトリ内のファイル名を取得
filenames = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))]
# 並列アップロード
upload_files(bucket_name, filenames, destination_path, directory)

結果は以下です。

$ python parallel.py
アップロード時間: 9.91 秒

transfer_manager を使った時とほぼ同じ、10秒ほどでアップロードできました。

サイズの大きいファイルをアップロード

先ほどは 10MB × 100個 のファイルをアップロードしましたが、ファイルサイズが大きい場合はどうなるのでしょうか?

テスト用ファイル作成スクリプトの FILE_SIZEFILE_COUNT の値を変更して、100MB × 10個 のテスト用ファイルを作成し、同様の Python コードでアップロードを実行してみました。

結果は以下です。

  • 直列アップロード
$ python serial.py
アップロード時間: 17.23 秒
  • transfer_manager でアップロード
$ python transfer_manager.py
アップロード時間: 4.57 秒
  • 並列アップロード
$ python parallel.py
アップロード時間: 4.24 秒

直列でもそれほど時間がかからないこともありますが、速度向上率がそれほど高くないことから、ファイルをチャンクに分けてアップロードするような処理は入っていないように思われます。

transfer_manager のコードを確認

transfer_managerupload_many_from_filenames の実装を確認してみます。

upload_many_from_filenames からは upload_many メソッドを呼び出しており、

upload_many では _get_pool_class_and_requirements を使って executor を作成しています。

executor の実態を確認してみると

concurrent.futures.ProcessPoolExecutor クラスを返却していました。

ファイルアップロード処理など、CPU使用率がそれほど高くない I/O バウンドなタスクは、ProcessPoolExecutor よりも ThreadPoolExecutor を使った方が効率的に処理できるのではないかと思いましたが、いずれにせよ、自分で並列処理の実装が不要なのは嬉しい限りです。

まとめ(所感)

transfer_manager でファイルアップロード処理速度が向上することが確認できました。
今回は大量ファイル向けの upload_many の処理速度を確認してみましたが、transfer_manager にはサイズの大きいファイルをチャンクに分けて効率的にアップロードするインターフェース(upload_chunks_concurrently)もあるようです。

transfer_manager を使えば、並列処理やファイルのチャンク化などを自分で考慮する必要なく、効率的で保守性の高いコードをシンプルに実装することができます。
今後は積極的に transfer_manager を利用しようと思いました!

参考

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.