TT Lab
はじめる
学ぶ 学習パス コース

Ansible実戦

プレイブックが遅い本当の理由を測ってから直す

TT Labで続きを見る

目標

プレイブックが遅い理由をコールバックで先に測り、ファクトの収集・キャッシュ・パイプライニング・並行実行・非同期という5つのノブを、順に回します。最後に、測定結果から直すべき場所を抜き出してくれる小さなツールを、自分で作ります。

なぜ重要なのか

「プレイブックが遅い」という報告はいつも来ますが、その中には性格の異なる原因が混ざっています。対象100台でファクトをすべて集めるのに時間がかかっているのと、タスクごとにSSHを3回往復して遅いのと、1台が10分かかる作業をしている間に残りの99台が立って待っていて遅いのとでは、直し方がまったく違います。そのため順序が重要です。まず測り、そのあとで直します。測らずに直すと、知っているところを直すことになり、知っているところは、たいていボトルネックではありません。そして、ノブごとに代償があります。キャッシュは速いものの誤ったファクトを作り、freeストラテジーは速いものの、ホスト間の順序の約束を破ります。このラボは、各ノブを回してみて、その代償まで一緒に見ます。

ステップ

  1. /root/ansperf/ansible.cfgでデフォルトのインベントリを./inventory/hosts.iniにし、ansible.posix.profile_tasksコールバックを有効にしてください。/root/ansperf/inventory/hosts.iniには、webグループのweb1(work_sec 1)・web2(2)と、dbグループのdb1(3)を書きます(3つともansible_host=127.0.0.1、ansible_port=2222で、[all:vars]のansible_userはrootです)。/root/ansperf/slow.ymlは、ファクトをすべて集め、3秒・2秒・1秒をそれぞれ待つタスク3つと、/root/ansperf/out/baseline.markerを書くタスク1つで作成してください。実行結果を/root/ansperf/out/profile_baseline.txtに保存します。
  2. /root/ansperf/subsets.ymlを作成してください。プレイのレベルでgather_facts: falseにして自動収集を無効にし、web1でansible.builtin.setupを2回呼び出します。1回は引数なしですべて、もう1回はgather_subset: [min]で最小限だけを集めます。それぞれregisterで受け取って、/root/ansperf/out/facts_all.jsonと/root/ansperf/out/facts_min.jsonに、そのタスクが返したファクトをJSONで保存し、実行してください。
  3. /root/ansperf/ansible.cfgに、gathering = smart、fact_caching = ansible.builtin.jsonfile、fact_caching_connection = ./factcache、fact_caching_timeout = 3600を加えてください。そして、対象が自分で知らせてくれるローカルファクトを作成します。/etc/ansible/facts.d/lab.factに[app]セクションとrelease=1.0.0を書いてください。/root/ansperf/release.ymlは、web1のansible_local.lab.app.releaseを、変数outfileが指すファイルに書くプレイブックです。キャッシュディレクトリを空にしてから、このプレイブックを実行して、/root/ansperf/out/rel1.txtを残してください。
  4. 前のステップが終わった今、キャッシュには1.0.0が入っています。/etc/ansible/facts.d/lab.factのreleaseだけを2.0.0に上げてください。プレイブックをそのままもう一度実行して、/root/ansperf/out/rel2_cached.txtを残し、続けて、キャッシュを空にするオプションを付けてもう一度実行して、/root/ansperf/out/rel3_fresh.txtを残してください。3つのファイルの値がどう分かれるかを、自分で確認します。
  5. /root/ansperf/ansible.cfgに[ssh_connection]セクションを加え、pipelining = Trueを有効にしてください。そのあと、デフォルトと異なる設定をすべて含めた出力(ansible-config dump --only-changed -t all)を/root/ansperf/out/config.txtに保存し、パイプライニングを有効にしたまま、web1にアドホックのpingが通るかを確認してください。
  6. /root/ansperf/ansible.cfgの[defaults]にforks = 10を加えてください。/root/ansperf/free.ymlを作成します。strategy: freeで、ファクトは集めず、2つのタスクで構成されます。1つ目はそのホストのwork_secの分だけ待ち、2つ目はout/free-<호스트이름>.txtに、<호스트이름> <work_sec>の1行を書きます(プレースホルダーはどちらもホスト名です)。実行出力は/root/ansperf/out/free.txtに保存してください。
  7. /root/ansperf/async.ymlを作成してください。web1で8秒のジョブをasync: 120、poll: 0で投げておき、registerで受け取ります。次のタスクで/root/ansperf/out/meanwhile.txtを書き(その間にやることがあるという意味です)、ansible.builtin.async_statusで終わるまで待ってから、その結果を/root/ansperf/out/async.jsonにJSONで保存して、実行してください。
  8. /root/ansperf/slowest.shを作成してください。最初の引数で受け取った測定出力ファイルからコールバックのサマリーを読み、時間がかかった順に、<초>s <태스크이름>(プレースホルダーは順に秒数とタスク名です)を1行ずつ出力します。何行出力するかは2つ目の引数で受け取り、デフォルトは3です。ファイルがなければ標準エラー出力に知らせて、0以外の値で終了します。このツールをステップ1の/root/ansperf/out/profile_baseline.txtに実行して、結果を/root/ansperf/out/slowest.txtに保存してください。

