こんにちは、にょろりんこの備忘録技術ブログです。
最近、自分で構築・運用している翻訳システムの安定性がようやく落ち着いてきたので、そのエコシステム構成について記録しておこうと思います。
使っているのは、MythoMaxベースのLLM翻訳サーバー、Node.jsによるバッチ処理群、そしてSudachiを使った日本語の形態素解析。このあたりをそれぞれ分離・連携しながら、PM2でプロセス管理、Dockerで一部競合を回避する構成に落ち着きました。
独学ながらも、実運用で連日稼働している構成としては、安定性と制御性の両面でかなり堅実な設計だと感じています。

この記事では、実際の ecosystem.config.cjs の中身を紹介しながら、
- なぜGunicornで起動するのか?
- Node.jsとPythonをどう共存させるのか?
- なぜSudachiはPM2から外したのか?
といった点について、実例ベースで書いていきます。
なぜGunicornで起動するのか?
PythonでAPIサーバーを立てる場合、Flask や FastAPI をそのまま pythonで動かすこともできます。しかし、実運用においてはGunicorn(WSGI HTTPサーバー)での起動が必須だと感じています。その理由は大きく3つあります。
① 複数接続・タイムアウトへの強さ
Flask単体では、あくまで開発用のシングルスレッドサーバーにすぎず、複数のリクエストを処理する能力が限定的です。特にMythoMaxのようなLLM処理は、1件のリクエストに数十秒~数分かかることもあり、並列処理ができないとすぐ詰まります。
Gunicornなら -w 1 でもプロセスとしては安定し、タイムアウトなども --timeout 600 のように指定できて明確です。
② PM2との相性の良さ
PM2はNode.js用のプロセスマネージャーですが、interpreter: 'none' を指定すれば Python製のGunicornサーバーも一括で監視・再起動対象にできるのが魅力です。
例えば以下のように記述しています:
// Mythoサーバー(ポート8081、LLM生成系)
{
name: 'mytho-server',
script: '/root/venv/bin/gunicorn',
args: '-w 1 -k sync -b 0.0.0.0:8081 --timeout 600 --log-level debug mytho_server:app',
interpreter: 'none',
cwd: '/var/www/html',
instances: 1,
autorestart: true,
max_memory_restart: '4G',
env: {
OMP_NUM_THREADS: "8",
OMP_PROC_BIND: "false",
OMP_WAIT_POLICY: "passive",
PATH: '/root/venv/bin:' + process.env.PATH,
PYTHONPATH: '/var/www/html'
}
},
FlaskアプリをGunicorn経由で起動しつつ、PM2にログやメモリ上限も渡して全体の死活監視の一環にできるのは非常に便利です。
③ Python venv環境との両立
Dockerを使わずにPython仮想環境 (venv) を使っている場合、gunicorn のバイナリパスや PYTHONPATH の指定が問題になります。
Gunicornは venv/bin/gunicorn として個別指定できるため、システムPythonや他の仮想環境とバッティングしにくいという点でも有利です。
④補足:uvicornやhypercornではだめなのか?
選択肢として uvicorn(ASGI)や hypercorn もありますが、今回の構成はFlask+WSGIなので gunicorn で十分。ASGIにする理由もなく、シンプルに動く構成を優先しました。
Node.jsとPythonをどう共存させるのか?
① Node.jsは制御・連携担当、PythonはAPIサーバーとして分離
Node.js 側のバッチ処理(app160_batch.cjs)は、Python の翻訳ロジックを直接叩くのではなく、HTTP API経由で連携しています。
具体的には、Python側の mytho-server(Flask+Gunicorn)を http://127.0.0.1:8081/generate で待ち受けさせ、Node.js側は axios でPOSTします。
const res = await axios.post(
'http://127.0.0.1:8081/generate',
{ text: prompt },
{
timeout: 120000,
httpAgent: new http.Agent({ keepAlive: false }),
httpsAgent: new https.Agent({ keepAlive: false }),
}
);
この方式により、言語をまたぐプロセス呼び出しによる不安定さやエラー処理の煩雑さを回避しています。
② 共通のPM2で両者を監視
Node.jsの各アプリ(app160-batch など)も、Pythonの翻訳サーバー(mytho-server)も、PM2で一括管理しています。これにより、ログの統一管理、リスタート、自動再起動などの運用が簡潔になります。
Python側のプロセスは interpreter: 'none' を指定し、Node.js 側は interpreter: 'node' で制御しています。
③ 環境変数の統一で仮想環境の食い違いを防止
Python仮想環境(venv)で構築されたAPIサーバーに対し、Node.js 側からアクセスするときに環境の食い違いが起きないよう、PM2で明示的に環境変数 PATH と PYTHONPATH を渡しています。
env: {
PATH: '/root/venv/bin:' + process.env.PATH,
PYTHONPATH: '/var/www/html'
}
これにより、FlaskやLlamaなどの依存モジュールが確実に解決されるようになります
④ 補足:昔は spawnSync でPythonスクリプトを直接叩いていた
以前の構成では、Node.js から spawnSync('python3', [...]) を使って Python スクリプトを直接呼び出していました。しかしこの方法では、
- ログ管理やエラー伝播が難しい
- venv の有効化が不安定になる
- 同時実行時に競合しやすい
などの理由で、現在は完全に APIベースの非同期呼び出しに移行しています。
なぜSudachiはPM2から外したのか?
当初、形態素解析エンジンとして利用している Sudachi(SudachiPy + Word2Vec連携) も他のプロセスと同様に PM2で常駐・監視対象として動かしていました。しかし、運用中に特定の条件でMytho翻訳APIが2件目以降で必ず失敗するという現象が発生。調査の結果、SudachiをPM2経由で起動していたことが原因と判明し、最終的にPM2から除外しました。
① 症状:Mytho翻訳が1件目だけ成功、2件目以降は必ず失敗
翻訳バッチ(app160-batch)を実行すると、1件目は正常に翻訳されるのに、2件目以降は API が無反応またはエラーを返すという異常が発生。ログを確認しても、特に目立った例外やクラッシュはなく、プロセスは生きているのに応答が止まるという不気味な挙動でした。
② 原因:SudachiのプロセスとMythoサーバーの間でリソース競合が発生
調査を進めたところ、PM2経由でSudachiサーバーを起動すると、Python内部のスレッド周り(OMP/OpenMP)で競合が発生していたことがわかりました。
SudachiもLLMも同じ venv を使っており、OMP_NUM_THREADS などの環境変数が干渉
PM2経由で起動されたプロセスは、fork ベースでリソースを分け合っており、LLM処理中にSudachi側のスレッド設定が影響を及ぼす
結果として、Gunicorn上のMythoサーバーが2件目以降でハングするような状態になる
③ 対策:SudachiはPM2管理から外し、Dockerで独立常駐化
この問題を回避するため、Sudachiサーバーは以下のように分離しました:
- PM2の ecosystem.config.cjs から 該当プロセスをコメントアウト・削除
- sudachi_server.py を Dockerコンテナで単独起動 し、LLM群とはプロセス空間を完全に分離
- コンテナ起動スクリプトも作成し、明示的に docker start sudachi で起動・監視
この結果、Mythoサーバーの翻訳処理は安定して複数件処理できるようになり、問題は完全に解消しました。
結論:PM2は便利だが、プロセス空間が混ざると危険
PM2は便利なプロセスマネージャーですが、並列実行が多い処理系(LLM・形態素解析など)を混在させると、思わぬリソース干渉が起きることがあります。
そのため:
という方針にしたことで、運用の安定性が一気に向上しました。
まとめ:プロセスは「分離」と「責務」で安定する
今回紹介したように、Mytho翻訳APIやバッチ処理、形態素解析といった多様な処理を安定運用するためには、単に動かすだけでなく、それぞれのプロセスがどのように干渉し合うかを意識した設計が重要です。
- Node.js は制御系、Python はLLMや解析系として役割を分離
- 直接呼び出すのではなく、HTTP API で非同期に連携
- PM2は軽量プロセスの監視に、重い処理系はDockerで隔離
- 競合が発生する場合は「統合管理」よりも「分離運用」が有効
LLMや解析サーバーを絡めたマルチプロセス構成は、一見複雑に見えても、責務ごとに切り分けて管理することで、むしろトラブルを減らせるというのが今回の実感です。
今後さらにプロセス数が増えても、「言語」「処理内容」「リソース依存」の3点を軸にした整理ができていれば、大崩れはしないはず。この記事が同じような構成を考えている方の参考になれば幸いです。
今回の記事で紹介しているコードは、実際に現在も安定稼働している「あなたを追跡しないアダルト動画の検索エンジン|Sae‐Porns」で使われています。
技術面に興味のある方も、ぜひ一度使ってみてください。
それではみなさん、よき開発ライフを。