Расчёт производительности хранилища данных

В наше время глобализации, виртуализации и облачных технологий одним из краеугольных камней любой серьезной IT инфраструктуры становится общее хранилище данных. Это может быть как традиционные NAS или SAN, так и специализированная система вроде Amazon Dynamo. При этом при проектировании инфраструктуры первым встает вопрос оценки требуемой производительности хранилища. Конечно, в архитектуру всегда стараются заложить возможность легкого масштабирования, чтобы наращивать производительность по мере надобности. Однако это не отменяет полностью задачу первоначальной оценки.

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

Для примера мы рассмотрим построение общего дискового хранилища типа SAN, на котором располагаются образы дисков виртуальных машин. Для подобных хранилищ обычно используются две метрики: скорость линейного и произвольного доступа. Линейный доступ характерен для больших файловых хранилищ, видео-хостинга и т.д. Эта метрика довольно проста: после выхода на проектную мощность все клиенты потребляют постоянную полосу пропускания, и хранилище должно ее обеспечивать. То есть, если есть 100 сервисов, каждый рассчитан на отдачу контента со скоростью, скажем, 10 Мб/с, то хранилище должно предоставлять скорость 100 * 10 = 1000 Мб/с. Сложнее дело обстоит с произвольным доступом, который характерен для баз данных, систем поиска, хостинга множества мелких файлов и т.п. Такой доступ очень неоднородный и носит случайный характер. Единицей измерения скорости произвольного доступа является количество операций ввода-вывода в секунду (I/O per second или iops). Простейшая оценка сверху заключается в том, чтобы взять максимально возможную скорость для клиента и помножить ее на количество таких клиентов (при условии что клиенты примерно одинаковые). Пусть есть все те же 100 сервисов, каждый из которых в пике потребляет 500 iops (неважно на чтение или на запись). Получается, что необходимо хранилище с пропускной способностью 100 * 500 = 50000 iops! Это очень много. Это более 270 SAS дисков, а если учесть, что необходимо хотя бы однократное зеркалирование данных, то более 500 дисков. Цена подобного хранилища будет заоблачной. Да и не нужно оно.

Интуитивно ясно, что вероятность того, что все 100 клиентов одновременно потребуют по 500 iops, ничтожна, поэтому такой большой запас не нужен. Но как оценить, сколько же нужно реально без ущерба для качества обслуживания? Чтобы ответить на этот вопрос, мы начнем со сбора исходных данных.

Далее по тексту я буду ссылаться на различные скрипты, с помощью которых я обрабатывал данные. Полные листинги приводить здесь не буду, вместо этого опишу общий принцип работы. А если кому интересно, то архив со всеми скриптами можно скачать отсюда. Кроме того, время от времени будут встречаться ссылки на статьи из Википедии по теории вероятностей. Это сделано больше для красоты, так как за реальными знаниями по теме лучше обратиться к хорошей книге или ВУЗовскому учебнику. И, наконец, некоторые мои формулировки не обладают научной строгостью; это не со зла, а для легкости повествования.

Будем проектировать дисковое хранилище для виртуальных машин с базами данных. За образец возьмем уже существующую машину с MySQL, обслуживающую некий веб-проект с нагрузкой порядка 80000 посещений в сутки. Машина занимает 16 Гб оперативной памяти. По объему памяти можно будет впоследствии нормировать полученные результаты. Запустим на ней скрипт iomon.sh, который раз в несколько секунд читает файл /sys/block/$dev/stat (дело происходит на Linux) и записывает мгновенное значение потребляемых iops. Запустим и оставим на пару недель собирать информацию. В результате получился увесистый файл iomon.log на мегабайт с лишним примерно такого содержания.

 1287549095 9 8 1287549105 7 23 1287549115 49 12 1287549125 7 9 1287549136 35 10 

Метка времени в секундах, количество iops на чтение и на запись. Прежде всего посмотрим, как распределена нагрузка во времени. Проведем усреднение по дням недели с помощью скрипта week.rb и построим гистограмму.

Скрипты я писал на Ruby, чтобы попрактиковаться в этом языке.

