きり丸の技術日記

技術検証したり、資格等をここに残していきます。

Pythonの起動時のメモリ消費を何とかしたかった

始めに

※ 結果的に言えばどうにもできなかった話です。


検証環境へのデプロイが通常なら10分程度で終わるはずなのに、最悪のパターンだと30分くらいかかるようになりました。原因を追っていくと、どうやらFastAPIが起動時にメモリを大量消費していることがわかりました。特にライブラリのimport時に非常にメモリを消費しており、メモリ使用量が100%になって張り付いた結果、応答待ちになっていることがわかりました。取り急ぎ、検証環境のメモリを増やすことで通常通りの10分程度のデプロイ時間に戻せたのですが、通常利用時のメモリ使用量が多くないのに、起動時の問題だけでメモリを増やすのは根本解決にはならないと考えていたので、それを何とかしようとした試みについて書いたブログです。

環境

  • Python
    • 3.13
  • tuna
    • 0.5.11

実装

import時間を知りたい

まずは、import時間を可視化することを目指しました。import時間が長いモジュールはメモリ消費量が多い傾向があるためです。

Python 3.7以降であれば、標準機能としてimportにかかる時間を表示できる機能があります。 機能をオンにすると次のようにどのライブラリでimporttimeがかかっているか出力されます。

import time: self [us] | cumulative | imported package
import time:        88 |         88 | django
import time:       646 |        646 |       billiard.reduction
import time:      1221 |       1867 |     billiard.connection
import time:       662 |       2528 |   billiard.dummy
import time:      1798 |       4326 | billiard.pool
import time:       111 |        111 | redis
import time:        68 |         68 | uwsgi

起動オプションに-X importtimeを付与してください。または環境変数のPYTHONPROFILEIMPORTTIMEを設定してください。

# オプション
python3 -X importtime -m uvicorn src.main:app --host 0.0.0.0 --port 9997

# 環境変数
PYTHONPROFILEIMPORTTIME=1 python3 -m uvicorn src.main:app --host 0.0.0.0 --port 9997

ただし、これだけ見ても理解が難しいので、別のツールで可視化します。ファイルとして出力しましょう。

python3 -X importtime -m uvicorn src.main:app --host 0.0.0.0 --port 9997 2>&1 | grep "^import time:" > importtime.log

# `2>&1`  標準エラー出力を標準出力にリダイレクトします(importtimeの結果は標準エラーに出力されるため)

もし、Dockerで起動しているアプリケーションであれば、次のコマンドでログを取得しましょう。

# compose.yml に定義された fastapi サービスをログに出力する
## --no-log-prefix が付与されているとどのサービスから出力されたか、というログを削ります。この文言があると構造が変わるので可視化できません。

docker compose logs --no-log-prefix fastapi > importtime.log

import時間の可視化

今回、import時間の可視化にtunaを使用しました。使用方法は簡単で、先ほど出力したファイルを読み込ませるだけです。

uv add --dev tuna
uv run tuna importtime.log

私のソースコードではsrc.api.mainに依存しているgoogle.generativeaiライブラリが重そうであることがわかりました。

なお、もし可視化できない場合、内部的に起動が終わっておらず、可視化に必要な情報がそろいきっていない場合があるので、起動を待ってから再度ファイルにしましょう。

対策しようとした方法

基本的にはlazy loadで対応することになります。もしくは、先に重たいライブラリを読み込んでおくことで、メモリ消費タイミングをずらそうとしていました。

import sqlalchemy # noqa

起動時

次のように重たいライブラリを先に読み込んでおくことで対応しようとしていました。しかし、起動時にメモリ消費が多いのに、読み込むタイミングをずらしても意味はほとんどありませんでした。

lifespan時

FastAPIではアプリケーション起動後からリクエスト受信前に一度だけ実行されるロジックを記載できます。このタイミングでライブラリを読み込むことは一定の効果を得られました。ただし、一定といってもかなりごくわずかだったので意味が薄かったです。

ヘルスチェック時

ECSではヘルスチェックが通って問題ないときに旧アプリと新アプリを入れ替えるような設定を入れていました。そのため、ヘルスチェックのエンドポイントでライブラリを読み込むようにしました。ただ、このタイミングで読み込んでも起動時のメモリが消費されたままであることが多く、import timeには大きい影響を与えませんでした。

エンドポイントアクセス時

