INTERESTING FACTS BLOG

Painting and Sketches

5 ранних уроков быстрого, высокоуровневого масштабирования с рельсами

Оригинальная статья: 5 Early Lessons from Rapid, High Availability Scaling with Rails

Автор статьи: Майк Пак (Mike Pack)

Источник: http://mikepackdev.com/blog_posts/40-5-early-lessons-from-rapid-high-availability-scaling-with-rails

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

Как и у любого вируса, наблюдается массовый приток интереса в течение относительно короткого периода времени, после чего происходит медленное снижение, оставляя след после разрушения, когда субъект оседает в новую форму. С тех пор Элло устроился, поэтому какое лучшее время, чем сейчас, задокументировать некоторые из уроков, извлеченных при масштабировании в течение этих критических недель вируса. Я хочу, чтобы эти уроки были не просто легкими выниманиями, а скорее осязаемыми советами, которые вы можете применить, если вам когда-либо повезло или посчастливилось оказаться в подобной ситуации. Таким образом, части этой статьи будут специфичны для Ello и могут не применяться в других доменах.

Урок 1: Перемещение графика

Одним из наших первых препятствий, связанных с масштабированием, было управление графиком отношений между пользователями. Мы не просто интуитивно сказали: «Ох, график медленный», но он тоже не сильно подталкивал. Мы находимся на стандартной стойке Rails, используя Heroku и Postgres. У нас есть таблица, называемая отношениями, в которой хранятся все данные о том, как связаны пользователи. Вы дружите, заблокировали или не с кем-то дружат? Все это хранится в таблице отношений.

Мы строим социальную сеть. По определению наша таблица отношений - одна из самых горячих таблиц.Сколько людей вы следуете в общей сложности? Сколько в друзьях? Сколько в шуме? Кому следует уведомлять, когда вы создаете сообщение? Все эти вопросы зависят от таблицы отношений для ответов. Ответы на эти вопросы будут кэшироваться Postgres, поэтому только исходный запрос берет на себя затраты на вычисление результатов. Последующие запросы бывают быстрыми. Но только кеш запросов Postgres становится скудным по шкале. Как пользователь в новой социальной сети, аккумулирование отношений - это регулярная деятельность. Все новые отношения формируют бюджеты Postgres для запросов по этим данным.Это была таблица с высоким уровнем чтения, высокой записи.

Поскольку мы находимся на Heroku, у нас были феноменальные инструменты Heroku Postgres. Когда бросали в огонь, одним из лучших огнетушителей был heroku pg:outliers. Эта команда освещает первые 10 самых медленных запросов. Все 10 из нас были связаны с таблицей отношений. У нас были все правильные индексы, но некоторые запросы занимали до 10 секунд для получения результатов.

Решение такой проблемы относится к конкретным приложениям, но в нашем случае лучшим вариантом было денормализацию данных отношений в хранилище данных, которое могло бы легче ответить на наши актуальные и частые вопросы о социальном графе. Мы выбрали Редиса. В то время это была некоторая реакция коленного рефлекса, но техника, с которой у нас был успех в прошлом. Только после того, как это реализовано, мы наткнулись на обнадеживающую статью, в которой излагалось, как Pinterest использует Redis для своего графика. Чтобы быть ясным, мы не полностью перемещали данные, мы предоставили дополнительный уровень кэширования. Все данные сохраняются в Postgres для обеспечения долговечности и кэшируются в Redis для скорости. В случае катастрофы данные Redis могут быть перестроены в любое время.

Мы переместили все наши горячие запросы против таблицы отношений в Redis. Так как «последователи» и «следование» отображаются в каждом профиле, а счетчик(*) был нашим верхним нарушителем, нашим первым шагом было кэширование этих значений в счетчиках Redis. Мы использовали Redis Objects, чтобы сделать это простым и элегантным. В любое время, когда новая связь была создана или уничтожена, эти счетчики увеличиваются и уменьшаются. При просмотре профиля другого пользователя, чтобы отобразить пользовательский интерфейс, нам нужно было ответить на вопрос: «Вы следите за этим пользователем? Если да, то в друзьях или шумовом ковше?» Чтобы ответить на этот и подобные вопросы, мы кэшировали идентификаторы пользователей всех людей, которые у вас были в корзине друзей, ваш ведро шума и объединение обоих.

