Файлы — картинки, документы, бэкапы, статику сайта — в облаке не держат на диске виртуальной машины. Для этого есть объектное хранилище: отдельный сервис, который хранит сколько угодно файлов, доступен по сети и не требует своего сервера. В Azure это Blob Storage. Разберём основы; более общий разбор модели, применимый к любому объектному хранилищу, — в разделе Object storage (S3).

Archive минимум 180 дней, разморозка часами Cold минимум 90 дней Cool минимум 30 дней хранения Hot частое чтение, дорогое хранение

Чем выше уровень, тем дешевле хранить и дороже достать: у каждого холодного уровня есть минимальный срок и плата за чтение, поэтому переносят по возрасту правилом жизненного цикла, а не руками и не раньше времени.

Обязательно

Контейнер, блоб, ключ

В объектном хранилище нет привычных папок и файлов: пространство плоское, а то, что выглядит как путь images/2026/logo.png, — просто имя объекта. Модель такая:

  • Учётная запись хранилища (storage account) — верхняя единица, к которой привязаны настройки и биллинг.
  • Контейнер (container) — раздел внутри неё, куда складывают объекты.
  • Блоб (blob) — сам файл вместе с метаданными; его адресует ключ — строка вида images/2026/logo.png. Слэши в нём — просто часть имени. Одна оговорка: при создании учётной записи можно включить иерархическое пространство имён — это и есть ADLS Gen2. Тогда каталоги становятся настоящими, и переименование каталога — одна операция, а не перезапись всех объектов под ним. Решение принимают один раз при создании и обратно не переключают.

Доступ — только через HTTP API, не как к диску: объект читается и пишется целиком. Именно поэтому объектное хранилище масштабируется до огромных объёмов. Для большинства задач используют блочные блобы (block blobs) — обычные файлы.

Уровни доступа

Одни данные читают постоянно, другие лежат годами «на всякий случай». Держать всё одинаково — переплачивать. Поэтому у блобов есть уровень доступа (access tier):

  • Hot (горячий) — для частого доступа: дороже за хранение, дёшево читать;
  • Cool (прохладный) — для редкого доступа;
  • Cold (холодный) — ещё реже;
  • Archive (архив) — для данных, к которым почти не обращаются: копейки за хранение, но доступ медленный (объект надо сначала «разморозить») и с доплатой.

Логика простая: горячие данные — в Hot, холодные (старые логи, бэкапы) — в дешёвых уровнях. Вручную это делать необязательно: lifecycle-правила сами переносят блобы в холодные уровни по возрасту и удаляют совсем старые.

Прежде чем радоваться экономии, посмотрите на два ценника, которых не видно в цене «за гигабайт». Первый — минимальный срок хранения: Cool — 30 дней, Cold — 90, Archive — 180. Удалили блоб или подняли его обратно в горячий уровень раньше срока — заплатите как за полный срок. Поэтому правило «через неделю в Archive» на данных, которые ещё живут и меняются, даёт не экономию, а рост счёта. Второй — плата за извлечение: чтение с Cool, Cold и Archive тарифицируется отдельно, и чем холоднее уровень, тем дороже. Дешёвый архив перестаёт быть дешёвым ровно в тот момент, когда вы в него полезли.

SAS-токены и доступ

Контейнер по умолчанию приватный (доступ по RBAC или ключам). Чтобы дать временную ссылку на приватный объект — «скачать отчёт в течение часа» — используют SAS-токен (Shared Access Signature): подписанную ссылку с ограниченным сроком и правами, без раздачи постоянного доступа. Это аналог presigned URL в S3.

Видов SAS три, и опасны они по-разному. SAS учётной записи и сервисный SAS подписываются ключом учётной записи хранилища: кто держит ключ — выпускает любые ссылки, а отозвать одну выданную нельзя — только сменить ключ целиком, уронив заодно всех, кто им пользуется. SAS с делегированием пользователя (user delegation SAS) подписывается через Entra ID по правам вашей managed identity: ключ учётной записи для него не нужен, и доступ отзывается как обычные права. Берите его — он единственный не втаскивает вечный ключ обратно в систему, из которой вы его выгоняли.

Публичный доступ (объекты по прямой ссылке) включают осознанно — так раздают статику сайта.

Защита от «удалили не то»

Три механизма, которые включают один раз и которые спасают именно от человеческой ошибки, а не от отказа оборудования.