エンドポイントにアクセスされたタイミングでimportすることで、メモリ消費タイミングをずらそうとしました。

これもメモリに対しては一定の効果を得られたのですが、アプリケーションの挙動を安定させることができず断念しました。

唯一効果があった処理

ファイルの上部でimportするとファイルを読み込んだタイミングでライブラリを読み込んでしまうため、メソッド内でimport処理をしました。かなり重めな機械学習のライブラリをメソッド内に入れることで、メソッド実行時にライブラリが読み込まれるため改善が見込めました。ただ、多用するとソースコードの見通しも悪くなるため、部分的に適用したものの、基本的には対応していないです。

# メソッド内でimportする例(効果的な方法)
def process_data():
   # 重いライブラリはここでimportする
   import google.generativeai

学んだこと

アプリケーション起動時には、次のことを意識すると起動時のメモリ消費量を減らせます。

  • APIのroutingのファイルは最低限のファイルしか読み込ませない
  • routingの以外の処理はすべて別ファイルに追い出し、メソッド内で読み込ませる

ソースコード

なし

終わりに

最終的には何もできていないのですが、いろいろと学べました。あとでこういう問題に当たらないように、最初から意識して設計するべきですが、手遅れになってから気づくのでなかなか難しいですね…。

参考情報

PyCharmでJupyter Notebookを設定するだけの記事

始めに

最近、回帰分析をやるようになりました。分析そのものはまだまだ知識不足なものの、とりあえずPyCharmで分析を可視化するJupyter Notebookを使用する方法がわからなかったのでメモします。

なお、本当に初心者向けの記事となります。

環境

  • PyCharm
    • 2025.1.1.1

実装

リモートサーバを起動する

PyCharmはデフォルトでJupyter Notebookは使用できる状態を提供してくれます。しかし、私の環境が悪いのか、matplot等の追加でライブラリをインストールしたいときにうまくインストールできませんでした。そのため、ローカルにJupyter Labのサーバを起動して、PyCharmはそのサーバを参照するようにします。

# Jupyter インストール + 追加でインストールしたいライブラリ
uv add jupyter matplot
## もし、グルーピングしたいならこちらを使用する
uv add --group analyze jupyter matplot

# ローカルサーバの起動
## 起動するたびにトークンが変わってしまうので、パラメータとして渡すのがオススメです
uv run jupyter lab --ServerApp.token="test_token"

PyCharmで参照させる

適当にJupyter Notebookのファイルを作成してください。右クリックからNew -> Jupyter Notebookでもいいですし、ipynbの拡張子のファイルを作ってください。

その後、右上にJupyter Notebookの設定をする箇所があるので、Configure Jupyter Serverを選択してください。

サーバを追加してConfigure Serverを選んでください。

設定にhttp://localhost:8888と起動時に設定したトークンのtest_tokenを設定すれば実行可能です。

ソースコード

なし

終わりに

Webアプリケーションのことなら経験があるのでわかるのですが、急に分析タスクをアサインされて困りました。

最初はよくわかっていなかったので、とりあえずGoogle スプレッドシートでの分析を行っていました。このGoogleスプレッドシートの回帰分析については後ほど簡単ですがブログにします。ある程度方針が見えてきて、Pythonに分析手法をお引越しさせたところ、Jupyter Notebookなら簡単に可視化できるとのことでやってみたのですが、微妙に設定方法ががわからなくてハマってしまいました。

アプリケーションばっかり触ってきて、分析したことがない人にとっては、少しは役に立つ記事になったのではないかと思います。

参考情報

ログインの定義とプラポリ取得のタイミングを整理

※ タイトルのプラポリとはプライバシーポリシーのことを指します。また、サービスの性質によっても異なる可能性があるので、必ずしも鵜呑みにしないでください。

始めに

最近、GDPRを意識してプライバシーポリシーの承認を厳格化して、承認しない場合はサービス利用できない仕様にしようとしています。

それに伴い、何を考慮すべきで何を実装すればいいかを整理したので、自分用にまとめておきます。また、それに伴ってログイン(サインイン)の指す範囲が人によってずれていたのでそれも併せて整理します。

ログインとは

まずは、IT用語辞典のログイン/ログオンのページを引用します。

ログイン(login)とは、システムに自分の身元を示す登録情報を入力し、接続や利用開始を申請すること。本人であると確認されれば操作画面が表示され、利用を開始することができる。