Благодаря нашим данным графика в Redis, мы можем теперь запросить график таким образом, который был бы непомерно дорогостоящим для Postgres. В частности, мы используем его для влияния на нашу систему рекомендаций. «Дайте мне всех пользователей, за которыми следуют люди, за которыми я слежу, но я еще не последовал». Используя Redis установить пересечения, союзы и diffs, мы можем начать получать новые и интересные использования тех же данных.

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

Урок 2: Создавайте индексы рано, или вы влипли

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

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

Первый тип индекса, который мы забыли, был просто обычным старым btree на поле, которое было запрошено регулярно. Подобный индекс может быть легко создан, если никто не пишет в таблицу или время просто невозможно. Это высокая масштабируемость доступности, поэтому время простоя не является вариантом, и все записывалось в эту таблицу. Поскольку таблица активности имела чрезвычайно высокие записи, одновременное создание этих индексов никогда не закончилось. Хотя индекс создается одновременно (т. е. Без простоя), новые записи в таблице также добавляются в индекс. Если скорость добавления новых записей превосходит скорость, с помощью которой Postgres может индексировать сотни миллионов существующих строк, вам не повезло.

Решение? Если время простоя не является вариантом, вам нужно будет создать chokepoint в своем приложении.Все записи в определенную таблицу должны проходить через этот chokepoint, чтобы, если вы хотите прекратить запись, вы сжимаете chokepoint. В нашем случае мы используем Sidekiq. Мы используем работу Sidekiq как наш chokepoint, а это означает, что если мы когда-либо захотим остановить все записи в таблице действий, мы запустим всех рабочих Sidekiq для очереди, которая относится к записи активности.Неработающие задания будут скопированы и останутся без дела, пока мы не закроем рабочих резервом, поэтому предотвратим запись в таблицу действий. Сделав это на пару минут, он предоставил Postgres достаточно передышку, чтобы усердно работать над созданием индекса из существующих записей. Поскольку задания Sidekiq выполняются асинхронно, это не должно сильно влиять на пользователей. В нашем случае худшее, что произойдет, - это пользователь, создающий сообщение, обновляет страницу и видит, что сообщение не существует, потому что запись активности еще не создана. Это компромисс, который мы сделали, чтобы приложение было доступно.

Подобные ситуации на самом деле не хуже всего. Самое худшее - это когда вы забыли уникальный индекс.Теперь ваши данные повреждены. Мы забыли уникальный индекс, oops. Когда уровень параллелизма, необходимый для запуска приложения с быстрым масштабированием, достигает точки, в которой вы не можете расшифровать, является ли работа хорошим или ошибочным, вам нужно полагаться на характеристики ACID вашей базы данных. Вот почему Postgres потрясающий: что-то произойдет один раз и только один раз, независимо от параллелизма. Если два задания попытаются сделать то же самое параллельно, Postgres гарантирует, что только один из них победит. Только если у вас есть уникальный индекс.

Проницательный читатель может спросить: «Ну, почему две работы попытаются выполнить одно и то же?»Позвольте мне быстро объяснить. Все это связано с одним плохим фрагментом данных, который при использовании создает более плохие данные. Например, у нас не было уникального индекса в таблице отношений. Итак, я мог технически следовать за другим пользователем дважды. Когда пользователь, которого я следую, создает новую запись, и настало время спросить: «Кто должен получать этот пост в своем канале?», Если вы полагаетесь на таблицу отношений, чтобы ответить на этот вопрос, вы полагаетесь на плохие данные.Теперь система создаст две повторяющиеся действия. Это одна из причин дублирования рабочих мест. Другие включают компьютеры, которые глупы, компьютеры терпят неудачу, и компьютеры пытаются исправить свою собственную глупость и неудачи. Исправление источника плохих данных, неединственных отношений, стало отличным отправным пунктом для стабильности.

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