Мягкое удаление (soft delete). Удалённый объект не исчезает, а помечается удалённым и хранится заданное число дней (от одного до 365). Восстановить — одна операция. Включается на учётной записи и работает и для объектов, и для контейнеров целиком (это отдельная настройка — контейнер, удалённый со всем содержимым, тоже восстанавливается).

Версионирование. Каждая запись создаёт новую версию, старые остаются. Тогда «перезаписали файл неправильными данными» лечится откатом на предыдущую версию, а не восстановлением из копии. Цена — место: все версии тарифицируются, поэтому вместе с версионированием настраивают правило, которое удаляет старые версии через N дней или переводит их в дешёвый уровень.

Неизменяемость (immutability). Политика, которая запрещает изменять и удалять объекты в течение заданного срока — в том числе владельцу учётной записи. Это то, что нужно для требований регулятора и для защиты от злоумышленника с правами администратора: копия, которую нельзя удалить, остаётся копией даже в самом плохом сценарии. Бывает в двух видах — на срок (объект нельзя трогать N дней) и юридическая блокировка (нельзя, пока её явно не снимут).

Практический минимум для боевого контура: мягкое удаление объектов и контейнеров (7–30 дней), версионирование там, где файлы перезаписываются, и правило жизненного цикла, которое убирает старые версии. Неизменяемость — там, где есть требование или где хранятся резервные копии.

Избыточность

Сначала ответьте, какой сбой вы хотите пережить: диск, дата-центр или целый регион. От ответа зависит, сколько копий данных и где хранить:

  • LRS (Locally Redundant Storage) — копии в пределах одного дата-центра; дёшево, но не переживёт аварию площадки;
  • ZRS (Zone-Redundant Storage) — копии по нескольким зонам доступности региона; переживёт падение зоны;
  • GRS (Geo-Redundant Storage) — локальные копии в основном регионе плюс копия в парном;
  • GZRS (Geo-Zone-Redundant Storage) — копии по зонам в основном регионе плюс та же копия в парном.

Здесь легко обмануться лесенкой «чем дальше по списку, тем лучше». GRS не включает в себя ZRS: в основном регионе он остаётся локальным, и падение зоны переживёт не лучше LRS. Если нужна защита и от зоны, и от региона — это GZRS, а не GRS.

Со второй копией надо понимать ещё три вещи. Она асинхронная: отставание обычно меньше 15 минут, и это ровно то, что вы потеряете при внезапной аварии региона. Она не читается — если только вы не взяли вариант с доступом на чтение (RA-GRS или RA-GZRS). И переключение на неё не происходит само: его инициируете вы, и занимает оно до часа. Так что «защита от аварии региона» — это не автоматика, а план, который кто-то должен выполнить.

Выбор — это баланс цены и того, какой сбой вы хотите пережить.

Из чего складывается стоимость

Счёт складывается из четырёх частей: хранение (за гигабайты в месяц, зависит от уровня — архив в разы дешевле горячего), операции (за количество чтений/записей), извлечение данных (отдельная плата за чтение с холодных уровней — та самая статья, из-за которой дешёвый архив перестаёт быть дешёвым) и исходящий трафик (за данные наружу). Самая коварная статья — трафик: на раздаче популярной статики растёт незаметно. Помогает CDN (Azure CDN / Front Door) перед хранилищем — кэширует ближе к пользователю и снижает и задержку, и трафик.

Где это применяется

Blob Storage — стандартное место для файлового: пользовательские загрузки, картинки и видео, статика фронтенда, резервные копии, экспорты и логи. Приложение обычно не отдаёт файлы через себя, а кладёт их в контейнер и раздаёт ссылками — это разгружает сервис.

Типичные ошибки новичков:

  • держат файлы на диске машины — теряют их при пересоздании и не масштабируются;
  • раздают всё через приложение вместо ссылок или CDN — грузят сервис и переплачивают за трафик;
  • делают контейнер публичным по ошибке — открывают приватные данные;
  • не настраивают lifecycle — платят за горячий уровень даже за архив.
Дополнительно: при первом чтении можно пропустить

Глубже: пределы и большие файлырасширенное

Числа, которые определяют, что вообще можно положить в хранилище и как быстро.

Размер объекта. Блочный блоб собирается из блоков, и предел у него огромный — сотни терабайт. Практически важнее другое: за один запрос можно положить ограниченный объём (обычно сотни мегабайт, зависит от версии интерфейса), а файл больше загружают частями.

Пропускная способность учётной записи. У учётной записи хранения есть предел на число операций в секунду и на объём трафика (порядка десятков тысяч операций и десятков гигабит, точные значения зависят от вида и региона). Упираются в него редко, а когда упираются — получают ограничение частоты с ответом 503 и заголовком, сколько ждать. Лечится либо разнесением нагрузки по нескольким учётным записям, либо сетью доставки содержимого перед хранилищем.

