Всем привет! Меня зовут Кирилл Грищук, я Tech Lead в команде Инфомодели в Авито.
Мы развиваем платформу объявлений и их характеристики. Дело в том, что у разных категорий товаров есть свои особенности. В машинах это марки, модели, типы двигателей, в квартирах — количество комнат и площадь, а в кошках — порода или длина хвоста. И у каждой категории не только разные характеристики, но и разные правила подачи: например, новостройки публикуют pro-пользователи, а новые машины — только автодилеры.
Чтобы управлять всем этим разнообразием, мы создали no-code-платформу, которая позволяет настраивать характеристики, поля, виджеты — всё, что вы видите, когда подаёте объявление. По сути, мы обрабатываем все вводы пользователей.
В предыдущей статье я рассказал, как мы строим отказоустойчивые системы при работе с многомиллионным трафиком.
В этой статье рассказываю, почему, даже когда всё падает, никто этого не замечает. Статья будет полезна всем, кто хочет погрузиться в проблему раздачи и обработки редко изменяемых данных.

Что внутри статьи:
Почему данные так важны
У нас есть такая штука, как словари — по сути, это данные, как устроено объявление: из чего оно состоит, какие у него есть списки и характеристики. И вот в чём проблема: сервисов, которые хотят получить эти данные, очень много — больше 500. При этом у нас совершенно разные клиенты и разная нагрузка. Кто-то подгружает словари в фоне, кто-то скачивает их вживую, где-то они доступны всегда.
Почти все сценарии в Авито так или иначе связаны с нами: получить список категорий, проверить состав товара, допустимость марки при подаче объявления. Нагрузки тоже разные: от сотен тысяч RPM до единиц.
Чтобы стало понятнее, что такое словарь: слева, например, карточка товара со списком атрибутов и их возможных значений, а справа — дерево категорий, то есть навигация, которая используется при поиске и подаче объявлений ↓

Если эта система отказывает, ничего не работает: половина сценариев падает, бизнес несёт большие убытки. Малейший баг и у множества пользователей перестают работать привычные функции.
Поэтому нужно, чтобы всё работало.
При этом данные постоянно меняются. Бизнес растёт, добавляются новые свойства и характеристики, меняется отображение, появляются новые параметры. У нас есть отдельная команда контент-менеджеров, которая вносит эти изменения. После тестирования пользователи видят результат, например, галочку «шиномонтаж в подарок» при продаже зимних шин. Это как раз фича, реализованная на нашей платформе: новый атрибут «бесплатный шиномонтаж», который потом стал виден всем.
Обзор и сравнение решений
Если система недоступна, нам нужно подобрать решение. Начнём с требований, которые мы предъявляем:
SLI — 99.99% и по возможности больше;
устойчивость к взрывным нагрузкам и пикам с совершенно разными паттернами;
устойчивость к недоступности зависимостей, отказах частей системы или отдельного дата-центра.
Обычно такие задачи решают стандартным способом из серии: «давайте прикрутим кеш к сервису». Что-то в хранилище отказало, добавили кеш, и всё снова работает. Стандартное решение, которое используют все. Если вы работаете со словарями, например со списком городов, скорее всего, вы грузите их в память, в Redis или прямо в память сервиса.

Это стандартная практика, но есть нюансы.
Первый — Redis тоже может упасть. А при большой нагрузке хранилищу становится плохо. Даже при наличии реплик и шардов во время переключений всё равно случаются отказы. Бывает и так, что сами переходы между версиями создают проблемы, например, недавний переход с Redis на Valkey. Хоть протокол и обратно совместим, всё равно это downtime: нужно выключить, включить, проверить.

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

Теперь — следующая проблема.
К нам обращается очень много клиентов. Иногда система работает в обычном режиме — 10–100 RPM, 3–4 RPS, а в пике это может быть уже 120–130 тысяч RPS. И нужно сделать так, чтобы при такой нагрузке система не упала, особенно если данных в кеше нет.