Таким образом, вы удаляете дубликаты, отлично. Как насчет времени между удалением дубликатов и добавлением уникального индекса? Если в это время были добавлены дубликаты, индекс не будет создан. Итак, вы начинаете с квадратного. Удалить дубликаты. Создал ли индекс? Нет? Удалить дубликаты.

Урок 3: Осколок это круто, но не так круто

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

Для полноты, я хочу определить осколки. Как и большинство вещей в программном обеспечении, sharding объединяет определения, но вот мой. Sharding - это процесс взятия одной большой вещи и разбивания ее на более мелкие куски. У нас была одна огромная таблица записей за 750 м, которая становилась громоздкой.Перед разбиением таблицы действий мы перенесли ее из нашей первичной базы данных (с пользователями, сообщениями и т. Д.) В свою собственную базу данных, а также в форме осколков. Перемещение его в другую базу данных происходит горизонтально, разбивая одну таблицу на вертикальную окантовку или разбивку. Мы получили рекомендации от уважаемых сторон, чтобы подумать о вертикальном осколке, когда наша таблица достигла 100 ГБ данных. У нас было около 200 ГБ. Мы не следуем правилам хорошо.

Сейчас я не буду подробно описывать нашу настройку осколков, но упомянем, что потребовалось много планирования и практики, чтобы прибить ногти. Мы использовали драгоценный камень Octopus для управления всеми конфигурациями соединений ActiveRecord, но это, конечно, не так. Вот несколько статей, которые могут вам показаться интересными: общее руководство с диаграммамиBraintree on MySQL и Instagram on Postgres.

Когда оштрафованы, скажем, у нас есть база данных А, которая постепенно замедляется и ее необходимо разбить. Перед очертанием пользователи с модулями идентификаторов 0 и 1 имеют свои данные в базе данных A. После очертания мы хотим, чтобы пользователи с модулем идентификаторов 0 продолжали поступать в базу данных A и модуль 1 переходили в новую базу данных B. Таким образом, мы можем распространять загрузка между несколькими базами данных, и каждый из них будет расти примерно на половину скорости. Общий процесс осколки заключается в следующем: настройте новую базу данных реплики / следящего элемента B, остановите все записи в A, разорвите реплику (A и B теперь две точные дубликаты dbs), обновите конфигурацию осколков, чтобы некоторые данные переходили к A, а некоторые к B, возобновляет запись, обрезает устаревшие данные из A и B.

Так здорово, я люблю осколки.

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

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

В приведенном выше примере, поскольку данные для пользователей с модулем ID 1 больше не хранятся или не упоминаются в базе данных A, мы можем безопасно удалить все свои данные. Вам понадобится вторая пара нижнего белья. Упрощенный запрос обрезки базы данных A - «удалить все записи для пользователей с модулем ID 1». Обратное выполняется в базе данных B. В нашем случае мы удалили почти половину записей для каждого дополнительного осколка, который мы создали. Это был наш план: если каждый раз, когда мы обходим, базы данных хранят половину данных, нам нужно половину поля Postgres для обслуживания одних и тех же данных.

Представьте, что у нас есть четыре записи в базе данных A перед обрезкой и обрезкой: [W | X | Y | Z]. После ошпаривания и обрезки база данных A может выглядеть так:  [ W |      | Y |      ]. База данных B может выглядеть так:  [      | X |      | Z ]. Обратите внимание на пробелы. Это эквивалентно фрагментации жесткого диска. Это начало укусить нас в задницу и, скорее всего, сделало бы нашу жизнь ад, если бы у нас еще не было других трюков в наших рукавах.

Если база данных A выглядит так: [ W |      | Y |      ]. Когда я спрашиваю «дайте мне все записи для идентификатора пользователя 0», он должен вернуть W и Y. Но W и Y не находятся в смежных местах на диске. Поэтому, чтобы обслуживать этот запрос, Postgres должен сначала переместить диск в W, а затем переместить диск в Y, пропуская промежутки между ними. Если W и Y жили рядом друг с другом на диске, диск не должен был бы работать так сильно, чтобы извлекать обе записи. Чем больше работы нужно сделать, тем длиннее запрос.