Число объектов не ограничено — это важное свойство: миллионы файлов в одном контейнере нормальны, но перечисление такого контейнера дорого и медленно, поэтому раскладка ключей по префиксам (год/месяц/день) остаётся обязательной.

Загрузка частями. Большой файл разбивают на блоки, загружают их (можно параллельно и в любом порядке) и затем одной операцией собирают в объект.

var client = containerClient.getBlobClient("reports/2026/09/big.csv");
var options = new ParallelTransferOptions()
        .setBlockSizeLong(8L * 1024 * 1024)   // блок 8 МБ
        .setMaxConcurrency(4);                // четыре потока
client.uploadFromFile("big.csv", options, null, null, null, null, null);
file, err := os.Open("big.csv")
if err != nil {
	return err
}
defer file.Close()
_, err = client.UploadFile(ctx, "reports", "2026/09/big.csv", file, &azblob.UploadFileOptions{
	BlockSize:   8 * 1024 * 1024, // блок 8 МБ
	Concurrency: 4,               // четыре горутины
})
const blob = containerClient.getBlockBlobClient('reports/2026/09/big.csv');
await blob.uploadFile('big.csv', {
  blockSize: 8 * 1024 * 1024, // блок 8 МБ
  concurrency: 4,             // четыре параллельных запроса
});
blob = container_client.get_blob_client("reports/2026/09/big.csv")
with open("big.csv", "rb") as data:
    blob.upload_blob(data, max_concurrency=4)   # блоки по 4 МБ по умолчанию, четыре потока

Что это даёт: параллельность (быстрее в разы на большом файле), устойчивость к обрыву (повторно отправляется только неудавшийся блок, а не файл целиком) и возможность докачки — незавершённые блоки живут неделю, и загрузку можно продолжить.

Что ломается на нестабильной сети без этого: загрузка файла одним запросом обрывается на 90 % и начинается с нуля. Библиотеки Azure используют загрузку частями автоматически для файлов больше порога — но размер блока и параллельность стоит задавать осознанно, а не полагаться на значения по умолчанию.

И обратная сторона, о которой узнают по счёту: незавершённые блоки занимают место и тарифицируются. Прерванные загрузки надо убирать правилом жизненного цикла — иначе через год в хранилище лежат гигабайты мусора, которого не видно в списке объектов.

Глубже: согласованность и параллельная записьрасширенное

Два вопроса, которые возникают, как только в один объект пишут из нескольких мест.

Согласованность сильная. Записали объект — следующее чтение вернёт новое содержимое, без «а вдруг ещё старое». Это упрощает жизнь заметно: не нужно ни ждать, ни перепроверять.

А вот параллельная запись одного ключа решается вами. Два процесса читают объект, каждый меняет своё и записывает — побеждает тот, кто записал последним, а первое изменение теряется молча. Механизм защиты называется условной записью и опирается на метку версии (ETag), которую хранилище возвращает при чтении:

var response = blobClient.downloadContent();
var etag = response.getDeserializedHeaders().getETag();

var conditions = new BlobRequestConditions().setIfMatch(etag);
blobClient.uploadWithResponse(new BlobParallelUploadOptions(newData)
        .setRequestConditions(conditions), null, null);
// если объект изменился с момента чтения — ошибка 412, и надо перечитать и повторить
props, err := blobClient.GetProperties(ctx, nil)
if err != nil {
	return err
}
_, err = blobClient.UploadBuffer(ctx, newData, &blockblob.UploadBufferOptions{
	AccessConditions: &blob.AccessConditions{
		ModifiedAccessConditions: &blob.ModifiedAccessConditions{IfMatch: props.ETag},
	},
})
// если объект изменился с момента чтения — ошибка 412 (bloberror.ConditionNotMet), и надо перечитать и повторить
const { etag } = await blobClient.getProperties();

await blobClient.upload(newData, newData.length, { conditions: { ifMatch: etag } });
// если объект изменился с момента чтения — ошибка 412, и надо перечитать и повторить
etag = blob_client.get_blob_properties().etag

blob_client.upload_blob(new_data, overwrite=True, etag=etag, match_condition=MatchConditions.IfNotModified)
# если объект изменился с момента чтения — ошибка 412 (ResourceModifiedError), и надо перечитать и повторить

