Всем привет! Меня зовут Кирилл Грищук, я Tech Lead в команде Инфомодели в Авито. Мы занимаемся тем, что обрабатываем формы от пользователей: от 5 млн до 15 млн форм в минуту, а это более 150 тысяч в секунду. 

В этой статье на примере Авито я рассказываю, зачем мы стремимся к SLI 99.99%. Внутри — обзор и сравнение решений по доставке неизменных данных, учитывая специфику нашего сервиса. Показываю на примере форм, какие интересные доработки мы придумали, чтобы достичь этих четырёх девяток, и какие ошибки совершали по пути.

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

Содержание

Зачем нам нужен SLI 99.99%

Обзор и сравнение решений

Как работает система версионирования данных

Схема работы SQL/NoSQL решения

Схема работы передачи слепка данных

До 99.99% необходимы доработки

Какие ошибки мы совершили 

Что в итоге

Зачем нам нужен SLI 99.99%

В Авито более 3 тысяч категорий объявлений, начиная от животных и заканчивая автомобилями и недвижимостью. И все они разные, с разными свойствами. В квартирах нас интересует площадь, в автомобилях — мощность, в кошках — порода. Отрисовку объявления в конкретной категории на конкретной платформе называем формами. 

Наш сервис находится в самом низу инфраструктуры и отвечает за то, можно ли построить форму и какие параметры в ней корректные, а какие — нет. Через нас проходит всё: поиск, подача и карточка объявления. У нас очень много трафика: так, стандартный дневной — 5 млн запросов, часто бывает 15–20 млн, во время распродажи — сильно больше. 

Так выглядят формы поиска и подачи объявлений:

Поиск и подача — это две разные формы, потому что в разных категориях их настраивают по-разному в зависимости от параметров. 

Наша система состоит из двух частей:

  • админки для редактирования форм;

  • бэкенда для обработки всех форм объявлений, обработки вводов и обработки зависимостей.

В админке внутренние команды создают и редактируют формы. Например, подача авто на iOS и Android работает по-разному. За всем этим стоит бэкенд, который валидирует формы и использует весь трафик Авито.

Когда вы заполняете форму и выбираете, например, марку Audi и модель A6, это проходит через наш сервис. Мы проверяем корректность данных, обрабатываем все вводы и рассчитываем изменения в формах. К слову, карточка товара — это тоже форма:

У нас есть контент-менеджеры и внутренние разработчики, которые вносят изменения примерно 50–75 раз в день. Допустим, команда решает, что наличие турбины у двигателя автомобиля — важное свойство для поиска, и его нужно добавить. Это изменение добавляется к нам в систему, запускается релизный процесс и тестирование.

Особенно внимательно мы относимся к вертикалям вроде «Авто», потому что для них важно, чтобы ничего не сломалось. При добавлении нового фильтра должны стабильно работать все остальные: существующие поиски, сохранённые фильтры и прочие сценарии. Мы защищаемся от таких сбоев, потому что один нерабочий фильтр может остановить многие сценарии в Авито. 

После всех проверок изменения доходят до пользователей: они видят новые фильтры, свойства, подачу, а также баннеры, например, о старте распродажи. Баннеры тоже хранятся у нас.

Для нас критически важны надёжность и отказоустойчивость сервиса под нагрузкой и без неё. Любой сбой приводит к тому, что становятся недоступны ключевые сценарии Авито — поиск, подача, карточка объявления и многое другое. 

Если пользователи не могут внести нужные им изменения, это влияет на бизнес Авито. Например, у нас был кейс с зимней распродажей, которая должна была завершиться 31 декабря, а мы не смогли 31 декабря внести изменения и выключить акцию. Она продолжала отображаться на площадке и после окончания срока. Пользователи всё ещё видели распродажу, хотя она уже не действовала. Поэтому задержка в обновлениях для нас очень важна. 

Обзор и сравнение решений

Любое решение начинается с требований. Наши требования к системе такие:

  • SLI — 99.99% и по возможности больше;

  • устойчивость к нагрузкам и пикам;

  • возможность изменять данные в формах на лету;

  • изоляция версий данных и откаты к конкретному состоянию;

  • надёжность всей схемы получения данных при отказах частей системы.

