RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

Ansible実行を1/3に高速化した話

こんにちは、弊社サービスのインフラを運用している id:keijiu (ijikeman)です。

今回は、「Ansible実行を1/3に高速化した話」を記載します。

目次

背景

3年ほど前よりAnsible構成管理を推進を開始し、ようやくラクスの各商材のAnsible化の割合が大幅に増えました。
構成管理対象サーバの増加と構成管理範囲が増えるに伴い、Ansibleの実行完了までの時間が大幅に増えていきました。

また、インフラメンバー各自がAnsibleコードを記載し、テスト実行(--check)等をする回数も増えこの「実行時間の長時間化」が問題となりました。
この問題の改善にあたり、調査・検証し、実際に実行時間を大幅に削減した内容を公開させていただきます。

1. 実行時間の把握

まずはAnsibleの全体の実行時間や各タスク毎の実行時間の把握を行い、時間がかかっている処理がどの処理なのかを把握することが重要です。
各タスクの中で時間がかかりすぎているものを把握することで、処理や環境の見直しによって改善できるものがないかを確認します。

1-1. Callbackプラグイン[profile_tasks]を有効にする

各タスクの実行に掛かった実行時間を表示するために、callbackプラグインprofile_tasksを有効化します。

$ /etc/ansible/ansible.cfg
---
[defaults]
callback_whitelist = profile_tasks
---

もしくは
実行時にANSIBLE_CALLBACK_WHITELIST='profile_tasks'を設定

ANSIBLE_CALLBACK_WHITELIST='profile_tasks' ansible-playbook ...

1-2. CallBack Plugin有効化の確認

下記の様に各タスクの実行時間が表示され、実行完了後には時間がかかったタスクのTOP20までがリスト表示される様になります。

...

RUNNING HANDLER [os : Install Epel.repo] *****************************************
Sunday 05 June 2022  19:33:32 -0400 (0:00:01.327)  0:00:01.356 **********
skipping: [192.168.0.1]

PLAY RECAP *********************************************************************
192.168.0.1  : ok=34 changed=1 unreachable=0  failed=0  skipped=3 rescued=0  ignored=0

Thursday 05 May 2022  21:09:47 -0400 (0:00:00.058)       0:00:14.804 **********
===============================================================================
os : Install Epel.repo ------------------------------------------ 1.66s
os : Install python module for SELinux -------------------------- 1.12s
Gathering Facts ------------------------------------------------- 1.07s

2. Ansibleコードチューニング

2-1. 各タスク実行時間の確認

では、実行時間の把握ができるようになったところで、実際のPlaybookを実行しました。
未セットアップのCentOSに対するPlaybookの実行結果は以下のようになりました。(一部伏字)
圧倒的にパッケージのインストール時間が大半でしたので、まずはパッケージインストール処理のコードを確認してみます。
すると約6割がパッケージインストール処理でした。

Tuesday 07 June 2022  01:14:14 -0400 (0:00:03.472)       0:05:20.110 **********
common : Install Packages --------------------------------------------- 190.71s
common : Copy Scripts ------------------------------------------------- 14.21s
os : Server Reboot ------------------------------------------------------- 11.57s
os : Server Reboot ------------------------------------------------------- 9.78s
os : Install python module ----------------------------------------------- 8.90s
os : Install NetworkManager -------------------------------------------- 6.51s
...

2-2. パッケージインストール処理の見直し

2-2-1. コードの確認

実コードは記載できませんが、サンプルコードで説明すると以下の様な処理になっていました。

- set_fact:
    INSTALL_PACKAGES:
      -
        NAME: 'httpd'
        REPO: 'appstream'
      -
        NAME: 'httpd-devel'
        REPO: 'appstream'
      -
        NAME: 'httpd-tools'
        REPO: 'appstream'
      -
        NAME: 'httpd-manual'
        REPO: 'appstream'

- name: Install Packages
  yum:
    name: "{{ item.NAME }}"
    state: 'installed'
    enablerepo: '{{ item.REPO }}'
  with_items: "{{ INSTALL_PACKAGES }}"

実際にこのコードを実行すると以下のようになります。
パッケージを1つずつインストールしている為、パッケージ対象が増えれば増えるほど毎回yum moduleが呼び出され時間がかかる実装になっています。

