プレイブックが遅い本当の理由を測ってから直す
目標
プレイブックが遅い理由をコールバックで先に測り、ファクトの収集・キャッシュ・パイプライニング・並行実行・非同期という5つのノブを、順に回します。最後に、測定結果から直すべき場所を抜き出してくれる小さなツールを、自分で作ります。
なぜ重要なのか
「プレイブックが遅い」という報告はいつも来ますが、その中には性格の異なる原因が混ざっています。対象100台でファクトをすべて集めるのに時間がかかっているのと、タスクごとにSSHを3回往復して遅いのと、1台が10分かかる作業をしている間に残りの99台が立って待っていて遅いのとでは、直し方がまったく違います。そのため順序が重要です。まず測り、そのあとで直します。測らずに直すと、知っているところを直すことになり、知っているところは、たいていボトルネックではありません。そして、ノブごとに代償があります。キャッシュは速いものの誤ったファクトを作り、freeストラテジーは速いものの、ホスト間の順序の約束を破ります。このラボは、各ノブを回してみて、その代償まで一緒に見ます。
ステップ
/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に保存します。/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で保存し、実行してください。/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を残してください。- 前のステップが終わった今、キャッシュには
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つのファイルの値がどう分かれるかを、自分で確認します。 /root/ansperf/ansible.cfgに[ssh_connection]セクションを加え、pipelining = Trueを有効にしてください。そのあと、デフォルトと異なる設定をすべて含めた出力(ansible-config dump --only-changed -t all)を/root/ansperf/out/config.txtに保存し、パイプライニングを有効にしたまま、web1にアドホックのpingが通るかを確認してください。/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に保存してください。/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で保存して、実行してください。/root/ansperf/slowest.shを作成してください。最初の引数で受け取った測定出力ファイルからコールバックのサマリーを読み、時間がかかった順に、<초>s <태스크이름>(プレースホルダーは順に秒数とタスク名です)を1行ずつ出力します。何行出力するかは2つ目の引数で受け取り、デフォルトは3です。ファイルがなければ標準エラー出力に知らせて、0以外の値で終了します。このツールをステップ1の/root/ansperf/out/profile_baseline.txtに実行して、結果を/root/ansperf/out/slowest.txtに保存してください。
参考
- この環境はエミュレーションで動いているので、実行時間が大きく揺れます。そのため、このラボの判定は、時間ではなく残る証拠で行います。設定のダンプ、キャッシュファイル、コールバックが残した測定ファイル、非同期ジョブのファイルです。
- sshdは127.0.0.1の2222で動いていて、インベントリの3つのホストがすべてそこに接続します。
- 設定が本当に反映されたかは、常に
ansible-config dump --only-changedで確認します。接続プラグインの設定まで見るには、-t allを付けます。 - よくある間違い:
ansible.cfgを直したのに、別のディレクトリで実行して、そのファイルが読み込まれないことです。設定ファイルは1つだけが使われ、どれが使われたかはCONFIG_FILE行に出ます。 - よくある間違い: キャッシュを有効にしたあと、対象が変わったのに、古い値で判断することです。キャッシュは「もう一度尋ねない」という意味です。
- よくある間違い:
asyncの値を、ジョブが実際にかかる時間より短くして、問題のないジョブを打ち切ることです。 - Playbookのストラテジー・非同期タスク・setupモジュールとgather_subset・キャッシュプラグイン・sshの接続プラグイン・設定リファレンス
直す前に先に測る
/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で、名前に空白が入っているので、秒と名前の間をタブで区切っておくと、あとで扱いやすくなります。このツールがやることは小さいですが、位置づけが重要です。人々が最適化で最もよくやる間違いは、測らずに知っているところを直すことです。