Про SLI 99.99% я писал в предыдущей главе, поэтому переходим сразу к устойчивости к пикам и нагрузкам. У нас в любой момент может внезапно вырасти число пользователей или трафик, например, из партнёрских программ или новых источников данных. Ещё в Авито есть инструмент автозагрузки, когда пользователи сразу загружают большое количество объявлений. Мы должны быть готовы к таким пикам.

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

Ещё один важный момент — это изоляция версий данных и возможность отката к конкретному состоянию. Дело в том, что иногда можно выпустить изменение, которое работает не так, как ожидалось. Или провести A/B-тест, результаты которого не удовлетворяют нашим условиям. В таких случаях мы можем решить откатить изменения, и сделать это изолированно, не затрагивая остальную систему.

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

Как работает система версионирования данных

Как я писал выше, у нас есть разные версии объявлений, теперь кратко расскажу, как всё устроено. Наша система, в частности, админка, работает по аналогии с Git. Пользователи создают ветки и вносят в них свои изменения. Мы ведём changelog для каждой ветки, а изменения применяются через rebase.

На слайде ↑ показан пример ситуации, когда внести изменения напрямую в основную ветку невозможно. Допустим, полностью меняется процесс подачи объявления, например, на iOS подача недвижимости теперь должна выглядеть иначе. Это не просто новый баннер или свойство, а полностью новые экраны и логика. В такой ситуации нельзя напрямую вмешиваться в основную ветку, чтобы не затронуть уже запущенные A/B-тесты.

Вместо этого создаётся изолированная копия, например для категории «Недвижимость». В ней команда разрабатывает новую подачу: меняет порядок шагов, добавляет поля, например, ввод адреса на первом экране. Такие ветки сопровождаются отдельно, а трафик между ними распределяется через A/B-тестирование.

Основная ветка, с которой работает около 80% пользователей, содержит последние зафиксированные изменения — мы называем её Latest. Допустим, появилось новое изменение: теперь цвет глаз у кошки стал обязательным полем при подаче объявления. Это решение принимает бизнес, а вносят контент-менеджеры, и оно постепенно раскатывается на всех пользователей.

Параллельно могут существовать и другие ветки, например, для «Авто». Там, допустим, мы добавляем новое поле «Наличие турбины». Эта ветка может быть пока не слита в основную: изменения ещё не выкатаны и не участвуют в продакшене.

Так в целом работает наша система версионирования данных.

Схема работы SQL/NoSQL-решения

Далее мы рассматривали варианты, как хранить версии данных. Варианты были довольно стандартные:

  • Решение с реляционной базой данных и кешем.

  • Решение с нереляционной базой данных и кешем (NoSQL).

  • Хранение неизменных слепков данных и кеша (наше собственное решение).

Стандартное решение выглядит так:

В этом случае всё равно есть админка для работы, основное хранилище и обработчик форм. У обработчика форм, как правило, есть собственный кеш, где он хранит состояние и уже работающие формы. Допустим, сейчас чаще всего формы используются на iOS для «Авто», «Недвижимости» и так далее, или, например, на десктопе подают объявления о кошках.

Кликни здесь и узнаешь

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

У этого решения есть много плюсов. При такой схеме мы можем активно развивать систему, обрабатывать множество форм от пользователей и работать с большим трафиком. Эту систему легко разработать, запустить и поддерживать.

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

Но как быть с версиями, о которых я писал ранее? 

Одна версия данных весит примерно 200 мегабайт, и это без изображений, только текст. Хранить множество таких версий, например порядка десяти тысяч, становится крайне накладно. Масштабирование такого хранилища обойдётся очень дорого. Ещё возникают сложности с масштабированием чтения: даже при большом количестве реплик производительности всё равно не хватает.

Ещё раз кратко о плюсах и минусах SQL-решений:

Один важный момент, который я хотел бы отметить, — это изоляция изменений. Как я уже писал выше, мы хотели бы использовать разные версии данных. При работе с Postgres нам пришлось бы реализовывать какие-то блокировки схем или данных, но это не защищает нас полностью. Разработчик всё равно может запустить миграцию, сломать схему или испортить старую версию данных, которая уже используется, например, в A/B-тесте, который идёт несколько месяцев.

Нам важно защитить уже запущенные данные так, чтобы они никогда не менялись. С Postgres это возможно, но придётся писать много кода и реализовывать дополнительные механизмы защиты, чтобы любые изменения были невозможны.

