はじめに
NestJSとPrismaのイメージをCloudRunにデプロイしたら、クラッシュして起動しなくなったので、原因について調べたことまとめです。
事象
Segmentation fault になって、NestJSが起動してくれない。(Cloud Runのログ)
環境
Dockerfile
#==================================================
# Build Layer
FROM --platform=linux/amd64 node:18 as build
WORKDIR /app
COPY package.json yarn.lock ./
COPY prisma ./prisma
RUN yarn install --non-interactive --frozen-lockfile
RUN yarn prisma generate
COPY . .
RUN yarn build
#==================================================
# Package install Layer
FROM --platform=linux/amd64 node:18 as node_modules
WORKDIR /app
COPY package.json yarn.lock ./
COPY prisma ./prisma
RUN yarn install --non-interactive --frozen-lockfile --prod
RUN yarn prisma generate
#==================================================
# Run Layer
FROM --platform=linux/amd64 node:18-slim as node
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist /app/dist
COPY --from=build /app/prisma /app/prisma
COPY --from=node_modules /app/package.json /app/yarn.lock ./
COPY --from=node_modules /app/node_modules /app/node_modules
CMD ["/usr/local/bin/yarn", "start:prod"]
まずは、Segmentation fault について調べてみます。
Segmentation fault とは
- プログラムがOSによって設定された基本的な規則に違反したときに発生するエラーです。
- このエラーが起きたとき、OSはプロセスにシグナルを送信し、プロセスはシャットダウンします。
- 基本的には、低レベルの問題(ポインタやメモリ管理など)なので、JavaScriptを書くときは気にする必要はありませんが、Node.js で segfault が発生するパターンはいくつかあるようです。
- Nodejsのネイティブのアドオン実装で、そのアドオン自体にバグがあるか、Nodeのバージョンと互換性がない場合
- Node内部の状態の操作で、前提の状態が崩れ、Node の組み込みネイティブ コードが間違った動作をして、セグメンテーション違反が発生する場合
- Node.js 自体にバグがある場合
原因
ログには、Segmentation fault となっているものの、なぜ Segmentation faultになっているかの原因がわからず、社内Slackで困り果てていたところ、同僚に以下のIssueを教えてもらいました。
このIssueに原因とワークアラウンドが書かれてた(大感謝!!!)
直接的な原因は、NodeJS自体にバンドルされたOpenSSLとシステムで使われてるOpenSSLのバージョンが異なることに起因してるようです。
Node.js 17.0.0 以降、Node.jsにバンドルされてるOpenSSLのバージョンは3なのですが、システムが使っているOpenSSLのバージョンが1.1.xの場合、バージョンがずれて本事象が起こるようでした。
ワークアラウンド
回避策は以下の3つがあるようで、どちらかのOpensslのバージョンに合わせる、もしくはクエリエンジンタイプにバイナリエンジンを指定することで回避できるようです。
- Nodeのバージョンを 16.x に下げ、Node.jsにバンドルされてるバージョンが 1.1.x になるようにして、Nodeとシステムで使われてるOpenSSLのバージョンを合わせる。
- システムで使われてるOpenSSLのバージョンを3に上げて、Nodeとシステムで使われてるOpenSSLのバージョンを合わせる。
- デフォルトの設定では、PrismaクライアントはクエリエンジンをNodeAPIとして利用しますが(
engineType = library
)、クエリエンジンを利用する別の方法として、実行可能なバイナリーをサイドカープロセスとして実行するという方法もあり(engineType = binary
)、デフォルトの設定から、engineType = binary に切り替えると、システムで使っているOpenSSLとNodeで使ってるOpenSSLのバージョンが異なっていても、この問題を回避できます。
クエリエンジン
クエリエンジンのタイプをバイナリーエンジン(engineType = binary
)に切り替えると、Node.jsに含まれてるOpenSSLとシステムで使われてるOpenSSLのバージョンが異なっても Segmentation faultのエラーが回避できるのがなぜか気になったので、クエリエンジンについてのドキュメントをあらためて読んでみた。
クエリエンジンとは
- prisma client からdbへのconnectionが呼び出された時に、dbとのコネクションを管理する
- prisma client からのqueryをSQLに変換、DBに送信する
- rust実装でOSに依存してる
以下、ドキュメント
クエリエンジンのタイプ
クエリエンジンのタイプは以下の2パターンから選択することができます。
Prisma クライアントとクエリ エンジン間の通信オーバーヘッドが削減されるため、Node-API ライブラリ アプローチがおすすめのようです。(デフォルトの設定)
- Prisma Client にロードされる Node-API ライブラリとしてクエリエンジンを利用する方法(デフォルトの設定)
- 独自のプロセスで実行される実行可能バイナリとしてクエリエンジンを利用する方法
generator client {
provider = "prisma-client-js"
engineType = "binary"
}
気になっていたところとして、engineType = binary
の場合は、Node-API ではなく独自のプロセスでクエリエンジンが実行されるので、Node.jsが内包してるOpenSSLが使われず、バージョンが異なっていても動くって感じっぽさそうと思ったのですが、詳細はあってるか自信ないのでもう少し調べてみます。
おわりに
エラーを調べてて初めて知ったことが多かったので勉強になった。Prismaのクエリエンジンのタイプや環境に依存があったりなどそのあたりあまり意識せず使っていたので、ドキュメントを改めて読んで色々発見があってよかった。
参考
- Node.js OpenSSL Strategy
Discussion