--- 実行ログ
TASK [test : Install Packages] ************************************************
Friday 06 May 2022  01:34:02 -0400 (0:00:00.024)       0:00:07.367 ************
changed: [192.168.0.1] => (item={'NAME': 'httpd', 'REPO': 'appstream'})
changed: [192.168.0.1] => (item={'NAME': 'httpd-devel', 'REPO': 'appstream'})
changed: [192.168.0.1] => (item={'NAME': 'httpd-tools', 'REPO': 'appstream'})
changed: [192.168.0.1] => (item={'NAME': 'httpd-manual', 'REPO': 'appstream'})
...

2-2-2. コードの修正

以下ansible公式
ansible.builtin.yum module – Manages packages with the yum package manager — Ansible Documentation

yum moduleのサンプルコードにもあるようにパッケージ名は配列にて渡すことができます。

- name: Install a list of packages with a list variable
  yum:
    name: "{{ packages }}"
  vars:
    packages:
    - httpd
    - httpd-tools

以下様に書き換えを行う「パッケージ名をリスト形式に変更する」ことで、yumモジュールの実行回数を大幅に削減することができます。
実際にこのサンプルコードでも実行時間を約半分にすることができました。

- set_fact:
    INSTALL_PACKAGES:
      -
        NAME:
          - 'httpd'
          - 'httpd-devel'
          - 'httpd-tools'
          - 'httpd-manual'
        REPO: 'appstream'

--- 実行ログ
TASK [test : Install Packages] ************************************************
Friday 06 May 2022  01:36:19 -0400 (0:00:00.023)       0:00:03.246 ************
changed: [192.168.0.1] => (item={'NAME': ['httpd', 'httpd-devel', 'httpd-tools', 'httpd-manual'], 'REPO': 'appstream'})

2-3. 動的書き換えの見直し

ラクスで使っているansibleコードテンプレートでは、汎用的な処理を別のタスクファイルにして、
「include_role」や「include_tasks」で動的に読み込み書き換えることで、コードの再利用性を高める取り組みを行っております。
その為、繰り返し処理を行うと毎回コードの動的書き換えと実行が行われる為、処理時間が伸びていきます。

2-3-1. コードの確認

以下のように汎用的な処理を別のロールに集めており

$ roles/libraries/tasks/copy.yml
---
- name: FILE COPY
  copy:
    src: "{{ item.SRC }}"
    dest: "{{ item.DEST }}"
    owner: "{{ item.OWNER }}"
    group: "{{ item.GROUP }}"
    mode: "{{ item.MODE }}"

呼び出し元からinclude_role等で呼び出して利用しています。

$ roles/test/tasks/main.yml
---
- name: Copy files
  include_role:
    name: libraries
    tasks_from: copy.yml
  with_items:
    - { SRC: 'test1', DEST: '/tmp/test1', OWNER: 'root', GROUP: 'root', MODE: '0644' }
    - { SRC: 'test1', DEST: '/tmp/test2', OWNER: 'root', GROUP: 'root', MODE: '0644' }
    - { SRC: 'test1', DEST: '/tmp/test3', OWNER: 'root', GROUP: 'root', MODE: '0644' }
    - { SRC: 'test1', DEST: '/tmp/test4', OWNER: 'root', GROUP: 'root', MODE: '0644' }
    - { SRC: 'test1', DEST: '/tmp/test5', OWNER: 'root', GROUP: 'root', MODE: '0644' }
    - { SRC: 'test1', DEST: '/tmp/test6', OWNER: 'root', GROUP: 'root', MODE: '0644' }

実行すると、毎回copy.ymlが呼ばれる毎に処理が行われる為、毎回TASKが呼ばれます。

TASK [Copy files] ******************** Monday 06 June 2022  02:00:31 -0400 (0:00:01.356)       0:00:01.386 

TASK [libraries : FILE COPY] ********* Monday 06 June 2022  02:00:31 -0400 (0:00:00.087)       0:00:01.474
changed: [192.168.0.1]

TASK [libraries : FILE COPY] ********** Monday 06 June 2022  02:00:33 -0400 (0:00:01.099)       0:00:02.573
changed: [192.168.0.1]

