Бэкап в S3 падал с 403: виноват порог multipart в 8 МБ
PrimeCoder · 2026 · формат: tech
Разбор новинки простыми словами: что это, кому нужно и брать ли.
Команда aws s3 cp незаметно переключается с PutObject на multipart на ровно 8 МБ — значит, тесты бэкапов на мелких файлах не проверяют реальный боевой путь загрузки.
Связанные услуги: каталог, AI Boost Team, стать клиентом.
Четыре файла перестали уходить во внешнюю резервную копию. Ответ сервиса — CompleteMultipartUpload: AccessDenied, HTTP 403. Бакет тот же, сервисный аккаунт тот же, команда та же: aws s3 cp. А дамп PostgreSQL, который после сжатия и шифрования весил около 4,6 MB, при этом загружался штатно.
Дальше — как команда искала причину и что из этого следует для любого, кто держит бэкапы в S3-совместимом хранилище. Спойлер: дело не в правах и не в шифровании. Дело в том, что одна строчка скрипта прячет две разные последовательности API-вызовов.
8 388 607 и 8 388 608: разница в один байт
aws s3 cp — команда высокоуровневая. Вы видите «загрузить файл», клиент видит «выбрать способ передачи». За переключение отвечает параметр multipart_threshold, и относится он как раз к семейству aws s3, включая cp. Команда зафиксировала AWS CLI 2.36.33 и прогнала запросы через локальный HTTPS-приёмник с тестовыми учётными данными — чтобы увидеть, что именно строит клиент.
Картина на границе:
- 8 388 607 байт —
s3 cpотправляет одним PutObject; - 8 388 608 байт — уже multipart: CreateMultipartUpload, одна часть, CompleteMultipartUpload.
Ровно 8 MB, ни байтом меньше. Первая контринтуитивная деталь: multipart здесь определяется последовательностью вызовов, а не количеством частей. Одна часть — всё равно multipart.
Ровно на 8 MB клиент уже выбирал multipart. Причем с одной частью.
Вторая: в отладочном выводе встречаются Transfer-Encoding: chunked и Content-Encoding: aws-chunked. Искать по ним мультипарт бессмысленно — они были и у одиночного PutObject.
Почему это не вылезло на тестах
Потому что тестовые файлы не пересекали порог. Ни разу.
В истории внешнего хранилища нашлось 88 зашифрованных версий. Самая крупная — 7 897 419 байт. Ни одной больше 8 MB. Это не полная летопись всех загрузок, но сохранённые версии не подтверждают, что проблемный путь когда-либо проходил успешно. Мелкие тестовые файлы шли через PutObject. Дамп базы — тоже, он весил меньше порога. А вложения размером 8,7–9,2 MB исходного размера уходили в multipart и падали.
Отсюда главный вывод для SMB: тест бэкапа на файле в 200 КБ проверяет один сценарий из двух. Боевой путь загрузки остаётся непроверенным, и вы узнаете об этом в момент восстановления.
Ложное подтверждение
Сначала пошли проверять права — на 403 AccessDenied это разумный шаг. Добавили ещё одну роль, без выдачи администратора. Результат тот же: 403.
Потом собрали multipart руками: CreateMultipartUpload — 200, первый UploadPart — 200, второй UploadPart — 200, CompleteMultipartUpload — 403, AbortMultipartUpload — 204, HEAD итогового объекта — 404. Выглядит как независимое подтверждение. На деле это тот же клиент и тот же multipart-путь. Эксперимент сузил место отказа до завершения загрузки, но не отделил поведение клиента от реализации сервера.
Что дало ответ
Помогло изменение способа передачи при том же файле. Подготовили один зашифрованный файл размером 9 445 177 байт, посчитали SHA-256, отправили двумя способами под разными ключами. Endpoint, клиент и учётные данные — одинаковые, проверки checksum не отключали.
s3 cp— части приняты, CompleteMultipartUpload вернул 403;s3api put-object— успех.
Один и тот же зашифрованный файл с одними и теми же учетными данными не загрузился через multipart, но загрузился через PutObject.
Скачанную версию проверили отдельным читателем: метаданные, retention, полное скачивание. SHA-256 скачанного шифротекста совпал с локальным. На это уже можно опереться при починке: файл с этими правами проходит через PutObject и не проходит через multipart.
Подвох
Серверную причину 403 установить не удалось. Между двумя способами различаются запросы, передача контрольных сумм и завершение операции — по такому эксперименту нельзя честно выбрать между багом клиента, особенностью совместимости хранилища и политикой доступа.
Причину, по которой сервер отклонял CompleteMultipartUpload, мы не установили.
Ограниченное исправление — обход, не диагноз. Если у вас та же картина, обход рабочий, но вопрос к провайдеру хранилища всё равно останется открытым.
Что сделать на этой неделе
- Возьмите файл заведомо больше
multipart_threshold— например 20–30 MB случайных байт — и прогоните полный цикл бэкапа и восстановления. - Проверьте, чему равен
multipart_thresholdв вашем профиле, и держите одинаковое значение на всех машинах, где крутятся скрипты. - Сравнивайте не «файл загрузился», а SHA-256 скачанной версии с локальной.
- Добавьте явный
AbortMultipartUploadпри ошибке — иначе незавершённые загрузки копят мусор в бакете. - Поставьте отдельный алерт на 403 именно по CompleteMultipartUpload: он не похож на «кончилось место» и легко теряется в общем логе.
Брать или подождать
Ждать нечего. Порог в 8 MB — не баг и не свежая новость, он был и останется. Менять нужно не клиент, а тесты. Проверка бэкапа без файла больше порога — проверка наличия скрипта, не восстановления. Час на добавление одного крупного файла в тестовый набор дешевле, чем день, когда внешней копии не окажется.
FAQ
Почему 403, если учётные данные верные?
Потому что 403 на CompleteMultipartUpload не обязательно про IAM. В этом разборе права расширяли, отказ повторился. Точную серверную причину не нашли — это результат, а не незаконченное расследование.
Можно ли просто поднять multipart_threshold?
Можно, но это перекладывание проблемы. Файлы крупнее нового порога снова уйдут в multipart. Обход рабочий для конкретного набора вложений, а не для растущего объёма данных.
Как быстро проверить свой бэкап?
Один файл случайных байт больше порога, полный цикл загрузки, скачивание, сверка SHA-256. Падает на этом шаге — дальше смотреть нечего.
Если ваши бэкапы годами живут на файлах по 2 MB, вы не знаете, работает ли ваш бэкап. Проверьте до того, как понадобится.