← назад к разделу

Amazon S3 (Simple Storage Service) — первый коммерчески успешный сервис object storage, де-факто стандарт в индустрии. Сейчас «S3-API» — общий язык: его поддерживают MinIO, Cloudflare R2, Yandex Object Storage, Backblaze B2, Google Cloud Storage (через interoperability layer) и десятки других. Эта статья — про модель и ключевые свойства, без привязки к конкретному провайдеру.

Что такое object storage

Три уровня хранения, которые senior должен различать:

ТипМодельДоступКогда
Block storage (EBS, локальные диски)Сырые блокиДрайвер ФС поверхБД, любая файловая система
File storage (NFS, EFS, SMB)Иерархия папок и файловPOSIX-семантикаShared-storage между серверами
Object storage (S3, GCS, Azure Blob)Плоский namespace объектов с keyHTTP APIБольшие файлы, бэкапы, статика, неизменяемые данные

Object storage не файловая система. Нет seek, нет partial-write, нет rename, нет атомарных переименований папки. Объект — единица записи и чтения целиком (или диапазона). Это и ограничение, и источник масштабируемости: можно хранить триллионы объектов общим объёмом эксабайты.

Модель: bucket, object, key

s3://my-bucket/products/2026/05/cover-3.jpg
   │           │                  │
   │           └─ key ────────────┘
   └─ bucket
  • Bucket — корневой namespace. Имя глобально-уникально (в AWS S3). Регион — у каждого bucket один.
  • Object — единица хранения. Состоит из payload (байт), metadata (system + user-defined), и versionId (если versioning включён).
  • Key — путь внутри bucket. Это строка, не файловая система: products/2026/05/cover-3.jpg — это один ключ, никакой иерархии S3 внутри не строит. Префиксы (products/2026/) — только для удобства листинга.

Из этого вытекает: в S3 нет mkdir. Префикс существует ровно столько, сколько есть объект с этим префиксом. UI AWS Console показывает «папки», но это эмуляция.

Consistency-гарантии

До декабря 2020 S3 был eventually consistent: после PUT новой версии объекта read мог вернуть старую в течение секунд. Это рождало массу багов в production-сценариях.

С декабря 2020 — strong read-after-write consistency для всех операций. Read после Write возвращает самые свежие данные гарантированно.

Однако нюансы остались:

  • Listing (list-bucket) тоже strong-consistent, но не атомарный: между read'ом и обновлением объект может появиться/исчезнуть.
  • Versioning — Read возвращает «текущую» версию, если specifically не запрошен versionId.
  • MinIO / Yandex / GCS — поведение consistency может отличаться от AWS. Yandex Object Storage следует AWS S3 (strong consistency), MinIO — тоже. Cloudflare R2 — strong consistency.

Storage classes

S3 хранит объекты в разных классах с разным trade-off «стоимость хранения vs стоимость retrieval»:

КлассСтоимость храненияRetrievalКогда
Standardbase × 1мгновенно, бесплатноАктивные данные, hot path
Standard-IA (Infrequent Access)base × 0.5мгновенно, $/GB на readБэкапы, доступ раз в месяц-два
One Zone-IAbase × 0.4мгновенно, $/GBТо же, но один AZ — данные могут пропасть при сбое AZ
Glacier Instant Retrievalbase × 0.25мгновенно (с 12.2021), $$$/GBАрхивы с возможным быстрым доступом
Glacier Flexible Retrievalbase × 0.15минуты-часыАудит, compliance, редкий доступ
Glacier Deep Archivebase × 0.05часы-суткиДолгосрочный архив, compliance ≥7 лет
Intelligent-Tieringбазовая + monitoring feeавтоматический выбор класса по pattern доступаКогда не уверен в pattern доступа

Главное правило: storage classes не равны производительности — все, кроме Glacier Flexible/Deep, отдают объект мгновенно. Различие только в цене. Перевести объект в более холодный класс — дёшево; обратно — требует копирования.

Класс ставится при PUT через header x-amz-storage-class или через lifecycle policy (см. ниже).

Versioning

Bucket-level setting: при включении каждое PUT/DELETE создаёт новую версию, старые сохраняются.

PUT s3://bucket/file.txt → versionId=v1
PUT s3://bucket/file.txt → versionId=v2  (v1 остаётся доступной)
DELETE s3://bucket/file.txt → создаёт "delete marker", v1 и v2 остаются

Запрос GET s3://bucket/file.txt после DELETE возвращает 404 (читается delete marker). Но GET s3://bucket/file.txt?versionId=v1 всё ещё работает.