Видим вполне ожидаемые результаты: ровная нагрузка в течение всей рабочей недели с небольшими пиками в среду и четверг и некоторое падение в выходные. Отбросим выходные и проведем усреднение по часам (скрипт hour.rb).

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

Как я уже говорил, мгновенные значения потребляемых iops носят случайный характер. А любая случайная величина характеризуется своей плотностью вероятности, такой функцией f(x), которая показывает, с какой вероятностью потребляется x iops. Ясно, что сумма всех возможных f(x) равна 1. Для построения функции плотности вероятности воспользуемся скриптом dens.rb. Алгоритм очень простой: пробегаем по файлу данных, для каждого уникального значения iops подсчитываем, сколько раз оно встречается, затем делим на общее количество записей. В результате получился следующий график.

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

Наряду с плотностью вероятности случайная величина характеризуется распределением вероятностей. Распределение в нашем случае – это такая функция p(x), которая показывает, с какой вероятностью потребляемое количество iops не превышает x. Ясно, что p(x) = sumlimits_{t = 0}^x f(t). Скрипт dist.rb строит распределение из плотности по этой формуле.

Видно, что для записи после примерно 30 iops вероятность растет очень медленно. То есть большинство запросов не превышает 30 iops. Максимальное зафиксированное значение составляет 417 iops, что на порядок больше.

Надо сказать, что в зависимости от профиля нагрузки функции плотности вероятности могут сильно различаться. К примеру, вот как выглядят аналогичные графики для машины с HTTP backend того же проекта.

Итак, имея функцию плотности вероятности, мы фактически имеем математическую модель абстрактной нагруженной машины с MySQL. Как же на основе нее предсказать поведение 100 таких машин?

Теория вероятностей говорит, что если есть случайная величина x с плотностью вероятности f(x) и случайная величина y с плотностью вероятности g(y), то плотность вероятности суммы x и y будет равна свертке функций f и g. В нашем случае свертку можно вычислить как h(t) = sumlimits_{tau = 0}^{I_{max}} f(tau)f(t - tau), где I_{max} – максимальное зафиксированное значение iops для одной машины. Таким образом, свернув нашу функцию плотности 100 раз саму с собой, мы получим функцию плотности вероятности суммарной нагрузки на хранилище. Вычислять свертку можно «в лоб» по вышеприведенной формуле, можно с помощью обратного преобразования Фурье, воспользовавшись тем фактом, что Фурье-образ свертки равен произведению Фурье-образов сворачиваемых функций. Но мы пойдем другим путем.

Проведем прямое моделирование, то есть будем генерировать случайные значения с заданной плотностью вероятности и складывать их. Таким образом на выходе получим набор мгновенных значений iops, потребляемых одновременно всеми одинаковыми машинами. Алгоритм генерирования случайной величины с заданной плотностью вероятности известен и очень прост. Расположим все вероятности в виде последовательных отрезков на общем отрезке [0;1]. Затем сгенерируем стандартным генератором псевдослучайных чисел равнораспределенное на отрезке [0;1] случайное число и посмотрим, в какой вложенный отрезок оно попало. Значение iops, соответствующее этому отрезку, и будет искомая случайная величина. Алгоритм реализован в программе sim.c.

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

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

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

В теории вероятностей есть такая замечательна вещь как центральная предельная теорема (ЦПТ), которая утверждает, что если есть набор независимых одинаково распределенных случайных величин с математическим ожиданием mu и дисперсией sigma^2, то распределение их суммы будет стремиться к нормальному при количестве слагаемых, стремящемуся к бесконечности. При этом математическое ожидание итогового распределения будет близко к Nmu, а дисперсия к Nsigma^2, где N – число слагаемых. Таким образом при достаточно большом количестве одновременно работающих виртуальных машин можно считать, что общая нагрузка на дисковое хранилище подчиняется нормальному распределению, а его математическое ожидание и дисперсия вычисляются на основе первоначальных экспериментальных данных, полученных с одной машины. Для вычисления этих величин служит скрипт musigma.rb.

