Ansible Playbook で極端にループが遅くなる

はじめに

Red Hat でコンサルタントをしている id:nanodayo です。
タイトル通り、Ansible Playbook で極端にループが遅くなった話となります。
※実案件での知見ですが、案件特有の情報を載せない形であればOKと、お客様の許可を得て記事にしています。

Ansible - Qiita Advent Calendar 2024 - Qiita の12/21 の記事になります(1日遅れ)

結論(わかったこと)

  • include_tasks や flatten を伴うループは、ループ数が多いと極端に遅くなる(測定環境では500ループで5h程度)
  • ループ数100では 3min 程度で実行完了する。(単純な比例ではない)

なお、根本原因は調べておらず、あくまで事象の紹介のみとなります。

背景: 案件で受けた Playbook のパフォーマンス改善の相談

  • ユーザを作成するPlaybook(Role)実行に時間がかかっている(ユーザ数は500くらい・時間は5時間くらい)
  • 実行中、AAPホストのCPU利用率が高くなっている。
  • LDAPなど、ユーザ認証関連での通信はない状態

やったこと

ソフトウェアレベルで再現環境を構築

RPM, pip でバージョン指定でパッケージをインストールし、同一バージョンのソフトウェアが入っている環境を構築しました。(手順は割愛)
他、仮想マシンで環境を構築しましたが、お客様の環境と同等スペックのVMを作成しました。(メモリ・コア数)

お客様と同じ Playbook (Role) で測定

顧客情報のため、詳細は割愛しますが、Role は以下のような作りになります。

  • input 情報は作成したいユーザのリスト(user 用の辞書構造のリスト)
  • 内部構造として、loop 分の中で include_tasks を用いている。(上記のリストを flatten=1 して指定)

特徴を元に chatGPTに書かせたものが以下になります。

.
├── main.yml
├── create_user.yml
└── vars.yml
  • playbook (実際は role の tasks/main.yml がこのような内容でした)
---
- name: Create users based on the given list
  hosts: localhost
  vars_files:
    - vars.yml  # ユーザリストを外部のvarsファイルから読み込む
  tasks:
    - name: Loop through users and create them
      include_tasks: create_user.yml
      loop: "{{ users|flatten=1 }}"
      loop_control:
        loop_var: user
  • create_user.yml
---
- name: Create a user
  user:
    name: "{{ user.name }}"        # ユーザ名
    state: present                 # ユーザが存在することを確認
    password: "{{ user.password }}" # パスワード (プレーンテキスト)
    shell: "{{ user.shell | default('/bin/bash') }}" # デフォルトのシェル
    comment: "{{ user.comment | default('No comment') }}" # コメント
    create_home: "{{ user.create_home | default(true) }}" # ホームディレクトリを作成するか
  • 想定データ(vars)
---
users:
  - name: "alice"
    password: "alicepassword"
    comment: "Alice User"
    shell: "/bin/zsh"
  - name: "bob"
    password: "bobpassword"
    comment: "Bob User"
  - name: "charlie"
    password: "charliepassword"
    comment: "Charlie User"
    create_home: false

ダミーデータで測定

ダミーデータで100ユーザ分の vars で実行したものの、Job 実行にかかった時間は 3分程度でした。
当時は「これを5倍にしても15分だしなぁ」と、何か別の差異があると考え、データ量は考慮しておりませんでした。

再現度合いを上げるためにパスワード文字列を複雑にする等の試行錯誤をしましたが、実行時間に大きな影響は出ませんでした。

お客様の実データで測定

データ構造の複雑さに起因しているのではと考え、実データ(vars)を共有いただいて測定したところ、500ユーザの作成に5時間かかる事象が確認できました。

測定の小ネタ

profile_tasks を設定

どの task に時間がかかっているかを特定するために設定。 参考: AnsibleのCallback Pluginをためしてみよう - 赤帽エンジニアブログ

task の timestamp を確認

Role の構造上、include_tasks を経由して全ての task が実行されているため、profile_tasks での出力は include_tasks の時間のみが表示され、内訳を確認するに至りませんでした。 以下のように、被疑箇所となる候補が複数あったため、task 実行時のタイムスタンプが手がかりとなりました

  • include_tasks に時間がかかっている
  • vault で暗号化したパスワードの復号化
  • user の作成処理そのものに時間がかかっている

事象が再現した際は、タイムスタンプと、ログの表示のされ方から、include_tasks に極端に時間がかかっていることが確認できました。

リアルタイムに眺めていたら、以下のような具合に、include_tasks に入ったところで何10分もそのまま でした。

AAPのログ画面(一部切り抜き)、この状態で何10分も動かない。

多数のループと相性の悪かった記述

構造上、include_tasks や flatten を使わない形に修正できたので、その上で測定すると以下のようになりました。

include_tasks あり include_tasks なし
flatten=1 あり 遅い 遅い
flatten=1 なし 遅い 速い

include_tasks or flatten のどちらかがあると、今回のデータ量では実行時間が5時間近くになりました。

調べてないこと

深掘りの余地のあるトピックですが、根本原因は調べていません。
例えば、以下は深掘りの余地がありそうです。

  • 「極端に遅くなるループ数」のしきい値はどのくらいか(100から500の間にあるはず)
  • 上記の数は何に起因するか(ハードウェアスペックで変わるか)

今回はユーザの作成で発覚しましたが、一定数のループ+(flatten or include_tasks) で確認できた事象ですので、task としては debug module でも確認可能と思います。
反響があれば調べるかもしれません。それでは Happy Automation。

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。