Переработка 1 миллиона запросов в минуту C идет

Переработка 1 миллиона запросов в минуту C идет

446

Перевод статьи главенствующего конструктора компании действиях, как они достигли 1 миллиона запросов в минутку Итого на 4 сервера.
Мы на действиях мы переживаем бурный рост, и с тех пор, как я пришел в компанию около года назад в Кремниевой равнине, один из моих главных обязательств является проектирование и разработка пары систем архитектур для развития быстро возрастающей компании и всю нужную инфраструктуру для поддержки продукта, которым пользуются миллионы людей каждый день. Я работал в антивирусной промышленности на протяжении 12 лет в несколько различных компаний, и я знаю, как трудно получить эти системы, ввиду больших размеров данных, которые приходится иметь дело раз в день.
Любопытно то, что крайние 9 лет, всю разработку интернет-бэкендов, с которым я столкнулась, был реализован в Рубин на рельсы. Не поймите меня некорректно, я люблю Ruby на Rails и я считаю, что это хорошая среда, но через некое время вы привыкнете к мысли о системах дизайн в стиле Руби, и вы забудете, как действенный и обычный архитектуры может быть раз Вы употребляли мультипотентные, параллелизм, скорое выполнение и действенное внедрение памяти. В течение почти всех лет я писал на C/С++, Delphi и C#, и я начал осознавать, как много наименее сложные вещи могут быть, раз вы избрали верный инструмент для работы.
Как основной конструктор, я не поклонник Поливанова о языках и фреймворках, которые так популярны в сети. Я считаю, что эффективность, производительность и сопровождаемость кода во многом зависит от того, как просто можно выстроить свое решение.
Интернет-обработчик обязана была получить json-документ, который может содержать сбор данных (нужная перегрузка), которая, в свою очередь, необходимо хранить на Amazon S3, потому, что наша карта-понизить системные позднее обработанные данные. Неувязка
Работая над одной из частей нашей системы анонимного сбора телеметрии и аналитики, нашей задачей было обработать большущее количество запросов Post миллионы клиентов.
Обычно, мы поглядели в сторону работника-уравнений / (рабочего уровня) архитектуре, и применять такие вещи как:

Sidekiq
Спасательных
DelayedJob
Elasticbeanstalk Рабочего Яруса
В rabbitmq
и так дальше
И поставил бы 2 различных кластера, один для интернет-фронтенд, а другую для рабочих (работников), чтоб иметь возможность масштабировать фоновую задачку.
Но с самого начала наша команда знала, что мы должны написать его в идти, так как на шаге обсуждения мы сообразили, что эта система будет управляться с большущим трафиком. Я хожу уже около 2 лет, и мы разработали несколько систем, но ни один из их не работал до таковых нагрузок.
Мы начали с сотворения пары структур для описания данных запросы будут приниматься запросы Post, а способ, чтоб загрузить их в нашей памяти S3.
Тип PayloadCollection структуры struct {
WindowsVersion строчку в формате json:»версия»`
Маркер строчку в формате json:»маркер»`
Полезные перегрузки []нужная перегрузка `Формат json:»данные»`
}

Тип полезной перегрузки структуры struct {
// [отредактировано]
}

функ (р *нужная перегрузка) UploadToS3() ошибка {
// в storageFolder метод гарантирует отсутствие конфликтов имен в
// случае мы получаем тот же timestamp в имени ключа
storage_path := дрм.На sprintf(«%в/%в», стр. storageFolder, время.На данный момент().UnixNano())

ведро := S3Bucket

б := новейший(байтов.Буфера)
encodeErr := Формат json.NewEncoder(б).Кодировать(нужная перегрузка)
раз encodeErr != ноль {
возвращение encodeErr
}

// Все мы в должность ведро S3 должен быть личным
вар обк = П3.Личная
вар contenttype = «приложение/октет-поток»

вернуть ведро.PutReader(storage_path, б, в int64(б.Функция Len()), Тип содержимого, обк, П3.Варианты{})
}
Мы на сто процентов недооценили количество трафика. Мы ждали, что запросов будет много, но когда мы выкатили первую версию в создание, то сообразил, что ошибся на несколько порядков. Решение “в лоб” с помощью ГОУ-процедуры
Сначало, мы приняли обычное доверчивое решение постпроцессор, просто пробую распараллелить обработку с помощью обычных рутинных:
функ payloadHandler(Вт протоколу http.ResponseWriter, р *протокол http.Запросу) {

раз R.Метод != «Пост» {
Вт.WriteHeader(протокол http.StatusMethodNotAllowed)
возвращение
}

// Чтение тела в строчку для json декодирования
Варе содержание = &PayloadCollection{}
ошибка := Формат json.NewDecoder(ИО.LimitReader(р.Тела, Наибольшая)).Декодировать(&материалы)
раз индикатор err != ноль {
Вт.Заголовок().Установить(«Тип содержимого», «приложения/json; Шифровка=Шифровка utf-8»)
Вт.WriteHeader(протокол http.StatusBadRequest)
возвращение
}

// Пройти каждой полезной перегрузки и частей очереди, чтоб быть персонально размещено до S3
для _, нужная перегрузка := спектр Контента.Полезные перегрузки {
идите полезной перегрузки.UploadToS3() // <—— не делай этого
}

Вт.WriteHeader(протокол http.StatusOK)
}
Для средних нагрузок, этот подход будет работать для большинства людей, но она быстро оказалась малоэффективной в большем масштабе.
Нет никакого метода, чтоб контролировать, сколько Gortin мы запускаем. Post-запросов в минутку, этот код, естественно, и быстро свалился крашичах. Описанный выше подход плох по нескольким причинам. И так мы получили 1 млн.
д… Так мы бы употребляли обыденное решение для таковых заморочек, таковых как художественных, Sidekiq, ЗК, и т. Попытайтесь еще раз
Мы должны были отыскать иной метод. С самого начала мы обсудили, что нам необходимо уменьшить время обработки запроса до минимума и томных задач в фоновом режиме. перечень велик, так как существует множество методов выполнить поставленную задачку. Естественно, это, как вы должны сделать это в мире Руби на рельсах, по другому вы заблокируете все доступные интернет-обработчиков, и независимо от того, используете ли вы Пума, единорог либо пассажира (давайте лишь не дискуссировать тут в jruby, пожалуйста).
И 2-ая наша попытка была сотворения буферизованных канала, в котором мы могли бы поместить в очередь задач, и загружать их на S3, и так как мы можем контролировать Наибольшее число объектов в нашей очереди, и у нас есть куча оперативной памяти, чтоб держать все в памяти, мы решили, что было бы довольно, чтоб буфер задачки, в канале очереди. вар очереди полезной перегрузки Чан

