Как разработчики, мы часто оказываемся в ситуациях, когда нам нужно что-то построить, но у нас нет достаточно времени, чтобы разработать так, как нам хотелось бы. И мы не всегда можем отодвигать сроки, потому что время выхода на рынок иногда играет решающую роль в успехе продукта. Так что же нам делать? Мы срезаем углы, идем на просчитанные риски, избавляемся от пуризма внутри вас (лучшие практики, покрытие модульными тестами и бла-бла-бла). Думаю, для большинства из вас это было бы ужасно похоже.

Сегодняшняя история о том, как мы построили высокомасштабную, отказоустойчивую, распределенную таблицу лидеров/систему подсчета очков примерно за одну неделю за три месяца. -человек команда.

В этом блоге будут в первую очередь эти ведра обучения

  1. Технические: как вы на самом деле строите такую ​​систему, должны быть рассмотрены такие темы, как проектирование распределенной системы, масштабируемость, отказоустойчивость, доступность.
  2. Работа со временем: как быстро выполнить работу, на какой компромисс пойти, как быстрее принимать решения.
  3. Жизненный цикл разработки. Вы также получите представление о жизненном цикле разработки продукта и о том, что необходимо для создания хорошего программного обеспечения.

Так что следите за новостями, обещаю, я вас не разочарую 🤗

Глава 1: Требования

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

Вариант использования заключался в том, что в начале превышения пользователей просили предсказать сценарий, для которого у них было бы 20/30 секунд. А в конце над модератором будет представлено, что же произошло на самом деле среди всех предсказанных сценариев. И оценка будет работать на основе того, кто ответил правильно и сколько времени ушло на ответ.

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

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

Глава 2: Дизайн

Далее, очевидно, нужно было спроектировать Систему, широко известную как Проектирование Высокого Уровня. Это была самая сложная часть, а также самая веселая часть для меня. Мы не занимались обширным низкоуровневым проектированием и выполняли все вызовы моделирования на лету во время реализации.

Шаг 1: Оценка масштаба

Прежде чем начать что-либо, мы сделали приблизительную оценку того, какой трафик нас ожидает. Учитывая масштаб нашей системы, мы подсчитали, что около 50 тысяч человек примут участие в викторине. Поэтому мы поставили цель создать его для 100 тыс. пользователей. Но большинство людей, вероятно, ответили бы в первые 10 секунд в 30-секундном окне. Что примерно дает нам целевой qps 20k.

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

(Мы также сделали другие оценки в отношении хранения в кеше, но пропустили их, чтобы статья была короткой)

Шаг 2. Определение высокомасштабируемых API

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

Итак, это высокомасштабируемые API, которые мы определили.

  1. Получить оценку и рейтинг пользователя
  2. Получить таблицу лидеров
  3. Расчет таблицы лидеров (в основном обработка данных, а не API)
  4. Опубликовать ответ пользователя

Мы займемся их проектированием через некоторое время

Шаг 3. Рекомендации по дизайну

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

  1. Нам понадобятся асинхронная и распределенная обработка для подсчета очков и таблицы лидеров по наиболее очевидным причинам. Если вы планируете выполнять подсчет баллов синхронно на одном узле для 1 миллиона пользователей или более, удачи вам 😏. Идея проста: мы разбиваем большую задачу на более мелкие и запускаем их на разных узлах, позволяя им делать это в своем собственном темпе. А также мы хотим, чтобы этот процесс был отказоустойчивым.
  2. Кэш будет нашим другом для чтения рангов пользователя и таблицы лидеров. В первую очередь по двум причинам: скорость и простота масштабирования. Кэш намного быстрее, чем БД, для простых операций чтения, и, как правило, проще масштабировать кластер Redis/Memcached/Hazelcast, чем Postgres.

Поэтому я хотел проектировать системы в основном с учетом этих аспектов.

Шаг 4: Спроектируйте систему

Принимая дизайнерские решения на этот раз, я выбираю технологии, которые я знал и не хотел рисковать, пытаясь найти лучшую технологию для этого варианта использования. Например, я был знаком с Redis, поэтому для кеша это был очевидный выбор. Кроме того, всякий раз, когда я слышу список лидеров, он автоматически преобразуется в отсортированные наборы Redis в моем сознании. А для асинхронной обработки Kafka по-прежнему остается моим выбором номер один. Если бы у меня было время, я бы, наверное, провел немного больше исследований, но на этот раз я не собирался бродить по диким землям в поисках лучшей технологии, потому что у меня не было ВРЕМЕНИ!!!!

а. API ответа пользователя

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

  1. Уменьшить нагрузку на БД
  2. Создайте своего рода противодавление или позвольте операторам БД работать в своем собственном темпе, чтобы БД не перегружалась

Если вы не знаете, что такое противодавление, то смело читайте здесь.

Мое решение goto для снижения нагрузки на запись состоит в том, чтобы выполнить пакетную обработку. Поэтому, если бы мне нужно было выполнить 10 операций записи, я бы объединил их, чтобы получить один запрос, а затем выполнил 1 операцию записи БД.

