【Docker】S3互換ストレージのベンチマーク Garage vs MinIO

Docker

先日、S3互換のMinIOの代替えとしてGarageの記事を書きましたが、やっていなかったパフォーマンスの計測をしてみました。

MinIOとGarageをそれぞれDockerコンテナとして起動した状態で、LaravelのArtisanコマンドからS3ドライバのleague/flysystem-aws-s3-v3 経由でファイルのアップロード、ダウンロード、ファイル削除について時間計測していきます。

テストケース

GarageとMinIOをそれぞれDockerコンテナとして起動します。

Laravel12 + league/flysystem-aws-s3-v3 を使って、

次の8つのケースで時間計測をします。

公平を期す為に、Laravelの .env を、Garage用とMinIO用を用意し、

.env を交換することで接続先を切り替えることにします。

つまり、同じソースコードを使ってGarageとMinIOのテストを実行します。

▼1:ファイルアップロード(数での勝負:API経由)

数十バイト程度の小容量のファイルを1000個、連続で順次アップロードします。

▼2:ファイルアップロード(容量での勝負:API経由)

百メガバイトの大容量のファイルを10個、連続で順次アップロードします。

▼3:ファイルダウンロード(数での勝負:API経由)

数十バイト程度の小容量のファイルを1000個、API経由で順次連続でダウンロードします。

▼4:ファイルダウンロード(容量での勝負:API経由)

百メガバイトの大容量のファイルを10個、API経由で連続で順次ダウンロードします。

▼5:ファイルダウンロード(数での勝負:HTTP経由)

数十バイト程度の小容量のファイルを1000個、HTTP経由で順次ダウンロードします。

▼6:ファイルダウンロード(容量での勝負:HTTP経由)

百メガバイトの大容量のファイルを10個、HTTP経由で順次ダウンロードします。

▼7:ファイル削除(数での勝負:API経由)

数十バイト程度の小容量のファイルを1000個、順次連続で削除します。

▼8:ファイル削除(容量での勝負:API経由)

百メガバイトの大容量のファイルを10個、連続で順次削除します。

実行環境

  • Ubuntu24.04.3 LTS (WSL2 on Windows11)
  • Docker Desktop v4.51.0
  • PHP 8.4.14
  • Laravel Framework 12.39.0
  • OS: Ubuntu24.04.3 LTS on WSL2 (Windows11 OSビルド:26200.7171)
  • Machine: AMD Ryzen 7 7730U(2.00GHz) / RAM 16GB / 64bit

フォルダ構成

コンテナ構築:GarageとMinIO

Garage、MinIO それぞれ執筆時点での最新のDcokerイメージで構築します。

▼「docker-compose.yml」

services:
  garaged:
    image: dxflrs/garage:v2.1.0
    container_name: garaged
    volumes:
      - ./docker/garage/garage.toml:/etc/garage.toml
      - ./docker/garage/meta:/var/lib/garage/meta
      - ./docker/garage/data:/var/lib/garage/data
    restart: unless-stopped
    ports:
      - 3900:3900
      - 3901:3901
      - 3902:3902
      - 3903:3903
  minio:
    image: minio/minio:RELEASE.2025-09-07T16-13-09Z
    ports:
        - '9000:9000'
        - '9001:9001'
    environment:
      - MINIO_ROOT_USER=minio
      - MINIO_ROOT_PASSWORD=minio123
    volumes:
      - ./docker/minio/data:/data
      - ./docker/minio/certs:/root/.minio/certs
    command: server /data --console-address ":9001"

コンテナ構築:

docker compose up -d

コンテナ構築後にGarageの初期設定をします。

  • クラスターレイアウト作成:2GBくらい
  • APIキー作成:コピーして保存
  • バケット作成:laravel
  • バケットへのAPIキー割り当て
  • バケットのWebアクセスの許可

▼初期設定は次の記事を参照

MinIOは動作確認も含めて http://localhost:9001/ から minio / minio123 でログインして、バケット「laravel」を作成しておきます。

いつの間にかMinIOのWeb UIからバケットの公開設定が出来なくなっていたので😡💢、mcコマンド直叩きでバケットを公開に設定。