Как правило, когда новые данные добавляются в таблицу, они помещаются в смежные слоты в конце диска (независимо от идентификатора пользователя). Затем мы запустили VACUUM ANALYZE на столе. Postgres теперь говорит: «О, есть пробел между W и Y, я могу поместить туда новые данные!» Поэтому, когда новые данные добавляются, а затем извлекаются, Postgres должен полностью вернуться к началу диска для извлечения некоторых записей, в то время как другие записи для одного и того же пользователя находятся в конце диска.Фрагментация, связанная с запуском VACUUM ANALYZE, заставила нас засохнуть ручей. Пользователи с большой активностью просто не могли загружать свои каналы. Единственный санкционированный способ исправления фрагментации - это часы простоя.

Хорошо, я надеюсь, ты все еще со мной. Решение и урок здесь важны. Во-первых, если бы наши ящики Postgres были на SSD, возможно, фрагментация не была бы такой большой проблемой. Мы не были на SSD. Решение для нас состояло в том, чтобы построить индекс покрытия, чтобы мы могли обслуживать только индексирование. Фактически это означает, что все поля, используемые для фильтрации и извлечения данных из таблицы, должны храниться в индексе. Если это все в индексе, Postgres не нужно переходить на диск для данных. Таким образом, мы добавили индекс покрытия для нашего самого горячего запроса и видели в среднем 100-кратное улучшение в среднем, до 7000х улучшений для пользователей с большой активностью.

Урок здесь двоякий. Обслуживание данных из памяти экспоненциально быстрее, чем обслуживание с диска.Будьте осторожны в обслуживании данных с диска по шкале. Второй урок не менее важен. Вероятно, мы должны были максимально масштабировать по вертикали. Webscale был слишком сексуальным, чтобы этого избежать. «Осколок всех вещей» - это мему, который я ищу. Sharding был сложным и лучшим долгосрочным решением, но если бы мы применили индекс покрытия для всей таблицы, прежде чем делать какие-либо вертикальные осколки, я думаю, мы могли бы сэкономить массу времени и усилий, просто добавив больше оперативной памяти по мере роста нашей базы данных.

Урок 4: Не создавать узкие места, или сделать

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

Все проблемы масштабирования, которые были бы неактуальными в течение нескольких месяцев или лет, смотрели на нас прямо в лицо в течение первого месяца, когда у вас была значительная база пользователей.Автоматически следуя за пользователем Ello, это означало, что почти все пользователи получат любой опубликованный контент из этой учетной записи. Фактически, миллионы записей будут создаваться каждый раз, когда пользователь Ello будет опубликован. Это продолжает быть и благословением, и проклятием.Обсуждение базы данных? Возможно, пользователь Ello публикует сообщения. Резервные очереди? Возможно, пользователь Ello публикует сообщения. К счастью, мы контролируем эту учетную запись, и нам на самом деле пришлось отключить ее, пока не завершился осколок, и были созданы уникальные индексы.

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

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

Урок 5: Он всегда занимает 10 раз дольше

В приведенных выше разделах мне удалось проделать некоторые трудные уроки масштабирования.Кэширование, окантовка и оптимизация - нетривиальные инженерные цели. До сих пор я был ошеломлен тем, насколько трудны эти усилия на практике.

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

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

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

Извините за бессвязное, но вы понимаете. Урок здесь для вашего психического здоровья во время быстрого масштаба: план на все, что занимает 10 раз дольше, чем вы ожидаете. Это будет просто.

Заключительные мысли

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

На самом деле, TL;DR здесь довольно короткий: кеш агрессивно, упреждающий, перетаскивая данные в память и масштабируя по вертикали, выделяя узкие места масштабирования раньше, чем позже, и выделяет много человеко-часов.

Наслаждайтесь поездкой.

Опубликованном Майком Паком на 12/22/2014 в 09:37