บทที่ 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 + sequence64-bit, เรียงตามเวลาได้ประมาณหนึ่ง, ไม่ต้องมี coordinatorต้องมี machine ID ที่ไม่ซ้ำ; ต้องจัดการ clock skewดีมากสำหรับอัตรา write สูงมาก
ULID / UUIDv7เรียงตามเวลา + มีส่วนสุ่ม; index locality ดี; ไม่ต้องประสานงาน26 chars ถ้าเก็บเป็น text (ให้เก็บเป็น 16 bytes binary); ส่วน timestamp เปิดเผยเวลาสร้าง ซึ่งบางกรณีไม่ต้องการdefault ที่แนะนำสำหรับ ID ที่คนนอกเห็น — เว้นแต่การเปิดเผยเวลาสร้างเป็นปัญหา ให้ใช้ UUIDv4

2 กฎที่ใช้ได้ทุกแบบ:

  1. ใส่ prefix ตามชนิด (trf_…, acc_…, usr_…) เพื่อให้ ID ที่ส่งผิดที่ พังเสียงดัง แทนที่จะพังเงียบๆ
  2. ห้าม 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

เรื่องทางเลือกข้อควรรู้
DiscoveryDNS-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 ตัวแก้ปัญหาคลาสนี้ได้เกือบหมด:

  1. อัตราผลลัพธ์ทางธุรกิจที่สำเร็จต่ำกว่าพื้นที่คาดไว้ (transfer/นาที, การสมัครสำเร็จ/ชั่วโมง)
  2. สิ่งที่ควรเกิดแล้วไม่เกิด (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 กับ สิ่งที่ควรเกิดแล้วไม่เกิด