測らずに直すと、知っているところだけを直すことになる
一言でいうと
プレイブックが遅い理由は、たいてい5つのうちのどれかで、直し方はそれぞれ異なります。だからまずprofile_tasksで先に測り、そのあとでノブを選びます。そして、すべてのノブには代償があります。
なぜ必要なのか
「デプロイに40分かかる」という報告は、どのチームにも来ます。ところが、その40分の中には、性格がまったく異なる時間が混ざっています。
- 対象200台でファクトをすべて集めるのにかかった5分
- タスク1つごとにSSHを3–4回往復して積み重なった時間
- デフォルト値が5の
forksのせいで、200台を40バッチに分けて実行した時間 - 1台で動く10分かかるパッケージのインストールを、残りの199台が立って待っていた時間
- 実は単にリモートのサービスが遅いだけのもの
測らずに直し始めると、人々は知っているところを直します。たいていはforksを上げて終わりです。ところが、ボトルネックがファクトの収集だったなら、forksを上げてもほとんど速くならず、むしろコントローラーのメモリと対象の負荷が増えるだけです。測定はパフォーマンス作業の準備運動ではなく、作業そのものの最初の段階です。
どう動くのか
まず測る: profile_tasks
Ansibleのコールバックは、実行中に起きることを受け取って出力を作るプラグインです。ansible.posix.profile_tasksを有効にすると、タスクごとにかかった時間が出力され、実行の終わりに時間がかかった順のサマリーがもう一度出ます。インストールするものはなく、設定1行で済みます。
[defaults]
callbacks_enabled = ansible.posix.profile_tasks
サマリーはタスク単位です。そのため「どのタスクか」まではわかりますが、「そのタスクの何か」まではわかりません。そのあとは、以下のノブを1つずつ当ててみる作業です。
ノブ1: ファクトをどれだけ集めるか
gather_facts: trueは、プレイの先頭にsetupタスクをこっそり1つ差し込むのと同じです。このタスクは、対象のCPU・メモリ・ディスク・ネットワークインターフェース・マウント一覧をすべて集めてきます。100個を超えるファクトが返ってきますが、実際に使うのは、たいていansible_distribution1つです。
選び方は2つです。
gather_facts: falseで無効にし、本当に必要なプレイでだけansible.builtin.setupを直接呼びます。gather_subsetでグループを絞ります。minはディストリビューションとホスト名程度だけを、networkはインターフェースだけを取得します。!all,!min,networkのように、引いて足す表記も使えます。
ノブ2: ファクトキャッシュと誤ったファクト
fact_cachingを有効にすると、一度集めたファクトをファイルやRedisに置き、次回の実行で再利用します。jsonfileキャッシュは、ホストごとにJSONファイルを1つ残すので、目で開いて確認でき、学習にも向いています。gathering = smartと一緒に置くと、「キャッシュにあれば、もう一度尋ねない」になります。
ここに、このモジュールで最も重要な代償があります。もう一度尋ねないということは、対象が変わってもAnsibleが知らないということです。昨日キャッシュに入ったディスク容量・カーネルのバージョン・社内のローカルファクトを、今日もそのまま信じて判断します。条件文がファクトを見ていれば、その条件は昨日の事実で分かれます。実務で使う防御は3つです。キャッシュの有効時間をデプロイの周期より短くする、デプロイパイプラインの最初の段階で--flush-cacheで空にする、そしてキャッシュされたファクトで危険な判断をしないことです。
ノブ3: SSHの往復とパイプライニング
パイプライニングを無効にすると、Ansibleはタスクごとにモジュールファイルを対象にコピーし、実行し、削除します。SSHの往復が何回も発生します。有効にすると、モジュールをリモートのPythonの標準入力に流し込んで、往復を1回に減らします。たいてい、最も安く、最も大きな効果があります。
デフォルトが無効になっているのには理由があります。対象のsudoersでrequirettyが有効になっていると、パイプライニングは動作しません。そのため、Ansibleは「使える場所で有効にする」という扱いにしました。この設定は[ssh_connection]セクションに入り、本当に反映されたかは、ansible-config dump --only-changed -t allで確認する必要があります。-t allがないと、接続プラグインの設定は表示されません。
ノブ4: forksとstrategy
2つは別の軸です。
| 決めるもの | デフォルト値 | |
|---|---|---|
forks |
同時に掴む対象の数 | 5 |
strategy |
ホスト同士がタスクごとに互いを待つか | linear |
forksが5で対象が200台なら、Ansibleは40バッチに分けて実行します。上げれば速くなりますが、コントローラーのCPUとメモリ、そして開かれるSSH接続の数も一緒に増えます。
strategy: linearは、すべてのホストが1つのタスクを終えて、はじめて次へ進みます。freeは、各ホストが自分の速さで最後まで走ります。遅い1台が全体を引き止める状況では、freeが大きく有利です。ただしfreeは、ホスト間の順序の約束を破ります。「すべてのWebサーバーから外してからデプロイする」のようなプレイブックは、freeのもとでは意味が崩れます。serialやrun_onceと混ぜて使うときは、特に注意が必要です。
ノブ5: 非同期
asyncとpollは1組です。pollが0でなければ、Ansibleはその場で待ち、0なら、ジョブ番号だけを受け取って、すぐに次のタスクへ進みます。あとでasync_statusで結果を受け取ります。
- name: Start the long job
ansible.builtin.command: /opt/bin/reindex
async: 1800
poll: 0
register: job
asyncの値は、そのジョブに許可した最大時間です。短く設定すると、問題のないジョブが途中で打ち切られます。この方式が合う場面は、「時間はかかるが、互いに独立したジョブ」で、結果がすぐ必要なジョブに使うと、プレイブックが複雑になるだけです。
現場での姿
1つ目は、最大の効果はたいていファクトから出ることです。対象が増えるほどファクトの収集は線形に増え、その大部分は使われません。gather_facts: falseの1行が、forksを2倍にするよりはるかに効くことがよくあります。
2つ目は、設定が反映されたかを必ず確認することです。ansible.cfgは複数の場所に置けますが、使われるのは1つだけです。直したのに、別のディレクトリで実行して何も変わらず、「効果がない」と言うことがよくあります。ansible-config dump --only-changedのCONFIG_FILE行が、その場所を教えてくれます。
3つ目は、時間を測る場所と判定する場所を区別することです。エミュレーションや共有ランナーでは、同じプレイブックの実行時間が2倍まで揺れます。そのため「速くなった」を証明するときは、1回の実行時間ではなく、構造が変わったという証拠を残します。設定のダンプ、キャッシュファイル、コールバックが残した測定ファイルなどです。
4つ目は、測定結果を読む習慣をツールにすることです。サマリーを目で眺めることと、上位のいくつかを抜き出して記録に残すことは、違います。記録が積み重なれば、「今週遅くなったタスク」を言えるようになり、そのときから、パフォーマンスは管理されるものになります。
参考ドキュメント
- プレイブックのストラテジーとforks: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_strategies.html
- 非同期タスクとポーリング: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_async.html
- setupモジュールとgather_subset: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/setup_module.html
- キャッシュプラグイン: https://docs.ansible.com/ansible/latest/plugins/cache.html
- sshの接続プラグイン(パイプライニング): https://docs.ansible.com/ansible/latest/collections/ansible/builtin/ssh_connection.html
次のラボですること
profile_tasksでベースラインを先に残し、ノブを1つずつ回します。ファクトをすべて集めた結果と、最小のグループだけを集めた結果を並べて保存して、何が抜けるかを数え、jsonfileキャッシュを有効にしたあと、対象のローカルファクトを変えてキャッシュが古い値をそのまま返すことを自分で確認し、--flush-cacheで解除してみます。パイプライニングを有効にして、設定のダンプで本当に反映されたかを確認し、forksとstrategy: freeを適用し、長いジョブをpoll: 0で投げておいて、async_statusで結果を受け取ります。最後に、測定結果から最も遅いタスクを抜き出してくれる小さなツールを、自分で作ります。