参考

直す前に先に測る

/root/ansperf/ansible.cfgでデフォルトのインベントリを./inventory/hosts.iniにし、ansible.posix.profile_tasksコールバックを有効にしてください。/root/ansperf/inventory/hosts.iniには、webグループのweb1(work_sec 1)・web2(2)と、dbグループのdb1(3)を書きます(3つともansible_host=127.0.0.1、ansible_port=2222で、[all:vars]のansible_userはrootです)。/root/ansperf/slow.ymlは、ファクトをすべて集め、3秒・2秒・1秒をそれぞれ待つタスク3つと、/root/ansperf/out/baseline.markerを書くタスク1つで作成してください。実行結果を/root/ansperf/out/profile_baseline.txtに保存します。

コールバックはインストールではなく設定です。callbacks_enabledに名前を書けば有効になります。ansible.posixコレクションは、このイメージにすでに入っています。このコールバックは、タスクごとにかかった時間を出力し、実行の終わりに、時間がかかった順にサマリーをもう一度出力します。このステップの価値は最適化ではなく、ベースラインです。測らずに直すと、何がよくなったのか、誰も言えません。

ファクトの収集が何を持ってくるかを数える

/root/ansperf/subsets.ymlを作成してください。プレイのレベルでgather_facts: falseにして自動収集を無効にし、web1でansible.builtin.setupを2回呼び出します。1回は引数なしですべて、もう1回はgather_subset: [min]で最小限だけを集めます。それぞれregisterで受け取って、/root/ansperf/out/facts_all.jsonと/root/ansperf/out/facts_min.jsonに、そのタスクが返したファクトをJSONで保存し、実行してください。

gather_facts: trueは、プレイの先頭にsetupタスクをこっそり1つ差し込むのと同じです。無効にして、必要な場所で直接呼べば、いつどれだけ集めるかを自分で決められます。setupタスクをregisterで受け取ると、その実行が返したファクトだけが<이름>.ansible_facts(プレースホルダーは名前です)に入ります。すでにホストに付いているファクトと混ざらないので、比較に向いています。JSONをきれいに保存するフィルターはto_nice_jsonです。何が抜けたかはjq 'keys'で見ます。

ファクトをファイルにキャッシュして、2回目の実行を省く

/root/ansperf/ansible.cfgに、gathering = smart、fact_caching = ansible.builtin.jsonfile、fact_caching_connection = ./factcache、fact_caching_timeout = 3600を加えてください。そして、対象が自分で知らせてくれるローカルファクトを作成します。/etc/ansible/facts.d/lab.factに[app]セクションとrelease=1.0.0を書いてください。/root/ansperf/release.ymlは、web1のansible_local.lab.app.releaseを、変数outfileが指すファイルに書くプレイブックです。キャッシュディレクトリを空にしてから、このプレイブックを実行して、/root/ansperf/out/rel1.txtを残してください。