認識がずれたのが、「プライバシーポリシーはログイン前に承認すべきだ」等の発言があり、ログインおよびログイン前ログイン後を指す範囲が人によって微妙に違っていたのです。そのため、次のように社内では定義しなおしました。

  • ログイン
    • ユーザを特定するための処理。認証。
  • ログイン前
    • ユーザが特定できていない状態。このタイミングでプライバシーポリシーを取得できる場合、第三者が勝手に承認してしまう可能性がある
  • ログイン後
    • ユーザが特定できた状態。≒ サービス利用できる状態。

また、ログインだけだとプライバシーポリシーの未承認によるサービス利用停止ができないため、次のステータスを追加することを合意しました。

  • プライバシーポリシー未同意
    • サービスが利用できない状態。ユーザを特定できたログイン後のみ同意ができる。
  • プライバシーポリシー同意
    • 言葉の通り。ただし、プライバシーポリシーが更新されたら未同意状態になる

プライバシーポリシーを同意するタイミング

サービスの性質によって異なります。

  • toBで一般ユーザ
    • ログイン後に承認する
  • toC または toBで管理者ユーザ
    • ログイン前に承認する

toBのサービスの場合、管理者が各ユーザに対してすぐに利用できるように事前にユーザ登録しているパターンもあるので、各ユーザがプライバシーポリシーを承認しないままサービス登録されるケースがあります。その場合は、各ユーザの承認を取得するタイミングがないのでログイン後に承認をもらう必要があります。

toCのサービスの場合、第三者が事前にユーザ登録するパターンがないと思うので、ユーザ登録時にプライバシーポリシーを同意する画面があると思います。また、カンファレンスの勉強会等の参加応募の場合、事前に名前やメール、所属会社等を入力することがあるので、それも応募前にプライバシーポリシーの承認をする必要があります。

その他

プライバシーポリシーの更新

更新後のアプリの挙動

※ 解釈間違っているかもしれないので、ここは気を付けてください。

プライバシーポリシーが更新されたからと言って必ずしも同意を得る必要はないはずです。更新後にアプリが行うべきアクションについても2種類あります。

  • 通知・周知
    • 誤字脱字等の単純な変更のみを想定。
  • 強制同意
    • 合意しないといけない変更。

事前告知について

法律上、2026年4月から施行される等で厳格に期限が決まっているとします。1-3か月ほど前にプライバシーポリシーを更新する必要がある場合もあります。

その場合、2026年1月時点でプライバシーポリシーを更新して通知・周知しておいて、2026年4月時点で強制同意を取らせるようなスケジューリング機能も必要です。更新したタイミングで強制同意をするほうが、管理すべきプライバシーポリシーのバージョンが減るので楽ではあるのですが、少しでもユーザに使っていただけるように通知・周知と強制同意のタイミングをずらすことも考慮が必要でしょう。

同意の初期状態について

絶対にデフォルトを同意状態にしてはいけません。みなし同意と判断される場合、法律上敗訴となる可能性があります。デフォルトを同意状態にしているシステムは見たことないですが、うっかり実装してしまうと大変です。

また、デフォルトでは同意することはできず、プライバシーポリシーを全部読ませた後に同意させるパターンもあります。訴訟リスクがある金融関係は特にプライバシーポリシーを全部読ませる仕組みが必要そうです。

基本的なサービスではチェックボックス形式にして横にプライバシーポリシーに対してのリンクを踏ませるだけでもいいかもしれませんが、少しでも訴訟リスクがあるようなサービスを運営している場合は全部スクロールしてからの同意が必要となりますので注意しましょう。顧問弁護士にスクロールが必要かどうかを確認すると安心です。

  1. 個人情報保護法の要件
    1. 日本の個人情報保護法でも、個人データの第三者提供には事前の本人同意が必要です
  2. GDPRの明示的同意要件
    1. GDPRではデータ処理に関して明示的な同意取得が必要です。これは日本の法律よりも厳格で、デフォルトで同意状態にする「みなし同意」はGDPRに違反します
  3. みなし同意のリスク
    1. 事業者側としては、不適切な同意取得方法(デフォルト同意など)を採用した場合、利用規約を適用できないリスクがあります

終わりに

ユーザから見たプライバシーポリシーって簡単な実装ではあると思うのですが、法的リスクを回避するための仕組みと考えると、考慮することが多くて大変ですね。この辺をちゃんと整理できるPdMがいれば非常に楽になると思います。

参考情報