И Back-pressure почти кричит message-queues.

Итак, объединив оба, мы пришли к следующему решению…

Не волнуйтесь, позвольте мне объяснить, что происходит

  1. Ответы пользователей принимаются получателем ответов, и возвращается код состояния HTTP 202. Это все равно, что сказать, что я получил ваш запрос и собираюсь его обработать, но вы делаете то, что делали. Это первый шаг в асинхронной обработке, когда мы не блокируем вызывающую сторону.
  2. Получатель ответа помещает ответ пользователя в очередь сообщений, которая снова разделена в целях масштабируемости/доступности/избыточности. Вы можете довольно легко понять партиционирование, если вы уже знакомы с Kafka. Если вы этого не сделаете, просто считайте, что это способ распределить ваши сообщения в вашей очереди на несколько меньших изолированных очередей, которые технически могут находиться на разных узлах. Если вы знаете сегментирование БД, то это то же самое, но в основном для очередей сообщений. Не стесняйтесь читать больше о Кафке здесь.
  3. Теперь эти необработанные ответы получает дозатор. Затем он создает пакеты из 10 сообщений и отправляет их на следующий этап обработки.
  4. Модуль записи БД берет пакеты и делает запросы на вставку в БД. Обратное давление в основном создается модулем записи БД, он собирает сообщения в своем собственном темпе, поскольку потребитель сообщений был на основе извлечения. И таким образом мы предотвращаем перегрузку БД. И поскольку он работает с пакетами вместо запуска 100 запросов к БД, мы выполнили только 10 запросов к БД.

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

Это решает 30% нашей проблемы, давайте перейдем к следующей.

б. Подсчет очков и таблицы лидеров

Теперь слон в комнате, главная проблема, расчет таблицы лидеров. Если вы видите эту систему, она не похожа на традиционную систему викторин, где мы заранее знаем правильный ответ. Таким образом, мы не можем подсчитать баллы и таблицу лидеров, как только кто-то ответит. Мы должны подсчитать баллы и рейтинг всех пользователей после того, как модератор отправил правильные ответы. Так что огромный кусок работы за один раз. Совершенно очевидно, что ни наши узлы, ни наш сервер БД не могут справиться с этим должным образом в режиме синхронизации. Так что же нам делать? Мы снова возвращаемся к нашим знакомым очередям сообщений для асинхронной обработки. Круто, так что мы можем подсчитывать баллы асинхронно, но как насчет таблицы лидеров? Это должно быть доступно все время, верно? А как же ранг? До тех пор, пока не будут подсчитаны баллы для всех пользователей, вы не сможете поставить ранги, верно? И конкретное вычисление ранга может быть сложной проблемой.

Теперь, кто спасет нас от этого? Не волнуйся, друг мой, помнишь, я кратко упомянул Redis, говоря о кеше? У них есть прекрасная штука под названием отсортированный набор (какое потрясающее творение, спасибо Redis Labs 😅). В отсортированный набор вы можете добавить ключи с оценкой, и Redis упорядочит их соответствующим образом в O(log(N)). Это решит нашу проблему с ранжированием 😉. Это также позволяет нам выполнять запросы диапазона, например, дать мне топ-5 или получить мне ранг для определенного ключа, и все это происходит за O (log (N)). Это именно то, что нам здесь нужно.

Элементы добавляются в хэш-таблицу, сопоставляющую объекты Redis с оценками. В то же время элементы добавляются в список пропуска, сопоставляющий баллы с объектами Redis (таким образом, объекты сортируются по баллам в этом «представлении») — Внутренние элементы отсортированного набора

Бамммммм проблема с таблицей лидеров также решена.

Ладно-ладно, это непросто, я был просто счастлив, что смог быстро найти работоспособное решение. Теперь вернемся к нашей чертежной доске.

Выглядит немного пугающе, нет? Позвольте мне объяснить

  1. Как только модератор отправит правильный ответ, будет отправлено триггерное сообщение о подсчете баллов, которое должно запустить весь конвейер обработки.
  2. Дозатор получает триггерное сообщение и генерирует пару объектов смещения и ограничения БД в зависимости от того, сколько людей ответили правильно. Например, если 10 человек ответили правильно, а размер пакета равен 5, то будет сгенерировано два объекта (пакета). Пакет 1 {смещение: 0, лимит: 5}, пакет 2 {смещение: 5, лимит 5}. Почему мы это делаем? Чтобы мы могли запускать пакетную обработку или запускать запросы БД с разбивкой на страницы, и мы не вызывали БД без каких-либо ограничений. Поэтому, если бы мне нужно было получить 1 миллион записей из БД, а я делаю это за один раз, это создало бы множество проблем во многих местах. Поэтому мы разбиваем его на более мелкие части и запускаем меньшие, но множественные запросы, которые будут возвращать меньшее количество строк.
  3. Пользовательский пакетный процессор теперь будет получать эти пакетные сообщения и соответственно выполнять запросы к БД. Процессор, который получает сообщение {offset: 0, limit: 5}, получит идентификаторы первых 5 пользователей из БД (также выполнит еще одну пакетную операцию, но здесь это сложно объяснить, поэтому пропускаем). И после этого мы прощаемся с пакетной обработкой и переключаемся на потоковую обработку. Потому что теперь пакетный процессор поместит в очередь 5 идентификаторов пользователей, которые будут обработаны следующим процессором.
  4. Теперь калькулятор оценок пользователей получает индивидуальные идентификаторы пользователей, запускает логику подсчета очков для расчета индивидуальных оценок пользователей. Затем сделайте 1 обновление БД, чтобы изменить оценку пользователя. Затем он обновляет оценку для этого конкретного пользователя в отсортированном наборе, а Redis внутренне присваивает или обновляет ранг. И как только этот этап завершится, у нас будут оценки всех пользователей в нашей БД и рейтинг + оценка всех пользователей в нашем Redis. А так как у нас есть ранг каждого в отсортированном наборе, мы можем просто выполнить запрос диапазона, чтобы получить таблицу лидеров с молниеносной скоростью.

