いろいろ頑張ってみたものの、Docker本家とかその他の情報によると、Dockerfile内部でマルチコア対応は頑張らない方が良いらしい。せっかくがんばったので供養のためにまとめておきます。
- DockerでNode.jsアプリをイイ感じに保つ4つの方法 #docker
- nodejs.org: Dockerizing a Node.js web app
- Docker and Node.js Best Practices
マルチコア対応したい!
Node.jsはシングルコアで動くのでマルチコア対応したいですよね?コア数が生かせないのはかっこ悪いですよね?サーバーにそのままデプロイする場合はpm2とかのプロセスマネージャを使います。pm2のウェブサイトをみると、Docker用のpm2があるじゃないですね。
というわけでそれを使うようにしてみました。
- package.jsonのdependenciesにはpm2のみ
- package.jsonのその他の依存はdevDependenciesに
- npm run buildでサーバーコードはdist/index.jsというシングルjsファイルになる(
@zeit/ncc
利用)
package.jsonは以下の通り(ESLintとかビルドに不要なものは省いた)。
{"name":"webserver","version":"1.0.0","scripts":{"build":"ncc build src/main.ts"},"dependencies":{"pm2":"^4.4.0"},"devDependencies":{"@types/body-parser":"^1.19.0","@types/compression":"^1.7.0","@types/express":"^4.17.7","@zeit/ncc":"^0.22.3","body-parser":"^1.19.0","compression":"^1.7.4","express":"^4.17.1","http-graceful-shutdown":"^2.3.2","typescript":"^3.9.7"}}
サンプルのウェブアプリはこんな感じで作ってみました。大事なのはシグナルを受け取って終了するということです。
importexpress,{Request,Response}from"express";importcompressionfrom"compression";importbodyParserfrom"body-parser";importgracefulShutdownfrom"http-graceful-shutdown";constapp=express();app.use(compression());app.use(bodyParser.json());app.use(bodyParser.urlencoded({extended:true}));app.get("/",(req:Request,res:Response)=>{res.json({message:`hello ${req.headers["user-agent"]}`,});});consthost=process.env.HOST||"0.0.0.0";constport=process.env.PORT||3000;constserver=app.listen(port,()=>{console.log("Server is running at http://%s:%d",host,port);console.log(" Press CTRL-C to stop\n");});gracefulShutdown(server,{signals:"SIGINT SIGTERM",timeout:30000,development:false,onShutdown:async(signal:string)=>{console.log("... called signal: "+signal);console.log("... in cleanup");// shutdown DB or something},finally:()=>{console.log("Server gracefulls shutted down.....");},});
こちらがDockerfileです。pm2とpm2-runtimeはだけはパスを通すようにしています。
# ここから下がビルド用イメージFROM node:12-buster AS builderWORKDIR appCOPY package.json package-lock.json ./RUN npm ci
COPY tsconfig.json ./COPY src ./srcRUN npm run build
# ここから下が実行用イメージFROM node:12-buster-slim AS runnerWORKDIR /opt/appCOPY package.json package-lock.json ./RUN npm ci --prodRUN ln-s /opt/app/node_modules/.bin/pm2 /usr/local/bin/pm2
RUN ln-s /opt/app/node_modules/.bin/pm2-runtime /usr/local/bin/pm2-runtime
COPY ecosystem.config.js ecosystem.config.jsCOPY --from=builder /app/dist ./USER nodeEXPOSE 3000CMD ["pm2-runtime", "start", "ecosystem.config.js"]
PM2の設定ファイルは次の通り。instances: "max"
が勇者の証。
module.exports={apps:[{name:"greeting-server",script:"/opt/app/index.js",env:{NODE_ENV:"development",},env_production:{NODE_ENV:"production",},instances:"max",exec_mode:"cluster",},],};
これでビルドして実行すると、コア数分プロセスが立ち上がっていることがわかります(Dockerは4コア使うように設定してあります)。
$ docker build -t webserver .$ docker --name webserver --rm-it webserver
$ docker exec webserver pm2 list
┌─────┬────────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id│ name │ namespace │ version │ mode │ pid │ uptime│ ↺ │ status │ cpu │ mem │ user │ watching │
├─────┼────────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ greeting-server │ default │ 1.0.0 │ cluster │ 18 │ 55s │ 0 │ online │ 0% │ 40.8mb │ node │ disabled │
│ 1 │ greeting-server │ default │ 1.0.0 │ cluster │ 25 │ 55s │ 0 │ online │ 0% │ 41.3mb │ node │ disabled │
│ 2 │ greeting-server │ default │ 1.0.0 │ cluster │ 32 │ 55s │ 0 │ online │ 0% │ 41.2mb │ node │ disabled │
│ 3 │ greeting-server │ default │ 1.0.0 │ cluster │ 39 │ 55s │ 0 │ online │ 0% │ 41.2mb │ node │ disabled │
└─────┴────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
pm2-runtimeはpm2 --no-daemon相当のツールで、フォアグラウンドで動作し、シグナルなどはDockerの作法に従ってきちんと動作するように作られています。
めでたしめでたし・・・ではなかった
Dockerのベストプラクティスを諸々調べると、どれもnode スクリプトで起動せよ、プロセスマネージャとかランチャーは挟むな、と書かれています。ランチャー(npm run)はシグナルを適切に伝達しないということで、pm2-runtimeはそこはきちんとしているので、停止できないとかクリティカル無問題はないです。とはいえ、オーケストレーションツール側でオートスケール、みたいな話とちょっと喧嘩する可能性があるので、そこはもっと深く検証が必要なのかもしれません。
便利なところといえば、ロードバランサー的なものをおかなくても手っ取り早くパフォーマンスはあげられる・・・ぐらいですかね。
pm2はSaaSでObservabilityなサービスをしているようですね。pm2-runtimeはそこにつなげるためのエージェントという色合いが強いのかも・・・という気もしました。npm install pm2すると30MBぐらいどかっとイメージが大きくなってしまうのもいまいちだし、とりあえずこの努力はこのエントリーで供養します。