Наверх ▲

Как разработать вычислительную инфраструктуру для большого кластера

Станислав Лагун Станислав Лагун Senior Software Developer, Mirantis Inc. Евгений Кирпичев Евгений Кирпичев Ведет проект в области высокопроизводительных вычислений в компании Mirantis.

Станислав Лагун: Всем привет! Начинаем. Мы расскажем вам об опыте, который мы получили при создании большого проекта для инфраструктуры вычислительного кластера. Я думаю, что полученный нами опыт настолько универсален, что будет полезен даже тем, кто далек от HPC.

Итак. Что, собственно, мы строим?

Мы решаем тяжелые вычислительные задачи. Это задачи, которые занимают порядка недели, если делать все вычисления на одном ядре одной машины. Чтобы они занимали не недели, а какое-то приемлемое время, задачи надо разбросать по кластеру (разбить их на "куски" и эти "куски" распределить между кучей разных ядер в большом-большом кластере).

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

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

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

Кроме того, задачи должны выполняться надежно. Вся система должна быть отказоустойчивой. В большой системе всегда что-нибудь может "упасть". Если что-нибудь "упадет", ничего не должно потеряться.

Как обычно это решается? Традиционный метод решения – использование в системе вида Platform LSF, Microsoft HPC, Condor и так далее. В сочетании с MPI или его вариациями. Это называется "batch scheduler". Но в нашем случае это не работает.

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

Традиционные системы занимаются не столько решением задач, сколько выделением ядер. Мы говорим: «Нам нужно 100 ядер». Когда 100 ядер появится, нам дадут их. Появится 99 – будем ждать. Много разных опций, но нет возможности оптимизировать все под конкретный поток задач.

Можно сделать это более эффективно. Посмотрите, мы сделали сравнение пакетных планировщиков и нашего подхода. С пакетными все понятно. Ядра выделили – дальше делаем на них что хотим.

Что делаем мы? Наше приложение говорит: «Я хочу что-то вычислить». Планировщик говорит: «Пожалуйста. Задачи кидай сюда, результаты читай отсюда. А уж как они там поделятся на кластере – я об этом позабочусь».

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

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

 

Более удачная архитектура подразумевает, что задачи идут мимо планировщика. Планировщик принимает команды о запуске заданий и дает демонам команду вычислить что-то. Демоны "слушают" планировщика и сами берут задачи. Задачи доставляются специальным каналом, мы его называем «трубы». 

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

 

Пример. Клиент "говорит" планировщику: «Создай задачу, выдели на нее 30 % кластера». Планировщик выбирает, какие демоны должны заниматься этой задачей и "говорит" им: «Все вы сейчас переключаетесь на эту задачу». Демоны бросают свои задачи, возможно, те задачи, которыми мы не занимались до того, и начинают "вытягивать" "куски" нашего большого задания. 

Клиент публикует задачи, демоны их "вытаскивают". Планировщик ничего об этом не знает. Он знает только то, какие у нас сейчас есть демоны, чем они, в принципе, занимаются.

Краеугольный камень всего этого - система оповещений. Каким образом задачи попадают от клиента демонам, каким образом команды попадают к планировщику. 

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

В первую очередь, нам важна надежная доставка. Задача, которая была отправлена на кластер, не должна нигде потеряться. Даже если "умрет" демон, процесс RabbitMQ, планировщик или любая база данных, нужно сделать, чтобы задача не терялась.

Жизненный цикл демона таков. Получить задачу – посчитать ее – отослать ответ и только потом подтвердить получение задачи. До тех пор, пока он не подтвердит получение задачи, RabbitMQ "помнит", что он доставлял эту задачу данному демону.

Разберем пример на графике. Есть один демон, он вычисляет задачу и в конце ее подтверждает. На вычислении следующей задачи неожиданно происходит сбой. RabbitMQ "помнит", что эту задачу он отдавал этому демону и в случае, если теряется соединение (демон больше не отвечает на ping), он отдает эту задачу следующему демону. Таким образом, ничего не теряется. 

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

Почему RabbitMQ нам не совсем подходит? У нас 10 тысяч ядер в кластере. 10 тысяч клиентов – это тяжеловато для RabbitMQ. Он столько не выдерживает.

Встроенная кластеризация делает совсем не то, что вы могли бы подумать. Лучше даже не пытаться ее использовать. Реально она создаст больше проблем, чем решит. На самом деле, она предназначена совсем не для того, чтобы все ресурсы равномерно распределялись между экземплярами RabbitMQ. 

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

