Привет! Это обертка над HttpClient для работы с S3 хранилищами. Мотивация создания была простейшей - я не понимал, почему клиенты AWS (4.0.0) и Minio (6.0.4) потребляют так много памяти. Результат экспериментов: скорость почти как у AWS, а потребление памяти почти в 150 раз меньше, чем клиент для Minio (и в 17 для AWS).
BenchmarkDotNet v0.14.0, Debian GNU/Linux 12 (bookworm) (container)
Intel Xeon CPU E5-2697 v3 2.60GHz, 1 CPU, 56 logical and 28 physical cores
.NET SDK 8.0.408
[Host] : .NET 8.0.15 (8.0.1525.16413), X64 RyuJIT AVX2
.NET 8.0 : .NET 8.0.15 (8.0.1525.16413), X64 RyuJIT AVX2
Job=.NET 8.0 Runtime=.NET 8.0| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|
| Aws | 5.788 s | 0.0297 s | 0.0264 s | 1.05 | 0.02 | 1000 | 25.78 MB | 17.14 |
| Minio | 7.016 s | 0.0824 s | 0.0730 s | 1.27 | 0.03 | - | 274.03 MB | 182.13 |
| Storage | 5.510 s | 0.1063 s | 0.0994 s | 1.00 | 0.02 | - | 1.5 MB | 1.00 |
Для работы с хранилищем необходимо создать клиент.
varstorageClient=new(newS3BucketSettings{AccessKey="ROOTUSER",Bucket="mybucket",EndPoint="http://localhost:9000",// для Yandex.Objects это "https://storage.yandexcloud.net"SecretKey="ChangeMe123",UseHttp2=false// Yandex.Objects позволяет работать по HTTP2, можете указать true})Также, в конструктор клиента можно передать имплементацию интерфейса IArrayPool, которая позволяет тонко настроить переиспользование массивов клиента.
Minio предоставляет playground для тестирования (порт для запросов всё тот же - 9000). Ключи можно найти в документации. Доступ к Amazon S3 не тестировался.
Мы передаём название bucket'a в настройках, поэтому дополнительно его вводить не надо.
boolbucketCreateResult=awaitstorageClient.CreateBucket(cancellationToken);Console.WriteLine(bucketCreateResult?"Bucket создан":"Bucket не был создан");Как и в прошлый раз, мы знаем название bucket'a, так как мы передаём его в настройках клиента.
boolbucketCheckResult=awaitstorageClient.IsBucketExists(cancellationToken);if(bucketCheckResult)Console.WriteLine("Bucket существует");boolbucketDeleteResult=awaitstorageClient.DeleteBucket(cancellationToken);if(bucketDeleteResult)Console.WriteLine("Bucket удалён");Напомню, что объект в смысле S3 это и есть файл.
Создание, то есть загрузка файла в S3 хранилище, возможна двумя путями: можно разбить исходные данных на кусочки ( multipart), а можно не разбивать. Самый простой способ загрузки файла - воспользоваться следующим методом (если файл будет больше 5 МБ, то применяется multipart):
boolfileUploadResult=awaitstorageClient.UploadFile(fileName,fileContentType,fileStream,cancellationToken);if(fileUploadResult)Console.WriteLine("Файл загружен");Для самостоятельного управления multipart-загрузкой, можно воспользоваться методом UploadFile без указания данных. Получится примеоно такой код:
usingS3Uploadupload=awaitstorageClient.UploadFile(fileName,fileType,cancellationToken);awaitupload.AddParts(stream,cancellationToken);// загружаем части документаif(!awaitupload.AddParts(byteArray,cancellationToken)){// загружаем другую часть документаawaitupload.Abort(cancellationToken);// отменяем загрузку}else{awaitupload.Complete(cancellationToken);// завершаем загрузку}В коде клиента именно эту логику использует метод PutFileMultipart. Конкретную реализацию можно подсмотреть в нём.
S3FilefileGetResult=awaitstorageClient.GetFile(fileName,cancellationToken);if(fileGetResult){Console.WriteLine($"Размер файла {fileGetResult.Length}, контент {fileGetResult.ContetType}");returnawaitfileGetResult.GetStream(cancellationToken);}else{Console.WriteLine($"Файл не может быть загружен, так как {fileGetResult}");}varfileStream=awaitstorageClient.GetFileStream(fileName,cancellationToken);В случае, если файл не существует, возвратится Stream.Null.
boolfileExistsResult=awaitstorageClient.IsFileExists(fileName,cancellationToken);if(fileExistsResult){Console.WriteLine("Файл существует");}Метод проверяет наличие файла в хранилище S3 и формирует GET запрос файла. Параметр expiration должен содержать время
валидности ссылки начиная с даты формирования ссылки.
string?preSignedFileUrl=storageClient.GetFileUrl(fileName,expiration);if(preSignedFileUrl!=null){Console.WriteLine($"URL получен: {preSignedFileUrl}");}Существует не безопасный способ создать ссылку, без проверки наличия файла в S3.
stringpreSignedFileUrl=awaitstorageClient.BuildFileUrl(fileName,expiration,cancellationToken);Удаление объекта из S3 происходит почти мгновенно. На самом деле в S3 хранилище просто ставится задача на удаление и клиенту возвращается результат. Кстати, если удалить файл, который не существует, то ответ будет такой же, как если бы файл существовал. Поэтому этот метод ничего не возвращает.
awaitstorageClient.DeleteFile(fileName,cancellationToken);Console.WriteLine("Файл удалён, если он, конечно, существовал");Локальное измерение производительности и тестирование осуществляется с помощью Minio в Docker'e по http. Понимаю, что это не самый хороший способ, но зато он самый доступный и простой.
- Файл
docker-composeдля локального тестирования можно найти в репозитории. - Запускаем
docker-compose up -d. Если всё хорошо, то бенчмарк заработает в Docker'e. - Если нужно запустить бенчмарк локально, то обращаем внимание на файл
appsettings.json. В нём содержатся основные настройки для подключения к Minio. - Свойство
BigFilePathфайлаappsettings.jsonсейчас не заполнено. Его можно использовать для загрузки реального файла (больше 100МБ). Если свойство не заполнено, то тест сгенерирует случайную последовательность байт размером 123МБ в памяти.
У меня есть канал в TG: @csharp_gepard. К нему привязан чат - вопросы можно задавать в чате, либо в любом из последних постов.