Разберём четыре рычага: forks (сколько параллельных соединений), serial (батчи для rolling update), strategy (как именно гонять задачи по хостам) и порядок плюс мелкие, но важные throttle, run_once, order.
Как Ansible выполняет задачи: forks и параллелизм
По умолчанию Ansible работает не последовательно хост за хостом, а параллельно. Берётся первая задача плея, и она запускается сразу на пачке хостов. Размер этой пачки определяет параметр forks - количество одновременных процессов-соединений. Дефолт скромный: 5. То есть на 100 хостах задача выполнится пятью волнами по 20, даже если железо контроллера тянет больше.
Поднять лимит можно тремя способами. Разово через флаг командной строки:
Код: Выделить всё
ansible-playbook deploy.yml -f 30
Код: Выделить всё
[defaults]
forks = 30
inventory = ./inventory
host_key_checking = False

ansible serial: батчи и rolling update без даунтайма
forks - это про скорость на уровне всего плея. А вот когда задача "не свалить прод" важнее скорости, в дело вступает директива serial на уровне плея. Она режет выполнение на батчи: Ansible полностью прогоняет ВЕСЬ плей (все задачи и хендлеры) на первой группе хостов, и только потом берётся за следующую. Это и есть классический ansible rolling update - выкатка волнами.
Код: Выделить всё
---
- name: Rolling update веб-фермы
hosts: webservers
serial: 2
tasks:
- name: Вывести хост из балансировщика
ansible.builtin.command: /usr/local/bin/lb-drain {{ inventory_hostname }}
- name: Обновить пакет приложения
ansible.builtin.dnf:
name: myapp
state: latest
- name: Перезапустить сервис
ansible.builtin.systemd:
name: myapp
state: restarted
- name: Вернуть хост в балансировщик
ansible.builtin.command: /usr/local/bin/lb-undrain {{ inventory_hostname }}
Код: Выделить всё
serial:
- 1
- 5
- "30%"
Важная связка - max_fail_percentage. По умолчанию Ansible останавливает плей, как только в текущем батче упал хоть один хост (точнее, когда падают ВСЕ хосты батча - но с serial батчи маленькие, и это срабатывает быстро). С max_fail_percentage ты задаёшь порог терпимости:
Код: Выделить всё
- name: Безопасная выкатка с порогом ошибок
hosts: webservers
serial: 10%
max_fail_percentage: 30
tasks:
- ansible.builtin.dnf:
name: myapp
state: latest
ansible strategy: linear, free и host_pinned
Теперь самое тонкое. Параметр strategy управляет тем, как Ansible синхронизирует хосты между задачами. Значений три.
linear - дефолт. Все хосты батча выполняют задачу №1, Ansible ждёт, пока КАЖДЫЙ закончит (или упадёт), и только тогда все вместе идут на задачу №2. Это барьерная синхронизация: самый медленный хост тормозит всю группу на каждом шаге. Зато предсказуемо и удобно для отладки.
free - барьер снимается. Каждый хост несётся по списку задач в своём темпе, не дожидаясь соседей. Быстрый сервер может уже доделать весь плей, пока тормозной всё ещё на третьей задаче. Ускоряет выполнение на разнородном парке, но порядок завершения непредсказуем, и логика "сначала все обновились, потом все рестартят" ломается.
host_pinned - компромисс. Хост, попавший в слот (слотов столько, сколько forks или сколько разрешает serial), проходит задачи без ожидания остальных, как во free. Но как только он закончил весь плей, его слот освобождается и занимает следующий хост из очереди. Удобно, когда важно держать ровно N хостов в работе одновременно.
Задаётся на уровне плея:
Код: Выделить всё
- name: Быстрая независимая настройка
hosts: all
strategy: free
tasks:
- ansible.builtin.dnf:
name: chrony
state: present
- ansible.posix.firewalld:
service: http
permanent: true
state: enabled
immediate: true
Точечный контроль: order, throttle и run_once
order - в каком порядке Ansible обходит хосты внутри плея. По умолчанию inventory - как они идут в инвентаре. Доступны: inventory, reverse_inventory, sorted (по алфавиту), reverse_sorted и shuffle (случайно каждый раз). shuffle полезен, чтобы не долбить всегда первым один и тот же хост; sorted - когда хочешь детерминированности независимо от порядка в файле.
Код: Выделить всё
- name: Обход хостов по алфавиту
hosts: dbservers
order: sorted
serial: 1
Код: Выделить всё
- name: Дёрнуть капризный внешний API
ansible.builtin.uri:
url: https://api.internal/register
method: POST
throttle: 5
Код: Выделить всё
- name: Накатить миграции один раз
ansible.builtin.command: /opt/myapp/bin/migrate
run_once: true
delegate_to: "{{ groups['webservers'][0] }}"
Типичные грабли
- Рестарт сервиса без serial. Самая дорогая ошибка - снести сервис на всём парке одновременно. Любая выкатка с рестартом за балансировщиком должна идти через serial.
- serial внутри задачи. serial - директива ПЛЕЯ, не задачи. Засунешь в task - получишь ошибку парсинга. То же касается strategy и order.
- Думать, что forks=100 ускорит всё. Контроллер с 2 ядрами на сотне форков начнёт свопиться и работать медленнее, чем на двадцати. Меряй.
- free там, где нужен порядок. При strategy: free хост может дойти до "рестарт" раньше, чем все прошли "обнови конфиг". Для согласованных выкаток оставляй linear.
- Забыть max_fail_percentage. С большим serial без порога плей может укатать половину парка, прежде чем заметит, что новый пакет битый.
- run_once без delegate_to. run_once выполнит задачу на ПЕРВОМ хосте текущего батча - какой именно это хост, зависит от order. Если нужна конкретная машина (например, мастер БД), фиксируй её через delegate_to.
Мини-лаба
Подними 4-5 тестовых хостов (хоть контейнеры, хоть ВМ) в группе webservers и руками прощупай:
- Запусти любой плей с -f 1 и с -f 5, сравни время. Почувствуй, что такое forks.
- Поставь serial: 1 на плей с тремя задачами и посмотри в выводе, как Ansible полностью проходит весь плей на одном хосте, прежде чем взяться за второй.
- Сделай serial: [1, "50%"] и убедись, что первый батч - один хост, а дальше половина от остатка.
- Переключи strategy с linear на free на плее с разными по скорости задачами (добавь искусственный ansible.builtin.pause или command sleep на один хост) и посмотри, как ломается синхронный порядок.
- Добавь задачу с throttle: 1 и run_once: true, проверь по выводу, что одна идёт строго по одному хосту за раз, а вторая - вообще единожды.
- Поиграй с order: shuffle и order: sorted, запусти пару раз и сравни порядок хостов в выводе.
- Чем forks отличается от serial - что из них про скорость, а что про безопасность выкатки, и на каком уровне (плей/задача) задаётся каждый?
- Ты катишь обновление на 40 серверов и хочешь сначала проверить на одном, потом на пяти, потом по четверти. Как выглядит директива serial?
- В чём практическая разница между strategy: linear и strategy: free, и в каком случае free тебя укусит?
- Зачем нужны throttle и run_once, и почему run_once часто пишут вместе с delegate_to?
forks решает, сколько хостов Ansible трогает разом ради скорости. serial режет плей на батчи ради безопасных rolling-выкаток, а max_fail_percentage страхует от выката битого релиза на весь парк. strategy (linear, free, host_pinned) определяет, ждать ли хосты друг друга между задачами. throttle, run_once и order - точечные рычаги для особых случаев. Запомни главное правило прода: любой рестарт за балансировщиком - только через serial. Для RHCE держи в голове, что serial и strategy живут на уровне плея, а не задачи.
Источники по стратегиям и порядку: [Controlling playbook execution: strategies](https://docs.ansible.com/ansible/latest ... egies.html), [host_pinned strategy](https://docs.ansible.com/projects/ansib ... ategy.html), [Index of Strategy Plugins](https://docs.ansible.com/ansible/latest ... ategy.html).