Это ровно та же оптимистическая блокировка, что в базах данных, и для файлов, которые правятся из нескольких мест, она обязательна. Есть и вторая форма: If-None-Match: * — «записать, только если объекта ещё нет», то есть безопасное создание без перезаписи чужого.

Ещё одна пара условий полезна для скачивания: If-Modified-Since и If-None-Match при чтении позволяют клиенту не забирать файл, который не изменился, — экономия трафика и денег на большом числе обращений.

Глубже: публичный доступ выключен дваждырасширенное

Про «публичный доступ включают осознанно» стоит уточнить механику, потому что она изменилась и теперь двухуровневая.

У новых учётных записей публичный доступ запрещён на уровне самой учётной записи. Это значит, что попытка сделать контейнер публичным не даст ничего: настройка контейнера есть, а разрешение уровня учётной записи её перекрывает. Симптом: «поставил публичный доступ, а файлы всё равно не отдаются».

То есть чтобы отдать файлы публично, нужно два разрешения: на учётной записи (allowBlobPublicAccess) и на контейнере (уровень доступа). Это сделано намеренно: случайно открыть контейнер теперь нельзя, нужно осознанное действие на двух уровнях.

И правильный ответ, который избавляет от необходимости это включать: не делать контейнеры публичными вовсе, а отдавать файлы по подписанным ссылкам с ограниченным сроком (о них раздел выше) или через сеть доставки содержимого с ограниченным доступом к хранилищу. Тогда запрет на уровне учётной записи остаётся включённым навсегда, и один класс утечек закрыт архитектурно.

Глубже: сеть доставки содержимого: две деталирасширенное

Про сеть доставки (CDN или глобальный распределитель) сказано как про способ экономии, и к этому нужны два практических дополнения.

Кэш надо уметь сбрасывать. Файл, отданный один раз, лежит в кэше по всему миру заданное время — и обновление в хранилище само по себе его не заменит. Два подхода. Первый: сброс кэша (purge) после обновления — операция есть, выполняется минуты и годится для редких обновлений. Второй, правильный для часто меняющихся файлов: версия в имени (app.5f3a2b.js вместо app.js) — тогда сбрасывать нечего, новый файл имеет новый адрес, а старый спокойно доживает в кэше. Второй способ избавляет от целого класса вопросов «почему пользователь видит старую версию».

Кэш перед приватным контейнером — отдельная схема. Если контейнер не публичный (а он и не должен быть, см. выше), сеть доставки не сможет просто так забрать из него файл. Нужен способ дать ей доступ: подписанная ссылка в настройках источника, управляемое удостоверение самой сети доставки или отдельный компонент, который выдаёт подписанные ссылки. Из этого следует, что связка «приватное хранилище плюс сеть доставки» — это настройка, а не галочка, и её планируют заранее.

Заодно два приятных свойства, о которых стоит знать: сеть доставки снимает с хранилища нагрузку по числу операций (то есть отодвигает предел из раздела выше), а трафик через неё обычно дешевле, чем прямая отдача из хранилища.

Коротко

  • Пространство плоское: учётная запись хранилища, контейнер, блоб с ключом, где слэши это часть имени; объект читают и пишут целиком по HTTP.
  • Уровни Hot, Cool, Cold и Archive различаются кратно по хранению и цене чтения, а у холодных есть минимальный срок и плата за досрочное удаление; переносят по возрасту правила жизненного цикла.
  • Контейнер приватный, временную ссылку даёт SAS-токен: пользовательский SAS с делегированием через RBAC отзываем, SAS по ключу учётной записи нет.
  • Большие файлы грузят блоками параллельно с докачкой, а незавершённые блоки тарифицируются, пока их не уберёт правило жизненного цикла; листинг миллионов объектов дорог, ключи раскладывают по префиксам.
  • От «удалили не то» спасают мягкое удаление на 7–30 дней, версионирование там, где файлы перезаписывают, и неизменяемость для требований регулятора.
  • Согласованность сильная, а параллельная запись одного ключа решается условными заголовками If-Match по ETag и If-None-Match для «создать, только если нет».
  • Публичный доступ выключен дважды: на учётной записи и на контейнере; правильный ответ вовсе не включать его, а раздавать подписанными ссылками или через сеть доставки.
  • LRS, ZRS, GRS, GZRS: GRS не включает ZRS, вторая копия асинхронная, не читается без отдельного режима и переключается вручную.
  • Счёт из хранения, операций, извлечения и трафика; сеть доставки кэширует по всему миру, требует сброса при обновлении и отдельной схемы перед приватным контейнером.

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