Привет! Меня зовут Антон Головенко и я backend-разработчик команды Search-Quality в Авито. Вот уже полтора года я ускоряю производительность поиска и повышаю его надежность, а также участвую в интеграции новых продуктов. Когда не пишу код, путешествую с семьей или участвую в футбольных баталиях. А также с недавних пор решил делиться инженерными находками, об одной из которых эта статья.
Расскажу о том, как мы тестировали Redis под нагрузкой, с какими проблемами столкнулись, как их решали и какие выводы сделали. Заголовок звучит кликбейтно — но вы скоро поймёте, что он полностью оправдан.

Содержание:
Введение
Сразу обозначим, чего в этой статье не будет. Мы не будем углубляться в архитектуру Redis — он для нас просто инструмент. Мы не будем обсуждать CAP-теорему и консистентность, это за пределами задачи. Также не будет сравнительного анализа видов репликации — мы работаем с тем, что даёт нам DBaaS. Идеального кэш-слоя тут тоже не будет — будет то, что реально работает в продакшене.
А вот о чём поговорим: как устроен наш кэш для поиска, какие сложности возникли, как именно мы их решали, как сравнивали коробочный Redis от DBaaS с тем, что настроили сами, и кто в итоге оказался быстрее и надёжнее. И самое интересное — какие неожиданные bottleneck'и мы встретили.
Как Redis работает у нас
Когда пользователь заходит на Авито и вводит в строку, например, «кроссовки», формируется поисковый запрос. Этот запрос проходит через цепочку сервисов и попадает в сервис формирования выдачи. Там создаётся ключ, с которым происходит обращение в Redis. Если такой ключ уже есть — отдаём результат сразу. Если нет — обращаемся к поисковому движку (Sphinx), получаем результат, отправляем его пользователю и одновременно кладём в Redis.
Даже в самые тихие часы система обрабатывает более 50 тысяч операций в секунду на чтение, не считая записи. Redis настроен с мастером и двумя репликами (слейвами), которые участвуют только в failover и не обрабатывают запросы. Репликация у нас асинхронная. TLS выключен. Вытеснение ключей настроено по LRU. Подключена персистентность — регулярно снимаются снапшоты для восстановления, в том числе при необходимости full-resync.
Первый тревожный звоночек
На одном из плановых нагрузочных тестов Redis начал деградировать. Из-за синтетической нагрузки ключи вытеснялись чаще, возрастала нагрузка на удаление и синхронизацию реплик. Реплики отставали настолько, что выходили за размер репликационного буфера, требовали full-resync, но мастер уже не мог его выполнить. Это приводило к тому, что мастер просто простаивал. Система свалилась при 2000 коннектах на шард — при том что нужно поддерживать это значение в рамках 60–70 тыс.
Быстрые, но временные решения
Первое, что мы сделали — утроили количество шардов. Это позволило перераспределить нагрузку, и тесты показали положительный эффект: система выдерживала ожидаемую нагрузку. Но решение было временным.
Следующим шагом стали эксперименты с тайм-аутами. Мы заметили, что при коротком тайм-ауте на клиенте резко растёт количество разорванных соединений, а вместе с ними и общее число соединений к Redis. Это создаёт дополнительную нагрузку и может привести к исчерпанию лимитов. Решение оказалось простым, но эффективным: длинный тайм-аут на уровне клиента и короткий — на уровне приложения. Это позволило существенно снизить количество соединений и одновременно сократить latency операций в пять раз.

Оба эти шага были быстрыми фиксами — постоянным стало только решение с таймаутами. Мы понимали, что настоящая причина требует более глубокой проработки.
Наш self-hosted Redis: шаг за шагом
Redis в DBaaS не даёт возможности отключить репликацию или персистентность. Попросить внедрить такие фичи можно, но нужно сначала убедиться, что они действительно важны. Мы решили проверить это экспериментом и развернули собственный Redis на выделенном сервере. Конфигурация предполагала шардинг без репликации, но с включёнными снапшотами. Мы написали вспомогательный код, который позволял направлять shadow-трафик с прода на кастомные Redis. Так мы смогли прогревать кэш и читать из него, воспроизводя условия максимально приближенные к продакшену. Мы провели тест и вот, что показали первые замеры:

При нагрузке в два раза выше обычной мы увидели незначительный рост ошибок, но задержки были заметно ниже, чем в DBaaS-решении. При увеличении нагрузки до трёхкратной появилась линейная зависимость роста ошибок и задержек. После анализа метрик стало понятно, что мы упёрлись в пределы по IOPS, поскольку Redis работал на RAID 10. Мы разобрали RAID, разделили шарды по отдельным физическим дискам и отключили персистентность. Перепровели тестирование.

Это позволило выдерживать нагрузку x3 с 7000 запросов на шард и показать более стабильную производительность, чем коробочный Redis от DBaaS, при этом с в 15 раз меньшим количеством шардов.
Мы также протестировали сценарий полной потери кэша. Переключили трафик на наш тестовый Redis (развернутым для сравнения с продовым), а затем поэтапно сбрасывали кэш. Результат оказался положительным — потерю кэша система переживала без серьёзных последствий, хоть и с заметным ростом трафика на поисковый движок.
Сравнение: DBaaS против self-hosted
После удачного тестирования мы обратились к команде DBaaS с просьбой отключить репликацию и персистентность, но требовалось обосновать необходимость и ценность этого фича-реквеста.
Мы воссоздали тот же сетап на DBaaS, но пока без отключения персистентности, для того чтобы проверить, какой overhead она вносит. В первом тестировании потерпели неудачу: упёрлись в лимит по сети. После снятия сетевого лимита — упёрлись в CPU.
После снятия всех ограничений Redis в DBaaS начал показывать результаты чуть лучше, чем ранее. Однако error rate оставался выше, чем показатели прод и кастомного Redis, развернутого нами.

Дополнительно мы отключили поддержку персистентности в DBaaS Redis. При трёхкратной нагрузке всё было в порядке, но при четырёхкратной снова наблюдался рост ошибок и задержек.

Нам предстояло разобраться что же вносит такой overhead при превышении продового трафика.
Bottleneck, о котором мы не думали
Истинная причина деградации оказалась не в Redis, не в CPU и не в диске. Мы обнаружили, что виной всему сетевой лимитер netochka, обёртка над tc
, использующая TBF. Там были неверно настроены параметры burst
и cburst
, из-за чего даже при наличии свободной полосы происходили локальные «затыки» и сброс пакетов. После фикса задержки сравнялись, Redis начал работать стабильно.


В целом, судя по графикам, DBaaS и наш Redis теперь имеют идентичную производительность. Внимательный читатель может обратить внимание на то, что error-rate у них немного отличен, но это связано с наличием шумных соседей у DBaaS Redis (свой мы развернули на отдельном сервере) и тем, что в self-hosted мы использовали прямые IP вместо DNS.
На графике ниже можно наблюдать результат фикса сетевого лимитера на одной из продовых БД.

А что с альтернативами?

Мы протестировали KeyDB, и он показал лучшие результаты по сравнению с Redis, особенно при нагрузке x5 и с включённым TLS. Однако к моменту подготовки статьи мы уже перешли на Valkey — его лицензия, производительность и совместимость оказались для нас более подходящими. После внедрения Valkey error rate на одном из сервисов заметно снизился (приятный результат за простую замену бинарника).
Выводы
Нагрузочные тесты — это обязательная практика. Они помогают не только найти слабые места, но и доказать необходимость изменений.
Репликация и персистентность — не всегда благо, особенно если они не нужны по бизнес-логике.
Bottleneck может скрываться не в том месте, где его ожидаешь. И даже такие мелочи, как тайм-ауты, могут спасти ваш сервис от краха.
Закончить хочется цитатой: «Если дурак учится на своих ошибках, умный — на чужих, а мудрый использует опыт обоих».
Желаю вам быть мудрыми. Спасибо за уделённое статье внимание!
Выдерживал ли Redis ваши тесты? С какими проблемами столкнулись? Делитесь впечатлениями в комментариях.
А если хотите вместе с нами помогать людям и бизнесу через технологии — присоединяйтесь к командам. Свежие вакансии есть на нашем карьерном сайте.