▼「docker-compose.yml」のあるフォルダで実行

docker compose exec minio /bin/mc alias set myminio http://localhost:9000 minio minio123
docker compose exec minio /bin/mc anonymous set public myminio/laravel

テストコードについて

公平を期す為に、テストコードは一か所に保存し、

.envファイルをGarage用とMinIO用とを用意して、

.envファイルを切り替えて接続先を変更することにします。

LaravelはComposerでインストールします。(インストーラー使うまでもないので)

composer create-project laravel/laravel:^12 laravel

S3ドライバーをインストールしておきます。

composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies

▼.env.garage:AWS_*** の箇所のみ掲載、Keyは各自の環境に合わせて修正

AWS_ACCESS_KEY_ID=GKf8a28b48acae1b70abb15f8d
AWS_SECRET_ACCESS_KEY=eb59ab4e5ca8e1f4d15885d121e93867e8d033c544bcbc8e82dc37d4d8aca578
AWS_DEFAULT_REGION=garage
AWS_BUCKET=laravel
AWS_ENDPOINT=http://localhost:3900
AWS_USE_PATH_STYLE_ENDPOINT=true
AWS_URL=http://laravel.web.garage.localhost:3902

▼.env.minio:AWS_*** の箇所のみ掲載

AWS_ACCESS_KEY_ID=minio
AWS_SECRET_ACCESS_KEY=minio123
AWS_DEFAULT_REGION=ap-northeast-1
AWS_BUCKET=laravel
AWS_USE_PATH_STYLE_ENDPOINT=true
AWS_ENDPOINT=http://minio:9000
AWS_URL=http://minio:9000/laravel

▼Garage用の「.env」に切り替え:laravelフォルダ内で

cp -f .env.garage .env

▼MinIO用の「.env」に切り替え:laravelフォルダ内で

cp -f .env.minio .env

100MBのファイルは次のコードで「100mb.txt」として作成し、

Laravelプロジェクトの「storage/100mb.txt」としてブッ込んでおきます。

テストコードからは「storage_path(‘100mb.txt’)」として参照します。

▼create100mb.php

<?php

ini_set('memory_limit', -1);
$content = "";
for ($i = 0; $i < 100 * 1024 * 1024; $i++) {
    $content .= "A";
}
file_put_contents(__DIR__ . '/100mb.txt', $content);

テストコードはArtisanコマンドとして作成することにします。

cd laravel
php artisan make:command BenchmarkS3UploadSmall
php artisan make:command BenchmarkS3UploadBig
php artisan make:command BenchmarkS3DownloadSmallViaApi
php artisan make:command BenchmarkS3DownloadBigViaApi
php artisan make:command BenchmarkS3DownloadSmallViaHttp
php artisan make:command BenchmarkS3DownloadBigViaHttp
php artisan make:command BenchmarkS3DeleteSmall
php artisan make:command BenchmarkS3DeleteBig

テストケース1:ファイルアップロード(数での勝負)

▼テストコード:app/Console/Commands/BenchmarkS3UploadSmall.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Benchmark;
use Illuminate\Support\Facades\Storage;

class BenchmarkS3UploadSmall extends Command
{
    protected $signature = 'benchmark:s3:upload:small';
    protected $description = 'ファイルアップロード(数で勝負)';

    public function handle()
    {
        $iterations = 1000;
        $time = Benchmark::measure(function () use ($iterations) {
            $content = "This is a sample file content for S3 upload benchmarking.";
            $basename = "small";
            $extension = ".txt";
            $nl = strlen((string) $iterations);
            for ($i = 1; $i <= $iterations; $i++) {
                $filename = sprintf("%s%0{$nl}d%s", $basename, $i, $extension);
                Storage::disk('s3')->put($filename, $content);
            }
        });
        $this->info("Time for S3 upload over {$iterations} iterations: {$time} ms");
    }
}

▼Garageでの実行結果

▼MinIOのでの実行結果

テストケース2:ファイルアップロード(容量での勝負)

