背景
PythonはGILがあるためマルチコアの恩恵を受けづらいです。そのためGunicornのようなプロセスマネージャを利用してマルチプロセスで稼働させ、パフォーマンスを上げるといった手法があります。
しかしマルチプロセスにした場合、Prometheusのメトリクス値が収集するプロセス毎に異なってしまい、本来単調増加するはずの値が増減してしまいおかしなメトリクスになってしまいます。
ref: Strange drops in total requests · Issue #50 · trallnag/prometheus-fastapi-instrumentator · GitHub
そこでPythonのPrometheus clientにはマルチプロセスモードが用意されており、外部共有ファイルにstatsを書き出すことで整合性を保証します。
今回はFastAPIでマルチプロセスモードを用いたメトリクスの収集方法を説明します。
環境
- Python 3.9
- FastAPI 0.85.0
- prometheus-fastapi-instrumentator 5.9.1
- Gunicorn 20.1.0
- Uvicorn 0.18.3
マルチプロセスモードのメリットデメリット
マルチプロセスモードは手軽に導入できる手段ですが、結構デメリットがあります。
メリット
- 各プロセスで共有のprometheusレジストリを作成するので値が減少しない適切な集計値になる
デメリット
マルチプロセスモードでは以下の制約があります。
To handle this the client library can be put in multiprocess mode. This comes with a number of limitations:
- Registries can not be used as normal, all instantiated metrics are exported
- Registering metrics to a registry later used by a MultiProcessCollector may cause duplicate metrics to be exported
- Custom collectors do not work (e.g. cpu and memory metrics)
- Info and Enum metrics do not work
- The pushgateway cannot be used
- Gauges cannot use the pid label
- Exemplars are not supported
ref: https://github.com/prometheus/client_python#multiprocess-mode-eg-gunicorn
これらを加味した上でマルチプロセス化するかどうかを判断すると良いでしょう。
実装
では具体的な実装をしていきます。
FastAPI
FastAPIの方は通常通りのPrometheus設定で問題ありません。
from fastapi import FastAPI from prometheus_fastapi_instrumentator import Instrumentator app = FastAPI() Instrumentator( excluded_handlers=["/metrics"], ).instrument(app).expose(app=app, endpoint="/metrics") @app.get("/") def read_root(): return {"Hello": "World"} @app.get("/items/{item_id}") def read_item(item_id: int, q: str = None): return {"item_id": item_id, "q": q}
通常起動にはUvicornを用いますが、マルチプロセスで起動する際はGunicornを利用します。
$ poetry run gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker
マルチプロセスモードにするには
python-clientを直接使う場合はドキュメントの通りに実装が必要です。
https://github.com/prometheus/client_python#multiprocess-mode-eg-gunicorn
ただprometheus-fastapi-instrumentatorの場合、環境変数prometheus_multiproc_dir
を設定するだけで済みます。
$ export prometheus_multiproc_dir=./metrics $ poetry run gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker
実は内部で対応しているためです。
if "prometheus_multiproc_dir" in os.environ: pmd = os.environ["prometheus_multiproc_dir"] if os.path.isdir(pmd): registry = CollectorRegistry() multiprocess.MultiProcessCollector(registry) else: raise ValueError( f"Env var prometheus_multiproc_dir='{pmd}' not a directory." )
動作確認
100件ほどリクエストを投げてみます。
$ ab -n 100 -c 1 http://127.0.0.1:8000/items/6
通常
通常ではHTTP系のメトリクスに加え、様々なメトリクスがあります。
マルチプロセスモード
マルチプロセスモードでは前述の制約のように
といった状態で、最低限HTTP系のメトリクスが生成されます。
しかしながらちゃんと単調増加して100件リクエストがカウントされたことが確認できます。
その他
サンプルコード
今回のサンプルコードはこちら
Q&A
なぜ環境変数が小文字?
prometheus-clientは大文字の環境変数を使っていますが、prometheus-fastapi-instrumentatorでは大文字にすると期待しない挙動になるため小文字の状態で設定します。
他の方法は?
他にも参考リンクのように
- ラベルで区別する
- StatsD exporterを使う
といった回避策がありますが、既存でPrometheusを使った収集システムがある場合はマルチプロセスモードにするのが一番楽でしょう。
まとめ
FastAPIのマルチプロセスでのPrometheusメトリクスの収集方法を説明しました。