Carpe Diem

備忘録

FastAPIのメトリクスをマルチプロセスモードで収集する

背景

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."
                )

ref: https://github.com/trallnag/prometheus-fastapi-instrumentator/blob/v5.9.1/src/prometheus_fastapi_instrumentator/instrumentation.py#L189-L197

動作確認

100件ほどリクエストを投げてみます。

$ ab -n 100 -c 1 http://127.0.0.1:8000/items/6

通常

通常ではHTTP系のメトリクスに加え、様々なメトリクスがあります。

マルチプロセスモード

マルチプロセスモードでは前述の制約のように

  • python_gc_objects_collected_totalがない
  • gaugeがない

といった状態で、最低限HTTP系のメトリクスが生成されます。

しかしながらちゃんと単調増加して100件リクエストがカウントされたことが確認できます。

その他

サンプルコード

今回のサンプルコードはこちら

github.com

Q&A

なぜ環境変数が小文字?

prometheus-clientは大文字の環境変数を使っていますが、prometheus-fastapi-instrumentatorでは大文字にすると期待しない挙動になるため小文字の状態で設定します。

github.com

他の方法は?

他にも参考リンクのように

  • ラベルで区別する
  • StatsD exporterを使う

といった回避策がありますが、既存でPrometheusを使った収集システムがある場合はマルチプロセスモードにするのが一番楽でしょう。

まとめ

FastAPIのマルチプロセスでのPrometheusメトリクスの収集方法を説明しました。

参考