На слайде отображен пример того, как это может быть реализовано. Задача разделяется на 4 очереди. Все это вместе называется «труба». По очереди задачи поступают то в один, то в другой экземпляр RabbitMQ (это всем знакомый алгоритм Round Robin).

То же самое происходит и с ответами. По очереди ответы приходят в 4 разных очереди на четырех разных экземплярах. Таким образом, нагрузка между ними балансируется. 

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

 

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

• Во-первых, RabbitMQ "не тянет" большое количество соединений (под Windows, по крайней мере). Чтобы это решить, надо иметь больше одного экземпляраа и как-то балансировать нагрузку. Балансировка, естественно, лежит на нас. Она не предоставляется «из коробки». 

• При сбое данные теряются. Если мы отослали задачу, "положили" ее в RabbitMQ, это не значит, что она туда дошла. Она даже могла туда дойти, но не сохраниться на диск. Решение тоже есть, о нем я поговорю чуть позже. 

• Соединение может спонтанно оборваться. При нагрузке RabbitMQ обрывает соединение. В принципе, любые соединения могут рваться. Это надо тоже как-то обрабатывать. Ничто не должно зависать.

• Нужно мгновенное переключение между заданиями. Сложная часть. Если демон вычисляет задания, нужно успеть моментально переключиться на какое-то другое, более срочное, нужно правильно обработать это на уровне соединений с RabbitMQ (в том числе и с ситуацией, когда у нас остались неподтвержденные задачи, что-то не доставлено и так далее). 

• В очереди на конкретном экземпляре RabbitMQ могут кончиться задачи. Часть демонов мы берем с одного экземпляра, часть демонов – с другого. На одном экземпляре задачи кончились, а на втором их еще полно. Чтобы не получилось такого, что половина кластеров простаивает, а вторая половина завалена задачами, нужно как-то грамотно все балансировать.

Пример на тему сохранности данных при сбоях RabbitMQ. Делается это через технологию, которая называется публикацией подтверждений (англ. Publish confirmations). Клиент отсылает свои сообщения, но при этом их помнит. Периодически RabbitMQ выполняет fsync и высылает подтверждение, сообщая, какие сообщения он записал. Только в этот момент мы "забываем" эти сообщения.

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

В RabbitMQ есть еще один механизм, называется «транзакция». Есть гарантированная доставка при определенных условиях (более или менее гарантированная, скажем так). Но использование транзакций означает большие "тормоза". Если есть сообщение, которое гарантированно должно быть доставлено по slave-транзакции, то производительность упадет в разы, а, может, и в порядки.

Голос из зала: На два порядка.

Станислав Лагун: На три порядка. Использование подтверждений – гораздо меньше (уже незначительно). Транзакции лучше вообще не использовать или использовать для каких-то особо критичных сообщений.  

Не стоит перегружать RabbitMQ. Если слишком интенсивно отсылать сообщения, начинается куча проблем, которые сложно даже диагностировать. Начинает неожиданно рваться соединение, разные странные ошибки начинают идти. Если смотреть по логам, то зачастую даже не получается разобраться, что был именно "перегруз" RabbitMQ.

Что бывает при перегрузке? Каждая горизонтальная линия на графике слайда – это одно ядро в кластере. Если посмотрите, видно, что левый нижний угол белый. То есть задачи этим ядрам приходили с большим опозданием. Одни ядра уже начинали что-то считать (пошли желтые полосы), а другие просто ждали, пока до них дойдет очередь. Значит, утилизация кластера была не очень хорошо реализована.

Если бы использование кластера было 100 %, то здесь был бы желтый квадрат. Все время считается.

Вот, собственно, еще один график на ту же тему. 4 задания – 4 разных экземпляра RabbitMQ. Видно, что 3 работают ровно, один отстает. Если мы не понижаем нагрузку на него, то отставание начинает нарастать. Он не успел обработать одно сообщение, а мы ему уже новые дали. Его отставание будет все увеличиваться и увеличиваться. 

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

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

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

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

Просто прервать ожидание.

Прервать соединения с "трубами" предыдущего задания (по сути, разрывать соединения с очередями, которые использовались).

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

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

 

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

• Бури реконнектов.

• Голодание.

• Дисбаланс.

• Соединения могут кончиться, если все набросятся на один RabbitMQ.

Как вы помните, количество соединений в Windows очень небольшое. Если все 10 тысяч ядер кластера обратятся к одному RabbitMQ, он не выдержит. 

Решение есть, но оно более хитрое. К сожалению, я о нем не расскажу.