Включать versioning обязательно для production-bucket'ов с пользовательскими файлами. Защита от:

  • Кейс «случайно удалили». Восстановить версию.
  • Кейс ransomware / компрометация AccessKey. Атакующий не сможет физически удалить, только пометить как удалённое.
  • Кейс «выкатили баг, перезаписали неправильным контентом». Откатить на старую версию.

Минус: stale-версии копятся. Решается lifecycle policy (см. ниже).

Encryption

S3 шифрует данные на сервере (server-side encryption, SSE). Четыре варианта:

ВариантКлюч управляетсяКогда
SSE-S3 (default с 2023)AWSСамый простой, бесплатно, для большинства случаев
SSE-KMSAWS KMS (ваш ключ под AWS-управлением)Compliance, audit trail на ключи, разделение прав
SSE-CКлиент держит ключ, передаёт с запросомОчень специфичные требования compliance
Client-sideКлиент шифрует до отправкиКогда S3 не должен видеть plaintext

С 5 января 2023 AWS включает SSE-S3 по умолчанию для всех новых bucket. Ничего настраивать не нужно.

В UCP-стеке: SSE-S3 для большинства, SSE-KMS для финансовых/медицинских данных где нужен audit trail.

TLS in transit — отдельный аспект. Все запросы к S3 идут через HTTPS, ничего не настраивать.

Lifecycle policies

XML/JSON-правила на уровне bucket: «через N дней после создания — сделать действие». Действия:

  • Transition в более холодный класс хранения (Standard → IA → Glacier).
  • Expire (удалить).
  • Abort incomplete multipart uploads (см. ниже).
  • Expire non-current versions (удалить старые версии при versioning).
<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>

Это обязательный конфиг для любого production-bucket'а. Без него:

  • Старые версии копятся бесконечно — стоимость хранения растёт.
  • Incomplete multipart uploads (см. ниже) — невидимые в обычном листинге, но за них платится.
  • Логи / временные файлы / экспорты — никогда не удаляются.

Presigned URLs — прямая загрузка/скачивание

Типичная задача: пользователь загружает аватарку. Наивная реализация — клиент → backend → S3 — гоняет файл через backend, нагружая канал и CPU.

Лучше — presigned URL: backend генерирует временную (TTL 5-15 минут) подписанную ссылку, клиент шлёт PUT напрямую в S3.

// backend
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();
// client
fetch(presignedUrl, {
    method: 'PUT',
    body: fileBlob,
    headers: { 'Content-Type': 'image/jpeg' }
});

То же работает для скачивания: presignGetObject даёт ссылку, по которой клиент скачивает напрямую с S3. Подходит для шеринга приватных файлов с ограниченным временем доступа.

Безопасность

  • TTL — минимально нужный. 10 минут на upload, 5 минут на download.
  • В подпись включаются bucket, key, метод (PUT/GET), content-type. Клиент не может изменить что-либо из этого, иначе подпись невалидна.
  • Дополнительно: размер файла можно ограничить через conditions в S3 POST policy.

Multipart upload — большие файлы

Для файлов > 100 MB рекомендуется multipart upload: файл бьётся на части (5 MB — 5 GB каждая), части грузятся параллельно и независимо, в конце — CompleteMultipartUpload собирает их в один объект.

1. InitiateMultipartUpload  → uploadId
2. UploadPart (part 1, uploadId)  → ETag-1
3. UploadPart (part 2, uploadId)  → ETag-2
   ... параллельно
4. CompleteMultipartUpload (uploadId, [ETag-1, ETag-2, ...]) → объект собран

Преимущества:

  • Параллельность — N частей одновременно, ограничено только bandwidth.
  • Resume на ошибку — упала загрузка части 5 из 10? Можно ретраить только её.
  • Single-request limit обход — без multipart S3 принимает максимум 5 GB в одном PUT. С multipart — до 5 TB.

Подводный камень: incomplete multipart uploads = деньги. Если процесс прервался между InitiateMultipart и Complete, части лежат в storage, видны через ListMultipartUploads, но не видны через ListObjects. Платится за них так же. Решение — lifecycle rule «abort incomplete after 7 days».

В AWS SDK v2 multipart прозрачен через S3TransferManager:

S3TransferManager tm = S3TransferManager.builder().s3Client(s3).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 или regular put, в зависимости от размера

Что почитать дальше

  • Spring + AWS SDK v2 — клиентский код.
  • Operations — backup, replication, costs.
  • AWS S3 docs — официальная документация.