zaki work log

作業ログやら生活ログやらなんやら

[Ansible] asyncとpollを使った非同期処理とループの並列実行

Ansibleのループ処理はデフォルトでは逐次処理で動作します。
例えばループの回数が5回で、1回の処理に10秒ずつかかる場合は合計50秒はかかる計算になります。

Ansibleではループによって複数回実行される処理がお互いに干渉せずに並列に実行可能なのであれば、asyncpollの指定で並列に処理することができます。
1回10秒(x5)の処理を並行で同時に実行することで10秒で完了する計算になります。

docs.ansible.com

デフォルト(逐次処理)の動作

まずは普通の逐次処理されるループ処理のplaybookです。

---
- hosts: localhost
  gather_facts: false

  tasks:
    - name: clear log
      file:
        path: async.log
        state: absent

    - name: loop sample
      shell: echo "$(date +%Y.%m.%d_%H.%M.%S); {{ item }} begin" >> async.log; sleep "{{ item }}"; echo "$(date +%Y.%m.%d_%H.%M.%S); {{ item }} end" >> async.log
      loop:
        - 2
        - 3
        - 4

ごちゃごちゃ書いてるけど、shellでやってるのは以下の3つのコマンドを実行している。

  • 現在日時とsleepする時間とbeginをファイル書き込み
  • 指定秒数sleep
  • 現在日時とsleepした時間とendをファイル書き込み

loopを使って[2, 3, 4]のリストを渡すことで、「2秒スリープ」「3秒スリープ」「4秒スリープ」が順番に処理されることを出力されるファイルで確認できます。
出力されるファイルはこんな感じ。

2021.04.06_20.06.50; 2 begin
2021.04.06_20.06.52; 2 end
2021.04.06_20.06.53; 3 begin
2021.04.06_20.06.56; 3 end
2021.04.06_20.06.56; 4 begin
2021.04.06_20.07.00; 4 end

shellで実行される処理が並行して実行されていないことが確認できました。

今回は、このスリープ処理を同時に実行させてみます。

並列処理

docs.ansible.com

Ansibleの並列処理に必要な非同期の処理は基本全部ここにのってる。はず。
ちなみにループの場合の応用もこのページに載ってます。

asyncとpollによる非同期処理

今回使うのはasyncpoll。どちらも整数(秒)を引数に取ります。

async

asyncを単体で使用すると、「指定秒数以内にtaskが完了しない場合はエラー」となります。

    - name: async sample
      shell: echo "start" >> async-poll.log; sleep 10; echo "end" >> async-poll.log
      async: 4

これを実行すると、sleep 10の最中にasync: 4によるタイムアウトが発動し、以下のようにエラーとなります。

fatal: [localhost]: FAILED! => changed=false 
  msg: async task did not complete within the requested time - 4s

async + poll

pollの指定が無い場合は、デフォルト値は15のため、15秒経ってエラーとして動作します。
なので、asyncに指定した値より大きい値はあまり意味がない(と思う)ので、おそらくasyncの秒数の約数にしておくとベターと思う。

    - name: async sample
      shell: echo "start" >> async-poll.log; sleep 10; echo "end" >> async-poll.log
      async: 4
      poll: 1

このようにpoll: 1にしておくと、実行しているモジュールが完了するかasyncで指定した4秒が経過するかを、pollに指定した1秒毎にチェックする動作になります。

poll:0

poll: 0は特殊で、asyncで指定しているtaskを非同期で処理しつつ、その完了を待たずに次のtaskへ処理が進むという動作になります。

    - name: async sample
      shell: echo "start" >> async-poll.log; sleep 10; echo "end" >> async-poll.log
      async: 11
      poll: 0
      # shellで実行している処理がタイムアウトしないように`async`の値は11秒に変更

上記のtask定義でansible-playbookを実行すると、shellの完了を待たずに非同期で処理が継続され、taskが完了します。
また、他にtaskがなければansible-playbookの実行は終了しシェルに処理が戻りますが、バックグラウンドで非同期処理用のプロセスが動作し続けます。

ansible-playbookは終了しても処理が実行してる最中にps auxfwするとこんな感じ。