Для нормального распределения известна еще одна замечательная вещь, называемая правилом трех сигм. Согласно ему для нормально распределенной величины более 99.7% ее значений лежат на отрезке [mu - 3sigma; mu + 3sigma]. На основе этого правила и делается оценка.

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

Хорошо видно, как с ростом числа клиентов ЦПТ выполняется все лучше и лучше, хотя и видны некоторые расхождения в определении математического ожидания (пика функции плотности вероятности), связанные то ли с погрешностью вычислений, то ли работой симулятора, то ли еще с чем. Но в целом теория хорошо согласуется с практикой. Для 100 клиентов правило трех сигм покрывает 99.2% запросов на запись, для 1000 – 99.7%.

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

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

Допустим, что мы построили хранилище на 100 клиентов с общей производительностью на запись в 2900 iops. Мы считаем, что на запись среднему клиенту больше 400 iops ни к чему . Какова вероятность того, что он столько и получит? Это вероятность равна вероятности того, что все остальные клиенты запросят 2900 – 400 = 2500 iops или меньше. А эту вероятность можно посмотреть на уже известном нам графике распределения вероятностей.

Видно, что вероятность такого события близка к 1, а точнее (если заглянуть в файл с данными для графика) равна 99.93%. Таким образом SLA для клиента можно сформулировать следующим образом: «Не менее 99.93% запросов на запись, не превышающих 400 iops, будет выполнено». Собственно данный график и есть SLA. На нем видно, что если клиент захочет 1500 iops на запись, то остаток будет равен 2900 – 1500 = 1400, вероятность такого события близка к нулю. А с вероятностью 50% клиент может получить 2900 – 1700 = 1200 iops. Аналогично делается расчет для операций чтения.

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

В заключение вернемся к вопросу о нормировке. Разумно предположить, что занимаемая виртуальной машиной память, потребляемая процессорная мощность и ее дисковая активность жестко связаны. Таким образом, задав любую из этих величин, можно получить две оставшиеся. Это позволяет эффективно распределять ресурсы между клиентами в условиях их ограниченности. То есть на каждые X Гб ОЗУ клиенту полагается Y% процессорной мощности и Z iops. Такой подход не универсален, так как есть клиенты, которым, например, нужно очень много памяти и мало iops. Но такие клиенты не выгодны, так как они сильно фрагментируют ресурсы облака. Поэтому в этих случаях им просто приходится платить больше, оплачивая неиспользуемые iops или CPU. Собственно такая методика является общепринятой в хостинге.

Сервер, с которого мы изначально снимали статистические данные, можно считать хорошо спроектированным и настроенным, т.е. в нем соотношение ОЗУ и iops оптимально. Этот факт позволяет масштабировать полученные данные по памяти как вверх, так и вниз. Моделируемый нами сервер занимает 16 Гб ОЗУ. Значит, на хосте виртуализации с 96 Гб ОЗУ можно разместить 96 / 16 = 6 таких машин (памятью для привилегированного домена пренебрегаем). Допустим, мы строим небольшое облако для высоконагруженных веб-проектов на 40 хостов. Общее число виртуальных машин получается 40 * 6 = 240. Математическое ожидание и дисперсия для одной машины, вычисленные на основании экспериментальных данных, равны соответственно mu = 21, sigma^2 = 197 для чтения и mu = 17, sigma^2 = 327 для записи. Значит, для 240 машин эти значения будут mu = 240 cdot 21= 5040, sigma^2 = 240 cdot 197 = 47280 для чтения и mu = 240 cdot 17 = 4080, sigma^2 = 240 cdot 327 = 78480 для записи. По правилу трех сигм более 99.7% значений на чтение не превышает 5040 + 3 cdot sqrt{47280} = 5694, а на запись 4080 + 3 cdot sqrt{78480} = 4923. Получается, что для комфортной работы клиентов в нашем облаке нам нужно хранилище, дающее одновременно 6000 iops на чтение и 5000 iops на запись. Это примерно 120 SAS дисков с учетом зеркалирования данных или 5 4-юнитовых массивов по 24 диска.

Автор: Disorder