TASK [libraries : FILE COPY] ********** Monday 06 June 2022  02:00:33 -0400 (0:00:00.910)       0:00:03.484
changed: [192.168.0.1]

TASK [libraries : FILE COPY] ********** Monday 06 June 2022  02:00:34 -0400 (0:00:00.869)       0:00:04.354
changed: [192.168.0.1]

TASK [libraries : FILE COPY] *********** Monday 06 June 2022  02:00:35 -0400 (0:00:00.889)       0:00:05.243
changed: [192.168.0.1]

TASK [libraries : FILE COPY] ********* Monday 06 June 2022  02:00:36 -0400 (0:00:00.854)       0:00:06.098
changed: [192.168.0.1]

PLAY RECAP *********************************************
192.168.0.1             : ok=7    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Monday 06 June 2022  02:00:37 -0400 (0:00:00.830)       0:00:06.928 *********** 
===================================================
Gathering Facts --------------- 1.36s
libraries : FILE COPY ---------- 1.10s
libraries : FILE COPY ---------- 0.91s
libraries : FILE COPY ---------- 0.89s
libraries : FILE COPY ---------- 0.87s
libraries : FILE COPY ---------- 0.85s
libraries : FILE COPY ---------- 0.83s
Copy files --------------------- 0.09s

2-3-2. コードの修正

動的書き換え(include_*)を使わずに静的処理に変更します。

$ roles/test/tasks/main.yml
---
- name: Copy files
  copy:
    src: "{{ item.SRC }}"
    dest: "{{ item.DEST }}"
    owner: "{{ item.OWNER }}"
    group: "{{ item.GROUP }}"
    mode: "{{ item.MODE }}"
  with_items:
    - { SRC: 'test1', DEST: '/tmp/test1', OWNER: 'root', GROUP: 'root', MODE: '0644' }
    - { SRC: 'test1', DEST: '/tmp/test2', OWNER: 'root', GROUP: 'root', MODE: '0644' }
    - { SRC: 'test1', DEST: '/tmp/test3', OWNER: 'root', GROUP: 'root', MODE: '0644' }
    - { SRC: 'test1', DEST: '/tmp/test4', OWNER: 'root', GROUP: 'root', MODE: '0644' }
    - { SRC: 'test1', DEST: '/tmp/test5', OWNER: 'root', GROUP: 'root', MODE: '0644' }
    - { SRC: 'test1', DEST: '/tmp/test6', OWNER: 'root', GROUP: 'root', MODE: '0644' }

Ansibleを実行するとわずかですが、実行時間が短くなりました。
書き換えを行う回数が増えるほど効果は高くなりますが、変更による効果はそれほど高くない為、「コードの再利用性」とどちらがよいかはで使い分ける必要がありそうです。

TASK [test : Copy files] ***********************************
Monday 06 June 2022  02:01:26 -0400 (0:00:01.413)       0:00:01.437 *********** 
changed: [192.168.0.1] => (item={'SRC': 'test1', 'DEST': '/tmp/test1', 'OWNER': 'root', 'GROUP': 'root', 'MODE': '0644'})
changed: [192.168.0.1] => (item={'SRC': 'test1', 'DEST': '/tmp/test2', 'OWNER': 'root', 'GROUP': 'root', 'MODE': '0644'})
changed: [192.168.0.1] => (item={'SRC': 'test1', 'DEST': '/tmp/test3', 'OWNER': 'root', 'GROUP': 'root', 'MODE': '0644'})
changed: [192.168.0.1] => (item={'SRC': 'test1', 'DEST': '/tmp/test4', 'OWNER': 'root', 'GROUP': 'root', 'MODE': '0644'})
changed: [192.168.0.1] => (item={'SRC': 'test1', 'DEST': '/tmp/test5', 'OWNER': 'root', 'GROUP': 'root', 'MODE': '0644'})
changed: [192.168.0.1] => (item={'SRC': 'test1', 'DEST': '/tmp/test6', 'OWNER': 'root', 'GROUP': 'root', 'MODE': '0644'})

