Загружаем научный разбор
Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.
Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.
Автор: Казачкин Даниил Михайлович · Обновлено
Историческая архитектура Borg, размещение по нескольким ресурсам и собственный перебор назначений: плотность, фрагментация и разделение реплик.
На панели кластера видны свободные CPU и память, но новая задача остаётся в очереди. Разработчик увеличивает общий пул серверов, хотя нужные ресурсы уже существуют. Причина может быть в том, что они находятся на разных машинах: свободную память одного узла нельзя автоматически соединить с процессором другого для запуска одного обычного процесса.
Размещение — это проверка совместимости задачи с конкретным местом, а не только сложение ресурсов кластера. К CPU и памяти добавляются архитектура, доступность данных, допустимое соседство и устойчивость к отказам. Исследование Borg помогает увидеть, как эти требования превращаются в отдельные решения управляющей системы. Ниже мы разберём историческую работу и проведём маленький собственный эксперимент с точным перебором.
В статье Verma и соавторов, EuroSys 2015 представлен промышленный менеджер кластеров Google. Он принимает декларативные задания, размещает задачи, наблюдает их состояние и восстанавливает выполнение после отказов. В описанной архитектуре центральный управляющий компонент взаимодействует с агентами на машинах. Решение о размещении разделено на поиск допустимых узлов и оценку подходящих кандидатов.
Авторы анализируют плотность размещения, совместное использование машин, резервирование и использование незанятых ресурсов менее приоритетными задачами. Помимо опыта эксплуатации, работа содержит количественные эксперименты с политиками и симуляцией. Эти наблюдения показывают компромиссы конкретной системы и нагрузки; они не устанавливают единственный лучший планировщик для всех кластеров. Полный текст сопровождается исправлениями, которые важно учитывать при чтении графиков. Первичная публикация Borg.
Статья является историческим источником идей. Текущие правила Kubernetes нужно смотреть отдельно: сходство терминов не означает одинаковых объектов API, алгоритмов и поведения при отказах. Официальная документация kube-scheduler также различает фильтрацию и оценку узлов, но конкретная настройка относится уже к Kubernetes.
Для собственного учебного кластера сначала спросим: разрешено ли пользователю заказать эту работу? Это вопрос квоты и допуска. Затем: существует ли машина, удовлетворяющая запросу? Это вопрос размещения. Наконец: получает ли запущенная работа достаточно ресурсов для своей цели? Это вопрос выполнения. Положительный ответ на первый не гарантирует положительный ответ на второй, а успешный запуск не гарантирует нужную задержку обработки.
Пусть задача заявляет два ядра и четыре гигабайта памяти. На первом узле свободно четыре ядра и ноль памяти, на втором — ноль ядер и восемь гигабайт. Сумма ресурсов достаточна, но задача не помещается никуда. Это фрагментация по нескольким измерениям. Сообщение «в кластере свободно четыре ядра» не показывает её причину.
Ограничения могут быть и качественными. Узел подходит по памяти, но имеет другую архитектуру CPU; рядом уже находится реплика того же сервиса; нужный диск доступен в другом месте. При диагностике удобно хранить причины отклонения каждого кандидата. Тогда ожидание в очереди превращается из общего симптома в конкретное противоречие между требованиями и доступными местами.
Создадим три одинаковых узла по четыре условных ядра и восемь единиц памяти. Есть две реплики веб-сервиса и один пакетный работник. Первый вариант разрешает размещать реплики вместе, второй требует разных узлов. Все числа и правила принадлежат этому примеру; код не реализует планировщик Borg. На Python 3 достаточно стандартного itertools.
from itertools import product
capacity = (4, 8)
jobs = [(1, 2), (1, 2), (2, 4)] # web-a, web-b, batch
node_count = 3
def fits(assignment, separate_replicas):
if separate_replicas and assignment[0] == assignment[1]:
return False
for node in range(node_count):
used = [sum(job[d] for job, host in zip(jobs, assignment)
if host == node) for d in range(2)]
if any(used[d] > capacity[d] for d in range(2)):
return False
return True
def best(separate_replicas):
placements = (assignment for assignment
in product(range(node_count), repeat=len(jobs))
if fits(assignment, separate_replicas))
return min(placements, key=lambda a: (len(set(a)), a))
packed = best(False)
spread = best(True)
assert len(set(packed)) == 1
assert len(set(spread)) == 2
remaining = [(4, 0), (0, 8)]
request = (2, 4)
assert all(sum(r[d] for r in remaining) >= request[d] for d in range(2))
assert not any(all(r[d] >= request[d] for d in range(2)) for r in remaining)
print('packed:', packed, 'spread:', spread)Получатся назначения (0, 0, 0) и (0, 1, 0). Оба соблюдают ресурсные ограничения. Первое использует одну машину, второе — две. Если узел 0 потерян, в первом варианте исчезнут обе веб-реплики; во втором одна останется. Мы заплатили дополнительной занятой машиной за выбранное свойство размещения. Это ещё не доказательство доступности сервиса: обе машины могут зависеть от одного сетевого коммутатора или диска.
Последние утверждения в коде отдельно подтверждают пример фрагментации. Они нужны потому, что минимум числа узлов и возможность запустить новую задачу — разные вопросы. План, оптимальный для текущих трёх работ, не обязан быть удобным для четвёртой, которая поступит позже.
При n задачах и m машинах наш перебор рассматривает m в степени n назначений. Уже двадцать задач на десяти узлах делают такой подход непрактичным. Реальная управляющая система должна принимать решения при постоянном поступлении работ, изменении состояния и ограниченном времени. Поэтому качество размещения оценивают вместе с ценой самого принятия решения.
Кроме того, наш алгоритм знает весь набор работ заранее. В потоке первый выбор приходится делать без знания будущего. Перемещение ранее размещённых задач тоже стоит времени: потребуется остановка, восстановление, прогрев кеша или перенос состояния. Нельзя считать новое красивое распределение бесплатным. Для своей модели можно добавить штраф за изменение узла и увидеть, как это влияет на выбор.
Метрика «минимум занятых машин» полезна для оценки плотности, но недостаточна для эксплуатации. Отдельно нужны время ожидания в очереди, число отклонений, число перемещений и потерянная работа при отказах. Если эксперимент уменьшил число серверов ценой многократного роста ожидания приоритетных задач, его нельзя описать одним процентом экономии.
Представим, что все пользователи нашего кластера запрашивают восемь единиц памяти «на всякий случай», а обычно потребляют две. Планировщик, верящий заявкам, будет резервировать значительный объём. Если вместо этого ориентироваться только на вчерашнее потребление, одновременный пик может превысить вместимость. Между этими крайностями необходима политика: какие ресурсы можно временно предоставить соседям и кого остановить при дефиците.
CPU и память при этом не симметричны. Работе часто можно дать меньше процессорного времени ценой задержки; нехватка памяти способна завершить процесс. Поэтому одно правило «разрешить превышение на 20%» для всех ресурсов скрывает разные последствия. В собственной оценке фиксируйте, чем оплачивается плотность: ожиданием, вытеснением, повторным запуском или потерей прогресса.
Добавим ещё один инженерный вопрос: как приложение переживает повторный запуск? Если пакетная работа сохраняет контрольные точки, вытеснение может быть приемлемым. Если она отправляет внешний платёж без защиты от повторения, запуск второй копии требует других гарантий. Планировщик способен возобновить процесс, но семантика внешнего действия остаётся задачей приложения.
Добавьте четвёртую задачу с запросом (3, 6) и сравните решения. Затем ограничьте доступные узлы двумя. Программа должна явно сообщать об отсутствии решения вместо неясной ошибки min на пустом наборе. После этого назначьте узлам зоны отказа и потребуйте, чтобы веб-реплики находились в разных зонах. Объясните, почему разные имена машин сами по себе не выполняют это требование.
Чтобы связать размещение с повседневной разработкой, пройдите [трек Docker](/lessons/without-university/docker-containerization) и [урок Linux о процессах и службах](/lessons/without-university/linux-developer-foundations/linux-developer-11). Контейнер задаёт удобную единицу запуска, но планировщику всё ещё нужны честные требования к ресурсам, сигнал готовности и понятное поведение при остановке. Именно эти свойства превращают упаковку программы в управляемую нагрузку.
Самостоятельный русскоязычный разбор ЯдроКода по указанным первичным источникам. Учебные модели, примеры и выводы редакции отделены от результатов исследований. Материал не является переводом или перепечаткой.
Текст, учебные данные и Python-примеры созданы для этой публикации. Чужие программные реализации, таблицы, схемы и иллюстрации не воспроизводятся.