บทที่ 10 · Part 2 — Building Blocks
Cross-Cutting Blocks
ID generation, config & secrets, service discovery/mesh, coordination และ observability 3 สัญญาณ + 1
มีการตัดสินใจ 4 อย่างที่ไม่มีใครทำ design review ให้ เพราะมันไม่อยู่ในกล่องไหนเลย — รูปแบบของ ID, ที่มาของ config และ secret, วิธีที่ service หากัน, และสิ่งที่คุณมองเห็นตอนระบบพัง ทั้ง 4 อย่างแก้ทีหลังแพงมาก
จบบทนี้คุณจะ
- เลือกรูปแบบ ID ได้ก่อนสร้างตารางแรก และรู้ว่าห้าม expose อะไร
- แยก config จาก secret และรู้ว่าทำไม config change ล้มระบบใหญ่มากกว่าโค้ดเสีย
- รู้ว่าเมื่อไหร่ควรใช้ service mesh และเมื่อไหร่แค่ตามกระแส
- เข้าใจว่าทำไม distributed lock ห้ามใช้กับเงิน
- ตั้ง observability ครบ 3 สัญญาณ + 1 — และรู้ว่าสัญญาณที่ 4 คือตัวที่จับ outage ที่แย่ที่สุดได้
ID generation — เลือกก่อนเขียนตารางแรก
| แบบ | ข้อดี | ข้อเสีย | คำตัดสิน |
|---|---|---|---|
| Auto-increment integer | เล็ก, เรียงลำดับ, index locality ดีมาก | จุดจัดสรรจุดเดียว; รั่วปริมาณธุรกิจและลำดับให้คนนอกรู้; เจ็บตอนรวม shard | ใช้ภายในได้ ห้าม expose ใน public URL หรือ API |
| UUIDv4 (สุ่ม) | สร้างที่ไหนก็ได้ ไม่ต้องประสานงาน | 16 bytes, ลำดับสุ่มทำลาย insert locality ของ B-tree และทำให้ index บวม | รับได้ แต่ควรใช้แบบที่เรียงตามเวลามากกว่า |
| Snowflake-style — timestamp + machine + sequence | 64-bit, เรียงตามเวลาได้ประมาณหนึ่ง, ไม่ต้องมี coordinator | ต้องมี machine ID ที่ไม่ซ้ำ; ต้องจัดการ clock skew | ดีมากสำหรับอัตรา write สูงมาก |
| ULID / UUIDv7 | เรียงตามเวลา + มีส่วนสุ่ม; index locality ดี; ไม่ต้องประสานงาน | 26 chars ถ้าเก็บเป็น text (ให้เก็บเป็น 16 bytes binary); ส่วน timestamp เปิดเผยเวลาสร้าง ซึ่งบางกรณีไม่ต้องการ | default ที่แนะนำสำหรับ ID ที่คนนอกเห็น — เว้นแต่การเปิดเผยเวลาสร้างเป็นปัญหา ให้ใช้ UUIDv4 |
2 กฎที่ใช้ได้ทุกแบบ:
- ใส่ prefix ตามชนิด (
trf_…,acc_…,usr_…) เพื่อให้ ID ที่ส่งผิดที่ พังเสียงดัง แทนที่จะพังเงียบๆ - ห้าม encode ความหมายที่อาจต้องเปลี่ยน (รหัสสาขา, รหัสสินค้า) ลงใน primary key
Auto-increment ที่ expose คือการรั่วข้อมูลธุรกิจ
ถ้า URL คือ /transfers/48213 คู่แข่งสมัคร 2 บัญชี สร้าง transfer ห่างกัน 1 วัน
แล้วลบกันก็รู้ว่าคุณมี transfer กี่รายการต่อวัน
และแย่กว่านั้น — ถ้า authorization มีรูสักที่เดียว การ enumerate /transfers/1..N
ก็อ่านรายการของคนอื่นได้ทั้งหมด (นี่คือ IDOR — ช่องโหว่ที่พบบ่อยที่สุดใน API จริง)
[!TIP] อ้างอิง — Sharding & IDs at Instagram Instagram อัด timestamp + shard ID + sequence ลงใน 64 bit ทำให้ ตัว ID เองบอกได้ว่าแถวนี้อยู่ shard ไหน — ตัดการ lookup ทั้งชั้นออกไป — Sharding & IDs at Instagram
Configuration และ secret
Config มาจาก environment, artefact เหมือนกันทุก environment
ถ้า build ของคุณต่างกันตาม environment คุณเชื่อไม่ได้ว่า staging ทดสอบ production แล้ว (หลัก 12-factor)
Secret ห้ามอยู่ที่ไหนบ้าง
Secret ห้ามอยู่ใน
โค้ด · image · ไฟล์ env ที่อยู่ใน git · log ของ CI · wiki · Slack · ticket · screenshot ใน design doc
ใช้ managed secret store ที่มี rotation และ audit อ้างถึง secret ด้วยชื่อ แล้ว inject ตอน runtime
ในเอกสารและตัวอย่างทุกชิ้น เขียนเป็น placeholder เสมอ เช่น <API_KEY>, <DB_PASSWORD>
# ถูก — อ้างถึง secret ด้วยชื่อ ไม่มีค่าจริงในไฟล์นี้
database:
host: ${DB_HOST}
user: ${DB_USER}
password_ref: secret://prod/wallet/db-password # ดึงตอน runtime
rails:
promptpay:
endpoint: https://api.partner.example/v2
api_key_ref: secret://prod/wallet/promptpay-key
timeout_ms: 3000
max_retries: 3
Feature flag คือ config ที่มีทางด่วน
มันทำให้คุณ deploy โค้ดแบบปิดไว้, ค่อยๆ เปิดสัดส่วน, และเปลี่ยน rollback จากการ release เป็นการแก้ config
Flag ที่ไม่มีวินัยล้างกลายเป็น branch ที่ซ่อนอยู่ถาวร
ทุก flag ที่ค้างคือเส้นทางโค้ดที่ไม่มีใครทดสอบ ตั้งกฎ: flag ทุกตัวมี เจ้าของ และ วันหมดอายุ และมี ticket ล้างในสprint ถัดไปหลังเปิด 100%
Dynamic config ต้องมีตะแกรงรับ
Config change ล้มระบบใหญ่มากกว่าโค้ดเสีย
ค่าที่ผิดต้อง fail closed กลับไปที่ค่าที่รู้ว่าดีล่าสุด และการเปลี่ยน config ต้องผ่าน review และมี audit trail เหมือนกับโค้ด
สำหรับค่าที่กระทบเงิน (เพดานการโอน, อัตราค่าธรรมเนียม, threshold ของ fraud rule) ต้องมี ผู้อนุมัติที่มีอำนาจ และมีร่องรอยว่าใครเปลี่ยนอะไรเมื่อไหร่ — ไม่ใช่ค่าที่ engineer แก้ใน dashboard ได้คนเดียว
Service discovery, mesh และ coordination
| เรื่อง | ทางเลือก | ข้อควรรู้ |
|---|---|---|
| Discovery | DNS-based (ง่าย แต่ติดข้อจำกัด cache) หรือ registry (Consul, etcd, Kubernetes Service) | Kubernetes ให้มาฟรีอยู่แล้ว — อย่าเพิ่มกลไกที่ 2 |
| Mesh (Envoy-based: Istio, Linkerd) | ได้ mTLS ทั่วระบบ, retry, timeout, circuit breaking และ golden metric โดยไม่แก้โค้ด application | ต้นทุน: learning curve ที่จริงจัง และ hop เพิ่มอีกหนึ่ง — รับมาเพราะ requirement เรื่อง mTLS-everywhere และ telemetry ที่สม่ำเสมอ ไม่ใช่เพราะกระแส |
| Coordination (etcd, ZooKeeper) | leader election, distributed lock, membership | ใช้ระบบที่พิสูจน์แล้ว — โค้ด consensus ที่คุณเขียนเองจะผิด |
Distributed lock กับเงิน
อ่านก่อนใช้ Redis lock กับอะไรที่แตะเงิน
Lock ที่มี lease หมดอายุได้ขณะที่ผู้ถือยังเชื่อว่าตัวเองถืออยู่ — process หยุดเพราะ GC pause 8 วินาที, lease หมด, อีก process เข้ามาทำงาน, แล้ว process แรกฟื้นขึ้นมาเขียนต่อ ผลคือทั้ง 2เขียน
Pattern ที่ปลอดภัยสำหรับเงินคือ transaction ของฐานข้อมูลพร้อม row lock หรือ fencing token — ไม่ใช่ advisory lock
อ่าน How to do distributed locking ของ Kleppmann ก่อนพึ่ง Redis lock กับอะไรที่แตะเงิน
-- ปลอดภัย: ความถูกต้องอยู่ที่ DB ไม่ใช่ที่ lock ภายนอก
BEGIN;
SELECT balance_minor, version FROM accounts
WHERE account_id = $1 FOR UPDATE;
UPDATE accounts
SET balance_minor = balance_minor - $2, version = version + 1
WHERE account_id = $1 AND version = $3; -- fencing ด้วย version
-- rows affected = 0 -> มีคนอื่นแก้ไปแล้ว ให้ล้มเหลวและ retry ทั้ง transaction
COMMIT;
Observability — 3 สัญญาณ + 1
| สัญญาณ | ตอบคำถามว่า | กฎการออกแบบ |
|---|---|---|
| Metrics | "พังไหม พังแค่ไหน ตั้งแต่เมื่อไหร่" | cardinality ต่ำ · rate/errors/duration ต่อ endpoint และต่อ dependency · ใช้ histogram ไม่ใช่ average เพราะกู้ p99 จากค่าเฉลี่ยไม่ได้ |
| Logs | "request นี้เกิดอะไรขึ้นบ้างแน่ๆ" | structured (JSON) พร้อม trace ID และ tenant ID · mask PII, ห้าม log เลขบัตร, token, OTP, ชื่อ-นามสกุลคู่กับธุรกรรม, หรือ auth header · ปริมาณ log เป็นทั้งค่าใช้จ่ายและพื้นที่ข้อมูลรั่ว |
| Traces | "900 ms ไปอยู่ที่ไหน" | propagate context ทุก hop รวมถึงผ่าน queue (ใส่ trace_id ไปใน message) · sample แบบ tail-based เพื่อเก็บตัวช้าไว้ |
| Business events | "ลูกค้าทำสำเร็จจริงไหม" | สัญญาณที่จับสิ่งที่ metric ของ infrastructure มองไม่เห็น: จำนวน transfer ต่อนาทีตกลงเป็น 0 ขณะที่ทุก server รายงาน 200 OK — ตั้ง alert กับตัวนี้ |
Alert ที่น่าจะจับ outage ที่แย่ที่สุดของคุณได้
ถามคนที่อยู่มานานว่า incident ที่แย่ที่สุดเป็นอย่างไร คุณมักจะได้ยินว่า "ทุกอย่างเขียวหมด" CPU ปกติ error rate ปกติ — เพราะ request ไม่เคยมาถึง, หรือถูกปฏิเสธที่ต้นน้ำ, หรือ job ประมวลผล 0 แถวอย่างเงียบๆ
Alert 2 ตัวแก้ปัญหาคลาสนี้ได้เกือบหมด:
- อัตราผลลัพธ์ทางธุรกิจที่สำเร็จต่ำกว่าพื้นที่คาดไว้ (transfer/นาที, การสมัครสำเร็จ/ชั่วโมง)
- สิ่งที่ควรเกิดแล้วไม่เกิด (batch ไม่จบภายใน 03:00, ไม่ได้รับไฟล์ settlement ภายใน 09:00)
ทั้ง 2 ตัวราคาถูก และทั้ง 2 ตัวมักจะไม่มี
สิ่งที่ห้ามอยู่ใน log
Log คือพื้นที่ข้อมูลรั่วที่คนลืมบ่อยที่สุด
Log ถูก copy ไปที่ log aggregator, ถูกเก็บไว้เป็นเดือน, และคนในองค์กรเข้าถึงได้กว้างกว่าฐานข้อมูลมาก
ห้าม log: เลขบัตรเต็ม (PAN), CVV, OTP, token/refresh token, private key,
รหัสผ่าน, Authorization header, เลขบัญชีเต็ม, เลขบัตรประชาชนเต็ม
ให้ mask: 4111********1111, 08X-XXX-1234, acc_***_123
และตรวจให้แน่ใจว่า error handler ไม่ dump request body ทั้งก้อน — นี่คือช่องทางรั่วที่พบบ่อยที่สุด
ผิด:
log.Error("transfer failed", "request", req)
--> dump ทั้ง body รวม proxy_id, ชื่อผู้รับ, และอาจมี token
ถูก:
log.Error("transfer failed",
"transfer_id", t.ID,
"trace_id", traceID,
"error_code", code,
"amount_minor", t.AmountMinor, // จำนวนเงินไม่ใช่ PII
"destination_type", t.Dest.Type) // type ได้ แต่ไม่เอาเลขบัญชี
Checklist cross-cutting
- เลือกรูปแบบ ID แล้ว มี prefix ตามชนิด และ ไม่ expose auto-increment
- ไม่มี secret ใน git, image, log หรือเอกสาร — มี secret store ที่ rotate ได้
- Config มาจาก environment; artefact เหมือนกันทุก environment
- Dynamic config fail closed กลับค่าที่ดีล่าสุด และมี audit trail
- ค่าที่กระทบเงินมีผู้อนุมัติที่มีอำนาจและมีร่องรอยการเปลี่ยน
- Feature flag มีเจ้าของและวันหมดอายุ
- ไม่ใช้ distributed lock เป็นกลไกความถูกต้องของเงิน
- Metric เป็น histogram และ cardinality จำกัด
- Log เป็น structured มี trace ID และ mask PII แล้ว (มี test ยืนยัน)
- Trace propagate ผ่าน queue ด้วย ไม่ใช่แค่ HTTP
- มี alert 2 ตัว: ผลลัพธ์ธุรกิจต่ำกว่าพื้น และ สิ่งที่ควรเกิดแล้วไม่เกิด
สรุปบทนี้
เลือกรูปแบบ ID ก่อนตารางแรกและอย่า expose auto-increment · config change ล้มระบบมากกว่าโค้ดเสีย จึงต้อง review และ fail closed · distributed lock ไม่ใช่กลไกความถูกต้องของเงิน — transaction กับ row lock คือคำตอบ · metric ต้องเป็น histogram · log ต้อง mask · และสัญญาณที่จับ outage ที่แย่ที่สุดได้ คือ business outcome กับ สิ่งที่ควรเกิดแล้วไม่เกิด