PLAY RECAP *********************************************************************
192.168.0.1            : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Monday 06 June 2022  02:01:31 -0400 (0:00:05.252)       0:00:06.689 *********** 
===================================================
test : Copy files --------------------5.25s
Gathering Facts -------------------1.41s

3. Ansible実行環境チューニング

3-1. 並列実行数の変更

Ansibleの標準設定では並列で実行するターゲット数が"5"と設定されている為、多くのターゲットに対して実行する場合に時間がかかります。
その為、並列実行数の変更を行います。
FORKS設定はAnsibleの子プロセスを増やす設定の為、増やせば増やすほどCPUへの負荷がかかります。
各CPUの使用率を確認し、不足しているようであればCPU数を増やすことを検討してください。

$ /etc/ansible/ansible.cfg
---
[defaults]
forks = 5

もしくは
実行時にANSIBLE_FORKS=NUMを設定

ANSIBLE_FORKS=20 ansible-playbook ...

3-2. Pipelining

ansibleのデフォルト設定の場合、各タスクを実行する度に以下の例の様にターゲット側の"~{ansible実行ユーザのホームディレクトリ}/.ansible/"にpythonプログラムの展開を行いAnsibleを実行しています。
その為、ターゲットへのデータ転送時間とこれらのファイルの書き込み等により、各タスクの実行に時間がかかります。

/home/ansible_user/.ansible/ansible-tmp-1651804170.1883245-2652826-75480101479031/
合計 120
drwx------. 2 root root     31  5月  5 22:28 .
drwx------. 3 root root     68  5月  5 22:28 ..
-rw-------. 1 root root 119155  5月  5 22:28 AnsiballZ_stat.py

Pipeliningの有効化

Pipeliningを有効化することで、sshのpipeを経由してansibleを実行することで高速化を行うことができます。

$ /etc/ansible/ansible.cfg
---
[ssh_connection]
pipelining = True

※設定を記載するセクションは[defaults]ではなく[ssh_connection]であることに注意してください

もしくは
実行時にANSIBLE_PIPELINING=True か ANSIBLE_PIPELINING=1を設定

ANSIBLE_PIPELINING=1 ansible-playbook ...

公式ドキュメントAnsible Configuration Settings — Ansible Documentationにもある通り、
requrettyが設定されているユーザでansibleを実行する場合は、sudo設定にて無効にする必要がある為注意が必要です。

Pipelining有効化の確認

Ansible実行時にPipeliningが有効になっているかは、-vvvオプションを付けることで確認することができます。
以下の例の様に"Pipelining is enabled"と出力されていることを確認してください。

...
Pipelining is enabled.
<192.168.0.1> ESTABLISH SSH CONNECTION FOR USER: ansible_user
...

3-3. サードパーティ製 Strategy Plugin (Mitogen for Ansible)

Mitogen — Mitogen Documentation

Other Toolsとして公式で紹介されているMitogen for Ansibleを使うことで、上記Pipeliningよりも実行速度の高速化を行うことができます。
Mitogenは

  • 実行コードのRAMへのキャッシュ
  • pipelineと同様ターゲットへの書き込み削減
  • 従来のPreforkモデルに加えて、Thread化によるさらなる並列化・高速化
  • ネットワークコネクションの再利用等

によりAnsibleの実行を高速化することができます。

Mitogen利用時に気付いた点

Mitogen利用時に以下の事象が確認できましたので、利用するには事前に検証が必要です。

  • 現行v0.2.9ではターゲットサーバ側に"/usr/bin/python"が必要(/usr/bin/python3ではダメ)
  • ansible 2.10以降には未対応(2022/06/13時点でAnsible v2.10以上に対応するv0.3.xは未リリース 2022/06/13追記)
  • ターゲットのpython環境によっては動かないコードがある
  • include_roleは未対応
  • CPU使用率がPipeliningよりかなり高い
  • メモリの使用率が高い
  • Pipeliningとの併用はできない

どなたかv2.10以降でmitogen for Ansibleが動くよというコメントをいただきましたが、公式ニュースに記載されているように

  • v0.2.xは2.10未満
  • v0.3.x系は2.10以上

をサポートという風にバージョンが分かれるようです(2022/06/13追記) Release Notes — Mitogen Documentation

