Шардирование сервиса объявлений в Авито Доставке: как мы масштабировали систему
Меня зовут Артём, и я с 2016 года работаю в компании Авито. Начинал свой путь как тестировщик, затем стал backend-инженером, а с 2019 года занимаюсь разработкой на Go. Сейчас руковожу командой, отвечающей за развитие Авито Доставки. В этом материале расскажу, как мы подошли к масштабированию ключевого сервиса — delivery-item, обслуживающего объявления с доставкой, — и почему выбрали стратегию шардирования.
На старте 2023 года наш сервис уже обрабатывал порядка 1,6 миллиона запросов в минуту. Все данные — 148 миллионов записей — хранились в одном экземпляре Postgres, нагрузка на который достигала 50% от доступных 20 CPU. Мы понимали, что дальнейший рост приведёт к исчерпанию ресурсов, а значит, нужно было срочно находить решение, способное обеспечить устойчивую работу при вчетверо большей нагрузке — до 6,4 млн RPM, удвоении объема данных и высоком уровне доступности (SLI > 99,9%) при задержках менее 80 миллисекунд. Причем всё это нужно было реализовать до начала осени — пикового сезона.
Варианты масштабирования
Мы рассмотрели несколько подходов:
1. Вертикальное масштабирование — увеличение ресурсов текущей базы;
2. Внедрение кэширования с помощью Redis;
3. Модификация существующей архитектуры Postgres — шардирование, партицирование или глубокий рефакторинг;
4. Миграция на новую СУБД (например, CockroachDB);
5. Отказ от хранения данных в delivery-item и перенос их в другой сервис;
6. Полный отказ от хранения и генерация данных на лету;
7. Использование реплик для разгрузки чтения.
Каждое из решений имело свои плюсы и минусы. Мы последовательно проанализировали каждое, начиная с очевидного — вертикального масштабирования.
Почему вертикальное масштабирование не сработало
На тот момент мы использовали сервер с 20 CPU. Максимальный сервер, доступный в инфраструктуре, имел 30 CPU. Однако даже в случае перехода на него мы не могли бы выдержать четырёхкратную нагрузку, так как для этого база должна была бы стабильно работать при использовании 80 CPU (при нормативной утилизации не более 50%). Более того, pgbouncer, используемый для управления соединениями с базой, оказался узким местом — он однопоточный, и уже тогда его загрузка достигала 60% одного ядра.
Также мы упёрлись в аппаратные ограничения: серверы с 70 и более CPU не были доступны, а это означало, что даже при максимальной конфигурации база оказалась бы под критической нагрузкой. Вердикт: вертикальное масштабирование не решает проблему — мы упрёмся в физические пределы.
Эксперимент с кэшированием
Следующим шагом стало тестирование гипотезы: если мы сможем закешировать значительную часть читающих запросов, то снизим нагрузку на Postgres. Мы реализовали прототип с использованием Redis и алгоритма сквозного кэширования, направив через него часть реального трафика.
Кэшировать удалось около 20 миллионов ключей — это порядка 15% от общего объёма данных. Но при этом только 12% запросов на чтение шли через Redis. Замер показал HitRate на уровне 19% среди этих 12%, что при пересчёте на весь трафик дало реальный эффект около 2,3%.
Такое снижение нагрузки оказалось недостаточным для достижения наших целей. Кэширование помогло, но лишь в ограниченном объеме. Мы пришли к выводу, что нужен более масштабный и структурный подход.
Почему выбрали шардирование
Взвесив все альтернативы, мы остановились на шардировании. Это позволяло:
- равномерно распределить нагрузку между несколькими базами данных;
- масштабировать систему горизонтально, добавляя шарды по мере роста;
- избежать ограничений вертикального масштабирования;
- сохранить текущую архитектуру и логику, минимизируя глубину изменений.
Ключ к эффективному шардированию — правильно выбрать алгоритм и ключ шардирования. Мы выбрали идентификатор айтема как ключ, поскольку он равномерно распределён и используется в большинстве запросов. Это обеспечивало хорошую предсказуемость и балансировку нагрузки между шардовыми инстансами.
Алгоритм и конфигурация шардирования
Мы реализовали простой, но надёжный алгоритм распределения: хэш от идентификатора айтема определяет номер шарда. Шарды настроены с одинаковой конфигурацией, что обеспечивает унификацию мониторинга и масштабируемость. Также мы разработали конфигурационный слой, позволяющий централизованно управлять количеством шардов и их параметрами.
План миграции включал следующие этапы:
- Подготовка инфраструктуры и настройка новых баз;
- Миграция части данных и трафика на новые шарды;
- Мониторинг и отладка;
- Постепенное увеличение доли шардированных данных;
- Полный переход на шардированную архитектуру.
Промежуточные результаты и наблюдения
Спустя почти два года после начала шардирования мы увидели значительные улучшения:
- Удалось справиться с ростом нагрузки;
- Производительность системы стабилизировалась;
- Улучшилось время отклика;
- Повысилась отказоустойчивость: сбой в одном шарде не затрагивает другие.
Дополнительно мы получили возможность масштабировать ресурсы отдельных шардов в зависимости от их нагрузки, что дало гибкость в управлении системой.
Что важно учитывать при шардировании
1. Балансировка данных. Неравномерное распределение может привести к перегрузке отдельных шардов. Поэтому важно выбрать правильный ключ и следить за метриками.
2. Сложность миграции. Перенос существующих данных и трафика требует тщательного планирования. Ошибки на этом этапе могут привести к потере данных или простою.
3. Усложнение архитектуры. С появлением шардов усложняется логика работы с данными, мониторинг и отладка.
4. Управление транзакциями. Транзакции между разными шардами требуют дополнительной логики и могут повлиять на согласованность данных.
5. Изменения в коде. Приложение должно уметь определять, в какой шард отправлять запрос, и корректно обрабатывать возможные ошибки.
Что мы планируем дальше
После успешной реализации шардирования мы рассматриваем следующие шаги:
- Автоматизация масштабирования: добавить возможность автоматически увеличивать количество шардов при достижении порогов;
- Расширение использования кэша, включая write-through и prewarm-стратегии;
- Внедрение read replicas для каждого шарда, чтобы разгрузить чтение;
- Миграцию повторяющихся или редко изменяемых данных в отдельные сервисы;
- Использование асинхронных очередей для снижения write-нагрузки.
Кроме того, мы продолжаем следить за новыми технологиями в области распределённых баз данных и не исключаем возможный переход на более масштабируемые решения в будущем, если текущая архитектура приблизится к своим пределам.
Итог
Шардирование стало для нас наиболее подходящим способом масштабирования сервиса delivery-item в условиях растущей нагрузки. Это решение потребовало времени и усилий, но обеспечило устойчивость, производительность и готовность к будущим вызовам. Мы продолжаем развивать архитектуру, делая её гибкой и отказоустойчивой — так, чтобы Авито Доставка могла расти вместе с потребностями миллионов пользователей.



