Под капотом d2c.io мы используем Ansible. С его помощью мы создаем виртуальные машины у облачных провайдеров, устанавливаем программное обеспечение, а также управляем Docker-контейнерами с приложениями клиентов.
Ansible – удобный инструмент, который готов к работе почти без настройки. Это возможно благодаря отсутствию агентов (agentless system), поэтому не нужно ничего предустанавливать на обслуживаемые хосты.
Для подключения к хостам в большинстве случаев используется ssh
. Однако оборотной стороной этой медали является определенная медлительность, так как вся логика находится на управляющем сервере и каждую задачу (task) Ansible формирует локально и отправляет на исполнение через SSH-подключение. Затем он принимает результат, анализирует его и переходит к следующему шагу. В статье мы рассмотрим, как можно ускорить работу Ansible.
Начнем с измерений
Нельзя улучшить то, что невозможно измерить, поэтому напишем небольшой скрипт для подсчета времени выполнения.
Для начала создадим тестовый плейбук:
---
- hosts: all
# gather_facts: no
tasks:
- name: Create directory
file:
path: /tmp/ansible_speed
state: directory
- name: Create file
copy:
content: SPEED
dest: /tmp/ansible_speed/speed
- name: Remove directory
file:
path: /tmp/ansible_speed
state: absent
А теперь напишем скрипт для подсчета времени выполнения:
#!/bin/bash
# calculate the mean average of wall clock time from multiple /usr/bin/time results.
# credits to https://stackoverflow.com/a/8216082/2795592
cat /dev/null > time.log
for i in `seq 1 10`; do
echo "Iteration $i: $@"
/usr/bin/time -p -a -o time.log $@
rm -rf /home/ubuntu/.ansible/cp/*
done
file=time.log
cnt=0
if [ ${#file} -lt 1 ]; then
echo "you must specify a file containing output of /usr/bin/time results"
exit 1
elif [ ${#file} -gt 1 ]; then
samples=(`grep --color=never real ${file} | awk '{print $2}' | cut -dm -f2 | cut -ds -f1`)
for sample in `grep --color=never real ${file} | awk '{print $2}' | cut -dm -f2 | cut -ds -f1`; do
cnt=$(echo ${cnt}+${sample} | bc -l)
done
# Calculate the 'Mean' average (sum / samples).
mean_avg=$(echo ${cnt}/${#samples[@]} | bc -l)
mean_avg=$(echo ${mean_avg} | cut -b1-6)
printf "\tSamples:\t%s \n\tMean Avg:\t%s\n\n" ${#samples[@]} ${mean_avg}
grep --color=never real ${file}
fi
Запускаем наш тестовый плейбук 10 раз и берем среднее значение.
SSH multiplexing
В локальной сети: было 7.68 с, стало 2.38 с
На удаленных хостах: было 26.64 с, стало 10.85 с
Первое, что нужно проверить – работает ли переиспользование SSH-соединений. Так как все действия Ansible выполняет через SSH, любая задержка при установке соединения существенно замедляет выполнение плейбука (playbook) в целом. По умолчанию, данная настройка в Ansible включена. В конфигурационном файле она выглядит следующим образом:
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
Однако будьте внимательны, если по каким-то причинам вы переопределяете параметр ssh_args
, то необходимо явно указать значения для ControlMaster
и ControlPersist
иначе Ansible про них «забудет».
Проверить работает ли в вашем случае переиспользование SSH-соединений можно следующим образом – запустить Ansible с параметром -vvvv
:
ansible test -vvvv -m ping
В выводе мы должны увидеть запуск SSH с нужными параметрами:
SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s ... -o ControlPath=/home/ubuntu/.ansible/cp/7c223265ce
И далее среди множества отладочной информации:
Trying existing master
Control socket "/home/ubuntu/.ansible/cp/7c223265ce" does not exist
setting up multiplex master socket
Также можно убедиться, что втечение 60 секунд после завершения задачи открытый сокет можно наблюдать в виде файла (в нашем примере /home/ubuntu/.ansible/cp/7c223265ce
).
Внимание: если вы работаете с несколькими идентичными средами с одной управляющей машины (blue/green развертывания, stage/prod), убедитесь, что не выстрелите себе в ногу! Если, к примеру, вы сначала скачали актуальные настройки с прода и следующим шагом хотите обновить тестовую среду новой версией компонентов, а SSH-сокеты остались открытыми от прода, то ой… Так что нужно либо делать ControlPath
у этих сред разный, либо принудительно закрывать мастер-сессии или удалять сокеты перед началом работы с другой средой.
Pipelining
В локальной сети: было 2.38 с, стало 1.96 с
На удаленных хостах: было 10.85 с, стало 5.23 с
По умолчанию Ansible выполняет модули на целевых хостах следующим образом:
- Генерирует Python-файл с модулем и его параметрами для выполнения на целевой машине
- Подключается по SSH, узнает домашнюю директорию пользователя
- Подключается по SSH, создает временную директорию для работы
- Подключается по SSH, по SFTP копирует Python-файл во временную директорию
- Подключается по SSH, запускает Python-файл на целевой машине и удаляет временную директорию
- Получает результат выполнения модуля со стандартного вывода
Учитывая, что весь список выполняется для каждой задачи, издержки набегают значительные. Для ускорения этого процесса в Ansible существует режим pipelining, по аналогии с конвейрером передачи ввода-вывода между командами в Linux. В конфигурационном файле настройка режима выглядит так:
[ssh_connection]
pipelining = true
При использовании режима pipelining Ansible работает следующим образом:
- Генерирует Python-файл с модулем и его параметрами для выполнения на целевой машине
- Подключается по SSH, запускает интерпретатор Python
- Отправляет содержимое Python-файла на стандартный ввод интерпретатора
- Получает результат выполнения модуля со стандартного вывода
Итого: было 4 SSH-подключения и несколько лишних команд, теперь – одно. Ускорение очевидно, особенно для удаленных серверов в WAN.
Чтобы проверить работает ли у вас режим pipelining, нужно запустить Ansible с расширенным протоколированием, например так:
ansible test -vvv -m ping
Если в выводе есть несколько вызовов ssh
вида:
SSH: EXEC ssh ...
SSH: EXEC ssh ...
SSH: EXEC sftp ...
SSH: EXEC ssh ... python ... ping.py
Значит режим конвейера не работает. Если вызов ssh
один:
SSH: EXEC ssh ... python && sleep 0
Значит pipelining работает.
Важно: по умолчанию данная настройка в Ansible отключена, так как может конфликтовать с требованием requiretty
в настройках sudo
. В момент написания этой статьи в самых свежих образах Ubuntu и RHEL в Amazon EC2 requiretty
отключен, поэтому pipelining можно смело использовать.
PreferredAuthentications и UseDNS
В локальной сети: было 1.96 с, стало 1.92 с
На удаленных хостах: было 5.23 с, стало 4.92 с
Если управляете десятками целевых машин, любая мелочь, влияющая на скорость подключения, важна. Одной из таких мелочей могут быть Reverse-DNS запросы на стороне сервера и клиента, особенно если используется не локальный DNS-сервер.
UseDNS
UseDNS — это настройка SSH-сервера (файл /etc/ssh/sshd_config), которая заставляет его проверять PTR-запись к IP-адресу клиента. К счастью, в современных дистрибутивах она отключена по умолчанию. Но на всякий случай её стоит проверить на своих целевых хостах, если первичное SSH-подключение «тормозит».
PreferredAuthentications
Это настройка SSH-клиента, которая информирует сервер о тех способах аутентификации, которые клиент готов использовать. По умолчанию Ansible использует
-o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey
И если на серверах разрешен GSSAPIAuthentication
, как например в образе RHEL в Amazon EC2, то этот режим будет опробован в первую очередь. Это приведет к лишним шагам и попытками проверять PTR-записи со стороны клиента.
Чаще всего нам нужна только аутентификация по ключу, поэтому укажем это явно в ansible.cfg
:
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o PreferredAuthentications=publickey
Такая настройка избавит клиента от лишних переговоров с сервером и ускорит установку master-сессии.
Сбор фактов
В локальной сети: было 1.96 с, стало 1.47 с
На удаленных хостах: было 4.92 с, стало 4.77 с
При выполнении плейбуков (ad-hoc команд это не касается) Ansible по умолчанию собирает факты об удаленной системе. Этот шаг аналогичен запуску модуля setup
и точно также требует отдельного SSH-подключения. В нашем тестовом плейбуке мы не используем ни единого факта, поэтому этот шаг мы можем пропустить, указав параметр плейбука:
gather_facts: no
Если вы часто запускаете плейбуки с использованием фактов, но сбор фактов значительно влияет на скорость исполнения плейбуков, рассмотрите возможность использования внешней системы кэширования фактов (частично об этом рассказано в документации Ansible). К примеру, настраиваете вы Redis, который раз в час наполняете свежими фактами, а во всех рабочих плейбуках принудительный сбор фактов отключаете. Они теперь будут браться из локального кэша.
WAN → LAN
Было 4.77 с, стало 1.47 с
Несмотря на быстрые каналы связи, в локальной сети обычно все работает быстрее, чем в глобальной. Поэтому, если вы часто запускаете плейбуки на множестве серверов, скажем на Amazon EC2 в регионе eu-west-1, то и основной сервер управления резонно разместить на той же площадке. В зависимости от сценария использования, перемещение сервера управления ближе к целевым серверам поможет существенно ускорить выполнение плейбуков.
Pull-режим
Было 1.47 с, стало 1.25 с
Если нужно больше скорости, то можно выполнять плейбук локально на целевом сервере. Для этого существует утилита ansible-pull
. Прочитать о ней можно также в официальной документации Ansible. Работает она следующим образом:
- Клонирует репозиторий в локальную директорию
- Запускает указанный плейбук локально (с паремтром
-c local
) - Если плейбук не указан, пробует запустить:
<fqdn>.yml
<hostname>.yml
local.yml
Один из вариантов использования – по расписанию опрашивать репозиторий и выполнять плейбук, если что-то изменилось (параметр --only-if-changed
).
Fork
В предыдущих разделах мы говорили об ускорении выполнения плейбука на каждом хосте в отдельности. Но если вы запускаете плейбук сразу для десятков серверов, то узким местом может оказаться количество «форков» – отдельных параллельных процессов для выполнения задач на разных серверах. В конфигурационном файле эта настройка выглядит следующем образом:
[defaults]
forks = 20
По умолчанию значение "forks" равно 5, а значит Ansible общается с не более чем пятью хостами одновременно. Зачастую возможности CPU на сервере управления и канал связи позволяет обслуживать большее число хостов одновременно. Оптимальное количество подбирается экспериментальным путем в конкретном окружении.
Еще один момент относительно параллелизма — если количество потоков маленькое, а серверов много, то могут «протухать» мастер-сессии ssh, что приведет к необходимости заново устанавливать полноценную SSH-сессию. Это происходит из-за того, что по умолчанию Ansible использует стратегию выполнения linear
. Он дожидается пока одна задача будет выполнена на всех серверах и лишь затем переходит к следующей задаче. Может так получаться, что «первый» сервер быстро выполнит задачу и потом ждет, пока задача выполнится на всех остальных серверах, так долго, что время ControlPersist
истекает.
Poll Interval
После запуска модуля на целевом сервере, процесс Ansible на машине управления переходит в режим постоянного опроса (polling) на предмет поступления результатов выполнения модуля. То, на сколько агрессивно он это делает, влияет на загрузку CPU на машине управления. А процессорное время нам нужно, чтобы увеличивать количество параллельных процессов (см. предыдущий раздел). Настройка времени ожидания между такими внутренними опросами указывается в секундах в файле настроек:
[defaults]
internal_poll_interval = 0.001
Если нужно запускать на большом количестве хостов «долгоиграющие» задач, результат которых нет смысла проверять так часто, или же на управляющей машине не так много свободного CPU, можно интевал опроса сделать побольше, например, 0.05
.
--
PS: Из основных моментов, пожалуй – всё. Дальнейшую скорость нужно искать уже в оптимизации самих плейбуков, но это уже совсем другая история.