tazzz.ru · инженерная записка · 31 августа 2026

Ярусное фотохранилище tazzz

Как отдавать пользователю 10–26 фотографий на каждую машину мгновенно и со своего сервера, не храня 885 ГБ и не платя за облако. Все цифры в записке сняты с боевой базы и живых CDN 31.08.2026.

Вердикт. Хранить нужно не «все фото всех машин», а два яруса: обложки всех машин (первые 3 кадра в превью-размере, 15,5 ГБ — раз и навсегда) и полные галереи только просматриваемых (рабочее множество: сегодня 0,3 ГБ, при росте трафика в 100 раз — 27 ГБ). Это укладывается в 32 ГБ, уже свободные на прод-диске, стоит 0 ₽/мес и не зависит от баланса стороннего облака. Почти вся машинерия для этого уже написана и лежит в коде за выключенным флагом — нужны три правки политики, а не новая система.
885 ГБ
зеркало оригиналов иностранных площадок — тупик, в который упёрлись
15,5 + 0,3 ГБ
обложки всех машин + рабочее множество галерей сегодня
0 ₽/мес
на текущем диске (свободно 32 ГБ); S3 не нужен
1,5 с
прогрев полной галереи dongchedi (20 кадров, 13 фото/с); первые 3 кадра — 0,23 с
01

Как мы здесь оказались

Система прошла три состояния, и каждое упёрлось в своё ограничение:

  • Локальный диск, «греть всё». Кэш фото рос на 3–4 ГБ/сутки на массовых кампаниях и занял 77 ГБ, покрыв лишь ~10% фотографий базы. Диск прода — 78 ГБ; дальше расти некуда (ADR 0007).
  • Yandex Object Storage. Хранение дёшево, но платен каждый PUT, а при нуле на счету Yandex отдаёт 403 на все операции — прогрев молча переставал наполнять облако (инцидент 27.06.2026, ADR 0010). Сейчас ключи ротированы и мертвы (SignatureDoesNotMatch), бакет недоступен — то, что было залито, потеряно.
  • Хотлинк (текущее состояние). Кэш выключен рубильником, браузер пользователя тянет фото напрямую с CDN площадок. Для российских площадок это работает; для китайских (byteimg, autoimg) и части зарубежных — фото из РФ грузятся медленно или не грузятся вовсе. Это был принятый в ADR 0010 риск — он и сработал.

Важно: задача — не «придумать кэш» (он написан: WebP-энкодер, дедуп по content-hash, read-through, рабочее множество lastview:*, GC, бэкфилл), а выбрать политику, при которой объём и цена сходятся.

02

Замеры: что мы на самом деле храним и показываем

База на 31.08.2026 — 481 476 машин и 8,6 млн фото. Кэшировать нужно только иностранные площадки (российские CDN браузер грузит сам): 272 702 машины, 5 433 320 фото. Размеры сняты живьём с CDN (выборка n=6 на площадку — уточнится на бэкфилле):

ПлощадкаМашинФотоФото/маш.ОригиналWebP-1600WebP-480Политика full-варианта
dongchedi104 7152 073 97419,8365 КБ 2048px214 КБ23 КБперекодировать (−41%)
mobile_de144 8572 822 03819,556 КБ 1024px71 КБ ↑19 КБхранить как есть
encar15 455408 28226,418 КБ 640px!17 КБ8 КБкак есть + брать крупный вариант URL
che1687 675129 02616,845 КБ ~1024px54 КБ ↑18 КБхранить как есть

↑ — перекодирование раздувает файл: исходники mobile_de/che168 уже мелкие и плотно сжатые. Правило: full = min(оригинал, WebP-1600). Превью 480px выгодно всегда. Encar отдаёт в объявлениях кадры 640×360 — крупный вариант надо запрашивать другим URL (правка адаптера, не хранилища).

Вторая половина картины — просмотры. Телеметрия карточек (car_views): 1 055 открытий, 683 уникальные машины, 15 пользователей за всё время; за последние 30 дней — 108 уникальных машин. Просмотры размазаны тонко (в среднем 1,5 открытия на машину), топ-10% машин собирает 29% просмотров. Вывод: выгоду даёт не «кэш популярного» (популярного почти нет), а ограничение хранения рабочим множеством — тем, что реально открывали за окно TTL.