Все знают про прогрев кеша: фоновый прогрев, демоны, кроны, триггеры. Мы тоже используем эти механизмы, чтобы поддерживать кеш в актуальном состоянии и обеспечивать высокий хитрейт — идеальное значение около 99%. То есть почти все данные должны быть в кеше, иначе система долго не проживёт. Обычно это два процесса — само приложение и фоновый демон.

Но есть нюансы. Инфраструктура может упасть, и ошибки случаются не только там. В коде тоже можно легко накосячить: не обработал, выкатил баг, не протестировал corner case. В итоге сервис недоступен, система падает. Казалось бы, что тут сделаешь? Но можно придумать, как повысить отказоустойчивость.

Например, у нас есть автопиары, которые автоматически обновляют библиотеки. Всё вроде покрыто тестами, но иногда просто заканчивается память, и сервис падает.

В итоге мы решили добавить кеширование на стороне клиентов. Теперь клиенты тоже кешируют данные. Пусть это не всегда самые свежие версии и не самые актуальные категории или характеристики, но если что-то случилось: упал сервис, возникла ошибка в коде, отключился дата-центр, клиент всё равно сможет показать хотя бы старые данные. Например, пользователь сможет подать объявление, и оно сохранится с этими данными. Так мы защищаемся от внешних и внутренних сбоев.

|
Мы живем в мире eventual consistency — данные приходят не мгновенно, но в пределах разумного времени. В среднем полная синхронизация занимает около 15 минут. Мы стараемся гарантировать, что в течение этого времени изменения будут доставлены. У нас используется событийная модель: клиенты реагируют на события, когда мы вносим правки или делаем hotfix-переключения данных. Если что-то становится невалидным, система рассылает ивенты, чтобы клиенты обновились. Если событие не доехало, в документации для клиентов прописано, что в течение 15 минут данные точно актуализируются. Это бизнес-требование: новое свойство должно появиться в проде не позже чем через четверть часа. Поэтому и в клиентских кешах, и в библиотеках заложен именно этот интервал по умолчанию. На практике обновление чаще происходит быстрее, даже за секунды. Событие приходит, обрабатывается, кеш обновляется. Но мы учитываем возможные сбои: отказ шины данных, проблемы с передачей сообщений между дата-центрами. В этом случае мы всё равно укладываемся в те же 15 минут. Для нас важно не просто уложиться в таргет, согласованный с бизнесом, но и постоянно снижать задержку. Особенно это критично во время распродаж: если акция заканчивается в 23:59, никто не хочет видеть баннер о ней в 00:01. Поэтому мы делаем всё, чтобы данные обновлялись максимально быстро, хотя нюансы, конечно, бывают. Главный вывод из этого опыта заключается в том, что отказоустойчивая система — это не та, в которой ничего не ломается, а та, которая продолжает работать, даже если что-то ломается. Например, если не доехало событие, а система всё равно функционирует — это и есть показатель устойчивости, те самые «девятки», о которых шла речь. |
Улучшение утилизации кешей
Но возникла новая проблема: кешей стало слишком много. Каждый слой их греет, делает запросы, и вся инфраструктура перегружена. Мы начали тратить огромные ресурсы, в первую очередь оперативку, — чтобы кешировать данные, причём одни и те же.

Плюс эти кеши подогреваются в фоне, создавая дополнительную нагрузку. Растёт CPU, расходуется память, а из-за большого числа клиентов и трафика начинает заканчиваться пропускная способность сети, особенно у баз данных. В итоге, чтобы не дублировать работу, нас всё больше интересует переиспользуемость кешей. Об этом далее.
Когда происходит промах в кеш, то есть один клиент не попадает в кеш, за ним сразу идут сотни таких же запросов. Все они начинают одновременно тянуть одни и те же данные. Если при этом кеш не успевает обновиться, возникает каскадный отказ системы.

Для таких ситуаций существует специальный паттерн Singleflight. В его основе — идея, придуманная в основном для баз данных, что одинаковые запросы можно соединять в один.

