🎯 ゴール
ゴールは「メモリの状況を監視できること」です。
具体的には、以下の2種類の問題のどちらか(または両方)が起きているかを特定します。
-
メモリスパイク: 5万件のデータを取得した瞬間、
nodeプロセスのメモリ使用量が急激に跳ね上がり、限界に達する。 -
スローリーク: データを取得するリクエストを繰り返し送ると、Prismaの
query-engine...プロセスのメモリ使用量が徐々に増加し、リクエストが止まっても解放されない。
🐳 準備編1: アプリをDockerコンテナで動かす
監視対象を明確にするため、RemixアプリをDockerコンテナとして起動します。
1. Dockerfile の作成
もしプロジェクトに Dockerfile がなければ、最小限のものとして以下のようなファイルをプロジェクトのルートに作成します。(node:18-slim の部分は、ご自身のNode.jsバージョンに合わせてください)
Dockerfile
# 1. ベースイメージを選択
FROM node:18-slim
# 2. 監視ツール (htop) と Prisma のために OpenSSL をインストール
# これが「スローリーク」を特定する鍵になります
RUN apt-get update && apt-get install -y procps htop openssl-3.0
# 3. アプリケーションの作業ディレクトリ設定
WORKDIR /app
# 4. package.json とロックファイルをコピー
COPY package.json pnpm-lock.yaml ./
# 5. 依存関係をインストール
RUN pnpm install
# 6. Prisma クライアントを生成
# (この時点で DATABASE_URL が必要なら --build-arg で渡す)
COPY prisma ./prisma/
RUN pnpm prisma generate
# 7. アプリのソースコードをコピー
COPY . .
# 8. Remix アプリをビルド
RUN pnpm build
# 9. アプリケーションを実行
CMD ["pnpm", "start"]
ポイント:
htopをインストールしています。これはコンテナ内部のプロセスを詳細に監視するための必須ツールです。procpsも入れています。これはpsコマンド(プロセス確認用)のためです。
📈 準備編2: 監視ツールと負荷ツールの使い方
監視のために、ターミナル(コマンドプロンプト)を3つ開いておくと便利です。
1. 監視ツール①: docker stats
- 役割: コンテナ全体(アプリ+Prismaエンジン+その他)が消費しているメモリの総量を監視します。
-
使い方:
docker stats <コンテナ名>
2. 監視ツール②: htop (最重要)
-
役割: コンテナの内部に入り込み、どのプロセス(
nodeなのかquery-engineなのか)がどれだけメモリを使っているかを詳細に監視します。 -
使い方:
docker exec -it <コンテナ名> htop -
見方:
-
COMMAND列: プロセス名です。node(Remixアプリ本体) と.prisma/client/query-engine...(Prismaエンジン) を探します。 -
RES(Resident) 列: これが最重要です。そのプロセスが実際に使用している物理メモリ量を示します。リークを監視する際は、この数値(例:450M= 450MB)が増え続けるかを見ます。
-
3. 負荷ツール: k6 または curl
- 役割: スローリークを再現するため、問題のAPIエンドポイント(5万件取得する関数)を繰り返し呼び出します。
-
curl(スパイク確認用):curl http://localhost:3000/your-problem-endpoint -
k6(スローリーク確認用):-
k6をインストールします(例:brew install k6)。 - 以下の内容で
script.jsファイルを作成します。// script.js import http from 'k6/http'; import { sleep } from 'k6'; export default function () { // あなたが特定した「5万件取得する関数」を呼び出す // RemixのloaderのエンドポイントURLに書き換えてください http.get('http://localhost:3000/your-problem-endpoint'); // 1秒待機 (リークをじっくり観察するため) sleep(1); }
-
🔬 実践編: メモリ監視ステップバイステップ
3つのターミナルを開いて、以下の手順を実行します。
-
【ターミナル1】アプリのビルドと起動
- Rancher Desktopが起動していることを確認します。
-
Dockerfileがあるディレクトリで以下を実行します。
# 1. Dockerイメージをビルド docker build -t remix-mem-test . # 2. Dockerコンテナを起動 (DBへの接続情報も環境変数 -e で渡す) docker run --rm -p 3000:3000 \ -e DATABASE_URL="postgresql://user:pass@host:port/db" \ --name remix-test \ remix-mem-test -
【ターミナル2】
docker statsで監視開始- コンテナが起動したら、以下を実行します。
docker stats remix-test-
MEM USAGE / LIMITの列に注目してください。これがコンテナ全体のメモリ使用量です。
-
【ターミナル3】
htopで監視開始- 以下を実行して、コンテナの「中」に入ります。
docker exec -it remix-test htop-
RES列に注目してください。
-
テストA: メモリスパイクの確認
- まず、アイドル状態(何もしていない状態)の
docker statsとhtopのRESの値をメモしておきます。(例:nodeが 150M、query-engineが 50M) -
別のターミナル(4つ目)を開き、
curlで問題のAPIを1回だけ叩きます。
curl http://localhost:3000/your-problem-endpoint-
結果の観察 (htop):
-
htop(ターミナル3)で、nodeプロセスのRES(メモリ) が 一瞬で数百MB〜数GBに跳ね上がらないか を確認します。 - もし跳ね上がり、
docker stats(ターミナル2)のメモリ使用量がコンテナの限界に達した場合、これが「大量データ取得によるメモリスパイク」です。
-
- まず、アイドル状態(何もしていない状態)の
-
テストB: スローリークの確認
- (一度コンテナを再起動して、クリーンな状態から始めると尚良いです)
-
script.jsファイルを用意したディレクトリで、別のターミナル(4つ目)からk6を実行します。(例: 10分間、1秒に1リクエストを送り続ける)
k6 run --duration 10m --vus 1 script.js-
結果の観察 (htop):
-
htop(ターミナル3)で、query-engine-debian-openssl...という名前のプロセスのRES(メモリ) に注目します。 -
k6がリクエストを送り続ける間、このRESの値が時間経過と共に(例: 1秒ごとに 0.1M ずつ)増加し続けるか を確認します。 -
最も重要な確認:
k6が終了(10分後)した後、このRESの値が減少しない(=高いまま解放されない) ことを確認します。
-
🏁 ゴール達成の確認
-
テストAで確認できた場合:
- 原因: 「大量データ取得によるメモリスパイク」(単一テーブルでも5万件をメモリに一括ロードしている)
- 上司の証言との一致: 「大量のdataを取得するとmemoryがオーバーする」
-
テストBで確認できた場合:
- 原因: 「Prismaクエリエンジンのスローリーク」
- 上司の証言との一致: 「取得したデータを握り続けているか何かしらでメモリリークが発生している」
これで、ローカル環境でメモリ問題を視覚的に監視・特定できたことになります。
この結果(どちらが原因だったか、あるいは両方だったか)を踏まえて、第3回「Kyselyを使った解決策」に進むと、より的確な対策が打てます。
承知いたしました。Prismaクエリエンジンの「スローリーク」を特定・観測することに特化し、ローカルのVM環境(Rancher Desktopを想定)で誰でも再現できるレベルの詳細なステップバイステップガイドを作成します。
🎯 目的:Prismaクエリエンジンの「スローリーク」を可視化する
ここでのゴールは、「5万件のデータを取得する」というリクエストを繰り返し実行した際に、Prismaの**クエリエンジン(query-engine)プロセスのメモリ使用量が徐々に増加し、リクエストが終了してもメモリが解放されない(=握り続けている)**現象を、あなたのローカルマシン(Rancher Desktop)上で観測・特定することです。
「VMで」という点は、Rancher Desktopが内部的にLinuxの仮想マシン(VM)を起動し、そのVM上でDockerコンテナを動かしているため、私たちがこれから監視するコンテナ内のプロセスは、まさに「VM上で動作しているプロセス」となります。
🛠 ステップ1:監視環境の構築(Dockerfileとk6)
まず、監視対象となるRemixアプリのコンテナイメージを作成し、負荷をかけるためのツールを準備します。
1. 監視ツール入り Dockerfile の準備
プロジェクトのルートディレクトリに、以下の内容で Dockerfile を作成(または修正)します。
# 1. ベースイメージ (ご自身のNode.jsバージョンに合わせてください)
FROM node:18-slim
# 2. 監視ツールとPrismaの依存関係をインストール (最重要)
# - procps: `ps`コマンド用
# - htop: 高機能プロセスビューア
# - openssl-3.0: Prismaのクエリエンジンが依存しているため
RUN apt-get update && apt-get install -y procps htop openssl-3.0
# 3. アプリケーションの作業ディレクトリ設定
WORKDIR /app
# 4. 依存関係のインストール (pnpmの例)
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install
# 5. Prisma クライアントの生成
COPY prisma ./prisma/
# (もしDB接続が必要なら、ビルド時に --build-arg で渡すか、
# この後の docker run 時に環境変数として渡す)
RUN pnpm prisma generate
# 6. アプリのソースコードをコピー
COPY . .
# 7. Remix アプリをビルド
RUN pnpm build
# 8. アプリケーションを実行
CMD ["pnpm", "start"]
2. 負荷テストツール k6 のインストール
もしローカルマシン(MacやWindows)に k6 がインストールされていなければ、インストールします。
-
macOS (Homebrew):
brew install k6 -
Windows (Chocolatey):
choco install k6
🏃♂️ ステップ2:監視対象(コンテナ)のビルドと起動
ターミナル(コマンドプロンプト)を開き、Dockerfile があるディレクトリで以下のコマンドを実行します。
-
Dockerイメージのビルド:
(ビルドに数分かかることがあります)docker build -t remix-leak-test . -
Dockerコンテナの起動:
YOUR_DATABASE_URL_HEREの部分は、あなたのデータベース接続文字列(コンテナからアクセス可能なもの。例:postgresql://user:pass@host.docker.internal:port/db)に置き換えてください。docker run \ --rm \ -p 3000:3000 \ -e DATABASE_URL="YOUR_DATABASE_URL_HERE" \ --name remix-test \ remix-leak-test
これで、VM(Rancher Desktop)上でRemixアプリのコンテナが起動しました。
📊 ステップ3:監視開始(htopでの初期状態確認)
スローリークを観測するために、2つ目のターミナルを開き、htop でコンテナ内部のプロセスを監視します。
-
コンテナ内部に入る:
以下のコマンドで、実行中のremix-testコンテナの中に入り、htopを起動します。docker exec -it remix-test htop -
htop画面の調整と初期状態の確認:-
htopが起動したら、キーボードのF5を押して「Tree View(ツリー表示)」に切り替えます。 -
nodeプロセス(pnpm startから起動されたもの)を探します。 -
その子プロセスとして、Prismaのクエリエンジン(例:
./query-engine-debian-openssl-3.0.x)が見つかるはずです。 -
初期状態のメモリを記録:
query-engineプロセスの行に注目し、RES(Resident Set Size: 実際に使用している物理メモリ量)の数値をメモします。
(例: アイドル状態でRES= 52.5M)
-
🧪 ステップ4:負荷テストの準備(k6スクリプト)
次に、3つ目のターミナルを開き、負荷テスト用のスクリプトファイルを作成します。
- 適当な場所に
script.jsという名前で以下のファイルを作成します。 -
http://localhost:3000/your-problem-endpointの部分は、あなたが特定した「5万件取得する関数(loader)」がマッピングされているURLに書き換えてください。
script.js
import http from 'k6/http';
import { sleep } from 'k6';
// スローリークを観測するためのテスト設定
export const options = {
stages: [
// 1VUser (1人のユーザー) が 10分間 実行し続ける
{ duration: '10m', target: 1 },
],
};
export default function () {
// 1. 問題のエンドポイントにリクエストを送信
http.get('http://localhost:3000/your-problem-endpoint');
// 2. 1秒待機 (重要)
// リクエストを詰め込みすぎず、
// メモリが徐々に増加する様子を観測しやすくするため。
sleep(1);
}
📈 ステップ5:スローリークの再現と観測
いよいよ、負荷をかけながら「スローリーク」を観測します。
-
負荷テストの実行:
【ターミナル3】で、k6を実行します。k6 run script.js(
running (10m00.0s), 1/1 VUs, 1 complete and 0 interrupted iterationsのように表示され、テストが開始されます) -
htop画面の監視(最重要):
すぐに【ターミナル2】のhtop画面に戻り、query-engineプロセスのRESの値に全集中します。観測すべき現象(スローリークの兆候):
-
k6がリクエストを1回送るたびに(script.jsで1秒ごとに設定)、query-engineのRESが ごく僅かに(例: 0.1M や 0.5M ずつ)増加 していきます。 - 1分後、5分後と時間が経つにつれて、
RESの値が着実に右肩上がりになっていることを確認します。
(観測例)
- テスト開始時:
RES= 52.5M - テスト 1分後:
RES= 68.0M - テスト 5分後:
RES= 130.2M - テスト 10分後:
RES= 215.8M
-
-
負荷テスト終了後の監視:
k6のテストが10分後に終了します。【ターミナル3】でdoneと表示されます。htop画面で最後の確認:- リクエストが完全に止まった後も、
query-engineのRESが高いまま(例: 215.8M 付近)維持され、初期値の 52.5M に戻らないことを確認します。 - もしメモリが解放されない(握り続けている)場合、それが**「スローリーク」が発生している決定的な証拠**となります。
- リクエストが完全に止まった後も、
🔬 ステップ6:結果の分析
-
観測A:
RESが増加し、解放されない- 結論: おめでとうございます(?)。上司の指摘通り、Prismaクエリエンジンによる「スローリーク」の再現に成功しました。これはPrismaのバイナリレベルの問題である可能性が非常に高いです。
-
観測B:
RESは増加するが、テスト終了後に初期値に戻る- 結論: これは「リーク」ではありません。ガベージコレクション(GC)が正常に機能していますが、大量のデータを扱うために一時的に多くのメモリが必要になっている状態です。
-
観測C:
query-engineのRESは増えず、nodeプロセスのRESが急増する- 結論: これは「スローリーク」ではなく、前回の「メモリスパイク」(5万件のデータをJSオブジェクトとしてメモリに一括ロードしている)が原因である可能性が高いです。
この詳細な手順により、問題が「スローリーク」なのか、それとも別の要因なのかを高い精度で切り分けることができるはずです。
やること
- brew watch install
- 起動しているコンテナ特定
- コンテナの中に入ってprisma query engineのPIDを取得
- クエリを叩く
- 監視モードON
- watch -n 2 'docker stats --no-stream <コンテナ名>'
- docker exec <コンテナ名> cat /proc/50/status | grep -E 'Name|VmRSS'でそのプロセスが使っているメモリを確認する
- 増え続けていたら成功