Вы планируете перенести свои серверы из локального дата-центра в облако Azure? Одним из шагов на этом пути является сопоставление существующих серверов с их аналогами в Azure. Цель — снизить затраты на весь пул серверов, получив при этом максимально возможную производительность. Теоретически мы можем сопоставить исходные ВМ с целевыми, основываясь на количестве виртуальных процессоров и объёме оперативной памяти. Хотя это не самая сложная задача, она не гарантирует наилучшее соотношение производительности и цены. Если всегда выбирать самую дешёвую ВМ, можно потерять в производительности. И наоборот, максимально производительные ВМ могут оказаться слишком дорогими. Есть ли золотая середина? Давайте разбираться.

MiniZinc

Наша отправная точка — MiniZinc. Вот как он описан на их сайте:

MiniZinc — это бесплатный и открытый язык моделирования ограничений.

Вы можете использовать MiniZinc для описания задач удовлетворения ограничений и оптимизации на высоком уровне, независимо от решателя, используя обширную библиотеку предопределённых ограничений. Ваша модель затем компилируется в FlatZinc — язык, понятный широкому кругу решателей.

MiniZinc разрабатывается в Университете Монаша в сотрудничестве с Data61 Decision Sciences и Мельбурнским университетом.

Проще говоря, MiniZinc — это особый декларативный язык программирования для описания и решения задач определённого класса. Он также служит абстрактным уровнем над набором решателей (solvers), позволяя написать код один раз и попробовать решить задачу с помощью разных решателей (solvers).

В нашем случае мы решаем задачу оптимизации с ограничениями, используя MiniZinc.

Что мы собираемся сделать

В этом разделе мы проведём небольшой эксперимент, проанализируем результат и объясним его. Следующий раздел содержит высокоуровневое описание, как всё это работает. Обратите внимание, что для запуска всего этого на Windows-системе, возможно, потребуется скачать и установить MiniZinc. Также убедитесь, что minizinc.exe находится в переменной окружения PATH.

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

Начнём с упрощённого варианта. Наше решение состоит из трёх основных компонентов:

Входные данные включают:

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

В лучшем случае решатель гарантирует, что решение оптимально.

Для запуска импортируем вспомогательные функции:

. .\vm-optimization-minizinc\helperFunctions.ps1

Мы запустим три разных теста:

Простой тест — оптимизация по отдельности

Минимизация стоимости:

Start-MinizincVMOptimizationModel -Costs
totalPrice totalACU vmRecords
---------- -------- ---------
42.4778     9200 {@{sourceVMName=vmN1; sourceVMCPU=2; sourceVMRAM=12; sourceVMDisk=460; selecte…

Максимизация производительности:

Start-MinizincVMOptimizationModel -Performance
totalPrice totalACU vmRecords
---------- -------- ---------
146.496    18860 {@{sourceVMName=vmN1; sourceVMCPU=2; sourceVMRAM=12; sourceVMDisk=460; selecte…

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

Минимизация стоимости при сохранении производительности

Мы используем Azure Compute Unit (ACU) как показатель производительности. Мы получаем его из Azure API и стараемся максимизировать.

В этом тесте сначала минимизируем стоимость, затем оптимизируем производительность:

$ret = Start-MinizincVMOptimizationModel -Costs | Start-MinizincVMOptimizationModel -Performance
$ret
totalPrice totalACU vmRecords
---------- -------- ---------
42.4778     9410 {@{sourceVMName=vmN1; sourceVMCPU=2; sourceVMRAM=12; sourceVMDisk=460; selecte…

Стоимость осталась минимальной, но производительность выросла.

Посмотрим, какие размеры ВМ были выбраны:

$csv = ($ret.vmRecords | ConvertTo-Csv -NoTypeInformation) -join "`n"
$df = [Microsoft.Data.Analysis.DataFrame]::LoadCsvFromString($csv)
[Microsoft.DotNet.Interactive.Kernel]::display($df)
dataframe01

Колонка targetVMACU всегда равна 100. Это и дало прирост.

Можно также использовать HTML-таблицу:

$view = $ret.vmRecords | ConvertTo-Html -Fragment
[Microsoft.DotNet.Interactive.Kernel]::HTML($view) | Out-Display

Минимизация стоимости без потери производительности

Теперь попробуем наоборот — сначала максимизируем производительность, потом минимизируем стоимость:

$ret2 = Start-MinizincVMOptimizationModel -Performance | Start-MinizincVMOptimizationModel -Costs
$ret2
totalPrice totalACU vmRecords
---------- -------- ---------
54.936    18860 {@{sourceVMName=vmN1; sourceVMCPU=2; sourceVMRAM=12; sourceVMDisk=460; selecte…

Цена чуть выше минимальной, но производительность максимальна — это победа!

$csv = ($ret2.vmRecords | ConvertTo-Csv -NoTypeInformation) -join "`n"
$df2 = [Microsoft.Data.Analysis.DataFrame]::LoadCsvFromString($csv)
[Microsoft.DotNet.Interactive.Kernel]::display($df2)
dataframe02

Модель выбрала другие размеры ВМ — это и помогло.

Как это работает

Тем, кто хочет заглянуть под капот — ниже диаграмма:

Главное — это модель. Мы описываем, что хотим найти, а решатель делает всё остальное.

Пример ограничения по RAM:

constraint forall(vm in existingVMs)(
    vmSizeRAM[selectedSize[vm]] >= vmRAM[vm]
);

А по CPU — допускаем 20% меньше:

constraint forall(vm in existingVMs)(
   vmSizeCPU[selectedSize[vm]] >=  vmCPU[vm] * 0.8
);

Целевые функции:

var int: totalPrice = sum(vm in existingVMs)(vmSizePrice[selectedSize[vm]]);
var int: totalACU = sum(vm in existingVMs)( vmSizeACU[selectedSize[vm]] );

Решаемые переменные:

array[existingVMs] of var vmSizes: selectedSize;

Ключевое слово solve определяет, что оптимизировать:

Мы запускаем модель дважды, передавая результат через пайплайн PowerShell.

Пример:

Start-MinizincVMOptimizationModel -Costs | Start-MinizincVMOptimizationModel -Performance

Так мы поочерёдно оптимизируем две функции.