Как все это закодировать? Можно сделать «лапшу». Понятно. Обработка всех различных действий. По сути, доставка сообщений, большая часть работы с ними, сводится к разным этапам обработки сообщений. 

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

Понятно, что решение – это слои.

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

– отослать;

– получить/сбросить в список неподтвержденных;

– завершить (dispose).

У "слушателя" все аналогично:

– доставить сейчас;

– доставить потом (асинхронно);

– завершить.

 

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

Какие слои могут быть? «Преобразование типа сообщений». «Повторы при ошибке». «Слушать сразу несколько очередей». «Игнорирование неподтвержденных при закрытии/при обрыве».

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

 

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

Первый слой просто принимает сообщения из RabbitMQ. Неограниченная выборка означает, что берется все, что прислал нам RabbitMQ. Слушаем сразу несколько, то есть сводим в одну очередь очереди разных экземпляров RabbitMQ. При ошибке переоткрываем. Логируем. При ошибке надо все повторять до успеха, потом идет десериализация. 

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

То же самое с ответом, но в обратном порядке (приблизительно).

Теперь Евгениц расскажет про отладку.

Евгений Кирпичев: Я действительно расскажу про отладку и анализ того, что мы написали. Как видите, дело не очень простое, так что отлаживать приходится много. 

 

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

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

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

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

Для этого у нас есть некоторое количество "фокусов в рукаве". 

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

У этого есть несколько недостатков. 

Во-первых, мы работаем на Windows, и там NTP плохой. Он гарантирует точность чуть ли не порядка секунды, что вообще ни в какие ворота не лезет. 

Во-вторых, даже нормальный NTP, который дает гарантии с точностью до гранулярно-системного таймера, тоже не очень хорошо подходит для отладки того, что происходит быстрее, чем эта гранулярность (в пределах микросекунд). 

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

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

Этот логгер общедоступен. Эта идея мне так понравилась, что я решил сделать открытые исходники.

Анализируем мы все это с помощью GNU-утилит и awk. До MapReduce над логами мы еще не доросли и большую часть того, что мы с ними делаем, трудно сделать с помощью того же MapReduce или Sazal.

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

По горизонтали – время, по вертикали – ядро кластера. Раскраска – в данном случае идет по тому RabbitMQ, к которому подключен конкретный демон.

Как видите, тут наблюдается такая аномалия, что есть белые полосы. Что еще более поразительно, эти полосы синхронно идут у "зеленых" и "пурпурных" экземпляров RabbitMQ. Оказалось, что была ошибка в файле конфигурации: у нас две записи о RabbitMQ указывали на одну и ту же машину. Попробуйте раскопайте по логам, почему тормозит, и вообще поймите, что действительно тормозит. Гораздо проще нарисовать картинку – и тут же все понятно. 

 

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

Все эти графики построены по похожим "сырым" логам одинакового формата, до которого самому инструменту рисования нет никакого дела. Берется лог, пропускается через простейший awk-скрипт, который можно написать за 30 секунд в командной строке, и пропускается через инструмент. Получается картинка. Можно практически любые аспекты системы визуализировать. Я не знаю такой задачи, которую я не смог бы нарисовать.

Что нужно для этого всего? Нужны, конечно, очень подробные логи. Об этом мы немного подробнее поговорим чуть позже.

Теперь будет обобщение нашего опыта. Поговорим о корректности, надежности и производительности.

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

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

Как из этого выпутываться? 

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

Еще один способ с этим бороться – это писать крайне подробные логи. Если вам кто-то скажет, что в вашем коде слишком много логирования, или что из-за логирования код стал некрасивым, потому что половину кода занимает логирование, смело шлите этого человека туда, откуда он пришел. Или дайте ему какой-нибудь лог вашей программы, из которого вырезано 90 %, казалось бы, неважных сообщений, и попросите его найти причину какой-нибудь проблемы. 

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

Еще приемы борьбы с несовершенством человеческой природы заключаются в том, чтобы минимизировать так называемое «ядро корректности» вашей программы. «Ядро корректности» – это термин, придуманный не мной. Он означает ту часть кода или семантики вашей программы, без правильности которой она не может функционировать вообще. 

Например, в случае веб-сервера можно сказать, что какой-нибудь пул сокетов и сервера относится к такому ядру, а логика обработки конкретного запроса нет, потому что если у вас отвалится один запрос, сервер не "упадет". Если будет блокировка в пуле сокетов, сервер перестанет принимать соединения.