При этом у реляционных баз есть и явные плюсы. Мы храним данные в реляционной модели, используем валидации на ключи, например, нельзя дважды добавить марку Audi или две модели A4 и A6. Все значения покрыты индексами и уникальны. Это защищает нас от банальных пользовательских ошибок и некоторых багов в коде.

Но что, если заменить основное хранилище с Postgres на что-то нереляционное, например Redis или Mongo? В целом, принцип работы от этого не меняется.

Здесь важно отметить, что большинство плюсов при этом сохраняются: 

  • систему по-прежнему легко разрабатывать и поддерживать;

  • можно хранить персистентные данные в Redis;

  • можно работать с большим объёмом данных;

  • есть возможность организовать хранение так, чтобы форму можно было вычитать целиком за один раз.

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

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

Ещё раз кратко о плюсах и минусах NoSQL-решений:

Схема работы передачи слепка данных

Решение, которое мы разработали, — это комбо из преимуществ двух предыдущих подходов.

Мы хотим иметь основную админку и основное хранилище как реляционную базу: с валидациями, связками ключей, стандартной и удобной работой с данными. При этом обработчик форм должен работать с данными как с файлом — вычитывать форму целиком и обрабатывать её в таком формате.

К этому мы и пришли. У нас есть основное админ-хранилище, где лежат все данные, и отдельное холодное хранилище S3, в котором работает обработчик. Между ними есть промежуточное звено, которое соединяет их и собирает файлы для передачи в это хранилище.

Тестирование и внесение изменений выполняются в рамках процесса релиза через размещение файлов в S3. Схема отличается тем, что обработчик форм не взаимодействует напрямую с базой данных, а работает с файлами, которые расположены по заданным путям. Обработчик собирает эти файлы и выполняет с ними необходимые операции.

Жми сюда!

Плюсы передачи слепка данных

Самый важный плюс в этой схеме — способность выдерживать безумно крутые нагрузки и легко масштабироваться. Надёжность и ограничения по масштабированию ограничены только сетевым каналом. Если нужно, можно добавить узлы в S3 или увеличить число инстансов: обработчик просто ходит за файлами, и всё продолжает работать. При росте трафика можно добавить мощности, и система будет функционировать без перебоев.

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

Изоляция и слепки данных также существуют отдельно друг от друга. Файлы могут представлять собой как слепки, так и собранные данные. Они полностью отделены от основного хранилища, с которым работают пользователи, и от хранилища, с которым работает обработчик.

Минусы передачи слепка данных

Есть и соответствующие минусы. 

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

Теперь всё вместе:

Теперь разберёмся, как выглядит схема раздачи данных потребителям. Есть разработчик или контент-менеджер, который добавляет новое поле в форму и выпускает релиз.

Есть множество обработчиков форм, которые принимают и обрабатывают пользовательские запросы. Они узнают о релизе двумя способами — через push и pull. Они, во-первых, слушают шину событий и реагируют, когда что-то произошло. Во-вторых, периодически проверяют, не пора ли обновить версию. Таймауты для таких проверок подбираются бизнесом.

В целом система ориентируется на события, а периодическое обновление служит подстраховкой на случай, если события не дошли. Затем происходит инвалидация кешей, загрузка новых форм, и в конце пользователь Авито видит обновлённые данные. Этот процесс отложенный: подтверждено, что задержка отображения форм на 1–2 минуты не влияет на результаты A/B-тестов и продуктовые метрики.

До 99.99% необходимы доработки

Выбрать правильное хранилище — это только часть задачи. Для SLI 99.99% этого недостаточно. Если правильно выбрать хранилище, можно достичь двух-трёх девяток, но потом появляются баги и технические проблемы. 

Доработка хранилища

Первое, что мы сделали — проработали сценарий graceful degradation. То есть определили, что делаем, если что-то недоступно.  

Все формы (мы называем их прототипами форм, например, форма без данных) вытаскиваем из базы и кладём целиком в кеш. Размер кешей подобран так, чтобы хранить последние версии всех форм in-memory. В случае отказа поставщиков данных мы продолжаем работать с кешем. Кеши рассчитаны на работу 3–5 часов при отключении даже одного дата-центра.

Кроме того, кешируем данные связанных сервисов, например, каталог → марка, модель, поколение. При отказе каталога мы будем работать с более старыми версиями, но данные останутся доступными, и запросы будут обрабатываться. Ещё привычные меры — retry-политики, экспоненциальный backoff и корректные таймауты.

