Skip to content

teoadal/Storage

Repository files navigation

.NET Core NuGet NuGet codecov CodeFactor

Клиент для S3

Привет! Это обертка над HttpClient для работы с S3 хранилищами. Мотивация создания была простейшей - я не понимал, почему клиенты AWS и Minio едят так много памяти. Результат моих экспериментов: скорость почти как у Minio, а памяти потребляю почти в 200 раз меньше, чем клиент для AWS.

BenchmarkDotNet v0.13.12, Debian GNU/Linux 12 (bookworm) (container)
Unknown processor
.NET SDK 8.0.204
[Host]   : .NET 8.0.4 (8.0.424.16909), Arm64 RyuJIT AdvSIMD
.NET 8.0 : .NET 8.0.4 (8.0.424.16909), Arm64 RyuJIT AdvSIMD

Job=.NET 8.0  Runtime=.NET 8.0
Method Mean Ratio Gen0 Gen1 Allocated Alloc Ratio
Aws 1.497 s 1.45 80 000 6 000 201 728.07 KB 333.24
Minio 1.468 s 1.43 - - 279 532.97 KB 461.76
Storage 1.031 s 1.00 - - 605.36 KB 1.00

Создание клиента

Для работы с хранилищем необходимо создать клиент.

var storageClient = new S3Client(new S3Settings
{
    AccessKey = "ROOTUSER",
    Bucket = "mybucket",
    EndPoint = "localhost",     // для Yandex.Objects это "storage.yandexcloud.net"
    Port = 9000,                // стандартный порт Minio - 9000, для Yandex.Objects указывать не нужно
    SecretKey = "ChangeMe123",
    UseHttps = false,           // для Yandex.Objects укажите true
    UseHttp2 = false            // Yandex.Objects позволяет работать по HTTP2, можете указать true
})

Minio предоставляет playground для тестирования (порт для запросов всё тот же - 9000). Ключи можно найти в документации. Доступ к Amazon S3 не тестировался.

Операции с S3 bucket

Создание bucket'a

Мы передаём название bucket'a в настройках, поэтому дополнительно его вводить не надо.

bool bucketCreateResult = await storageClient.CreateBucket(cancellationToken);
Console.WriteLine(bucketCreateResult
    ? "Bucket создан"
    : "Bucket не был создан");

Проверка существования bucket'a

Как и в прошлый раз, мы знаем название bucket'a, так как мы передаём его в настройках клиента.

bool bucketCheckResult = await storageClient.IsBucketExists(cancellationToken);
if (bucketCheckResult) Console.WriteLine("Bucket существует");

Удаление bucket'a

bool bucketDeleteResult = await storageClient.DeleteBucket(cancellationToken);
if (bucketDeleteResult) Console.WriteLine("Bucket удалён");

Операции с S3 object

Напомню, что объект в смысле S3 это и есть файл.

Создание файла

Создание, то есть загрузка файла в S3 хранилище, возможна двумя путями: можно разбить исходные данных на кусочки ( multipart), а можно не разбивать. Самый простой способ загрузки файла - воспользоваться следующим методом (если файл будет больше 5 МБ, то применяется multipart):

bool fileUploadResult = await storageClient.UploadFile(fileName, fileContentType, fileStream, cancellationToken);
if (fileUploadResult) Console.WriteLine("Файл загружен");

Управление Multipart-загрузкой

Для самостоятельного управления multipart-загрузкой, можно воспользоваться методом UploadFile без указания данных. Получится примеоно такой код:

using S3Upload upload = await storageClient.UploadFile(fileName, fileType, cancellationToken);

await upload.AddParts(stream, cancellationToken); // загружаем части документа
if (!await upload.AddParts(byteArray, cancellationToken)) { // загружаем другую часть документа
    await upload.Abort(cancellationToken); // отменяем загрузку
}
else {
    await upload.Complete(cancellationToken); // завершаем загрузку
}

В коде клиента именно эту логику использует метод PutFileMultipart. Конкретную реализацию можно подсмотреть в нём.

Получение файла

StorageFile fileGetResult = await storageClient.GetFile(fileName, cancellationToken);
if (fileGetResult) {
    Console.WriteLine($"Размер файла {fileGetResult.Length}, контент {fileGetResult.ContetType}");
    return await fileGetResult.GetStream(cancellationToken);
}
else {
    Console.WriteLine($"Файл не может быть загружен, так как {fileGetResult}");
}

Получение файла как Stream

var fileStream = await storageClient.GetFileStream(fileName, cancellationToken);

В случае, если файл не существует, возвратится Stream.Null.

Проверка существования файла

bool fileExistsResult = await storageClient.IsFileExists(fileName, cancellationToken);
if (fileExistsResult) {
	Console.WriteLine("Файл существует");
}

Создание подписанной ссылки на файл

Метод проверяет наличие файла в хранилище S3 и формирует GET запрос файла. Параметр expiration должен содержать время валидности ссылки начиная с даты формирования ссылки.

string? preSignedFileUrl = storageClient.GetFileUrl(fileName, expiration);
if (preSignedFileUrl != null) {
	Console.WriteLine($"URL получен: {preSignedFileUrl}");
}

Существует не безопасный способ создать ссылку, без проверки наличия файла в S3.

string preSignedFileUrl = await storageClient.BuildFileUrl(fileName, expiration, cancellationToken);

Удаление

Удаление объекта из S3 происходит почти мгновенно. На самом деле в S3 хранилище просто ставится задача на удаление и клиенту возвращается результат. Кстати, если удалить файл, который не существует, то ответ будет такой же, как если бы файл существовал. Поэтому этот метод ничего не возвращает.

await storageClient.DeleteFile(fileName, cancellationToken);
Console.WriteLine("Файл удалён, если он, конечно, существовал");

Измерение производительности и тестирование

Локальное измерение производительности и тестирование осуществляется с помощью Minio в Docker'e по http. Понимаю, что это не самый хороший способ, но зато он самый доступный и простой.

  1. Файл docker-compose для локального тестирования можно найти в репозитории.
  2. Запускаем docker-compose up -d. Если всё хорошо, то бенчмарк заработает в Docker'e.
  3. Если нужно запустить бенчмарк локально, то обращаем внимание на файл appsettings.json. В нём содержатся основные настройки для подключения к Minio.
  4. Свойство BigFilePath файла appsettings.json сейчас не заполнено. Его можно использвоать для загрузки реального файла (больше 100МБ). Если свойство не заполнено, то тест сгенерирует случайную последовательность байт размером 123МБ в памяти.

Вопросы

У меня есть канал в TG: @csharp_gepard. К нему привязан чат - вопросы можно задавать в чате, либо в любом из последних постов.