Как Braze использует Ruby в масштабе
Опубликовано: 2022-08-18Если вы инженер и читаете новости Hacker News, Twitter для разработчиков или любые другие подобные источники информации, вы почти наверняка сталкивались с тысячей статей с такими заголовками, как «Скорость Rust против C», «Что делает Node. js быстрее, чем Java?» или «Почему вам следует использовать Golang и с чего начать». В этих статьях обычно утверждается, что есть один конкретный язык, который является очевидным выбором с точки зрения масштабируемости или скорости, и единственное, что вам нужно сделать, это принять его.
Когда я учился в колледже и первый год или два работал инженером, я читал эти статьи и немедленно запускал любимый проект по изучению нового языка или фреймворка. В конце концов, он гарантированно работал «в глобальном масштабе» и «быстрее, чем все, что вы когда-либо видели», и кто может противиться этому? В конце концов я понял, что на самом деле мне не нужна ни одна из этих очень специфических вещей для большинства моих проектов. И по мере развития моей карьеры я понял, что ни один язык или фреймворк не дадут мне все это бесплатно.
Вместо этого я обнаружил, что именно архитектура является на самом деле самым большим рычагом, когда вы хотите масштабировать системы, а не языки или фреймворки.
Здесь, в Braze, мы работаем в огромном глобальном масштабе. И да, мы используем Ruby и Rails как два основных инструмента для этого. Однако не существует значения конфигурации «global_scale = true», которое делало бы все это возможным — это результат хорошо продуманной архитектуры, охватывающей все приложения и вплоть до топологий развертывания. Инженеры Braze постоянно изучают узкие места масштабирования и выясняют, как сделать нашу систему быстрее, и ответ обычно не «отойти от Ruby»: это почти наверняка будет изменение архитектуры.
Итак, давайте взглянем на то, как Braze использует продуманную архитектуру для реального решения задач скорости и глобального масштаба — и где Ruby и Rails подходят (и не подходят) для этого!
Сила лучшей в своем классе архитектуры
Простой веб-запрос
Из-за масштабов, в которых мы работаем, мы знаем, что устройства, связанные с пользовательскими базами наших клиентов, будут делать миллиарды веб-запросов каждый день, которые должны будут обслуживаться каким-либо веб-сервером Braze. И даже на самых простых веб-сайтах у вас будет относительно сложный поток, связанный с запросом от клиента к серверу и обратно:
Все начинается с того, что преобразователь DNS клиента (обычно это его интернет-провайдер) выясняет, на какой IP-адрес следует перейти, исходя из домена в URL-адресе вашего веб-сайта.
Как только у клиента появится IP-адрес, он отправит запрос на свой маршрутизатор-шлюз, который отправит его на маршрутизатор «следующего перехода» (что может произойти несколько раз), пока запрос не дойдет до IP-адреса назначения.
Оттуда операционная система на сервере, получающем запрос, будет обрабатывать детали сети и уведомлять ожидающий процесс веб-сервера о том, что входящий запрос был получен на сокете/порте, который он прослушивал.
Веб-сервер запишет ответ (запрошенный ресурс, возможно, index.html) в этот сокет, который будет проходить через маршрутизаторы обратно к клиенту.
Довольно сложный материал для простого веб-сайта, не так ли? К счастью, многие из этих вещей позаботились о нас (подробнее об этом чуть позже). Но в нашей системе по-прежнему есть хранилища данных, фоновые задания, проблемы параллелизма и многое другое, с чем ей приходится иметь дело! Давайте углубимся в то, как это выглядит.
Первые системы, поддерживающие масштабирование
В большинстве случаев DNS и серверы имен обычно не требуют особого внимания. Ваш сервер имен доменов верхнего уровня, вероятно, будет иметь несколько записей для сопоставления «yourwebsite.com» с серверами имен для вашего домена, и если вы используете такие службы, как Amazon Route 53 или Azure DNS, они будут обрабатывать имя. серверы для вашего домена (например, управление записями A, CNAME или другими типами записей). Обычно вам не нужно думать о масштабировании этой части, так как это будет выполняться автоматически используемыми вами системами.
Однако маршрутная часть потока может стать интересной. Существует несколько различных алгоритмов маршрутизации, таких как Open Shortest Path First или Routing Information Protocol, все они предназначены для поиска самого быстрого/кратчайшего маршрута от клиента к серверу. Поскольку Интернет фактически представляет собой гигантский связанный граф (или, альтернативно, поточную сеть), может быть несколько путей, которые можно использовать, каждый из которых имеет соответствующую более высокую или меньшую стоимость. Выполнять работу по поиску самого быстрого маршрута было бы невозможно, поэтому большинство алгоритмов используют разумную эвристику для получения приемлемого маршрута. Компьютеры и сети не всегда надежны, поэтому мы полагаемся на Fastly, чтобы расширить возможности наших клиентов по более быстрому маршрутизации к нашим серверам.
Быстро работает, предоставляя точки присутствия (POP) по всему миру с очень быстрыми и надежными соединениями между ними. Думайте о них как о межгосударственной магистрали Интернета. Записи A и CNAME наших доменов указывают на Fastly, что приводит к тому, что запросы наших клиентов направляются прямо на шоссе. Оттуда Fastly может направить их в нужное место.
Входная дверь для пайки
Итак, запрос нашего клиента прошел по шоссе Fastly и находится прямо у входной двери платформы Braze. Что будет дальше?
В простом случае эта входная дверь будет единственным сервером, принимающим запросы. Как вы можете себе представить, это не очень хорошо масштабируется, поэтому мы на самом деле указываем Fastly на набор балансировщиков нагрузки. Существуют всевозможные стратегии, которые могут использовать балансировщики нагрузки, но представьте, что в этом сценарии Fastly циклически распределяет запросы к пулу балансировщиков нагрузки равномерно. Эти балансировщики нагрузки будут ставить запросы в очередь, а затем распределять эти запросы на веб-серверы, которые, как мы также можем представить, обрабатывают клиентские запросы в циклическом режиме. (На практике могут быть преимущества для определенных видов близости, но это тема для другого раза.)
Это позволяет нам увеличить количество балансировщиков нагрузки и количество веб-серверов в зависимости от пропускной способности получаемых запросов и пропускной способности запросов, которые мы можем обработать. На данный момент мы создали архитектуру, способную без труда справляться с огромным потоком запросов! Благодаря эластичности очередей запросов балансировщиков нагрузки он может даже обрабатывать всплески трафика — и это здорово!
Веб-серверы
Наконец, мы подошли к захватывающей (Ruby) части: веб-серверу. Мы используем Ruby on Rails, но это всего лишь веб-фреймворк — настоящий веб-сервер — это Unicorn. Unicorn работает, запуская ряд рабочих процессов на машине, где каждый рабочий процесс прослушивает сокет ОС для работы. Он управляет процессами для нас и откладывает балансировку нагрузки запросов на саму ОС. Нам просто нужен наш код Ruby для максимально быстрой обработки запросов; все остальное эффективно оптимизировано для нас за пределами Ruby.
Поскольку большинство запросов, сделанных нашим SDK внутри приложений наших клиентов или через наш REST API, являются асинхронными (т. е. нам не нужно ждать завершения операции, чтобы вернуть конкретный ответ клиентам), большинство наших Серверы API необычайно просты — они проверяют структуру запроса, любые ограничения ключа API, затем помещают запрос в очередь Redis и возвращают клиенту ответ 200, если все проверено.
Этот цикл запроса/ответа занимает примерно 10 миллисекунд для обработки кода Ruby, и часть этого времени тратится на ожидание в Memcached и Redis. Даже если бы мы переписали все это на другом языке, на самом деле невозможно выжать из этого гораздо больше производительности. И, в конечном счете, именно архитектура всего, что вы прочитали до сих пор, позволяет нам масштабировать этот процесс приема данных для удовлетворения постоянно растущих потребностей наших клиентов.
Очереди заданий
Это тема, которую мы исследовали в прошлом, поэтому я не буду вдаваться в этот аспект так глубоко — чтобы узнать больше о нашей системе очередей заданий, ознакомьтесь с моей статьей «Достижение отказоустойчивости с помощью очередей». На высоком уровне мы используем многочисленные экземпляры Redis, которые действуют как очереди заданий, дополнительно буферизуя работу, которую необходимо выполнить. Подобно нашим веб-серверам, эти экземпляры разделены по зонам доступности, чтобы обеспечить более высокую доступность в случае возникновения проблемы в определенной зоне доступности, и они поставляются в виде пар первичный/вторичный с использованием Redis Sentinel для резервирования. Мы также можем масштабировать их как по горизонтали, так и по вертикали, чтобы оптимизировать как емкость, так и пропускную способность.
Рабочие
Это, безусловно, самая интересная часть — как нам заставить работников масштабироваться?
Прежде всего, наши работники и очереди сегментированы по ряду параметров: клиенты, типы работ, необходимые хранилища данных и т. д. Это позволяет нам иметь высокую доступность; например, если в конкретном хранилище данных возникают проблемы, другие функции будут продолжать работать нормально. Это также позволяет нам автоматически масштабировать рабочие типы независимо, в зависимости от любого из этих измерений. В конечном итоге мы можем управлять рабочими мощностями горизонтально масштабируемым образом, то есть, если у нас больше работы определенного типа, мы можем масштабировать больше работников.
Здесь вы можете начать понимать, что выбор языка или фреймворка имеет значение. В конечном счете, более эффективный работник сможет выполнять больше работы и быстрее. Компилируемые языки, такие как C или Rust, как правило, намного быстрее справляются с вычислительными задачами, чем интерпретируемые языки, такие как Ruby, и это может привести к более эффективной работе некоторых рабочих нагрузок. Тем не менее, я провожу много времени за просмотром трассировок, а необработанная обработка ЦП занимает на удивление мало места в общей картине Braze. Большая часть нашего времени обработки тратится на ожидание ответов от хранилищ данных или внешних запросов, а не на обработку чисел; для этого нам не нужен сильно оптимизированный код C.
Хранилища данных
До сих пор все, что мы рассмотрели, довольно масштабируемо. Итак, давайте уделим минуту и поговорим о том, где наши работники проводят большую часть своего времени — о хранилищах данных.
Любой, кто когда-либо масштабировал веб-серверы или асинхронные рабочие процессы, использующие базу данных SQL, вероятно, сталкивался с конкретной проблемой масштабирования: транзакциями. У вас может быть конечная точка, которая отвечает за выполнение Заказа, который создает два запроса FulfillmentRequest и PaymentReceipt. Если все это не происходит в транзакции, вы можете получить несогласованные данные. Одновременное выполнение множества транзакций в одной базе данных может привести к длительным затратам времени на блокировку или даже взаимоблокировку. В Braze мы решаем эту проблему масштабирования напрямую с самими моделями данных за счет независимости от объектов и согласованности в конечном итоге. Используя эти принципы, мы можем выжать из наших хранилищ данных большую производительность.

