13.11 Cron scheduler & operational gaps + scorecard
บทสุดท้ายของ Volume 13 สำรวจ cron scheduler ที่ใช้ในระบบ, ปัญหาเชิงปฏิบัติการที่เหลือ, และ scorecard สรุปของทั้ง volume
ภาพรวม scheduler
| Service | Library | ที่อยู่ | กลไก HA |
|---|---|---|---|
| vtrc-api | node-schedule | api/src/crons.js | เฉพาะ updateSuccessFundChangeRateByJob ใช้ APP_NO===1; job อื่นรันทุก replica |
| cu-central-api | node-schedule | main-api/src/cron.js | APP_NO gate ทั้งไฟล์ (ทุก job ใน if (+APP_NO === 1)) |
| centralize-api | ไม่มี | — | — |
| vtrc-web | ไม่มี (frontend) | — | — |
| vtrc-rc-backoffice | ไม่มี (frontend) | — | — |
vtrc-api — crons.js
vtrc-api/api/src/crons.js กำหนด cron job ผ่าน node-schedule:
ลักษณะสำคัญ:
node-scheduleเป็น in-process scheduler — รันใน Node process เดียวกับ HTTP server ไม่ใช่ worker แยก- APP_NO gate ใช้เฉพาะ fund rate job — บล็อก
if (+APP_NO === 1)ครอบคลุมแค่updateSuccessFundChangeRateByJob(crons.js:14-18) — ไม่ใช่ทุก job updateDelegatesExpireและstatLogลงทะเบียนนอกเงื่อนไข APP_NO — รันบน ทุก replica ที่ start (crons.js:20-27)- ไม่มี retry — ถ้า job fail จะไม่รันซ้ำจนกว่าจะถึง schedule ถัดไป
- ไม่มี visibility — ไม่มี API ที่บอกว่า job ใดรันล่าสุดเมื่อใด, สำเร็จหรือไม่
job ที่ใช้งานใน vtrc-api:
| Job | Schedule | APP_NO gate? | หน้าที่ |
|---|---|---|---|
updateSuccessFundChangeRateByJob | * 1 * * * * (วินาทีที่ 1 ของทุกนาที) | ใช่ — เฉพาะ APP_NO=1 | flip PENDING → SUCCESS ของ Fund rate change |
updateDelegatesExpire | 30 3 * * * (03:30 ทุกวัน) | ไม่ — ทุก replica | expire delegate ที่หมดอายุ |
statLog | 30 0 * * * (00:30 ทุกวัน) | ไม่ — ทุก replica | สถิติรายวัน |
auto-cancel leave | (disabled) | — | ยกเลิก leave ค้าง — ปิดไว้ |
cu-central-api — cron.js
cu-central-api/main-api/src/cron.js ห่อ ทุก job ใน if (+APP_NO === 1) — ต่างจาก vtrc-api ที่ gate เฉพาะ fund rate
ปัญหาของ APP_NO gate (ขอบเขตที่ถูกต้อง)
APP_NO gate เป็นวิธีป้องกันการรัน cron ซ้ำในหลาย replica แต่มีข้อจำกัด:
SPOF เฉพาะ job ที่ถูก gate — ถ้า replica APP_NO=1 ตาย, เฉพาะ
updateSuccessFundChangeRateByJob(vtrc) และ cron ทั้งชุดของ cu-central จะไม่รัน — อย่าสรุปว่าupdateDelegatesExpire/statLogหยุดด้วย เพราะ job เหล่านั้นรันทุก replica (และอาจรันซ้ำซ้อนกัน)ไม่ใช่ distributed lock ที่แท้จริง — ไม่มี atomicity ในการ acquire ถ้า APP_NO=1 start พร้อมกัน 2 instance (edge case ระหว่าง deploy) อาจรันซ้ำได้
job ที่ไม่ gate อาจรันซ้ำ —
updateDelegatesExpire/statLogบนทุก replica อาจเขียนซ้ำหรือแข่งกันถ้าไม่มี idempotency ในตัว handler
ทางเลือกที่ดีกว่าคือ Redis SET NX กับ TTL — เป็น distributed lock ที่ทำงานได้ถูกต้องแม้มีหลาย replica
ปัญหาเชิงปฏิบัติการ — สรุปทั้ง volume
ตารางสรุปปัญหาที่พบใน Volume 13 เรียงตามความสำคัญ:
| # | ปัญหา | Severity | ที่มา |
|---|---|---|---|
| 1 | Node runtime EOL (12, 14, 16) | Critical | Dockerfiles ทุกตัว |
| 2 | JWT_SECRET = 'secret' default | Critical | vtrc-api/api/src/config.js:12 |
| 3 | GCP SA key commit ใน repo | Critical | vtrc-api/vtrc-gcp-sa-key.json |
| 4 | phpMyAdmin expose port 8093 | Critical | vtrc-api/docker-compose.yml:35-46 |
| 5 | NODE_ENV=development ใน compose ของ cu-central-api | High | cu-central-api/docker-compose.yml:45,64,81 |
| 6 | ไม่มี health check endpoint | High | ทุก service |
| 7 | ไม่มี structured logger / log aggregation | High | ทุก service |
| 8 | ไม่มี CI/CD สำหรับ 3 repos | High | cu-central-api, centralize-api, backoffice |
| 9 | ไม่มี migration — syncModelToTable | High | vtrc-api, cu-central-api |
| 10 | APP_NO gate (fund rate / cu-central) ไม่ใช่ distributed lock | High | crons.js / cron.js |
| 11 | network_mode: host ของ nginx | High | vtrc-api, cu-central-api docker-compose |
| 12 | express.static('/', storage) ไม่มี auth | High | vtrc-api/api/src/routes.js:988 |
| 13 | ไม่มี lockfile ของ centralize-api | High | centralize-api/ |
| 14 | deploy ด้วยมือทุกครั้ง | Med | pipeline push image จบ, deploy manual |
| 15 | PII ใน URL path | Med | routes.js:51 |
| 16 | setTimeout(deletePDF, 5000) race | Med | routes.js:79 |
| 17 | ไม่มี resource limit ใน docker-compose | Med | ทุก compose |
| 18 | image: latest ทุก service | Med | ทุก compose |
| 19 | path src/storage/prs/ vs storage/prs/ | Med | หลายไฟล์ |
| 20 | trustServerCertificate: true (MSSQL) | Med | centralize-api app.module.ts |
| 21 | Redis ไม่มี replica | Med | docker-compose |
| 22 | TRANSFER_PROTOTAL typo | Low | .env.example:26 |
Scorecard สรุปของ Volume 13
Legend:
- 🟢 = ดี, ไม่มีปัญหาเร่งด่วน
- 🟡 = มีปัญหาปานกลาง, ควรปรับปรุง
- 🔴 = มีปัญหาวิกฤต, ต้องแก้ด่วน
| มิติ | vtrc-api | vtrc-web | vtrc-rc-backoffice | centralize-api | cu-central-api |
|---|---|---|---|---|---|
| Runtime ทันสมัย | 🔴 Node 12 | 🟡 Node 14 build | 🔴 Node 16 build | 🔴 Node 16 | 🔴 Node 12 |
| Container best practice | 🔴 ไม่มี multi-stage | 🟢 multi-stage | 🟢 multi-stage | 🟢 multi-stage | 🔴 ไม่มี multi-stage |
| Dockerfile security | 🔴 root, no healthcheck | 🟡 root, no healthcheck | 🔴 root, no healthcheck | 🔴 root, no healthcheck | 🔴 root, no healthcheck |
| docker-compose discipline | 🟡 nginx host; api bridge+ports | 🟢 bridge | 🟡 bridge, no limit | 🟡 external network | 🟡 nginx host; api bridge; NODE_ENV dev |
| nginx config quality | 🟡 ปนกับ secret | 🟢 เรียบง่าย | 🟢 เรียบง่าย | — | 🔴 upstream ชื่อผิด |
| env config safety | 🔴 JWT default 'secret' | 🟢 build-time | 🟢 build-time | 🟡 strictNullChecks off | 🔴 NODE_ENV dev default |
| DB migration | 🔴 sync ทุกครั้ง | — | — | 🟡 TypeORM sync | 🔴 Sequelize sync |
| CI/CD | 🟡 มี pipeline แต่ deploy manual | 🟡 เช่นเดียวกัน | 🔴 ไม่มี | 🔴 ไม่มี | 🔴 ไม่มี |
| Logging | 🔴 console.log + wlog ผสม | 🟡 CRA console | 🟡 CRA console | 🟢 I3GatewayLogger | 🔴 PM2 log ที่ไม่ได้ใช้ |
| Monitoring | 🔴 ไม่มี | 🔴 ไม่มี | 🔴 ไม่มี | 🔴 ไม่มี | 🔴 ไม่มี |
| Health check | 🔴 ไม่มี | — | — | 🔴 ไม่มี | 🔴 ไม่มี |
| Scheduler HA | 🟡 APP_NO เฉพาะ fund rate; delegate/stat รันทุก replica | — | — | — | 🔴 APP_NO gate ทั้งชุด |
ข้อแนะนำเรียงตาม priority
Phase 1 — แก้ด่วน (สัปดาห์ที่ 1-2)
| การกระทำ | ผลกระทบ |
|---|---|
ตั้ง JWT_SECRET ที่เป็นความลับจริง, service crash ถ้าว่าง | กัน auth bypass |
| ลบ phpMyAdmin ออกจาก compose ของ prod หรือจำกัด IP | ลด attack surface |
เปลี่ยน NODE_ENV=production ใน compose ของ cu-central-api | IS_PRODUCTION ทำงานถูก |
| Rotate GCP SA key + ย้ายออกจาก repo | ลดช่องทางดึงข้อมูล |
เพิ่ม /health endpoint ในทุก backend service | เปิดทาง monitoring |
Phase 2 — ปรับปรุงระยะกลาง (เดือนที่ 1-3)
| การกระทำ | ผลกระทบ |
|---|---|
| ยก Node runtime เป็น 20 LTS | ความปลอดภัย |
| เพิ่ม CI/CD สำหรับ 3 repos ที่ขาด | deploy สม่ำเสมอ |
| commit lockfile ของ centralize-api | build ทำซ้ำได้ผลลัพธ์เดิม |
| เปลี่ยน syncModelToTable เป็น migration | schema change reviewable |
| เพิ่ม structured logger (pino หรือ winston) | debug ง่ายขึ้น |
| เพิ่ม Sentry สำหรับ error tracking | รู้ปัญหาก่อนผู้ใช้รายงาน |
Phase 3 — ยกระดับระยะยาว (เดือนที่ 3-12)
| การกระทำ | ผลกระทบ |
|---|---|
| ย้ายจาก docker-compose ไป Kubernetes | scale ได้, self-healing |
| เปลี่ยน APP_NO gate เป็น Redis distributed lock (fund rate + cu-central; พิจารณา idempotency ของ delegate/stat ที่รันทุก replica) | cron job HA |
| เพิ่ม Prometheus + Grafana | metric monitoring |
| เพิ่ม log aggregation (Loki หรือ ELK) | debug ข้าม services |
| ใช้ secret manager (Vault, GCP Secret Manager) | secret rotation |
| เปลี่ยน file storage ไป GCS / S3 | durability, scalability |
| เพิ่ม distributed tracing (OpenTelemetry) | visibility ข้าม services |
สรุป Volume 13
Volume นี้เจาะลึกว่าระบบ VTRC ทำงานบน infrastructure ใด — ผสมระหว่าง GCP container registry และ on-prem database/LDAP, orchestrate ผ่าน docker-compose ไม่ใช่ Kubernetes, deploy ผ่าน Bitbucket Pipelines เฉพาะ 2 จาก 5 repos
ปัญหาเชิงโครงสร้างหลักคือระบบเก็บไว้ในสภาพ "ตอบโต้ได้" (works as-is) แต่ไม่ได้ออกแบบมาให้ scale, monitor, หรือ recover จาก failure โดยอัตโนมัติ — ทุกอย่างต้องดูแลด้วยมือของทีมปฏิบัติการ
ความเสี่ยงวิกฤตสูงสุดคือ Node runtime ที่ EOL มานาน, JWT secret ที่เป็นค่า default, และ GCP key ที่ commit ใน repo — ทั้งสามข้อแก้ไขได้ในเวลาไม่กี่วัน แต่ต้องการการประสานงานกับทีมปฏิบัติการและผู้ดูแลระบบ
Volume ถัดไปจะเจาะลึกเรื่องอื่น ๆ ที่ยังไม่ครอบคลุม (เช่น Operations Runbook, Disaster Recovery, Modernization Plan) ตามลำดับความสำคัญที่ผู้ใช้กำหนด