▼テストコード:app/Console/Commands/BenchmarkS3UploadBig.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Http\File;
use Illuminate\Support\Benchmark;
use Illuminate\Support\Facades\Storage;

class BenchmarkS3UploadBig extends Command
{
    protected $signature = 'benchmark:s3:upload:big';
    protected $description = 'ファイルアップロード(容量で勝負)';

    public function handle()
    {
        ini_set('memory_limit', -1);
        $iterations = 10;
        $time = Benchmark::measure(function () use ($iterations) {
            $basename = "big";
            $extension = ".txt";
            $nl = strlen((string) $iterations);
            for ($i = 1; $i <= $iterations; $i++) {
                $filename = sprintf("%s%0{$nl}d%s", $basename, $i, $extension);
                Storage::disk('s3')->putFileAs('/', new File(storage_path('100mb.txt')), $filename);
            }
        });
        $this->info("Time for S3 upload over {$iterations} iterations: {$time} ms");
    }
}

▼Garageでの実行結果

▼MinIOでの実行結果

テストケース3:ファイルダウンロード(数での勝負:API経由)

▼テストコード:app/Console/Commands/BenchmarkS3DownloadSmallViaApi.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Benchmark;
use Illuminate\Support\Facades\Storage;

class BenchmarkS3DownloadSmallViaApi extends Command
{
    protected $signature = 'benchmark:s3:download:small';
    protected $description = 'ファイルダウンロード(数で勝負)';

    public function handle()
    {
        $iterations = 1000;
        $time = Benchmark::measure(function () use ($iterations) {
            $basename = "small";
            $extension = ".txt";
            $nl = strlen((string) $iterations);
            for ($i = 1; $i <= $iterations; $i++) {
                $filename = sprintf("%s%0{$nl}d%s", $basename, $i, $extension);
                Storage::disk('s3')->get($filename);
            }
        });
        $this->info("Time for S3 download over {$iterations} iterations: {$time} ms");
    }
}

▼Garageでの実行結果

▼MinIOでの実行結果

テストケース4:ファイルダウンロード(容量での勝負:API経由)

▼テストコード:app/Console/Commands/BenchmarkS3DownloadBigViaApi.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Benchmark;
use Illuminate\Support\Facades\Storage;

class BenchmarkS3DownloadBigViaApi extends Command
{
    protected $signature = 'benchmark:s3:download:big:api';
    protected $description = 'ファイルダウンロード(容量で勝負:API経由)';

    public function handle()
    {
        ini_set('memory_limit', -1);
        $iterations = 10;
        $time = Benchmark::measure(function () use ($iterations) {
            $basename = "big";
            $extension = ".txt";
            $nl = strlen((string) $iterations);
            for ($i = 1; $i <= $iterations; $i++) {
                $filename = sprintf("%s%0{$nl}d%s", $basename, $i, $extension);
                Storage::disk('s3')->get($filename);
            }
        });
        $this->info("Time for S3 download over {$iterations} iterations: {$time} ms");
    }
}

▼Garageでの実行結果

▼MinIOでの実行結果

テストケース5:ファイルダウンロード(数での勝負:HTTP経由)

▼テストコード:app/Console/Commands/BenchmarkS3DownloadSmallViaHttp.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Benchmark;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;

class BenchmarkS3DownloadSmallViaHttp extends Command
{
    protected $signature = 'benchmark:s3:download:small:http';
    protected $description = 'ファイルダウンロード(数で勝負:HTTP経由)';

    public function handle()
    {
        $iterations = 1000;
        $time = Benchmark::measure(function () use ($iterations) {
            $basename = "small";
            $extension = ".txt";
            $nl = strlen((string) $iterations);
            for ($i = 1; $i <= $iterations; $i++) {
                $filename = sprintf("%s%0{$nl}d%s", $basename, $i, $extension);
                $url = Storage::disk('s3')->url($filename);
                Http::get($url);
            }
        });
        $this->info("Time for S3 download over {$iterations} iterations: {$time} ms");
    }
}

▼Garageでの実行結果

▼MinIOでの実行結果

テストケース6:ファイルダウンロード(容量での勝負:HTTP経由)