zaki     110284  0.0  0.1 215616 11040 ?        S    21:08   0:00 /home/zaki/src/ansible-sample/venv/a2.10/bin/python3 /home/zaki/.a
zaki     110285  0.0  0.1 215616 11412 ?        S    21:08   0:00  \_ /home/zaki/src/ansible-sample/venv/a2.10/bin/python3 /home/zak
zaki     110286  5.0  0.1 207000 14188 ?        S    21:08   0:00      \_ /home/zaki/src/ansible-sample/venv/a2.10/bin/python3 /home
zaki     110298  0.0  0.0 113288  1420 ?        S    21:08   0:00          \_ /bin/sh -c echo "start" >> async-poll.log; sleep 10; e
zaki     110299  0.0  0.0 108056   356 ?        S    21:08   0:00              \_ sleep 10

ループ処理に応用

ここまでの async + poll:0 設定を踏まえて、ループ処理とコンボすることで、ループで実行される各処理を逐次処理でなく並列に同時に実行することができます。

    - name: async loop
      shell: echo "{{ item }} begin" >> async.log; sleep "{{ item }}"; echo "{{ item }} end" >> async.log
      loop:
        - 2
        - 3
        - 4
      async: 5
      poll: 0

冒頭の逐次処理を行っていたtaskにasync+pollを追加。
これを実行した結果出力されるasync.logファイルは以下の通り。

2021.04.06_21.12.22; 2 begin
2021.04.06_21.12.22; 3 begin
2021.04.06_21.12.22; 4 begin
2021.04.06_21.12.24; 2 end
2021.04.06_21.12.25; 3 end
2021.04.06_21.12.26; 4 end

beginが同時に書き込まれ、endは各時間のスリープが終わったあとに書き込まれることを確認でき、ループ内の各処理が並列に処理されています。

非同期で実行した処理を待つ

asyncpoll:0の指定で並列処理できるようになったけど、実際のplaybookでは「並列処理したtaskの結果がどうだったか」というのを以降の処理を実行する前に結果を確認したうえで次の処理を行うことが多いと思います。
Ansibleのasyncを使った非同期処理はその結果を保持した上で、ansible.builtin.async_statusモジュールを使ってwaitできます。

docs.ansible.com

    # ループの並列処理
    - name: async loop
      shell: echo "$(date +%Y.%m.%d_%H.%M.%S); {{ item }} begin" >> async.log; sleep "{{ item }}"; echo "$(date +%Y.%m.%d_%H.%M.%S); {{ item }} end" >> async.log
      loop:
        - 2
        - 3
        - 4
      async: 1
      poll: 0
      register: result_async

    # 並列処理の完了を待つ
    - name: wait
      ansible.builtin.async_status:
        jid: "{{ item.ansible_job_id }}"
      loop: "{{ result_async.results }}"
      register: async_poll_results
      until: async_poll_results.finished
      retries: 10
      delay: 1

並列ループ処理はregisterを使って結果を保持。
そしてその結果にはansible_job_idという値が含まれており、これをjid(Job identifier)パラメタに指定します。

このときに「ループとregisterを組み合わせた場合はresultsにリスト形式で各ループごとの結果が保持される」という動作に合わせて、async_statusモジュールを使ったJob identifierの指定もloopディレクティブと{{ item.ansible_job_id }}を使ってループごとに指定します。

zaki-hmkc.hatenablog.com

そしてこのtaskは、前の並列ループ処理が非同期で処理される結果、まだ未完了の状態で起動されるため、「並列で動作している非同期処理がすべて完了するまで」リトライし続けるように記述します。
これは、このtaskの結果をregisterで保持し、その保持した結果をuntilretriesを使って指定の状態(registerで保持した辞書型の変数のfinishedの値)が真になるまで繰り返し実行します。

非同期処理を待つplaybook自体は以下のエントリを参考。

zaki-hmkc.hatenablog.com

これで、並列実行のループ処理を実現しつつ、後続のtaskではその結果を待ったうえで処理を続けることができます。

環境

確認した環境は以下の通り。

ansible 2.10.5
  config file = /home/zaki/.ansible.cfg
  configured module search path = ['/home/zaki/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/zaki/src/ansible-sample/venv/a2.10/lib64/python3.6/site-packages/ansible
  executable location = /home/zaki/src/ansible-sample/venv/a2.10/bin/ansible
  python version = 3.6.8 (default, Nov 16 2020, 16:55:22) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]