Доработка сервиса

Тюнинг хранилищ сам по себе не решает всех проблем, нужно дорабатывать и сам сервис. Типичный сценарий: кеши прогреты, всё работает стабильно, но нужно раскатить новую версию сервиса. Пока кеши не прогреются, появляются пики RT: ответы пользователям становятся медленнее, а прогрев может занимать до 10–15 минут. При замене узлов возникают значительные пики RT и ошибки, а потребители тоже завязаны на данных.

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

Есть одна специфика нашего продукта. Так, часть полей у нас необязательна, например, размер или марка шин. Если сервис, который отвечает за эти данные, недоступен, мы просто скрываем такие поля. Для пользователей это несущественно, ведь важнее сама информация об автомобиле. Такой подход тоже реализует принцип graceful degradation.

Какие ошибки мы совершили 

С отладкой передачи слепка данных возникло множество ошибок. 

Обращение к данным и отладка этого обращения. Выше я писал, что актуальная версия данных у нас одна, и с ней мы работаем. Но может случиться, что в хранилище вообще ничего нет. Например, приходит событие о необходимости обновить версии или срабатывает таймер обновления, мы идём за данными и видим, что их просто нет: S3 недоступен или в нём пусто. С CEPH такое тоже бывает. В итоге данные могут разъехаться: в одном хранилище они есть, в другом — нет.

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

Отладка доставки данных релизов. Важный момент здесь — что есть сами файлы, а есть версия, которая определяет, какой сейчас релиз. И этого файла тоже может не быть. У нас было много случаев с подстановкой неправильных версий. Например, недавно в Авито появлялась большая плашка Beduin — это случилось из-за того, что ребята подставили не тот A/B-тест, и он разошёлся по всем пользователям.

Ошибки могут происходить, когда запускаются A/B-тесты или неправильно подставляются версии как на уровне приложения, так и на уровне входящего трафика. Мы уже проработали много таких сценариев, построили глубокую пирамиду тестирования, но ошибки всё равно случаются.

Разделение форм на пять частей. Формы хорошо ложатся на реляционную модель, и, казалось бы, логично разбить её на пять файлов и загружать параллельно, ведь сейчас можно легко грузить много файлов одновременно. Но на практике оказалось, что файлы слишком маленькие: иногда форма содержит всего одно поле, например, только марку или модель. В итоге на установку TLS Handshake и поднятие сессии уходит больше времени, чем на скачивание самого файла.

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

Тут еще больше контента

После того как мы объединили все части в один файл, стало проще и с экспериментами. Раньше использовалась сериализация в JSON, потом мы попробовали бинарную сериализацию, другие способы хранения данных, денормализацию, и это разблокировало много экспериментов. В итоге система стала работать даже быстрее, хотя, казалось бы, раньше загружалось пять частей параллельно, а теперь всего один файл.

Библиотека-клиент для доступа к файлам в S3. Мы сделали эти файлы, но кому-то стало интересно не заполнять форму пользовательским вводом, а посмотреть, что в ней лежит. Когда-то давно мы решили, а почему бы не пустить пользователей прямо к этим файлам. Мы ведь складываем их в S3, давайте дадим доступ.

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

Что в итоге

  • Сейчас мы обрабатываем от 5 до 15 млн форм в минуту, работаем со слепками данных и кешируем эти слепки in-memory.

  • SLI сервиса — четыре «девятки». В 99% случаев время отклика не превышает 50 мс, и при этом на всём этом объёме запросов менее 30 завершаются с ошибкой.

  • Мы сами не занимаемся расчётами SLI. В Авито есть платформа, на которой развёртываются сервисы, и на ней автоматизирован расчёт.

  • Мы не пишем метрики вручную на уровне приложения, этим занимается инфраструктура. Мы просто опираемся на эти данные.

  • Более 10 тысяч релизов команд Авито прошло через наш продукт.

  • Команды вносят изменения до 100 раз в день, добавляют баннеры, поля, свойства, меняют поведение Авито.

В целом мне хотелось донести, что стандартная архитектура — самый предпочтительный вариант. Не для всех задач нужно хранить неизменные слепки данных: в большинстве случаев данные меняются, и их нужно применять сразу.

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

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

Спасибо большое! Рад ответить на ваши вопросы и комментарии!

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

Комментарии (0)