Когда нужно хранить файлы пользователей, видео, резервные копии или логи — первая мысль бывает «поставим NFS» или «сохраним на диск сервера». Это работает до какого-то предела, а потом начинаются проблемы: диск заканчивается, файлы не видны другим серверам, сделать резервную копию всего тяжело. Object storage решает именно эти проблемы — и устроен совсем иначе, чем файловая система.
Чем object storage отличается от файловой системы
На обычном диске файлы лежат в папках, папки вложены друг в друга. Есть операции rename, seek (прочитать кусок с середины), атомарное переименование директории.
Object storage — другая модель:
- Нет папок. Есть единое плоское пространство имён, где у каждого файла — ключ-строка.
- Нет
renameиseek. Объект читается и записывается целиком (или диапазоном байт). - Доступ — только через HTTP API.
Именно поэтому object storage масштабируется до триллионов объектов и эксабайт данных: нет накладных расходов файловой системы, нет блокировок папок.
Для ориентиру — три типа хранилищ в одной таблице:
| Тип | Модель | Доступ | Когда использовать |
|---|---|---|---|
| Block storage (EBS, локальные диски) | Сырые блоки | Драйвер файловой системы | Базы данных, ОС |
| File storage (NFS, EFS) | Папки и файлы | POSIX: read/write/seek | Шаринг файлов между серверами |
| Object storage (S3, MinIO, R2) | Плоский namespace, ключ → объект | HTTP API | Фото, видео, резервные копии, статика |
Amazon S3 (Simple Storage Service) — первый коммерчески успешный object storage и де-факто стандарт. «S3-совместимый API» сегодня понимают MinIO, Cloudflare R2, Yandex Object Storage, Backblaze B2, Google Cloud Storage и десятки других. Дальше в статье говорим о модели S3 — она применима ко всем ним.
Bucket, объект и ключ
Три главных понятия:
s3://my-bucket/products/2026/05/cover-3.jpg
│ │ │
│ └─ ключ (key) ─────┘
└─ bucket
Bucket — корневой контейнер. Имя глобально-уникально в AWS S3 (другие провайдеры ограничивают уникальность в рамках аккаунта). У каждого bucket — один регион.
Объект — единица хранения. Состоит из содержимого (байты), системных и пользовательских метаданных, и идентификатора версии (если версионирование включено).
Ключ — это просто строка, например products/2026/05/cover-3.jpg. S3 никакой иерархии внутри не строит. Слэши в ключе — это удобство для группировки при листинге, не структура каталогов. Консоль AWS показывает «папки» — это только визуализация поверх ключей.
Практическое следствие: в S3 нет mkdir. «Папка» существует ровно пока есть хотя бы один объект с таким префиксом.
Гарантии консистентности
До декабря 2020 года S3 был eventually consistent: после загрузки нового файла попытка его сразу прочитать могла вернуть старую версию или ошибку 404. Это рождало трудноуловимые баги.
С декабря 2020 — strong read-after-write consistency: прочитал после записи — всегда получишь актуальные данные, гарантированно. Это работает и для листинга: список объектов тоже консистентен.
MinIO и Yandex Object Storage тоже дают strong consistency. Если используете менее распространённый S3-совместимый сервис — стоит проверить его документацию отдельно.
Классы хранения
Не все данные нужны одинаково быстро. Логи прошлого года читают редко, а активные аватарки пользователей — постоянно. S3 предлагает классы хранения с разным балансом цены хранения и цены чтения:
| Класс | Стоимость хранения | Доступность | Когда |
|---|---|---|---|
| Standard | базовая | мгновенно, без доплаты | Активные данные |
| Standard-IA | примерно вдвое дешевле | мгновенно, доплата за чтение | Резервные копии, редкий доступ |
| One Zone-IA | чуть дешевле IA | мгновенно, доплата за чтение | То же, но в одной зоне доступности |
| Glacier Instant Retrieval | в четыре раза дешевле | мгновенно, доплата | Архивы, но иногда нужен быстрый доступ |
| Glacier Flexible Retrieval | в шесть раз дешевле | минуты–часы | Долгосрочный архив |
| Glacier Deep Archive | в двадцать раз дешевле | часы–сутки | Хранение от 7 лет, compliance |
| Intelligent-Tiering | базовая + плата за мониторинг | автоматически | Когда не знаете паттерн доступа |
Важный момент: все классы, кроме Glacier Flexible и Deep Archive, отдают объект мгновенно. Разница только в цене, не в скорости. Перевести объект в более холодный класс легко; обратно — требует нового копирования.
Класс указывается при загрузке через заголовок x-amz-storage-class, либо устанавливается автоматически через lifecycle policy.
Версионирование
По умолчанию новая загрузка файла с тем же ключом перезаписывает предыдущий. Включите версионирование на уровне bucket — и каждая загрузка создаёт новую версию, старые сохраняются:
# Загрузили файл первый раз
PUT s3://bucket/report.pdf → versionId=v1
# Загрузили новую версию
PUT s3://bucket/report.pdf → versionId=v2 # v1 остаётся доступной
# Удалили файл
DELETE s3://bucket/report.pdf → создаётся "delete marker", v1 и v2 остаются
После удаления GET s3://bucket/report.pdf вернёт 404 — S3 читает delete marker. Но GET s3://bucket/report.pdf?versionId=v1 по-прежнему работает.
Версионирование — обязательный атрибут production-хранилища с пользовательскими файлами. Оно защищает от случайного удаления, от атак с шифрованием данных (злоумышленник не сможет физически уничтожить версии) и от багов, перезаписавших контент неправильными данными.
Минус: старые версии копятся и занимают место. Решается lifecycle policy (см. ниже).
Шифрование
S3 шифрует данные на своей стороне — server-side encryption (SSE). С января 2023 года SSE-S3 включён по умолчанию для всех новых bucket — AWS управляет ключами автоматически, ничего настраивать не нужно.
Когда нужен больший контроль:
- SSE-KMS — ключи в AWS KMS (вашем), есть аудит-лог всех обращений к ключу. Используют в финансах и медицине, где требуется audit trail именно на уровне ключей шифрования.
- SSE-C — вы передаёте свой ключ с каждым запросом. Очень специфичные compliance-требования.
- Client-side encryption — шифруете до отправки. Когда S3 в принципе не должен видеть незашифрованные данные.
Передача данных по сети — всегда через HTTPS. Это не настраивается, так работает по умолчанию.
Lifecycle policy — автоматическое управление данными
Данные растут. Логи месячной давности никто не читает, но они занимают дорогое хранилище Standard. Lifecycle policy — это правила, по которым S3 автоматически переводит объекты в более дешёвый класс или удаляет их.
Типичная конфигурация для хранилища логов:
<LifecycleConfiguration>
<Rule>
<ID>logs-retention</ID>
<Filter><Prefix>logs/</Prefix></Filter>
<Status>Enabled</Status>
<Transition>
<Days>30</Days>
<StorageClass>STANDARD_IA</StorageClass>
</Transition>
<Transition>
<Days>90</Days>
<StorageClass>GLACIER</StorageClass>
</Transition>
<Expiration><Days>365</Days></Expiration>
</Rule>
<Rule>
<ID>cleanup-old-versions</ID>
<Status>Enabled</Status>
<NoncurrentVersionExpiration>
<NoncurrentDays>30</NoncurrentDays>
</NoncurrentVersionExpiration>
</Rule>
<Rule>
<ID>abort-incomplete-uploads</ID>
<Status>Enabled</Status>
<AbortIncompleteMultipartUpload>
<DaysAfterInitiation>7</DaysAfterInitiation>
</AbortIncompleteMultipartUpload>
</Rule>
</LifecycleConfiguration>
Три правила здесь решают три разные проблемы:
- Логи переводятся в холодный класс через 30 дней, в архив — через 90, удаляются через год.
- Старые версии (при включённом версионировании) удаляются через 30 дней после замены.
- Незавершённые многочастные загрузки (см. ниже) удаляются через 7 дней — иначе их не видно в обычном листинге, но за них всё равно платится.
Lifecycle policy — стандартная часть настройки любого production-bucket.
Presigned URL — прямая загрузка без прогона через сервер
Типичная задача: пользователь загружает аватарку. Очевидное решение — клиент отправляет файл на ваш backend, backend кладёт его в S3. Проблема: файл проходит через ваш сервер, нагружая канал и процессор.
Лучше — presigned URL: backend генерирует временную подписанную ссылку с коротким временем жизни (10–15 минут), клиент загружает файл напрямую в S3. Сервер в передаче данных не участвует.
Серверная часть — генерация ссылки:
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
import java.time.Duration;
// S3Presigner инжектится как бин
PresignedPutObjectRequest req = s3Presigner.presignPutObject(b -> b
.signatureDuration(Duration.ofMinutes(10))
.putObjectRequest(p -> p
.bucket("avatars")
.key("users/" + userId + "/avatar.jpg")
.contentType("image/jpeg")));
return req.url().toString();
// presignClient — *s3.PresignClient, созданный один раз при старте
presignReq, err := presignClient.PresignPutObject(context.Background(),
&s3.PutObjectInput{
Bucket: aws.String("avatars"),
Key: aws.String(fmt.Sprintf("users/%s/avatar.jpg", userID)),
ContentType: aws.String("image/jpeg"),
},
s3.WithPresignExpires(10*time.Minute),
)
if err != nil {
return "", fmt.Errorf("presign: %w", err)
}
return presignReq.URL, nil
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
// s3Client — экземпляр S3Client, созданный один раз
const command = new PutObjectCommand({
Bucket: "avatars",
Key: `users/${userId}/avatar.jpg`,
ContentType: "image/jpeg",
});
const url = await getSignedUrl(s3Client, command, { expiresIn: 600 });
return url;
# s3_client — boto3.client("s3"), созданный один раз
url = s3_client.generate_presigned_url(
"put_object",
Params={
"Bucket": "avatars",
"Key": f"users/{user_id}/avatar.jpg",
"ContentType": "image/jpeg",
},
ExpiresIn=600,
)
return url
Клиент загружает файл напрямую по полученной ссылке:
fetch(presignedUrl, {
method: 'PUT',
body: fileBlob,
headers: { 'Content-Type': 'image/jpeg' }
});
То же работает для скачивания (presignGetObject): раздавать приватные файлы с ограниченным временем доступа.
Несколько вещей о безопасности presigned URL:
- Ставьте минимальное время жизни: 10 минут на загрузку, 5 минут на скачивание.
- В подпись включены bucket, ключ, метод (PUT/GET) и content-type. Клиент не может изменить ни одно из них — иначе подпись не пройдёт проверку.
- Ограничить размер файла можно через условия S3 POST policy.
Multipart upload — файлы больше 100 МБ
Обычный PUT принимает файл размером до 5 ГБ. Для больших файлов есть multipart upload: файл разбивается на части (от 5 МБ до 5 ГБ каждая), части загружаются параллельно, в конце S3 собирает их в один объект.
1. InitiateMultipartUpload → uploadId
2. UploadPart (часть 1, uploadId) → ETag-1
3. UploadPart (часть 2, uploadId) → ETag-2
... параллельно
4. CompleteMultipartUpload (uploadId, [ETag-1, ETag-2, ...]) → объект готов
Зачем это нужно:
- Параллельность — части загружаются одновременно, ограничена только пропускная способность сети.
- Возобновление — если часть 5 из 10 не загрузилась, повторяется только она.
- Размер — с multipart максимум 5 ТБ вместо 5 ГБ.
Подводный камень: если загрузка прервалась между InitiateMultipartUpload и CompleteMultipartUpload, части остаются в хранилище. В обычном листинге их не видно, но платить за них придётся. Поэтому в lifecycle policy всегда добавляют правило «удалять незавершённые загрузки через 7 дней».
На практике вручную управлять multipart не нужно — SDK делают это автоматически:
import software.amazon.awssdk.transfer.s3.S3TransferManager;
import software.amazon.awssdk.transfer.s3.model.Upload;
import java.nio.file.Paths;
// S3TransferManager строится поверх готового S3AsyncClient
S3TransferManager tm = S3TransferManager.builder().s3Client(s3AsyncClient).build();
Upload upload = tm.uploadFile(b -> b
.source(Paths.get("big-video.mp4"))
.putObjectRequest(p -> p.bucket("videos").key("user-42/video.mp4")));
upload.completionFuture().join();
// SDK сам выбирает multipart или обычный PUT в зависимости от размера
// uploader — *manager.Uploader, созданный один раз поверх s3.Client
file, err := os.Open("big-video.mp4")
if err != nil {
return fmt.Errorf("open file: %w", err)
}
defer file.Close()
_, err = uploader.Upload(context.Background(), &s3.PutObjectInput{
Bucket: aws.String("videos"),
Key: aws.String("user-42/video.mp4"),
Body: file,
})
// manager.Uploader сам выбирает multipart при размере > PartSize (по умолчанию 5 МБ)
import { Upload } from "@aws-sdk/lib-storage";
import { createReadStream } from "fs";
// s3Client — экземпляр S3Client
const upload = new Upload({
client: s3Client,
params: {
Bucket: "videos",
Key: "user-42/video.mp4",
Body: createReadStream("big-video.mp4"),
},
});
await upload.done();
// Upload из @aws-sdk/lib-storage автоматически использует multipart
from boto3.s3.transfer import TransferConfig
# s3_client — boto3.client("s3")
config = TransferConfig(multipart_threshold=100 * 1024 * 1024) # 100 МБ
s3_client.upload_file(
Filename="big-video.mp4",
Bucket="videos",
Key="user-42/video.mp4",
Config=config,
)
# boto3 автоматически переключается на multipart при превышении порога
Коротко
- Object storage — плоский namespace объектов с HTTP-доступом. Нет папок, нет
rename, нетseek. За счёт этого масштабируется до триллионов объектов. - Три понятия: bucket (глобально-уникальный контейнер), объект (байты + метаданные), ключ (строка-адрес объекта).
- С 2020 года S3 даёт strong read-after-write consistency — данные после записи доступны мгновенно.
- Классы хранения различаются ценой, не скоростью (кроме Glacier Flexible/Deep). Горячие данные — Standard, редко используемые — IA или Glacier.
- Версионирование защищает от случайного удаления и перезаписи. Включать для любого production-хранилища с пользовательскими файлами.
- Lifecycle policy — автоматически переводит объекты в холодный класс и удаляет их. Необходима для контроля расходов и управления версиями.
- Presigned URL позволяет загружать и скачивать файлы напрямую из S3, минуя backend. Время жизни — минимально нужное.
- Multipart upload — для файлов больше 100 МБ. SDK управляют им автоматически.
Что почитать дальше
- Spring + AWS SDK v2 для S3 — конкретный клиентский код для Java.
- Operations: резервные копии, репликация, затраты — операционная сторона S3.