Prometheusを使ってPython Webアプリの監視

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614998/7e4735ff-d48c-9bc5-91a9-850858553f3d.png)

この記事では、[uWSGI](https://uwsgi-docs.readthedocs.io/) + [nginx](https://www.nginx.com/)で実行されている単純な[Flask](http://flask.pocoo.org/)アプリケーションを監視において完全に機能させる例を使用して、それをどのように行ったかについて説明します。

#ちょっとした歴史
Prometheusはもともと、[GoogleのBorgmon](https://landing.google.com/sre/sre-book/chapters/practical-alerting/)由来の監視ツールです。

ネイティブ環境では、Borgmonはユビキタスで簡単なサービスディスカバリに依存しています。監視対象のサービスは[Borg](https://ai.google/research/pubs/pub43438)によって管理されるため、簡単に見つけることができるはずです。 その例としては、特定のユーザーのクラスターで実行されているすべてのジョブだったり、より複雑な展開の場合、一緒にジョブを構成するすべてのサブタスクが存在しています。

これらはそれぞれ、Prometheusの```/ metrics```と同様に、Borgmonが```/ varz```エンドポイントからデータを取得するための単一のターゲットになる可能性があり、それぞれは通常、C ++、Java、Go、または（あまり一般的ではありませんが）Pythonで記述されたマルチスレッドサーバーです。

Prometheusは、その環境に関するBorgmanの多くの仮定を継承しています。 特に、クライアントライブラリは、メトリックが、共有アドレススペースで実行される複数の実行スレッドのさまざまなライブラリおよびサブシステムからのものであると想定しており、サーバー側では、Prometheusは1つのターゲットが1つの（おそらく）マルチスレッドプログラムであると想定します。

#このやり方じゃダメ

これらの前提は、特にPythonの世界で、Google以外の多くのデプロイメントで破壊されています。 ここでは、（たとえば、[Django](https://www.djangoproject.com/)や[Flask](http://flask.pocoo.org/)を使用して）リクエストを複数のワーカーに分散する[WSGI](https://wsgi.readthedocs.io/en/latest/what.html)アプリケーションサーバーで実行するのが一般的です（各ワーカーはスレッドではなくプロセスです）。

uWSGIの下で実行されているFlaskアプリ用のPrometheus Pythonクライアントの単純なデプロイメントでは、Prometheusサーバーから```/ metrics```への各リクエストが異なるワーカープロセスにヒットし、それぞれが独自のカウンター、ヒストグラムなどをエクスポートします。よって、結果としてモニタリングデータはごみとなります。

実際、特定のカウンターの各スクレイプは、ジョブ全体ではなく、1つのワーカーの値を返します。値はあちこちにジャンプし、アプリケーション全体について何も役に立たないことを教えてくれるはずです。

#解決策
[Amit Saha](https://echorand.me/0)は、同じ問題と[さまざまな解決策](https://echorand.me/your-options-for-monitoring-multi-process-python-applications-with-prometheus.html)を詳細な記事で話し合っています。 その中の一つの記事で説明されているのを参考にすると、PrometheusPythonクライアントには、この状況を処理することを目的としたマルチプロセスモードが含まれており、アプリケーションサーバーの動機付けの例は[gunicorn](https://gunicorn.org/)です。

これは、アプリケーション内のすべてのプロセス間で[mmap()'d dictionaries](https://github.com/prometheus/client_python/blob/624bb61e6f15e0c3739fec853edaea2b91d1674f/prometheus_client/mmap_dict.py)のディレクトリを共有することで機能します。 次に、各プロセスは、Prometheusによってスクレイピングされたときに、アプリケーション全体のメトリックの共有ビューを返すために計算を行います。

これには、ドキュメントに記載されているいくつかの「見出し」の[欠点](https://github.com/prometheus/client_python#multiprocess-mode-gunicorn)があります。例として、プロセスごとの無料のPythonメトリックスがない、特定のメトリックスタイプの完全なサポートの欠如、わずかに複雑なゲージタイプなどがあります。

エンドツーエンドの構成も困難です。 ここでは、何が必要で、どのように環境で各部分を達成したかを示します。 うまくいけば、この完全な例は、将来的に同様の作業を行うすべての人に役立つはずです。


1. 共有ディレクトリは、環境変数```prometheus_multiproc_dir```としてプロセスに渡す必要があります。
uWSGIの[env](https://uwsgi-docs.readthedocs.io/en/latest/Options.html#env)オプションを使用して渡します。[uwsgi.ini](https://github.com/hostedgraphite/pandoras_flask/blob/master/conf/uwsgi.ini#L13)を参照してください。

2. クライアントの共有ディレクトリは、アプリケーションの再起動後にクリアする必要があります。これは理解するのが少し難しいですが、uWSGIの[ハードコーディングされたフック](https://uwsgi-docs.readthedocs.io/en/latest/Hooks.html#the-hookable-uwsgi-phases)の1つである```exec-asap```を使用して、構成ファイルを読み取った直後で、他の処理を行う前に[シェルスクリプトを実行](https://uwsgi-docs.readthedocs.io/en/latest/Hooks.html#exec-run-shell-commands)します。 [uwsgi.ini](https://github.com/hostedgraphite/pandoras_flask/blob/master/conf/uwsgi.ini#L14)を参照してください。
このスクリプトでは、Prometheusクライアントの共有データディレクトリを[削除して再作成](https://github.com/hostedgraphite/pandoras_flask/blob/master/bin/clear_prometheus_multiproc)します。
適切な権限を確認するために、[スーパーバイザー](http://supervisord.org/)の下で```root```としてuwsgiを実行し、uwsgi内の[privsを削除](https://github.com/hostedgraphite/pandoras_flask/blob/master/conf/uwsgi.ini#L18)します。

3. アプリケーションは、Pythonクライアントのマルチプロセスモードを設定する必要があります。
これは主に、[Sahaの投稿](https://echorand.me/your-options-for-monitoring-multi-process-python-applications-with-prometheus.html)を介して行った[ドキュメント](https://github.com/prometheus/client_python#multiprocess-mode-gunicorn)に従うことです。[metrics.py](https://github.com/hostedgraphite/pandoras_flask/blob/master/pandoras_flask/metrics.py)を参照してください。
また、これには、応答ステータスとレイテンシのPrometheusメトリックスをエクスポートするきちんとした[ミドルウェア](https://github.com/hostedgraphite/pandoras_flask/blob/master/pandoras_flask/metrics.py#L17)が含まれていることに注意してください。

4. uWSGIは、アプリケーションがfork（）の後にロードされるようにアプリケーション環境を設定する必要があります。
デフォルトでは、uWSGIはアプリケーションをロードしてから```fork（）```することでメモリを節約しようとします。これには確かに[コピーオンライト](https://en.wikipedia.org/wiki/Copy-on-write)の利点があり、メモリを大幅に節約できます。
ただし、クライアントのマルチプロセスモードの操作に干渉しているようです。おそらく、このように[fork（）の前にロック](https://rachelbythebay.com/w/2011/06/07/forked/)がかかっているためでしょうか。
uWSGIの[lazy-appsオプション](https://uwsgi-docs.readthedocs.io/en/latest/articles/TheArtOfGracefulReloading.html?highlight=lazy-apps#preforking-vs-lazy-apps-vs-lazy)を使用すると、フォーク後にアプリケーションをロードできるため、よりクリーンな環境が得られます。

これらにより、uWSGIの下で実行されているFlaskアプリの```/ metrics```エンドポイントが機能し、 [pandoras_flaskデモ](https://github.com/hostedgraphite/pandoras_flask/)で完全に機能することができるかと思います。

デモでは、[別のポート](https://github.com/hostedgraphite/pandoras_flask/blob/master/conf/nginx.conf#L28)のメトリックエンドポイントを適切なアプリに公開していることに注意してください。これにより、ユーザーがアクセスすることなく、モニタリングへのアクセスを簡単に許可できます。



デプロイメントでは、[uwsgi_exporter](https://github.com/timonwong/uwsgi_exporter)を使用して、uWSGI自体からより多くの統計を取得できるはずです。

#特徴
[Sahaのブログ投稿](https://echorand.me/your-options-for-monitoring-multi-process-python-applications-with-prometheus.html)では、推奨されるソリューションとしてローカルの[statsd](https://github.com/etsy/statsd)を介してメトリックをプッシュすることで、一連の代替案を説明しています。 それは実際に私たちが好む方法ではありません。

最終的には、[kubernetes](https://kubernetes.io/)のようなコンテナオーケストレーションの下ですべてを実行することで、Prometheusが輝くネイティブ環境が提供されますが、これは既存のPythonアプリケーションスタックで他の利点を得るための大きなステップです。

おそらく最もプロメシアンの中間ステップは、各サブプロセスを個別にスクレイピングターゲットとして登録することです。 これは[django-prometheus](https://github.com/korfuri/django-prometheus/blob/master/documentation/exports.md#exporting-metrics-in-a-wsgi-application-with-multiple-processes-per-process)が採用したアプローチですが、提案されている「ポート範囲」アプローチは少し煩雑なものです。

私たちの環境では、次のような方法でこのアイデアを実装できます（まだ実装している可能性があります）。

1. 各プロセスのスレッド内でWebサーバーを実行し、一時的なポートでリッスンし、/ metricsクエリを処理します。
2. ウェブサーバーを登録し、短いTTL [etcd](https://github.com/etcd-io/etcd)パスでそのアドレス（例：hostname：32769）を定期的に更新します。ほとんどのサービスディスカバリニーズにすでにetcdを使用しています。
3. Prometheusで[file based service discovery](https://prometheus.io/docs/guides/file-sd/)を使用して、これらのターゲットを特定し、個人としてそれらをこすります。

このアプローチは、Pythonクライアントのマルチプロセスモードを使用する場合よりも複雑ではないと思いますが、独自の複雑さが伴います。

ワーカーごとに1つのターゲットがあることは、時系列の爆発の原因となることに注意してください。 たとえば、この場合、8つのワーカーにわたるPythonクライアントからの応答時間を追跡する単一のデフォルトのヒストグラムメトリックは、含める他のラベルを掛ける前に、約140の個別の時系列を生成します。 これは、Prometheusが処理する問題ではありませんが、スケーリングすると増加（または増加）する可能性があるため、注意してください。

#まとめ
とりあえず、標準のPythonウェブアプリスタックからメトリックをPrometheusにエクスポートすることは、どのような方法をとっても少し複雑です。 この投稿が、既存のnginx + uwsgi + Flaskアプリを使い始めたい人に役立つことを願っています。

コンテナーオーケストレーションの下でより多くのサービスを実行するとき（私たちがやろうとしていること）は、Prometheusモニタリングをそれらと統合することがより簡単になると期待しています。

Prometheusの定評のあるユーザーは、[Hosted Prometheus](https://try.metricfire.com/japan/?utm_source=blog&utm_medium=Qiita&utm_campaign=Japan&utm_content=Pandora's%20Flask%3A%20Monitoring%20a%20Python%20web%20app%20with%20Prometheus)のサービスをご覧になることをお勧めします。[デモ](https://calendly.com/metricfire-chatwithus/chat?utm_source=blog&utm_medium=Qiita&utm_campaign=Japan&utm_content=Pandora's%20Flask%3A%20Monitoring%20a%20Python%20web%20app%20with%20Prometheus)が必要な場合は、ご遠慮なくお申し付けください。