03

Математика: два закона, которые всё решают

Оба инструмента — из вашей курсовой «Подсистема ввода-вывода высоконагруженных систем» (гл. 1); здесь они применяются к сети вместо диска.

Эффективная пропускная способность: почему хотлинк из РФ в Китай обречён

Beff(n) = B / (1 + B·Tlat / n)
При маленьких запросах (фото 20–200 КБ) и большой латентности канала (РФ↔Китай: сотни мс и потери) знаменатель съедает всю полосу: браузер качает галерею поштучно и последовательно — каждый кадр платит полный Tlat. Сервер в Москве с прямым каналом до фото-CDN платит Tlat один раз на пачку из 30–50 параллельных запросов.

Это не теория: аудит прогрева замерил с прод-IP 72 фото/с (mobile_de), 41/с (encar), 17/с (che168), 13/с (dongchedi) — гео-блок этих площадок действует на страницы, но не на фото-CDN. Браузер россиянина до byteimg не дотягивается вовсе — наш сервер дотягивается со скоростью десятков кадров в секунду.

Закон Литтла: и параллелизм прогрева, и размер хранилища

L = λ · W
Одна формула отвечает на два вопроса. (1) Сколько запросов держать в полёте, чтобы выкачать галерею за секунды. (2) Сколько места займёт кэш в установившемся режиме: Lбайт = λновых машин в кэше × WTTL хранения × размер галереи. В курсовой тот же закон: QD=64 даёт 19-кратный рост IOPS — здесь та же очередь даёт 20-кратное ускорение прогрева (40 с → 2 с).

Проверка конфигурации прогрева законом Литтла — настроенные вручную потолки почти совпали с расчётом:

Площадкаλ, фото/с (замер)W, с/кадрL = λ·W (расчёт)WARM_CONCURRENCY (стоит)
dongchedi13~2,02630
mobile_de72~0,64350
encar41~0,72950
che16817~1,52630
Прогрев обгоняет палец. Человек листает галерею со скоростью ≤1 кадра/с; прямой канал качает 13–72 кадра/с. Если по клику греть кадры в порядке показа, пользователь ждёт только первый кадр (≈0,2–0,5 с), дальше очередь загрузки всегда впереди листания. Полная галерея dongchedi из 20 кадров — ~1,5 с.
04

Сценарии объёма: от 885 ГБ к 16

Сколько занимает каждый сценарий хранения

Только иностранные площадки (272,7 тыс. машин / 5,43 млн фото). Пунктир — свободное место на прод-диске сегодня.
Зеркало оригиналов (пробовали)
885 ГБ
Всё в WebP, full+превью
688 ГБ
Только превью-480, все фото
102 ГБ
Обложки ×3 всех машин
15,5 ГБ
Рабочее множество галерей, 30 дней
0,3 ГБ сегодня → 27 ГБ при ×100
0885 ГБ
отвергнутые сценарии рекомендуемые ярусы (хранятся вместе) 32 ГБ свободно на диске

Рабочее множество считается законом Литтла: уникально просмотренных машин за 30 дней × средневзвешенная галерея 2,6 МБ (взвешено по фактическому распределению просмотров между площадками):

Трафик карточекУник. машин / 30 дн.Галереи (ярус 2)+ Обложки (ярус 1)Итого на диске
Сегодня (факт)1080,3 ГБ15,5 ГБ≈16 ГБ
×101 0802,7 ГБ15,5 ГБ≈18 ГБ
×505 40013,6 ГБ15,5 ГБ≈29 ГБ
×10010 80027,2 ГБ15,5 ГБ≈43 ГБ → +диск за сотни ₽
Главная смена политики. Прежний рост «3–4 ГБ/сутки» создавал не пользователь, а парсер: полные галереи грелись для всего спарсенного, включая кампании по десяткам тысяч машин, которые никто не открыл. Хранение должно масштабироваться от λ просмотров (её создаёт живой трафик), а на парсинге фиксируется только дешёвый ярус обложек (3 превью ≈ 60 КБ на машину: кампания в 50 000 машин добавляет 3 ГБ, а не 200).
05

Рекомендуемая архитектура

Четыре яруса, от вечного к внешнему:

ЯрусЧто лежитКогда пишетсяСколько живётОбъём
1 · Обложкипервые 3 фото, WebP-480на парсинге (и кампаниях)вечно, вне GC15,5 ГБ
2 · Галереивсе кадры: full + превьюпо открытию карточки; для «Свежих» — на выдачеTTL 30 дн. от последнего показа (lastview, GC готов)0,3 → 27 ГБ
3 · Источникоригиналы у площадок—пока живо объявлениеread-through
0 · Плейсхолдер (позже)thumbhash / микро-превью в карточкена парсингевместе с карточкой~30 байт/фото

Путь фотографии к пользователю

Браузер
просит /images/<url-ключ>.webp — всегда наш домен, в Китай не ходит
→
nginx, статика с диска
попадание: отдача за миллисекунды; уже держит 800–1600 гостей
→
Промах → backend
read-through: скачал у источника, перекодировал, положил, отдал 302
→
CDN площадки
прямой канал с прод-IP, 13–72 фото/с; прокси-пул — фолбэк

Где лежат байты: локальный диск, не S3. Объём яруса 1+2 на годы вперёд меньше свободных 32 ГБ; раздача статикой из nginx уже обкатана; трафик у Timeweb безлимитный; и главное — исчезает режим отказа «кончился баланс отдельного сервиса → 403 на всё», который уже дважды ронял схему. S3 вернётся в игру, только если рабочее множество перерастёт разумный локальный диск (по таблице выше — это ×100 к трафику и дальше).

Ключи объектов — от URL для всех площадок (сегодня — только dongchedi, ADR 0009). sha256(url) известен до скачивания: наличие проверяется без БД, эвикция безопасна (промах после GC сам наполняется через read-through). Это заодно чинит скрытый баг: сейчас GC удаляет файлы encar/che168/mobile_de, а карта images_local_json в БД остаётся — фронт строит ссылки на удалённые файлы.

Гейт карточки смягчается: показывать карточку по готовности обложек, полную галерею догревать фоном в порядке показа. По математике §03 к моменту клика галерея почти всегда готова, а первый кадр — всегда.

06

Экономика

ВариантОбъём₽/месРазовоРежим отказа
Статус-кво (хотлинк)00—фото из РФ не грузятся — уже происходит
Зеркало в Yandex S3 (688 ГБ WebP)688 ГБ≈1 970≈5 400 (PUT)ноль на балансе → 403 на всё (случалось)
Ярусы в Yandex S3≈20 ГБ≈280≈300тот же биллинг-риск + латентность промахов
Ярусы на локальном диске16–43 ГБ00переполнение диска — лечится потолком GC и мониторингом

Цены Yandex Object Storage после повышения мая-2026: хранение 2,376 ₽/ГБ/мес, исходящий трафик 1,68 ₽/ГБ после 100 ГБ, операции — отдельно. Прод-VPS у Timeweb: трафик в тарифе, докупка диска при росте — порядка сотен ₽/мес. Скачивание при бэкфилле обложек (~818 тыс. фото ≈ 60–80 ГБ входящего) бесплатно и займёт ~8–10 часов фоном.

07

План включения

  1. Правки кода (одна связная итерация).
    • cache_car_images: режим covers_only (первые 3 URL, только превью) — им пользуются парсинг, кампании и бэкфилл; полный прогрев — по открытию карточки (request_gallery_warm уже есть) и на выдаче «Свежих».
    • URL-ключи для всех иностранных площадок + read-through в /api/image-proxy (обобщить ADR 0009); карту images_local_json — из горячего пути в наследие.
    • full-вариант = min(оригинал, WebP); у encar в адаптере запрашивать крупный вариант кадра.
    • GC: обложки — в referenced безусловно; добавить потолок по месту (size-based) как страховку к TTL.
    • Гейт карточки: готовность = обложки, галерея — фоном.
  2. Конфиг: IMAGE_STORE_BACKEND=local, IMAGE_CACHE_ENABLED=true; фронт PHOTO_CACHE_ON=true + пересборка. Точка отката — те же флаги назад.
  3. Бэкфилл обложек 272,7 тыс. машин через существующий backfill_uncached_images (дросселируя, вне кампаний). Заодно уточнятся средние размеры из §02 (там выборка n=6).
  4. Мониторинг: df диска и глубина image_fetch в админку; алерт при 80% диска.

Риски

Решения за владельцем