Ускоряем работу Ansible

    Под капотом d2c.io мы используем Ansible. С его помощью мы создаем виртуальные машины у облачных провайдеров, устанавливаем программное обеспечение, а также управляем Docker-контейнерами с приложениями клиентов.


    Ansible – удобный инструмент, который готов к работе почти без настройки. Это возможно благодаря отсутствию агентов (agentless system), поэтому не нужно ничего предустанавливать на обслуживаемые хосты.


    Для подключения к хостам в большинстве случаев используется ssh. Однако оборотной стороной этой медали является определенная медлительность, так как вся логика находится на управляющем сервере и каждую задачу (task) Ansible формирует локально и отправляет на исполнение через SSH-подключение. Затем он принимает результат, анализирует его и переходит к следующему шагу. В статье мы рассмотрим, как можно ускорить работу Ansible.


    Начнем с измерений


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


    Для начала создадим тестовый плейбук:


    test.yml
    ---
    - 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

    А теперь напишем скрипт для подсчета времени выполнения:


    time_test.sh
    #!/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: Из основных моментов, пожалуй – всё. Дальнейшую скорость нужно искать уже в оптимизации самих плейбуков, но это уже совсем другая история.

    • +33
    • 8,4k
    • 5

    D2C.io

    33,00

    Компания

    Поделиться публикацией
    Комментарии 5
      0
      Внимание: если вы работаете с несколькими идентичными средами с одной управляющей машины (blue/green развертывания, stage/prod), убедитесь, что не выстрелите себе в ногу! Если, к примеру, вы сначала скачали актуальные настройки с прода и следующим шагом хотите обновить тестовую среду новой версией компонентов, а SSH-сокеты остались открытыми от прода, то ой… Так что нужно либо делать ControlPath у этих сред разный, либо принудительно закрывать мастер-сессии или удалять сокеты перед началом работы с другой средой.

      Просто используйте разные сокеты, например ControlPath ~/.ssh/master-%r@%h:%p.

        0

        Верно, я и обратил внимание на это: делать ControlPath у этих сред разный.


        В проектах, где для разных сред хосты называются идентично (workerN, dbN, и т.д.), а отличаются лишь IP-адреса bastion-хостов, через которые пробрасываются ssh-подключения – имена сокетов по шаблону ~/.ssh/master-%r@%h:%p как раз не зависят от рабочей среды и инженер может попасть в описанную ситуацию! В таком случае правильнее в inventory для конкретной среды указывать шаблоны с разными префиксами (например, ~/.ssh/prod-%r@%h:%p). Либо (по аналогии с virtual env) делать разное окружения для запуска Ansible – выставлять ANSIBLE_SSH_CONTROL_PATH и выводить значение текущего окружения в prompt – меньше вероятности, что инженер запустит плейбуки не на тех серверах.

        0

        А в данном случае для профилирования profile_roles и profile_tasks callback'и не подходят?

          0

          Callback'и для профилирования скорее нужны для задач поиска узких мест. Мне для теста нужно было общее время + примитивная агрегация между повторными запусками. Для callback'ов (там есть общее время выполнения) все равно пришлось бы делать агрегацию – решил воспользоваться готовой реализацией для стандартной утилиты time. Для хардкорного профилирования (когда нужно отличать, где именно тормозит – в модуле, в соединении или во внутренностях ansible) можно использовать ANSIBLE_DEBUG=1 – тогда весь низкоуровневый вывод маркируется временнЫми штампами.

            0
            Подскажите пожалуйста как лучше добавлять общие модули (например, добавление администраторов) на большое кол-во серверов (около 600) через Ansile?
            Может ли Ansible вкладывать одни модули в другие модули?

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

            Самое читаемое