Привет! Я Мэдиган, студент факультета информатики Университета Ватерлоо. Это мой второй совместный срок работы с командой разработчиков платформы 500px.

Все началось с обновления Rails. В начале этого года компания 500px завершила крупное обновление Rails с версии 3.2 до версии 4.2. После нескольких недель устранения различных несовместимостей между старой и новой версиями, мы неуклонно развертывали обновление для разных частей нашей системы. Результат? Значительное снижение производительности наших серверов API.

Мы покопались в исходном коде Rails, чтобы увидеть, есть ли какие-либо изменения, которые могли бы объяснить снижение производительности, и представили свое открытие на Ruby Lightning Talks T.O.. Один член сообщества Toronto Rails обнаружил, что широко используемый метод Ruby на консоли Rails 4.2 был значительно медленнее, чем на консоли Rails 3.2. Это открытие побудило нас заняться дополнительным профилированием, и хотя оказалось, что эталонный тест не объяснил наблюдаемый нами скачок производительности, мы обнаружили, что большая часть времени ответа нашего сервера API была потрачена на сериализацию JSON в наших моделях презентаторов.

Модели Presenter - это модули Ruby, используемые нашими контроллерами API для рендеринга ответов JSON. Каждая модель презентатора соответствует модели Rails (пользователи, фотографии, галереи и т. Д.) И определяет, какие атрибуты возвращать клиенту. Со временем, и не по вине какого-либо человека, код модели докладчика стал страдать от условно отрисовываемых частей, специальных запросов к базе данных и практически полного отсутствия кэширования. Мы поняли, что не сможем исправить это с помощью дополнительных улучшений - нам нужно было сломать этого слишком сложного чудовища. Наши цели были следующими:

  • Улучшить производительность
  • Сохранять идентичную функциональность
  • Сохраняйте ремонтопригодность

Кэширование модели презентатора

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

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

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

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

Но ждать! Мы поняли, что такая логика кеширования хорошо подходит для Redis Hashes. В отличие от Memcached, Redis может хранить несколько разных типов данных, каждый из которых имеет свои преимущества перед простыми старыми строками. С помощью хэшей (которые представляют собой просто сопоставления между строковыми полями и строковыми значениями) мы можем связать каждый объект с каждым форматом презентатора, установив параметры в качестве поля и сериализованного презентатора в качестве значения:

Это значительно упрощает удаление всех записей кеша для одного объекта. Согласно первой стратегии, нам пришлось бы перебирать все ключи с префиксом «model_123». С Redis Hashes мы можем просто удалять записи с помощью основного ключа.

Подготовка к науке

Как вы доказываете, что ваша идея работает? Подкрепите это цифрами! Графики! Сроки! Нам нужно было продемонстрировать, что наш кэш модели презентатора, поддерживаемый Redis, повысил производительность, но мы также хотели убедиться, что он поддерживает идентичное поведение. Именно эту проблему может элегантно решить Ученый GitHub.

С помощью Scientist вы можете запускать эксперименты, которые выполняют как ваш старый код (элемент управления), так и новый код (кандидат) в некотором случайном порядке с одними и теми же входными данными. Результаты эксперимента содержат продолжительность и возвращаемое значение контроля и кандидата, которые могут быть опубликованы в системе мониторинга, такой как Datadog, для сравнения во времени. Чтобы использовать Scientist, мы сначала определили собственный класс Experiment, который публикует результаты в Datadog:

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

Чтобы запустить это в производстве, мы хотели измерить только небольшую выборку запросов, поэтому мы изменили файл конфигурации Nginx на одном из наших многочисленных серверов API, чтобы условно добавить параметр use_scientist = true на основе запроса. URI. Обратите внимание, что операторы if категорически не одобряются Nginx и должны использоваться с осторожностью. Вот фрагмент нашего всегда осторожного файла конфигурации:

server {
  ...
  try files $uri @proxy;
  location @proxy{
    if ($request_uri ~ "/cache/endpoint") {
      rewrite ^(.*)$ "$1?use_scientist=true" break;
    }
    ...
  }
}

Доказательство того, что пудинг есть в еде

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

Стоит отметить - хотя я плохой ученый и у меня нет записей об этом - что, когда мы использовали Memcached для хранения моделей презентаторов, продолжительность запроса практически не улучшилась. Мы подозреваем, что улучшение от использования Redis произошло благодаря их специальной схеме кодирования »для хэшей с менее чем 100 полями.

Выводы и следующие шаги

Кэш модели Presenter - это простое и эффективное решение наших проблем с производительностью, которое можно легко распространить на другие конечные точки API. Хотя он обеспечивает ту же функциональность и лучшую производительность, что и старые модели некэшированных презентаторов, он ввел больше условной логики, больше параметров и больше кода в наш монолит Rails. Мы задались вопросом: можем ли мы получить эти преимущества кэширования, не пренебрегая нашей целью ремонтопригодности, каким-то образом упростив модель презентатора или ускорив (или распараллелив) запросы к базе данных? Может быть, на другом языке? Возможно, как микросервис?

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

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