gatheringがsmartなら、「キャッシュにあれば、もう一度集めない」という意味です。キャッシュのないimplicitと、キャッシュを使わないexplicitの間の位置です。jsonfileキャッシュは、ホストごとにJSONファイルを1つ残すので、何が入っているかを目で開いて確認できます。/etc/ansible/facts.d/*.factは、setupモジュールが自動的に読み、ansible_local.<파일이름>(プレースホルダーはファイル名です)の下に入れます。INI形式なら、セクション名がそのまま1階層になります。

キャッシュが作る誤ったファクト

前のステップが終わった今、キャッシュには1.0.0が入っています。/etc/ansible/facts.d/lab.factのreleaseだけを2.0.0に上げてください。プレイブックをそのままもう一度実行して、/root/ansperf/out/rel2_cached.txtを残し、続けて、キャッシュを空にするオプションを付けてもう一度実行して、/root/ansperf/out/rel3_fresh.txtを残してください。3つのファイルの値がどう分かれるかを、自分で確認します。

キャッシュは「もう一度尋ねない」という意味で、そのため対象が変わってもAnsibleは知りません。これが、キャッシュを有効にするときに一緒に付いてくるリスクです。ansible-playbookには、その場でキャッシュを捨てて、もう一度集めさせるオプションがあります(--helpでcacheを探してみてください)。実務では、キャッシュの有効時間をデプロイの周期より短くするか、デプロイパイプラインの最初の段階でキャッシュを空にします。

SSHの往復を減らし、変更された設定をファイルに残す

/root/ansperf/ansible.cfgに[ssh_connection]セクションを加え、pipelining = Trueを有効にしてください。そのあと、デフォルトと異なる設定をすべて含めた出力(ansible-config dump --only-changed -t all)を/root/ansperf/out/config.txtに保存し、パイプライニングを有効にしたまま、web1にアドホックのpingが通るかを確認してください。

パイプライニングを無効にすると、Ansibleはタスクごとにモジュールファイルを対象にコピーし、実行し、削除します。SSHの往復が何回もあります。有効にすると、モジュールをリモートのPythonの標準入力に流し込んで、往復を1回に減らします。たいてい、最も安く、最も大きな効果があります。ただし、対象のsudoersでrequirettyが有効になっていると、有効にできません。そのため、デフォルトが無効です。この設定が本当に反映されたかは、ansible-config dump --only-changed -t allで確認します。-t allがないと、接続プラグインの設定は表示されません。

同時に何台を掴むか、互いを待たせるか

/root/ansperf/ansible.cfgの[defaults]にforks = 10を加えてください。/root/ansperf/free.ymlを作成します。strategy: freeで、ファクトは集めず、2つのタスクで構成されます。1つ目はそのホストのwork_secの分だけ待ち、2つ目はout/free-<호스트이름>.txtに、<호스트이름> <work_sec>の1行を書きます(プレースホルダーはどちらもホスト名です)。実行出力は/root/ansperf/out/free.txtに保存してください。

forksは、Ansibleが同時に掴む対象の数です。デフォルトの5は、サーバーが数十台あるインベントリでは、すぐにボトルネックになります。strategyは別の軸です。デフォルトのlinearは、すべてのホストが1つのタスクを終えて、はじめて次へ進み、freeは、各ホストが自分の速さで最後まで走ります。freeが常によいわけではありません。ホスト間に順序の約束があるプレイブック(先に外して、あとで戻す)では、その約束が破られます。そのため、serialやrun_onceと一緒に使うときは、特に注意が必要です。

長いジョブは投げておいて、あとで結果を受け取る

/root/ansperf/async.ymlを作成してください。web1で8秒のジョブをasync: 120、poll: 0で投げておき、registerで受け取ります。次のタスクで/root/ansperf/out/meanwhile.txtを書き(その間にやることがあるという意味です)、ansible.builtin.async_statusで終わるまで待ってから、その結果を/root/ansperf/out/async.jsonにJSONで保存して、実行してください。

pollが0でなければ、Ansibleがその場で待ちます。0にすると、ジョブ番号だけを受け取って、すぐに次のタスクへ進みます。これが「投げておく」ことです。asyncの値は、そのジョブに許可した最大時間です。短くすると、問題のないジョブが途中で打ち切られます。結果を受け取るのはasync_statusで、受け取ったansible_job_idを渡します。1回で終わっているとは限らないので、untilでもう何回か問い合わせます。この方式が合う場面は、「時間はかかるが、互いに独立したジョブ」です。結果がすぐ必要なジョブに使うと、複雑になるだけです。

測定結果から直すべき場所を抜き出すツール

/root/ansperf/slowest.shを作成してください。最初の引数で受け取った測定出力ファイルからコールバックのサマリーを読み、時間がかかった順に、<초>s <태스크이름>(プレースホルダーは順に秒数とタスク名です)を1行ずつ出力します。何行出力するかは2つ目の引数で受け取り、デフォルトは3です。ファイルがなければ標準エラー出力に知らせて、0以外の値で終了します。このツールをステップ1の/root/ansperf/out/profile_baseline.txtに実行して、結果を/root/ansperf/out/slowest.txtに保存してください。

サマリーは、等号でできた区切り線のあとから始まり、1行は<이름> ----- <초>s(プレースホルダーは順に名前と秒数です)の形です。区切り線の前には、タスクごとに出力された時刻の行が混ざっているので、区切り線に出会ってからだけを読む必要があります。秒単位の値は常に小数第2位まで出力されるので、そのまま移せば構いません。ソートはsort -rnで、名前に空白が入っているので、秒と名前の間をタブで区切っておくと、あとで扱いやすくなります。このツールがやることは小さいですが、位置づけが重要です。人々が最適化で最もよくやる間違いは、測らずに知っているところを直すことです。