13.7 Database & Redis topology
บทนี้สำรวจโครงสร้าง database และ cache ที่ใช้ในระบบ รวมถึง connection pool, การกระจาย schema, และจุดเชื่อมระหว่าง services
ภาพรวม database
┌──────────────────────────────────────────────────────────────────────┐
│ MariaDB cluster (on-prem) │
│ │
│ ├── schema "vtrc" ──── vtrc-api (db) │
│ │ └── ตารางหลัก: User, Session, Leave, Withdraw, Study, ... │
│ │ │
│ └── schema "vtrc-centralize" ──── vtrc-api (db2) │
│ └── ตารางหลัก: Profile, Organization (mirror ของ cu-central) │
│ │
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ MSSQL — HRMI (on-prem) │
│ │
│ ├── cu-central-api ──── Sequelize + tedious │
│ │ └── ข้อมูล HR ที่แท้จริง — เงินเดือน, ตำแหน่ง, วันลา │
│ │ │
│ └── centralize-api ──── TypeORM + mssql │
│ └── มุมมอง HR ผ่าน facade (read-only ส่วนใหญ่) │
│ │
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ Redis │
│ │
│ ├── vtrc-api ──── redis 6379 (container ใน compose ของ vtrc-api) │
│ └── cu-central-api ── redis 6379 (container แยกใน compose ของ cu) │
│ │
└──────────────────────────────────────────────────────────────────────┘vtrc-api — 2 connections ของ MariaDB
vtrc-api/api/src/connector.js:8-50:
const db = new Sequelize(DB_NAME, DB_USERNAME, DB_PASSWORD, {
host: DB_HOST,
port: DB_PORT,
dialect: DB_SCHEMA,
...
pool: { max: 100, min: 0, acquire: 60000, idle: 10000 }
})
const db2 = new Sequelize(DB2_NAME, DB2_USERNAME, DB2_PASSWORD, {
host: DB2_HOST,
...
pool: { max: 100, min: 0, acquire: 60000, idle: 10000 }
})จุดสังเกต:
สอง connection ไปยัง MariaDB เดียวกัน — ทั้ง
db(schemavtrc) และdb2(schemavtrc-centralize) อ้าง host เดียวกัน (DB_HOST = DB2_HOST = vtrc-databaseใน.env.example:15,22) ต่างกันเพียงชื่อ schemaconnection pool 100 —
max: 100ต่อ replica หมายความว่า 4 replica รวมกันเปิด connection ได้สูงสุด 400 — ต้องตรวจสอบว่า MariaDB รองรับขนาดนั้นไม่มีการ share connection ระหว่าง replica — แต่ละ replica มี pool ของตัวเอง ทำให้ภาระรวมต่อ database สูง
charset: 'utf8mb4'— รองรับภาษาไทยและ emoji แบบ 4-byte UTF-8useUTC: false— ไม่ใช้ UTC ทำให้ timestamp เก็บเป็นเวลาท้องถิ่น (Asia/Bangkok) โดยตรง — ทำให้การย้ายข้าม timezone ยากขึ้น
vtrc-api — connection lifecycle
vtrc-api/api/src/index.js:101-120:
db
.authenticate()
.then(async () => {
syncModelToTable()
console.log('Connection has been established successfully.')
})
.catch(err => {
console.error('Unable to connect to the database:', err)
})
db2
.authenticate()
.then(async () => {
createDatabases(db2)
console.log('Connection #2 has been established successfully.')
})
.catch(err => {
console.error('Unable to connect to the database:', err)
})จุดสังเกต:
.catchไม่ได้ crash process — ถ้า db เชื่อมต่อไม่ได้ service ยังรันต่อแต่จะ fail ทุก query ที่เข้ามาsyncModelToTable()รันหลังเชื่อมต่อสำเร็จ — อัปเดต schema อัตโนมัติตามโมเดลใน code (รายละเอียด Volume 8 บทที่ 4)
cu-central-api — MSSQL + multi-tenant sharding
cu-central-api เชื่อมต่อ MSSQL HRMI ผ่าน tedious driver และมีรูปแบบ SourceDB sharding — แต่ละ "source database" เป็น tenant ของหน่วยงานย่อยในกาชาด (รายละเอียด Volume 4 บทที่ 5)
sourceDB ปกติมีหลายค่า เช่น:
RC— กาชาดหลักHOS— โรงพยาบาล- และอื่น ๆ ตามที่กำหนด
การ query จะสลับ connection ตาม sourceDb ที่ระบุใน JWT หรือ parameter — ทำให้ต้องระวังเรื่อง permission scoping
centralize-api — TypeORM + 2 MSSQL connections
centralize-api/src/app.module.ts กำหนด TypeORM configuration สองชุด:
- Connection 1 — MSSQL HRMI หลัก
- Connection 2 — MSSQL HRMI สำรอง (รายละเอียด Volume 5 บทที่ 2)
TypeORM config มี flag ที่น่ากังวล:
extra: {
trustServerCertificate: true,
}ปิดการตรวจสอบใบรับรอง TLS ของ MSSQL — ทำให้การเชื่อมต่อสามารถถูก intercept ได้ถ้าอยู่บน network ที่ไม่น่าเชื่อถือ (รายละเอียด Volume 9 บทที่ 1)
Redis topology
| Service | Redis container | Port | Use case |
|---|---|---|---|
| vtrc-api | vtrc-redis-db (docker-compose.yml:3-13) | 6379 | cache ผลลัพธ์บาง query เป็นหลัก — ไม่ใช่ที่เก็บ OTP ของ registration |
| cu-central-api | redis-db (docker-compose.yml:3-13) | 6379 | session (sess:{userUid}_{sessionId}), profile/org cache, rate limit |
จุดสังเกต:
- Redis สอง instance แยกกัน — vtrc-api และ cu-central-api แต่ละตัวมี container Redis ของตัวเอง ไม่ share
- Redis config มาจาก volume —
./redis-db/conf/redis.confmount เข้า container ทำให้การตั้งค่า (เช่น maxmemory, persistence) อยู่ในไฟล์ที่ไม่ได้ commit - Persistence —
./redis-db/datamount สำหรับ AOF/RDB file แต่ไม่เห็น config ที่ระบุว่าใช้ mode ใด - ไม่มี Redis cluster — Redis แต่ละตัวรัน single instance ไม่มี replica หรือ sentinel
OTP — เก็บที่ MariaDB ของ cu-central (ไม่ใช่ vtrc Redis)
Registration / forget-password OTP ไม่ได้อยู่ใน vtrc Redis:
vtrc-apiproxy mutationrequestOtpCDไปยัง cu-central GraphQL (vtrc-api/api/src/centralize/controllers/verification.js)cu-central-apiเขียนแถวลงตารางotpผ่านModels.Otp(main-api/src/lib/otp/sendOtp.model.js,tableName: 'otp') — MariaDB ของ cu-central- TTL เริ่มต้น 3 นาที จาก
SMS_OTP_EXPIRE_MINUTES = 3(main-api/src/config.js)
อย่าใช้ KEYS otp:* บน vtrc-redis เพื่อ debug OTP — key pattern นั้นไม่ใช่ที่เก็บ OTP ของระบบ
การใช้ Redis ใน vtrc-api
vtrc-api/api/src/lib/redis.js เชื่อมต่อ Redis ผ่าน redis v3 (callback-style) — การใช้งานหลักที่ยืนยันจากโค้ด:
- cache ผลลัพธ์บาง query ผ่าน
redis.controller.js(setCacheData/getCacheData) - ไม่มี OTP store / session store หลักใน vtrc Redis — OTP อยู่ MariaDB ของ cu-central; session หลักของ vtrc อยู่ตาราง
Sessions(MariaDB)
vtrc Redis จึงถือว่า เกือบ vestigial สำหรับ cache — crash ของ vtrc Redis ไม่ทำให้ OTP หาย (แต่ cache miss อาจเพิ่มภาระ DB)
การใช้ Redis ใน cu-central-api
- session: key รูปแบบ
sess:{userUid}_{sessionId}(main-api/src/lib/session/session.js+ prefixsessจากredis.controller.js) - cache ของ profile / org unit
- ไม่ใช่ ที่เก็บ OTP — OTP อยู่ตาราง
Models.Otp
connection pool — ขนาดและความเสี่ยง
ตารางสรุป connection pool ของแต่ละ service:
| Service | Database | Max pool | จำนวน replica | รวม connection สูงสุด |
|---|---|---|---|---|
| vtrc-api (db) | MariaDB vtrc | 100 | 4 | 400 |
| vtrc-api (db2) | MariaDB vtrc-centralize | 100 | 4 | 400 |
| cu-central-api | MSSQL HRMI | (ตาม default) | 3 | ~ 30 |
| centralize-api | MSSQL HRMI | (ตาม default) | 1 | ~ 10 |
รวม connection ที่อาจเปิดไปยัง MariaDB ได้สูงสุด ~ 800 — ถ้า MariaDB ตั้ง max_connections ต่ำกว่านี้จะเกิด "Too many connections" error ในช่วงที่มี traffic สูง
การ migrate schema
| Service | กลไก migrate | รายละเอียด |
|---|---|---|
| vtrc-api | syncModelToTable() ทุกครั้งที่ start | ไม่มี migration file แยก — Sequelize auto-sync ตามโมเดล |
| cu-central-api | sync() ของ Sequelize | เช่นเดียวกัน |
| centralize-api | TypeORM synchronize: true หรือ false | ขึ้นกับ env (ดู app.module.ts) |
ปัญหาของการใช้ sync แทน migration:
- ไม่สามารถ rollback ได้
- การเปลี่ยนแปลง column type อาจ drop ข้อมูล
- ไม่มี review ก่อน deploy schema change
- ถ้าโมเดลมีความผิดพลาด schema จะถูกแก้ทันทีใน production
รายละเอียดเต็มอยู่ใน Volume 8 บทที่ 4
ปัญหาเชิงปฏิบัติการ
| ปัญหา | ที่มา | ผลกระทบ |
|---|---|---|
| ไม่มี migration file | syncModelToTable | schema change ไม่มี review |
| pool 100 × 4 replica | connector.js:22 | อาจทำ MariaDB overload |
| Redis ไม่มี replica | docker-compose | ถ้า Redis crash — cu-central session หาย; cache ของ vtrc หาย; OTP ไม่หาย (อยู่ MariaDB) |
trustServerCertificate: true (MSSQL) | app.module.ts | MITM risk |
useUTC: false | connector.js:13 | timezone migration ยาก |
| cross-schema write ไม่มี transaction | connector.js | partial write เมื่อ fail (Volume 8 บทที่ 11) |
ข้อแนะนำเพื่อปรับปรุง
- เปลี่ยนจาก sync เป็น migration — Sequelize CLI migration หรือ
golang-migrate - เพิ่ม Redis HA — Redis Sentinel หรือ Redis Cluster
- ลด pool size — หรือใช้ pgBouncer-style proxy (แม้จะใช้กับ MySQL/MariaDB ได้ผ่าน ProxySQL)
- ตั้ง
useUTC: trueและแปลง timezone ที่ presentation layer - เพิ่ม transaction สำหรับ cross-schema write — หรือ redesign ให้แต่ละ schema เป็น service boundary (รายละเอียด Volume 8)
- เปิด
trustServerCertificate: falseและติดตั้ง internal CA cert