Давайте первый выполнит, а остальные подождут и присоединятся к его выполнению. Таким способом мы кардинально снижаем нагрузку. Это must-have для словарей: когда несколько процессов обращаются к одним и тем же данным.
В нашем случае, как и в примерах с VictoriaMetrics на иллюстрации выше, количество запросов сокращается в разы — у них в тысячи, у нас в 16 раз. Эта схема действительно работает: она уменьшает число запросов и переиспользуемость данных.
Можно это организовать так: вы выстраиваете ключ запроса, и на этот ключ запроса подписываетесь.
Теперь о работе сети. Сейчас по ней передаётся большое количество одинаковых данных. Клиенты постоянно их запрашивают, хотя сами данные часто не меняются, особенно ночью, когда контент-менеджеры ничего не обновляют. Иногда причина — в некорректно настроенных политиках обновления, из-за чего одни и те же данные прогреваются по всей системе.

Для оптимизации используется механизм Not Modified — код ответа HTTP 304 и ETag в Nginx. Суть в том, что к запросу добавляется хеш. Если хеш ответа совпадает с переданным, сервер отвечает пустотой. Например, первый запрос получает список категорий с хешем ABC, второй делает запрос с тем же хешем и, поскольку данные не изменились, получает пустой ответ. На клиенте лежит специальная логика: он видит совпадение хеша, продлевает срок жизни кеша и не загружает данные повторно.

Так удалось значительно снизить нагрузку на сеть. Раньше использовалось 128 узлов Redis только для ответов по сети, теперь — 16. При этом объём данных небольшой. Обычно Redis шардируют ради производительности записи, но в нашем случае требовалось оптимизировать сеть. Если использовать хеши, можно заметно снизить нагрузку на сеть. Этот подход хорошо работает и для словарей — можно проверять, изменились ли данные, прежде чем их запрашивать.
Какие ошибки совершили
Система формировалась несколько лет и прошла через разные этапы. Изначально клиенты читали данные из S3 и напрямую с ними работали. С этим хранилищем сложно конкурировать: оно быстро отдаёт данные с диска и говорит, что всё хорошо, а разработчикам приходилось строить поверх него кеши, хеши и проверки. Все клиенты исторически были подключены напрямую, и теперь изменить это нельзя, потому что в системе 350–500 потребителей. Любое изменение, вроде замены «nil» на «0», приводило к сбоям, например, переставала работать подача объявлений или поиск.
Чтобы избежать подобных ошибок, было решено консолидировать данные и отдавать их через HTTP-интерфейс (REST). Конкурировать с S3 непросто, но благодаря стратегиям прогрева, Not Modified и Singleflight удалось достичь более высокой скорости.

Мы начали переводить клиентов на новую схему год назад — и всё ещё продолжаем. Для масштабных проектов, в которых меняется ключевое взаимодействие системы, нужно заложить несколько кварталов или даже год. Это нужно, чтобы организовать и спланировать всю работу, доказать, что всё это действительно нужно.

Спустя год схема полностью внедрена: 350 сервисов используют её без сбоев, данные надёжно кешируются, при отказах система остаётся стабильной.
Была разработана собственная клиентская библиотека, которая обеспечивает правильную работу с кешами и обновлением данных. Это позволило избежать дублирования решений и снизить количество ошибок.
Последние измерения SLI впервые в Авито показали уровень надёжности — 99.999%. Мы продолжаем в это инвестировать, потому что стабильность сервиса напрямую влияет на стабильность бизнеса. От работы сервиса зависит, подастся ли объявление, отработает ли поиск или фильтр. Важно, чтобы клиенты не страдали при отказах системы.

Большое спасибо! Оставляйте комментарии и вопросы — с радостью отвечу на них.

Узнать больше о задачах, которые решают инженеры AvitoTech, можно по этой ссылке. А вот тут мы собрали весь контент от нашей команды — там вы найдете статьи, подкасты, видео и много чего еще. И заходите в наш TG-канал, там интересно!