どのERPも導入初日は快適に動きます。しかし6か月後、カスタムフィールドが40個増えた頃には、営業チームからSales Orderの一覧画面が表示されるまで8秒かかると苦情が来て、誰かがひそかに「直るまでの代わり」としてExcelを使い始めている——これはよくあるサポートチケットのひとつであり、原因が一つだけということはほとんどありません。たいていは小さな原因が3つか4つ積み重なっています。
本稿では、OdooとERPNext(Frappe)の両方を対象に、診断のアプローチと具体的な対処法を扱います。両者は技術スタックこそ異なりますが、性能面で失敗するパターンは驚くほど共通しています。どちらもリレーショナルデータベースの上に乗ったPython ORMであり、限られた数のワーカープロセスの背後で稼働しており、遅くなる箇所も同じ3か所——データベース、ワーカー層、そしてその上に重ねられたカスタマイズ——です。
1. チューニングの前にまず診断する
「RAMを増やす」「ワーカー数を増やす」にすぐ飛びつきたくなりますが、それは我慢すべきです。インデックス不足が原因で遅い系に対してリソースを追加しても、同じ壁により速くぶつかるだけです。
実際にどこで時間を消費しているかを特定しましょう。
- 特定のページだけが遅いのか、全体が遅いのか? 特定のレポートだけが遅い場合はクエリやスクリプトが疑わしく、サイト全体が遅い場合はインフラ——ワーカー、データベース接続、ディスクI/O——を疑うべきです。
- 特定のユーザーだけが遅いのか、全ユーザーが遅いのか? 個人ダッシュボードが巨大な一人のユーザーや、5万行を返す保存済みフィルタは、月曜の朝9時に全員が遅くなる現象とは別の問題です。
- Odoo:
--log-level=debug_sql(または開発者モードの Performance/Query Count 機能)でリクエストごとのクエリ数と所要時間を確認します。200件を超えるクエリを発行する画面はインフラの問題ではなくカスタマイズの問題です。 - ERPNext/Frappe: 組み込みのRecorder(
bench --site [site] set-config recorder 1を実行し、UIのDeveloper Settings → Recorderから確認)でリクエストごとのSQLクエリ数と所要時間を記録します。MariaDBのスロークエリログ(slow_query_log = 1、long_query_time = 1)で残りを捕捉できます。
インフラ変更に手を付ける前にひとつだけやるとすれば、これです:最も遅いクエリまたはリクエストの上位5件を見つけ、それぞれを具体的なDocType/モデル、レポート、スクリプトまで追跡すること。
2. データベース層
両システムとも、負荷のほとんどをデータベースに依存しています——OdooはPostgreSQL、ERPNext/Frappeはデフォルトでは MariaDB です(FrappeはV14以降PostgreSQLもサポート)。実際のボトルネックがデータベースであることは、想像以上に多くあります。
カスタムフィールドへのインデックス不足。 これがERPの遅延の中で最も頻繁に見かける原因です。フィルタ、一覧画面のカラム、レポートで使用されるカスタムのLink/Selectフィールドにはすべてインデックスが必要です——標準フィールドと違い、カスタムフィールドは自動でインデックスが付きません。
- Odoo:フィールド定義で
index=Trueを設定するか、既存データに対してはSQL/マイグレーションで追加します。 - Frappe:
"in_list_view": 1を設定しただけではインデックスは作成されません。DocFieldで"search_index": 1を設定するか、bench --site [site] add-indexで手動で追加する必要があります。
統計情報の陳腐化と肥大化。 PostgreSQLは定期的な VACUUM ANALYZE を必要とします(通常はautovacuumが処理しますが、stock_move や account_move_line のような書き込みの多いテーブルで遅延なく実行されているか確認してください)。MariaDBのテーブルも、更新頻度の高いテーブルに対して定期的な ANALYZE TABLE と OPTIMIZE TABLE が有効です。特にFrappeの Version テーブルや Comment テーブルは肥大化しやすく、ほとんど整理されないまま放置されがちです。
コネクション枯渇。 Odooの各ワーカー、Frappeの各gunicornワーカーは、それぞれデータベース接続を1本保持します。中規模のデータベース環境では、負荷がかかると max_connections を使い切ってしまいやすく、これは一様な遅さではなく断続的なタイムアウトとして現れます。コネクションプーラー(Odoo/PostgreSQLならPgBouncer)は、同時利用者が一定数を超える環境では標準的な対策です。
トランザクション用データベース上での重い読み取りレポート。 Sales Invoice Item の1年分をスキャンするスケジュールレポートは、営業チームが今まさに必要としているリソースを奪います。重いレポートやBIダッシュボードを定期的に実行している場合は、プライマリではなくリードレプリカに向けましょう。
3. ワーカー層
OdooもFrappeも、固定数のワーカープロセスの背後で動くWSGIアプリケーションです。このプール数はインフラ上で最も大きなレバーであり、多くの場合、設定が誤っています。
Odooのワーカー。 標準的な計算式は workers = (CPUコア数 × 2) + 1 で、約20%をcronジョブ用に確保します(max_cron_threads)。各ワーカーは独立したメモリ空間を持つ別プロセスです(--limit-memory-soft / --limit-memory-hard で暴走リクエストから保護)。ワーカーあたりのRAMが不足していると、リクエストの途中でワーカーが強制終了・再起動され、ランダムな遅延や散発的な502エラーのように見えます。大量インポートやPDF生成のような長時間のリクエストは、通常のワーカーを占有せず、longpolling/キューの仕組みに分離すべきです。
Frappe/ERPNextのワーカー。 bench はWebリクエスト用のgunicornプールに加え、バックグラウンドジョブ用の独立したRQ(Redis Queue)ワーカーを実行し、short、default、long の3つのキューに分かれています。よくある失敗パターンは、重いジョブ(大量メール送信、大規模データインポート、PDFの一括生成)を default キューに乗せてしまい、他のすべてのバックグラウンドジョブがその後ろで詰まってしまうケースです。長時間かかるジョブは明示的に long キューへ振り分け、バックグラウンドジョブの量に対してキューごとに十分なワーカープロセスを確保してください——これは Procfile/supervisor.conf で設定するもので、自動的にスケールするものではありません。
ソケット/リアルタイム層。 Frappeのリアルタイム更新もOdooのlongpolling/busも、Redisと専用プロセス(Frappeのsocketio用の node、Odooのlongpollingワーカー)に依存しています。このプロセスが落ちると、ユーザーからは明確なエラーではなく「画面が固まっているように感じる」という報告が上がることが多くなります。
4. 正しく分離されたキャッシュ
両システムともRedisを複数の目的——一般にキャッシュ、バックグラウンドジョブキュー、リアルタイム用のpub/sub——で使用しています。よくある誤設定は、この3つすべてを同じRedisインスタンス/データベース番号に、メモリ上限も設けずに向けてしまうことです。メモリ逼迫時にキャッシュが退避されると、キューに入っていたバックグラウンドジョブまで巻き添えで失われることがあります。キャッシュとキューには別々のRedisデータベース(または別インスタンス)を使い、キャッシュ用インスタンスには適切な maxmemory とeviction policyを設定してください。
ERPNextでは、site_config.json の redis_cache、redis_queue、redis_socketio がそれぞれ実際に別のデータベース番号を指しているか確認してください。多くの自己運用環境ではbenchのデフォルト値のまま放置されており、単一サイトの開発環境なら問題ありませんが、実運用負荷のある本番環境には適していません。
5. 規模が拡大するまで表面化しないカスタマイズのアンチパターン
このカテゴリはチームの意表を突きます。テスト時は10件のサンプルデータで何も問題がなく見えるのに、5万件になった途端に破綻するからです。
- サーバースクリプト/カスタムスクリプト内のN+1クエリ。 1行ずつまとめて取得せず
frappe.get_doc()やself.env['res.partner'].browse()をループのたびに呼び出すコードは、10行では問題になりませんが、10,000行では致命的です。ループの前にfrappe.get_all()/search_read()でバッチ取得するようにしましょう。 - キー入力やフィールド変更のたびにサーバーへ問い合わせるクライアントスクリプト。 特に多くの行が表示されている一覧画面上では、1回1回がラウンドトリップになります。
- 不必要に広範な一覧画面/レポートのクエリ。 3列しか表示していないのにDocType/モデルの全カラムを取得したり、めったに開かない子テーブルの行を先読みしてしまったりするケースです。
- 行単位で評価が必要な権限・共有ルール。 ユーザー権限やレコードルールは強力な機能ですが、10万行の一覧に対して行ごとにサブクエリを評価するルールは、見た目どおり遅くなります。
- リクエストのサイクル内で同期的に実行される印刷フォーマットやPDF生成。 請求書1枚なら問題ありませんが、「200枚の請求書を一括印刷」という操作になると大きな負担になります。バックグラウンドジョブとして実行すべきです。
これらすべてに対する解決策は同じです。まず(第1節の方法で)プロファイリングし、具体的なスクリプトやクエリを特定してから修正する——「とりあえず大きなサーバーにして様子を見る」ではありません。
6. Filestoreと添付ファイル
両システムとも、添付ファイルはデフォルトでアプリサーバーのローカルディスクに保存されます。これは規模が小さいうちは問題ありませんが、いずれ限界が来ます。アプリサーバー上のローカルディスクは水平スケールできず、バックアップは遅くなり、一部のネットワークストレージ構成ではfilestoreのI/O遅延が添付ファイルプレビュー付きのすべてのドキュメントを直接遅くします。単一サーバー構成を超える規模では、添付ファイルをオブジェクトストレージ(S3互換)へ移行しましょう。Odoo(filestoreアドオン経由)、Frappe(ネイティブのS3バックアップ/添付ファイル対応)ともにこれをサポートしています。
7. インフラのサイジングと水平スケーリング
データベースに適切なインデックスが張られ、カスタマイズがプロファイリングによってクリーンな状態になれば、インフラサイジングの議論はぐっとシンプルになります。
- 多くの人が想像する以上にディスクが重要です。 回転式ディスクやIOPSが不足したクラウドストレージ上でのデータベース性能は、他のすべての改善のボトルネックになります。中規模以上の本番ERPデータベースにとって、十分なIOPSを持つSSDストレージは選択肢ではなく必須です。
- 水平スケーリングには共有状態が必要です。 アプリサーバーを2台目に増やすということは、filestoreとセッション/キャッシュ層を共有する必要がある(オブジェクトストレージ+共有Redis)ということです。そうでなければ、どのノードに接続されたかによってユーザーは一貫性のない挙動を目にすることになります。
- 静的アセットはCDN、少なくとも積極的にキャッシュするリバースプロキシの背後に置くべきです。 すべてのJS/CSSリクエストをアプリワーカー経由でプロキシするのではなく、nginxが
/assetsを直接配信するようにします。
診断チェックリスト
- 遅延を再現し、特定のページだけか全ページかどうか、特定のユーザーだけか全ユーザーかどうかを特定する。
- クエリプロファイリングを有効化し(Odooの
debug_sql、FrappeのRecorder)、最も遅いリクエストの上位5件を見つける。 - 同じ時間帯のMariaDB/PostgreSQLのスロークエリログを確認する。
- 遅いクエリごとに、
WHERE句で使われているカスタムフィールドにインデックスが欠けていないか確認する。 - ワーカー/キューの状態を確認する:Odooのワーカーがメモリ不足で強制終了されていないか、Frappeのバックグラウンドキューが滞留していないか。
- Redisを確認する:キャッシュがジョブキューとメモリを取り合っていないか。
- 遅延経路に関わるカスタムスクリプトにN+1パターンがないか確認する。
- 1〜7を終えてから初めて、CPU/RAM/ワーカーの追加を検討する。
ERPの遅延の多くは、ハードウェアのアップグレードなしに解決できます。データベース層とカスタマイズ層が実際のチケットの大半を占める原因であり、インフラサイジングは最初に手を付けるべきものではなく、最後に残る5%程度の話です。
出典:
- Odoo Deployment Documentation — Worker configuration
- Frappe Framework — Background Jobs
- Frappe Framework — Recorder
- PostgreSQL Documentation — Routine Vacuuming
最新の記事
- パスキーの「登録」が新しい攻撃面になる:Pass-the-Passkeyが IdP 運用に突きつけたもの August 22, 2026
- 攻撃者はすでにAIエージェントかもしれない — OpenAIとHugging FaceのインシデントがSOCに突きつけたもの August 17, 2026
- OCPI 2.2.1 実装ガイド:Locations、Sessions、CDR をどう構築するか August 7, 2026
- OCPIとは何か:CPOとeMSPがEVローミングのために本当に構築すべきもの August 7, 2026
- 海のない土地のシーバス:海から遠く離れた海水魚のための自動給餌システムを作る July 31, 2026
- 工場の現場は五つの方言を話している:OPC UAだけではプロトコルの分断は解決しない理由 July 30, 2026