▼テストコード:app/Console/Commands/BenchmarkS3DownloadBigViaHttp.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Benchmark;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;

class BenchmarkS3DownloadBigViaHttp extends Command
{
    protected $signature = 'benchmark:s3:download:big:http';
    protected $description = 'ファイルダウンロード(容量で勝負:HTTP経由)';

    public function handle()
    {
        $iterations = 10;
        $time = Benchmark::measure(function () use ($iterations) {
            $basename = "big";
            $extension = ".txt";
            $nl = strlen((string) $iterations);
            for ($i = 1; $i <= $iterations; $i++) {
                $filename = sprintf("%s%0{$nl}d%s", $basename, $i, $extension);
                $url = Storage::disk('s3')->url($filename);
                Http::get($url);
            }
        });
        $this->info("Time for S3 download over {$iterations} iterations: {$time} ms");
    }
}

▼Garageでの実行結果

▼MinIOでの実行結果

テストケース7:ファイル削除(数での勝負)

▼テストコード:app/Console/Commands/BencharkS3DeleteSmall.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Benchmark;
use Illuminate\Support\Facades\Storage;

class BenchmarkS3DeleteSmall extends Command
{
    protected $signature = 'benchmark:s3:delete:small';
    protected $description = 'ファイル削除(数で勝負)';

    public function handle()
    {
        $iterations = 1000;
        $time = Benchmark::measure(function () use ($iterations) {
            $basename = "small";
            $extension = ".txt";
            $nl = strlen((string) $iterations);
            for ($i = 1; $i <= $iterations; $i++) {
                $filename = sprintf("%s%0{$nl}d%s", $basename, $i, $extension);
                Storage::disk('s3')->delete($filename);
            }
        });
        $this->info("Time for S3 delete over {$iterations} iterations: {$time} ms");
    }
}

▼Garageでの実行結果

▼MinIOでの実行結果

テストケース8:ファイル削除(容量での勝負)

▼テストコード:app/Console/Commands/BenchmarkS3DeleteBig.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Benchmark;
use Illuminate\Support\Facades\Storage;

class BenchmarkS3DeleteBig extends Command
{
    protected $signature = 'benchmark:s3:delete:big';
    protected $description = 'ファイル削除(容量で勝負)';

    public function handle()
    {
        $iterations = 10;
        $time = Benchmark::measure(function () use ($iterations) {
            $basename = "big";
            $extension = ".txt";
            $nl = strlen((string) $iterations);
            for ($i = 1; $i <= $iterations; $i++) {
                $filename = sprintf("%s%0{$nl}d%s", $basename, $i, $extension);
                Storage::disk('s3')->delete($filename);
            }
        });
        $this->info("Time for S3 delete over {$iterations} iterations: {$time} ms");
    }
}

▼Garageでの実行結果

▼MinIOでの実行結果

評価

▼計測時間を表にまとめました。左側がミリ秒、右側が速い方に対する割合です。

テストケースGarageMinIOGarageMinIO
1:UL(数)3427.4249087318.6689051.002.14
2:UL(容)4629.3717656721.4167331.001.45
3:DL(数:API)2267.4549062744.0630431.001.21
4:DL(容:API)2712.9383506364.6514281.002.35
5:DL(数:HTTP)1089.6107212197.5797581.002.02
6:DL(容:HTTP)3418.7290895288.1711821.001.55
7:DEL(数)2532.8008052784.9174591.001.10
8:DEL(容)93.74582386.4412371.081.00

顕著な差が出ましたね。

削除のテストケース7,8以外はGarageの方が圧倒的に速いです。

容量勝負の削除は10ファイル分の削除フラグを立てているだけなので速いのでしょう。

筆者はストレージには詳しくないので、保存形式がどうとか、スループットがどうとか、その辺はよくわかりません。

詳しい人、解説してください。

とりあえず、パフォーマンス勝負ではGarageの圧勝という結果になりました。

MinIOよ今までありがとう、そしてさようなら。

  • 2
  • 1
  • 0
  • 0

コメント

タイトルとURLをコピーしました