Конечно, надо избегать различных опасных приемов типа многопоточности. Если у вас есть возможность без нее обойтись хоть как-то – обходитесь. Нет ничего крутого в том, что вы написали что-то многопоточное, что работает. Было бы гораздо круче, если бы вы написали это без многопоточности. По крайней мере, можно будет взглянуть на код и понять, что оно действительно работает.

Еще стоит использовать так называемое явление стремление к согласованности и так называемые «барьеры». Сейчас мы об этом поговорим.

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

Можно периодически перезапускать систему вообще. Если ваша система способна это выдержать, то вы никогда больше не спросите себя «почему система зависла», потому что вам пофиг. Зависла – ее перезапустят. Будет работать дальше. 

Конкретно в нашем случае можно закрывать соединения с очередью задач. После того как вы закрыли соединение, у вас больше нет беспокойства относительно того, не застрянет ли какая-то задача. Нет, не застрянет, потому что как только соединение будет оборвано, RabbitMQ передаст его другому. Вот что такое барьеры.

Надежность (она же – отказоустойчивость). 

Тут перечислено несколько принципов, по которым надо строить отказоустойчивые системы. Без чего их, по-моему, строить совсем невозможно. 

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

Далее. Только асинхронная коммуникация. Это прямое следствие из перезапускаемости. Коммуникация асинхронная нужна вот зачем: есть компонент, с которым у вас установлено TCP-соединение, по которому идет RPC-запрос. Если вы что-то перезапустили, то у вас произошла непонятная ошибка, и вам придется писать в коде логику переспроса. 

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

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

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

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

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

Например, вы видите, что система давно не принимала запросы (минуту). Это странно, потому что обычно она должна принимать их по несколько штук в секунду. Все, перезапускаете. Если там повисло какое-то соединение, соединения больше нет, и будем надеяться, что теперь не повиснет.

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

Мы несколько раз сталкивались с проблемами по вине библиотеки. Даже в dotNet-библиотеке NLog мы нашли ошибку, которая приводила к deadlock’у. К deadlock’у вообще всего процесса. Поправили, конечно, потому что в этом месте с перезапускаемостью было немного похуже, но впечатления были не самые приятные.

Асинхронные коммуникации

Я уже более или менее рассказал, о чем тут речь. Добавлю, что лучше использовать протоколы, не требующие восстановления соединения (те, в которых отсутствует понятие соединения). Например, такие, как UDP. Конечно, если это при вашей задаче допустимо. Опять же, потому, что у ваc полностью исчезает класс ошибок «что делать если связь порвалась». Нет связи – нет проблемы.

Eventual consistency (стремление к согласованности)

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

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

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

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

Тут, как видите, в системе знание о том, кто за что ответственен, согласуется постепенно. Не в каждый момент клиент и RabbitMQ вместе знают, на ком ответственность. Постепенно это знание становится все точнее и точнее. 

Еще один пример. Диалог планировщика и демона. Планировщик, например, хочет сказать демону: «Займись-ка ты заданием B». Демон полсекунды назад послал ему UDP-пакет, что он занимается заданием A, чтобы планировщик знал, чем он сейчас занимается, и мог принять какие-то решения по планировке. 

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

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

Теперь немного о производительности.

У производительности есть несколько аспектов. Во-первых, собственно, способность держать нагрузку, не ломаться и не терять работоспособность.

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

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

У нас было очень много ресурсов, в конечность которых мы "упирались". Начиная от количества Erlang-процессов в RabbitMQ с настройками по умолчанию, количества соединений, которые RabbitMQ может установить в секунду, или количества транзакций, и заканчивая местом на диске и, наконец, собственным терпением при анализе многогигабайтных логов.

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

Мораль такая: надо планировать потребление всех ресурсов. Особенно таких, которые начинают больше употребляться с масштабом.

У нас, например, внезапно появились некоторые проблемы при росте кластера с четырех тысяч до пяти тысяч ядер. На четырех тысячах что-то не проявлялось (кажется, лимит процессов RabbitMQ), а на пяти проявилось.

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

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

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

TCP, как известно, на сетях с большими задержками не может достичь их пиковой пропускной способности, потому что он начинает ждать подтверждения пакетов. Они доходят нескоро. Если вы выстроите канал, например, из 110-гигабитных каналов с буквально миллисекундными задержками между ними, десяти гигабит вы никогда не достигнете. У вас получится только один гигабит, может быть, или меньше, потому что вы все время будете ждать подтверждений. 

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

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

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

В общем-то, все. Вопросы, пожалуйста.

Вопросы и ответы

Вопрос из зала: Странный вопрос. Почему Windows у вас сейчас?