В случае, если наш кластер Redis выйдет из строя в какой-либо момент, поскольку у нас также есть оценка в БД, мы можем запустить процесс в любое время и снова построить отсортированные наборы в Redis. Устойчивость вооооооо 😬

Большая часть нашей проблемы теперь решена, так как для того, чтобы получить оценку и рейтинг пользователя, мы могли просто сделать запрос Redis и не обращаться к БД. Кроме того, это сокращает наше время отклика.

На этом завершается первичный этап проектирования. Были также БД, дизайн API и другие вещи, которые я здесь пропускаю.

Глава 4: Реализация

Завершение проектирования решило 70% наших проблем, и мы знали, что сможем решить эту проблему, поэтому быстро приступили к разработке.

В идеальном мире я бы пошел и создал новый сервис, попробовал новый язык, такой как Go, для более качественной и быстрой параллельной обработки и прочего. Но, учитывая сроки, это было неправильно. Мы придерживались нашего основного сервиса NodeJS и поместили туда все. Сторонники микросервисов могут растеряться, увидев это утверждение, но в гонке со временем вашим руководителям иногда приходится отходить на второй план. Среди многих компромиссов, это был один из основных вызовов, на который мы пошли.

Кроме того, нам также пришлось сократить модульные и интеграционные тесты, вина за которые до сих пор не дает нам покоя. Но мы постепенно заполняем тесты после релиза.

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

Глава 5: Развертывание и мониторинг

После нескольких исправлений ошибок и утверждения QA мы были готовы к работе. Работа сделана правильно? Нет, мой друг, нам все еще нужно было установить усиленный мониторинг для этой статьи. Поскольку он был разработан за очень короткое время, я, по крайней мере, был немного неуверен. У нас по умолчанию включена трассировка на этом сервисе через LightStep. Поэтому, помимо трассировки, я настроил специальный мониторинг трафика, частоты ошибок, панелей управления задержкой, предупреждений для всех API. И после выхода в эфир я и мои товарищи по команде связались по телефону и в течение как минимум часа отслеживали состояние системы, от использования ОЗУ и ЦП до журналов ошибок. Поэтому всегда придавайте равное значение наблюдаемости и мониторингу. В производственной системе были небольшие проблемы, и мы смогли обнаружить их на ранней стадии только благодаря мониторингу.

Глава 6: Ретроспектива

Система работает, но после передышки важно провести ретроспективу и определить вещи, которые мы пропустили, и поработать над ними. Я уверен, что мы упустили массу вещей, срезали множество углов и получили огромное количество улучшений. Вот, например, парочка…

Вещи, которые мы могли бы сделать лучше

  1. Для этого мы использовали уже существующую БД Postgres, так как драйвер, ORM и поддерживающая инфраструктура уже были там. Но я бы, наверное, немного изучил решения для баз данных.
  2. NodeJS великолепен, но я думаю, что Go будет лучшим решением для этого. Мы могли бы исследовать это.
  3. Я попытался написать запрос для расчета оценки только в БД и с треском провалился. Я, вероятно, мог бы написать это, а также выполнить пакетную обработку для расчета баллов, еще больше сократив операции с БД.
  4. Мы не смогли провести обширные нагрузочные тесты и тесты производительности, что является обязательным.
  5. Мы могли бы написать два разных этапа для обновления оценки БД и обновления отсортированного набора Redis, что было бы более чистой реализацией.

И это только то, что у меня было на уме, возможно, есть еще, и это оправдано.

Прощальные заметки

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

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

Кредиты

Привет моим замечательным товарищам по команде Акаш Радж и Ааширвад Кашьяп, мы все работали вместе, чтобы создать это всего за неделю.

Спасибо за прочтение!

Меня зовут Аритра Дас, я разработчик, и мне очень нравится создавать сложные распределенные системы. Не стесняйтесь обращаться ко мне в Linkedin или Twitter по любым вопросам, связанным с технологиями.

Удачного обучения…