Mitogenの利用方法

■Ansible実行環境にMitogenを設置

$ curl -kL -o https://networkgenomics.com/try/mitogen-0.2.9.tar.gz
$ tar zxvf mitogen-0.2.9.tar.gz -C /opt/

■Strategyプラグインとしてmitogenを設定

$ ansible.cfg
---
[defaults]
strategy_plugins = /opt/mitogen-0.2.9/ansible_mitogen/plugins/strategy

■各playbook毎にstrategyを設定してmitogenを有効化します。(ansible.cfgで全体に適用してもよい)

$ playbook.yml
---
- name: Test Playbook
  strategy: mitogen_linear
  hosts: test-servers
  ...

あるPlaybookの実行時間の差を確認したところ、以下様にかなりの差がありました。

Default状態 ... 0:00:22.399
Pipelining ... 0:00:17.744
Mitogen for Ansible ... 0:00:09.333

3-4. 上記実行環境毎の実行速度の計測比較結果

検証としてターゲットサーバ20台に対し、ディレクトリ10個作成する処理を行いました。
それぞれの環境毎に5回実行し、一番早い時間と一番遅い時間を除く3回の平均時間を記しています。

[検証結果]

AnsibleサーバCPU数FORK数CPU使用率平均実行時間コメント
[Default]
1520 - 40%01:39
11020 - 70%01:05
12030 - 100%00:42CPUが100%となる場合があり処理待ちが発生
2510 - 20%01:361 vCPUでもCPUに余裕があった為、CPU追加効果なし
21020 - 40%01:011 vCPUでもCPUに余裕があった為、CPU追加効果なし
22015 - 80%00:36CPU 100%が解消され処理時間が短縮
[Pipelining]
1530 - 40%00:40Pipeliningの効果が高いが若干CPU使用率が上昇
11030 - 70%00:28Pipeliningの効果が高いが若干CPU使用率が上昇
12030 - 100%00:21Pipeliningの効果が高いが若干CPU使用率が上昇
2515 - 30%00:381 vCPUでもCPUに余裕があった為、CPU追加効果なし
21020 - 40%00:261 vCPUでもCPUに余裕があった為、CPU追加効果なし
22030 - 50%00:17CPU 100%が解消され処理時間が短縮
[Mitoge]
15100%00:18Pipeliningよりさらに効果が高いがCPU使用率がかなり上昇
110100%00:16Pipeliningよりさらに効果が高いがCPU使用率がかなり上昇
120100%00:16Pipeliningよりさらに効果が高いがCPU使用率がかなり上昇
2525 - 50%00:19CPUに余裕がでたが、CPU追加効果なし
21040 - 60%00:16CPUに余裕がでたが、CPU追加効果なし
22030 - 70%00:14SWAPが発生
2+2GBメモリ2050%00:14メモリを追加することでSWAPが解消

実行速度検証総括

  • Pipelining
    • Ansible標準機能として実装されている為、手軽に利用が可能で効果が高い為、標準で利用できるようにしておくとよい。
    • Ansible実行側のCPU使用率の上昇が若干見られるが、実行環境の大幅な見直しは必要ない。
  • 並列実行数(ANSIBL_FORKS)
    • 並列実行数を上げて実行する場合はAnsible実行側のCPU使用率を確認して引き上げる必要がある。
  • Mitogen for Ansible
    • CPUの使用率が高い為、CPUの割り当て数に注意が必要。
    • 並列実行数を増やすAnsible実行サーバのメモリ使用量が上がる為、メモリの割り当てに注意が必要。
    • 初期導入作業が必要だが、Pipelineよりも効果が高い。

4. 高速化の結果

最終的に
Mitogen Plugin及び上記コードの改修等を進め
以下の様に

1/3に高速化することができました!!

■改修前
Tuesday 07 June 2022  01:14:14 -0400 (0:00:03.472)       0:05:20.110 ********** 
■改修後
Tuesday 07 June 2022  01:04:01 -0400 (0:00:02.327)       0:01:34.366 **********

終わり


◆TECH PLAY
techplay.jp

◆connpass
rakus.connpass.com

Copyright © RAKUS Co., Ltd. All rights reserved.