Независимые объекты данных
Мы активно используем MongoDB в Braze по очень веским причинам: а именно, это позволяет нам существенно горизонтально масштабировать сегменты MongoDB и получать почти линейное увеличение объема хранилища и производительности. Это очень хорошо работает для наших профилей пользователей из-за их независимости друг от друга — нет операторов JOIN или отношений ограничений, которые нужно поддерживать между профилями пользователей. По мере роста каждого из наших клиентов или добавления новых клиентов (или и того, и другого) мы можем просто добавлять новые базы данных и новые сегменты к существующим базам данных, чтобы увеличить нашу емкость. Мы явно избегаем таких функций, как транзакции с несколькими документами, чтобы поддерживать этот уровень масштабируемости.
Помимо MongoDB, мы часто используем Redis в качестве временного хранилища данных для таких вещей, как буферизация аналитической информации. Поскольку источник достоверной информации для многих из этих аналитических данных существует в MongoDB в виде независимых документов в течение определенного периода времени, мы поддерживаем горизонтально масштабируемый пул экземпляров Redis, которые действуют как буферы; при таком подходе хешированный идентификатор документа используется в схеме сегментирования на основе ключа, равномерно распределяя нагрузку благодаря независимости. Периодические задания сбрасывают эти буферы из одного хранилища данных с горизонтальным масштабированием в другое хранилище данных с горизонтальным масштабированием. Масштаб достигнут!
Кроме того, мы используем Redis Sentinel для этих экземпляров так же, как и для упомянутых выше очередей заданий. Мы также развертываем множество «типов» этих кластеров Redis для различных целей, обеспечивая нам контролируемый поток отказов (т. е. если у одного конкретного типа кластера Redis возникают проблемы, мы не видим, чтобы несвязанные функции начали выходить из строя одновременно).
Конечная согласованность
Braze также использует возможную согласованность в качестве принципа для большинства операций чтения. Это позволяет в большинстве случаев использовать чтение как из первичных, так и из вторичных членов наборов реплик MongoDB, что делает нашу архитектуру более эффективной. Этот принцип в нашей модели данных позволяет нам активно использовать кэширование по всему стеку.
Мы используем многоуровневый подход с использованием Memcached — в основном, при запросе документа из базы данных мы сначала проверяем локальный процесс Memcached с очень низким временем жизни (TTL), а затем проверяем удаленный экземпляр Memcached (с более высокий TTL), прежде чем напрямую запрашивать базу данных. Это помогает нам значительно сократить количество чтений из базы данных для общих документов, таких как настройки клиента или сведения о кампании. «Возможный» может звучать пугающе, но на самом деле это всего несколько секунд, и такой подход сокращает огромное количество трафика от источника правды. Если вы когда-либо посещали занятия по компьютерной архитектуре, вы могли заметить, насколько этот подход похож на то, как работает система кэширования ЦП L1, L2 и L3!
С помощью этих трюков мы можем выжать большую производительность из, возможно, самой медленной части нашей архитектуры, а затем горизонтально масштабировать ее по мере необходимости, когда наша пропускная способность или емкость увеличиваются.
Куда вписываются Ruby и Rails
Вот в чем дело: оказывается, когда вы тратите много усилий на создание целостной архитектуры, в которой каждый слой хорошо масштабируется по горизонтали, скорость языка или время выполнения гораздо менее важны, чем вы думаете. Это означает, что выбор языков, фреймворков и сред выполнения осуществляется с учетом совершенно другого набора требований и ограничений.
Ruby и Rails доказали свою способность помогать командам быстро выполнять итерации, когда Braze был запущен в 2011 году, и они до сих пор используются GitHub, Shopify и другими ведущими брендами, потому что они продолжают делать это возможным. Они продолжают активно разрабатываться сообществами Ruby и Rails соответственно, и у них обоих по-прежнему есть большой набор библиотек с открытым исходным кодом, доступных для самых разных нужд. Пара — отличный выбор для быстрой итерации, поскольку они обладают огромной гибкостью и сохраняют значительную простоту для обычных случаев использования. Мы обнаруживаем, что это абсолютно верно каждый день, когда мы его используем.
Это не значит, что Ruby on Rails — идеальное решение, которое подойдет всем. Но в Braze мы обнаружили, что он очень хорошо работает для обеспечения большей части нашего конвейера приема данных, конвейера отправки сообщений и нашей информационной панели для клиентов, которые требуют быстрой итерации и имеют решающее значение для успеха Braze. платформа в целом.
Когда мы не используем Ruby
Но ждать! Не все, что мы делаем в Braze, делается на Ruby. За прошедшие годы было несколько мест, где мы обращались к другим языкам и технологиям по разным причинам. Давайте взглянем на три из них, просто чтобы получить дополнительное представление о том, когда мы полагаемся на Ruby, а когда нет.
1. Службы отправителя
Как оказалось, Ruby не очень хорошо справляется с очень высокой степенью одновременных сетевых запросов в одном процессе. Это проблема, потому что, когда Braze отправляет сообщения от имени наших клиентов, некоторым поставщикам конечных услуг может потребоваться один запрос на пользователя. Когда у нас есть стопка из 100 сообщений, готовых к отправке, мы не хотим ждать завершения каждого из них, прежде чем переходить к следующему. Мы бы предпочли делать всю эту работу параллельно.
Введите наши «Sender Services» — то есть микросервисы без сохранения состояния, написанные на Golang. Наш код Ruby в приведенном выше примере может отправить все 100 сообщений в одну из этих служб, которая будет выполнять все запросы параллельно, ждать их завершения, а затем возвращать массовый ответ Ruby. Эти сервисы значительно более эффективны, чем то, что мы могли бы сделать с Ruby, когда речь идет о параллельной сети.
2. Токовые разъемы
Наша функция экспорта больших объемов данных Braze Currents позволяет клиентам Braze непрерывно передавать данные одному или нескольким нашим многочисленным партнерам по данным. Платформа работает на Apache Kafka, а потоковая передача осуществляется через Kafka Connectors. Технически вы можете написать их на Ruby, но официально поддерживается Java. А из-за высокой степени поддержки Java написать эти соединители гораздо проще на Java, чем на Ruby.
3. Машинное обучение
Если вы когда-либо занимались машинным обучением, вы знаете, что предпочтительным языком является Python. Многочисленные пакеты и инструменты для рабочих нагрузок машинного обучения в Python затмевают эквивалентную поддержку Ruby — такие вещи, как записные книжки TensorFlow и Jupyter, играют важную роль в нашей команде, а таких инструментов просто не существует или они плохо зарекомендовали себя в мире Ruby. Соответственно, мы опирались на Python, когда дело доходит до создания элементов нашего продукта, использующих машинное обучение.
Когда язык имеет значение
Очевидно, у нас есть несколько отличных примеров выше, когда Ruby не был идеальным выбором. Существует множество причин, по которым вы можете выбрать другой язык, — вот некоторые из них, которые мы считаем особенно полезными для рассмотрения.
Создание новых вещей без затрат на переключение
Если вы собираетесь создать совершенно новую систему с новой моделью предметной области и без жесткой интеграции с существующей функциональностью, у вас может быть возможность использовать другой язык, если вы того пожелаете. Особенно в тех случаях, когда ваша организация оценивает различные возможности, небольшой изолированный проект с нуля может стать отличным реальным экспериментом по испытанию нового языка или фреймворка.
Экосистема языков для конкретных задач и эргономика
Некоторые задачи намного проще выполнять с определенным языком или фреймворком — нам особенно нравятся Rails и Grape для разработки функциональных возможностей инструментальной панели, но писать код машинного обучения на Ruby было бы абсолютным кошмаром, поскольку инструментов с открытым исходным кодом просто не существует. Возможно, вы захотите использовать конкретный фреймворк или библиотеку для реализации какой-либо функциональности или интеграции, и иногда это будет влиять на ваш выбор языка, поскольку это почти наверняка приведет к более легкому или быстрому процессу разработки.
Скорость выполнения
Иногда вам нужно оптимизировать скорость выполнения, и используемый язык будет сильно влиять на это. Есть веская причина, по которой многие высокочастотные торговые платформы и системы автономного вождения написаны на C++; скомпилированный код может быть безумно быстрым! Наши службы отправителей используют примитивы параллелизма/конкурентности Golang, которые просто недоступны в Ruby именно по этой причине.
Знакомство с разработчиком
С другой стороны, вы можете создавать что-то изолированное или иметь в виду библиотеку, которую хотите использовать, но выбранный вами язык совершенно незнаком остальной части вашей команды. Представление нового проекта на Scala с сильным уклоном в сторону функционального программирования может создать барьер знакомства с другими разработчиками в вашей команде, что в конечном итоге приведет к изоляции знаний или снижению чистой скорости. Мы считаем, что это особенно важно в Braze, поскольку мы уделяем большое внимание быстрой итерации, поэтому мы склонны поощрять использование инструментов, библиотек, фреймворков и языков, которые уже широко используются в организации.
Последние мысли
Если бы я мог вернуться в прошлое и сказать себе одну вещь о программной инженерии в гигантских системах, я бы сказал следующее: для большинства рабочих нагрузок ваш общий выбор архитектуры будет определять ваши пределы масштабирования и скорость в большей степени, чем выбор языка. Это понимание подтверждается каждый день здесь, в Braze.
Ruby и Rails — невероятные инструменты, которые, будучи частью правильно спроектированной системы, невероятно хорошо масштабируются. Rails также является очень зрелой средой, и она поддерживает нашу культуру в Braze, заключающуюся в быстром повторении и создании реальной ценности для клиентов. Это делает Ruby и Rails идеальными инструментами для нас, инструментами, которые мы планируем использовать еще долгие годы.
Хотите работать в Braze? Мы набираем сотрудников на различные должности в наши команды по проектированию, управлению продуктами и работе с пользователями. Посетите нашу страницу вакансий, чтобы узнать больше о наших открытых вакансиях и нашей культуре.