func в способе init() {
Очереди = сделать(Чан полезной перегрузки, MAX_QUEUE)
}

функ payloadHandler(Вт протоколу http.ResponseWriter, р *протокол http.Запросу) {
… }
И позже, на самом деле, чтоб прочесть задачку из очереди и обрабатывать их, мы употребляли нечто схожее на этот код: // Пройти каждой полезной перегрузки и частей очереди, чтоб быть персонально размещено до S3
для _, нужная перегрузка := спектр Контента.Полезные перегрузки {
Очереди <- нужная перегрузка
}

Наш синхронный обработчик очереди загружен лишь один пакет данных на S3 в единицу времени, и так как частота поступающих заявок было еще больше способностей обработчик для загрузки файлов на S3, наши буферизованный канал чрезвычайно быстро достигла собственного предела и перекрыть возможность ставить в очередь новейшей задачки. StartProcessor клавишу func() {
для {
выберите {
случае задание := <-очереди:
работу.полезные данные.UploadToS3() // <— все еще не отлично
}
}
}
Честно говоря, я понятия не имею, что мы задумывались тогда. Этот подход не отдал нам неважно какая победа, мы просто торговали плохо конкурентоспособными на буферный канал и она просто отложила делему. Это, видимо, было поздно ночкой, с кучей опьяненных красно-Бык-ы.
Время отклика (задержка) была увеличена за счет роста в несколько минут опосля того, как мы tadalail это глючная версия. Мы молча проигнорировали делему и запустил обратный отсчет к краху нашей системы.

Наилучшее решение
Мы решили применять популярный шаблон работы с каналами в Go, чтоб сделать двухуровневую систему каналов, один для всех каналов, 2-ой для контроля числа обработчиков задач, работа с очередью в то же время.
Для тех, кто знаком с Java, C# и т. функ (работник Вт) стоп() {
иди клавишу func() {
Вт.кинуть <- правда
}()
}
Мы изменили наш обработчик запросов, чтоб сделать объект типа данные задания, и выслать это канал JobQueue, потом подобрал его обработчики задач. Вт.WorkerPool <- з.JobChannel

выберите {
случае задание := <-з.JobChannel:
// мы получили рабочий запрос. JobQueue <- работа
}

Вт.WriteHeader(протокол http.StatusOK)
}
Во время инициализации сервера, мы создаем диспетчерскую и вызова Run (), чтоб сделать пул работников (групп работников) и начать прослушивание входящих задач в JobQueue. раз индикатор err := работа.Полезные данные.UploadToS3(); индикатор err != ноль {
журнальчик.Errorf(«ошибка выгрузки до S3: на %s», err (ошибка).Ошибка())
}

вариант <-з.бросил:
// мы получили сигнал тормознуть
возвращение
}
}
}()
}

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

// Работник представляет работник, который выполняет работу
типа работник структуры struct {
WorkerPool Чан-Чан задания
JobChannel задания Чан
кинуть тян типа bool
}

функ NewWorker(workerPool Чан-Чан задания) работника {
возвращение работника{
WorkerPool: workerPool,
JobChannel: сделать(Чан задания),
кинуть курить: сделать(Чан Тип bool)}
}