Евгений Кирпичев: Потому что у заказчика все на Windows.

Вопрос из зала: Второй такой вопрос. Вы, получается, предоставляете клиенту какой-то API, в который он может послать задачу. Вы тоже говорите со свое стороны: «Будьте готовы к тому, что задача у нас, может быть, еще и не сохранилась».

Евгений Кирпичев: Нет, на уровне этого API такого уже не происходит.

Вопрос из зала: У вас оно какое-то синхронное с доставляемостью сразу?

Евгений Кирпичев: Нет, синхронности никакой нет. Просто мы, скажем так,  предполагаем, что API отсылки задач находится там же, где находится тот, кому надо их отсылать. Если этот процесс "грохнется", то задача, конечно, не дойдет, но всем все будет равно.

Вопрос из зала: Скажите что-нибудь про QPIT. Что вы думаете по поводу этого продукта?

Евгений Кирпичев: Мы его не тестировали сами, но я читал о том, как эти продукты тестировали другие. Вышло, что RabbitMQ – это единственное, что стабильно работает, не теряет данные и так далее. Поэтому мы, на всякий случай, и не стали его пробовать, чтобы не тратить время. Взяли сразу RabbitMQ. 

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

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

Вопрос из зала: Вы говорите, что задача порождает намного больше других задач. Нагрузка может внезапно возрасти. Например, на RabbitMQ очередь может стать очень большой. Вы это как-то обрабатываете?

Евгений Кирпичев: Что ты имеешь в виду? Перегрузку RabbitMQ мы обрабатываем тем, что ждем, пока он разгрузится, и тогда отправляем задачу. Ну, большая очередь так большая очередь. Ничего страшного. Мы обрабатываем в случае, когда маленькая очередь. Если в очереди недостаточно задач для какого-то демона, если она опустела, он пытается посмотреть в других очередях.

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

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

Вопрос из зала: В принципе, по этому можно догадаться, что она работает стабильно. 

Евгений Кирпичев: Да. 

Вопрос из зала: Здравствуйте. Есть какой-то мониторинг состояния всей системы в целом? Я сейчас не про анализ логов, не про парсинг их. Что-то типа Zabex - с графиками, с триггерами, с sms - есть?

Евгений Кирпичев: С sms, к сожалению, нет. Мы довольно сильно страдаем от того, что на мониторинг у нас не хватило ресурсов разработки. Но у нас довольно простой мониторинг. Все узлы "живы". Есть какой-то сбор статистики. Смотрим, как кластер загружен во времени и так далее. Но нормальной системы мониторинга нам, действительно, не хватает. 

Вопрос из зала: Здравствуйте. Вы сказали про EDP-протокол который нужен, чтобы не держать соединение постоянно. С RabbitMQ вы все-таки держите TCP-соединение?

Евгений Кирпичев: С RabbitMQ держим. Он по-другому не умеет. 

Вопрос из зала: Не научили его?

Евгений Кирпичев: Нет, не научили. Зато иногда соединение рвется. Поэтому у нас куча логики по одному. Что в этом случае делать? Такова цена.

Вопрос из зала: Нельзя ли чуть-чуть поподробнее о том, почему встроенные средства кластеризации RabbitMQ не подошли?

Евгений Кирпичев: Потому что они не имеют никакого отношения к балансировке нагрузки. Они имеют отношение только к репликации метаданных объектов AMQP.

Вопрос из зала: Они увеличивают надежность, на ваш взгляд?

Евгений Кирпичев: Нет, они не увеличивают надежность, потому что содержимое очередей не реплицируется. Сейчас уже, на самом деле, реплицируется. Но, во-первых, нам это не нужно, во-вторых, это появилось после того, как мы сдали проект.

Комментарии

Нет ни одного комментария

Только пользователи могут оставлять комментарии

Возможно, вам будет интересно:

Петр Зайцев

Петр Зайцев

Пётр Зайцев окончил МГУ им. М.В. Ломоносова, и ещё в студенческие годы являлся техническим директором проекта SpyLOG, сервиса статистики для веб-сайтов. В начале 2000-х Пётр стал сотрудником MySQL AB и возглавил группу оптимизации производительности (High Performance Group) внутри компании.

Петр Зайцев (Percona Inc) делится своим опытом оптимизации производительности с помощью архитектуры InnoDB.

Денис Бесков

Денис Бесков

Тренер, консультант, организатор "With a Little Help from My Friends".

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

Доклад от компании "Рамблер" о том, почему почтовые веб-приложения лучше старых добрых веб-сайтов.