13.3 Process manager & startup
บทนี้อธิบายว่าแต่ละ service เริ่มต้นอย่างไร ใช้ process manager ใด และทำไมค่าต่าง ๆ จึงตั้งอย่างที่เป็น
ภาพรวม startup
| Service | Process manager | Start command | Entry point | ฟังก์ชันเริ่มต้น |
|---|---|---|---|---|
| vtrc-api | Docker restart: always | npm start = node -r esm . | api/index.js → app.listen(APP_PORT) | เปิด port, เชื่อมต่อ db, db2, redis, start cron |
| cu-central-api | Docker restart: always | npm start = node -r esm . | main-api/index.js → import './src' | เช่นเดียวกัน |
| centralize-api | Docker (PM2 ไม่ได้ใช้) | yarn start:prod = node dist/src/main | src/main.ts → app.listen(port) | bootstrap NestJS |
| vtrc-web | nginx (foreground) | nginx default | ไม่มี — serve static | nginx โหลด default.conf |
| vtrc-rc-backoffice | nginx (foreground) | nginx default | ไม่มี — serve static | nginx โหลด default.conf |
vtrc-api — เริ่มต้นผ่าน node -r esm
vtrc-api/api/package.json scripts:
{
"start": "node -r esm .",
"run": "nodemon -r esm"
}หมายความว่า:
node -r esm— Node เริ่มต้นด้วยการ preload packageesmซึ่งเป็น shim ที่ทำให้ Node 12 เข้าใจimport/exportsyntax ของ ES Module (รายละเอียดใน Volume 3).— Node จะมองหาindex.jsใน directory ปัจจุบัน (จากpackage.json:main) นั่นคือapi/index.js- ใน production ใช้ Docker
restart: alwaysแทน PM2 — ถ้า Node crash, Docker daemon จะ restart container ให้ npm runสำหรับพัฒนา — ใช้nodemonรีสตาร์ทอัตโนมัติเมื่อไฟล์เปลี่ยน
ลำดับการเริ่มต้นของ vtrc-api (vtrc-api/api/src/index.js:1-121):
1. import express, routes, typeDefs, resolvers, ApolloServer, config, ...
2. สร้าง Express app + ติดตั้ง middleware (cors, bodyParser, graphqlUploadExpress)
3. สร้าง ApolloServer (typeDefs + resolvers + AuthDirective)
4. applyMiddleware ของ Apollo ลงบน Express
5. import './crons' → start scheduler (บทที่ 11)
6. app.listen(APP_PORT) → เริ่มฟัง port
7. redisConnection() → เชื่อมต่อ Redis
8. db.authenticate() → เชื่อมต่อ MariaDB schema 'vtrc'
9. syncModelToTable() → sync schema อัตโนมัติ (รายละเอียด Volume 8)
10. db2.authenticate() → เชื่อมต่อ MariaDB schema 'vtrc-centralize'
11. createDatabases(db2) → สร้าง database ถ้ายังไม่มีจุดสำคัญ:
- ไม่มี readiness check —
app.listenทำงานทันที แม้ว่า db จะยังเชื่อมต่อไม่ได้ ทำให้ nginx อาจส่ง traffic เข้ามาตอนที่ service ยังไม่พร้อม - sync schema ทุกครั้งที่ start —
syncModelToTable()รันทุกครั้ง ถ้าโมเดลมีการแก้ไข schema ใน code จะถูกนำไปใช้กับ database ทันทีโดยไม่มี migration review (รายละเอียด Volume 8 บทที่ 4)
cu-central-api — PM2 ecosystem ที่ไม่ได้ใช้
cu-central-api/ecosystem.config.js:
module.exports = {
apps: [{
name: 'cu-central-api',
script: './index.js',
node_args: '-r esm .',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
error_file: './logs/carpark-api-error.log',
out_file: './logs/carpark-api.log',
autorestart: true,
watch: false,
env: {
NODE_ENV: 'development'
},
env_production: {
APP_PORT: '8000',
NODE_ENV: 'production'
}
}]
}ไฟล์นี้กำหนดให้ PM2 เป็น process manager — แต่ Dockerfile ของ cu-central-api ไม่ได้ใช้ PM2:
CMD ["npm", "start"]ความขัดแย้งนี้บ่งชี้ว่า PM2 เคยใช้ในอดีตก่อนที่จะย้ายมารันบน Docker และ ecosystem.config.js ถูกทิ้งไว้โดยไม่ได้ใช้
จุดสังเกตเพิ่มเติม:
- ชื่อ log file คือ
carpark-api.log— บ่งชี้ว่า codebase นี้คัดลอกมาจากโครงการอื่น (carpark) และไม่ได้ปรับชื่อ env.NODE_ENV: 'development'เป็น default — ถ้ารันผ่าน PM2 โดยไม่ระบุ--env productionจะได้สภาพแวดล้อม development ทั้ง ๆ ที่อยู่ใน production server
centralize-api — bootstrap ของ NestJS
centralize-api/src/main.ts:12-44:
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new I3GatewayLogger('System'),
});
const port = config.get('port') || 3000;
app.setGlobalPrefix('api');
app.use(bodyParser.json({ limit: '50mb' }));
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalGuards(new JwtAuthGuard(reflector));
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));
app.enableCors({ origin: '*', allowedHeaders: '*', methods: 'GET,PUT,POST,DELETE,PATCH,OPTIONS' });
SwaggerModule.setup(`docs`, app, document);
await app.listen(port, '0.0.0.0', () => {
logger.log(`Listening at http://localhost:${port}/docs`);
});
}
bootstrap();จุดสังเกต:
I3GatewayLogger— centralize-api เป็น service เดียวที่ใช้ logger ที่กำหนดเอง (libs/log/logs.middleware.ts) แทนconsole.logorigin: '*'— CORS เปิดกว้างทุก origin (main.ts:31-35) เป็นปัญหาด้านความปลอดภัย (รายละเอียด Volume 9 บทที่ 1)- start:prod —
package.json:start:prod=node dist/src/main— รันจาก compiled output ในdist/ port: 3000เริ่มต้น — แต่อ่านจาก ConfigService ได้ถ้ามี env var- listen ที่
0.0.0.0— รับจากทุก interface เหมาะสำหรับ container
vtrc-web & vtrc-rc-backoffice — static file hosting
ทั้งคู่ไม่มี Node process รันใน production — Docker build stage ที่ 2 ใช้ nginx serve static file จาก build/
- vtrc-web —
vtrc-web/Dockerfile:11-19ใช้ nginx 1.24.0 +nginx-config/default.conf - vtrc-rc-backoffice —
vtrc-rc-backoffice/Dockerfile:13-17ใช้ nginx 1.24.0 +nginx/default.conf
nginx รันใน foreground ตาม default ของ image — ถ้า nginx process ตาย Docker daemon จะ restart container ผ่าน restart: always ใน docker-compose
APP_NO — ตัวระบุ replica
ใน docker-compose ของ vtrc-api และ cu-central-api มีการส่ง environment variable APP_NO ที่แตกต่างกันในแต่ละ replica:
| Service | APP_NO ที่ใช้ |
|---|---|
| vtrc-api | 1, 2, 3, 5 (ข้าม 4) |
| cu-central-api | 1, 2, 3 |
APP_NO ถูกใช้เป็นตัวระบุ replica และเป็น gate ของ cron บางตัวเท่านั้น:
- vtrc-api (
api/src/crons.js):if (+APP_NO === 1)ครอบคลุมเฉพาะupdateSuccessFundChangeRateByJob—updateDelegatesExpireและstatLogลงทะเบียนนอกเงื่อนไขนี้ จึงรันบนทุก replica - cu-central-api (
main-api/src/cron.js): job ทั้งหมดอยู่ในif (+APP_NO === 1)
จุดสังเกต:
- APP_NO=4 ถูกข้าม ใน vtrc-api — บ่งชี้ว่ามี replica ที่ถูกปิดไป (อาจด้วยเหตุผลด้านทรัพยากร) แต่ไม่มีเอกสารอธิบาย
- อย่าสรุปว่า APP_NO=1 ตายแล้ว cron ทั้งระบบหยุด — ที่ vtrc มีแค่ fund rate job ที่เป็น SPOF ของ APP_NO=1; delegate expire / statLog ยังรันบน replica อื่น (แต่เสี่ยงรันซ้ำ)
- APP_NO gate ไม่ใช่ distributed lock ที่แท้จริง — ถ้า replica 1 ตาย
updateSuccessFundChangeRateByJob(และ cron ของ cu-central) จะไม่รันจนกว่า replica 1 จะกลับมา online
สิ่งที่ขาด — readiness/liveness probe
ไม่มี service ใดรายงานสถานะ readiness หรือ liveness ผ่าน HTTP endpoint ทำให้:
- Docker compose ไม่สามารถรู้ได้ว่า Node process พร้อมรับ request หรือไม่ (ยกเว้น port เปิดหรือไม่)
- nginx upstream ส่ง traffic ไป replica ที่ยัง boot อยู่ ทำให้ผู้ใช้ได้รับ error 502 หรือ connection refused
- การ deploy ใหม่ต้องรอด้วยตนเองก่อนตัด traffic เข้า
ข้อแนะนำเพื่อปรับปรุง
- เพิ่ม endpoint
/healthในทุก backend service — ส่งคืน 200 เมื่อ db, redis เชื่อมต่อได้ - ใช้ readiness gate ก่อนฟัง port — อย่าเรียก
app.listenจนกว่าการเชื่อมต่อ db จะสำเร็จ - ลบ
ecosystem.config.jsของ cu-central-api — หรือปรับให้ใช้จริง ถ้าทีมตัดสินใจกลับไปใช้ PM2 - เปลี่ยน APP_NO gate เป็น distributed lock — ใช้ Redis SET NX สำหรับ job ที่ต้องรันครั้งเดียวต่อ cluster และเพิ่ม idempotency ให้ job ที่รันทุก replica อยู่แล้ว