// Способ Start запускает выполнить петли для работника, прослушивание кинуть в канал
// случае мы должны приостановить его
функ (работник ж) начать() {
иди клавишу func() {
для {
// регистр текущего работника в рабочей очереди. вар (
MaxWorker = ОС.Функции getenv(«MAX_WORKERS»)
MaxQueue = ОС.Функции getenv(«MAX_QUEUE»)
)

// Задание представляет собой задание для выполнения
Тип задания Тип struct {
Нужная Перегрузка Нужная Перегрузка
}

// Буферизованный канал, который мы можем выслать запросы на работу. вар (
MaxWorker = ОС.Функции getenv(«MAX_WORKERS»)
MaxQueue = ОС.Функции getenv(«MAX_QUEUE»)
)
Моментальный итог
Сходу опосля того, как мы tadalail крайнее решение, мы узрели, что Время отклика снизилось до незначимых цифр и обработки запросов резко возросло. Поэтому что мы употребляли Амазонки Elasticbeanstalk для этого проекта и решение идти-в среду, и постоянно старался следовать двенадцатифигурная методологии для опции наших систем в создание, мы читаем эти значения из переменных окружения. диспетчер := NewDispatcher(MaxWorker)
диспетчер.Пуск()
Ниже приведен код реализации нашего диспетчера:
Диспетчер типа struct {
// Резерва из рабочих каналов, зарегистрированных с диспетчером
WorkerPool Чан-Чан задания
}

функ NewDispatcher(инт maxWorkers) *Диспетчер {
бассейн := сделать(Чан-Чан задания, maxWorkers)
возвращение &диспетчерской{WorkerPool: бассейн}
}

функ (д *диспетчеру) запустить() {
// начиная с N числом работников
для я := 0; я < д.maxWorkers; я++) {
работник := NewWorker(разум.бассейн)
работник.Начать()
}

иди д’.диспетчерская()
}

функ (д *Диспетчер) диспетчерская() {
для {
выберите {
случае задание := <-JobQueue:
// работу, запрос был получен
иди функ(задание) {
// испытать получить работника работа канала, который доступен. Мысль заключалась в том, чтоб распараллелить загрузку до S3, контролируя этот процесс, чтобы не перегружать машинку и не отдыхать в, с S3. Потому мы избрали работу/работника эталон. функ payloadHandler(Вт протоколу http.ResponseWriter, р *протокол http.Запросу) {

раз R.Метод != «Пост» {
Вт.WriteHeader(протокол http.StatusMethodNotAllowed)
возвращение
}

// Чтение тела в строчку для json декодирования
Варе содержание = &PayloadCollection{}
ошибка := Формат json.NewDecoder(ИО.LimitReader(р.Тела, Наибольшая)).Декодировать(&материалы)
раз индикатор err != ноль {
Вт.Заголовок().Установить(«Тип содержимого», «приложения/json; Шифровка=Шифровка utf-8»)
Вт.WriteHeader(протокол http.StatusBadRequest)
возвращение
}

// Пройти каждой полезной перегрузки и частей очереди, чтоб быть персонально размещено до S3
для _, нужная перегрузка := спектр Контента.Полезные перегрузки {

// давайте сделаем задание с полезной перегрузкой
работы := задание{полезная перегрузка: грузоподъемность}

// Жмем на работе на очереди. // это перекрывает, пока работник находится в состоянии простоя
jobChannel := <-д’.WorkerPool

// диспетчеризации работы работнику работу канала
jobChannel <- задание
}(работу)
}
}
}
Обратите внимание, что мы указываем число процессоров, что будет запущен и добавлен в бассейн. Таковым образом мы можем контролировать число процессоров и наибольший размер очереди быстро возможность предоставили эти характеристики без повторного развертывания целого кластера.

Опосля пары минут прогрева упругой балансировки перегрузки-ы, мы узрели, что наши ElasticBeanstalk приложение обрабатывает около 1 млн запросов в минутку. Мы традиционно пару часов днем, когда трафик пики добиваются наиболее чем 1 млн запросов в минутку.
Как лишь мы tadalail новенькому кодексу, количество нужных серверов значительно свалился со 100 до 20 серверов.

Опосля того, как мы сделали наш кластер и настроить автоматическое масштабирование, мы смогли уменьшить их число еще больше — до 4 ЕК С4.огромные экземпляры и упругой Автоматическое Масштабирование запустила новейший экземпляр, раз загрузка процессора превосходит 90% в течение 5 минут.

Выводы
Я твердо убежден, что простота постоянно одолевает. Мы могли бы сделать сложную систему с огромным количеством очередей, фоновых действий, развертывания комплекса, но заместо этого мы решили применять силу автоматическое масштабирование ElasticBeanstalk и действенный, обычный подход к конкурентоспособности, что Golang дает из коробки.
Ведь не каждый день вы увидите группу лишь 4 машинки, которые даже слабее чем мой сегодняшний Макбук про, которые обрабатывают Post-запросов, написание на Amazon S3 в ведро 1 миллион раз каждую минутку.
Постоянно есть верный инструмент для задачки. habrahabr.ru И для тех случаев, когда собственный Рубин на рельсы системы нужен наиболее мощнейший интернет-обработчика, бросить незначительно на рубине экосистема наиболее обычный, наиболее массивные решения.