Файлы — картинки, документы, бэкапы, статику сайта — в облаке не держат на диске виртуальной машины. Для этого есть объектное хранилище: отдельный сервис, который хранит сколько угодно файлов, доступен по сети и не требует своего сервера. В Azure это Blob Storage. Разберём основы; более общий разбор модели, применимый к любому объектному хранилищу, — в разделе Object storage (S3).
Чем выше уровень, тем дешевле хранить и дороже достать: у каждого холодного уровня есть минимальный срок и плата за чтение, поэтому переносят по возрасту правилом жизненного цикла, а не руками и не раньше времени.
Контейнер, блоб, ключ
В объектном хранилище нет привычных папок и файлов: пространство плоское, а то, что выглядит как путь 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, вторая копия асинхронная, не читается без отдельного режима и переключается вручную.
- Счёт из хранения, операций, извлечения и трафика; сеть доставки кэширует по всему миру, требует сброса при обновлении и отдельной схемы перед приватным контейнером.
Что почитать дальше
- Объектные хранилища — общая модель S3-подобных хранилищ.
- Подключение SDK — как читать и писать блобы из кода и тестировать на Azurite.
- Оптимизация затрат в Azure — уровни хранения и трафик в счёте.
- Отказоустойчивость и восстановление в Azure — мягкое удаление и копии как первый эшелон защиты.