บทที่ 25 · Part 7 — Production Engineering

Runtime-Aware MySQL and Redis Clients

ตั้ง connection pools ต่างกันสำหรับ HTTP service, Lambda และ Temporal Worker โดยวัดจาก concurrency จริง

คำแนะนำว่า “สร้าง DB client ไว้ global จะได้ reuse” ถูกเพียงครึ่งเดียว สิ่งที่ต้องการจริงคือ 1 owner ต่อ runtime instance พร้อม pool ที่สัมพันธ์กับ concurrency และ shutdown ไม่ใช่ mutable global ที่ test เปลี่ยนได้ HTTP server, Lambda execution environment และ Temporal Worker มีอายุและการขยายจำนวนต่างกัน จึงใช้ DSN เดียวกันได้แต่ใช้ pool budget เดียวกันไม่ได้

จบบทนี้คุณจะ

  • เข้าใจว่า *sql.DB และ Redis client เป็น concurrency-safe pools ไม่ใช่ connection เดี่ยว
  • ตั้ง lifecycle/pool สำหรับ HTTP service, AWS Lambda และ Temporal Worker
  • ตรวจ saturation, stale connection และ integration behavior ด้วย metrics/tests

สร้าง Client ครั้งเดียวต่อ Runtime Owner

สำหรับ MySQL ใช้ database/sql กับ go-sql-driver/mysql เป็น default ที่ mature sql.Open มักยังไม่เปิด network connection จึงต้อง configure pool แล้วใช้ PingContext ใน startup/readiness ตาม policy:

db, err := sql.Open("mysql", dsn)
if err != nil {
    return nil, fmt.Errorf("opening mysql pool: %w", err)
}

db.SetMaxOpenConns(cfg.MaxOpenConns)
db.SetMaxIdleConns(cfg.MaxIdleConns)
db.SetConnMaxLifetime(cfg.ConnMaxLifetime)
db.SetConnMaxIdleTime(cfg.ConnMaxIdleTime)

if err := db.PingContext(ctx); err != nil {
    db.Close()
    return nil, fmt.Errorf("pinging mysql: %w", err)
}

*sql.DB safe สำหรับ concurrent use และจัด pool ภายใน อย่าเปิด/ปิดต่อ request เพราะเสีย handshake, ทำให้ pooling ไม่มีผล และสร้าง connection storm ส่วน MaxOpenConns กลายเป็น semaphore: ต่ำเกินไป goroutine รอจน deadline หมด สูงเกินไป database ถูกหลาย instance แย่ง connection

ใช้ SetConnMaxLifetime ต่ำกว่า limit ของ server/proxy/network เพื่อ recycle ก่อนถูกตัดแบบไม่คาดคิด เอกสาร MySQL driver แนะนำ lifetime สั้นกว่า 5 นาทีเป็นค่าที่ใช้ได้กว้างกับ middleware จำนวนมาก แต่ค่านี้ ไม่ใช่ guarantee สำหรับทุกระบบ ให้ยืนยันกับ RDS Proxy, load balancer และ MySQL configuration จริง

สำหรับ Redis คอร์สแนะนำ github.com/redis/go-redis/v9 client ปกติมี pool และใช้ร่วมหลาย goroutine ได้ สร้าง 1 client ต่อ logical configuration/credential, ตั้ง PoolSize, idle/max lifetime และ operation timeout ตาม workload อย่า redis.NewClient ต่อ handler

Pool Budget ต้องคิดทั้ง Fleet

เริ่มจาก database connection budget ที่ DBA/บริการยอมให้ application นี้ใช้ แล้วหารตาม maximum replicas, runtime types, deployment overlap และ operational tools อย่าตั้ง MaxOpenConns=100 ทุก pod โดยไม่คูณ replica count ใน rolling deploy

วัด db.Stats() ได้แก่ OpenConnections, InUse, Idle, WaitCount, WaitDuration หาก wait สูง ต้องดูทั้ง query latency, transaction length, pool size และ database saturation ไม่เพิ่ม poolทันที Redis ก็ต้องติดตาม pool hits/misses/timeouts, command latency และ server limits

Runtime Matrix

RuntimeClient lifecyclePool/concurrency defaultจุดเสี่ยง
Long-lived HTTP serviceสร้างใน main, inject, ปิดตอน shutdownอิง max in-flight DB work ต่อ replicaและ fleet budgetrolling deploy คูณ pool, long transaction, queue ซ้อน
AWS Lambdaสร้างนอก handlerเพื่อ warm reusepool เล็กและคิดจำนวน execution environments; ใช้ RDS Proxy เมื่อ short/high concurrencycold-start burst, idle stale connection, environment scale-out
Temporal Workerสร้าง 1 ครั้งต่อ worker process แล้ว inject เข้า Activity structอิง Activity concurrency/task queues ไม่ใช่ Workflow countretry ทำ I/O ซ้ำ, activity slots มากกว่า DB pool, shutdown/drain

HTTP service

Handler ส่ง r.Context() ถึง QueryContext/ExecContext, transaction สั้นและไม่ถือ connection ขณะ เรียก external API Server readiness อาจตรวจว่า dependency ที่จำเป็นพร้อม แต่ต้องระวัง health endpoint ยิง Ping ถี่จนสร้าง load เอง

AWS Lambda

AWS Lambda best practices แนะนำ initialize SDK clients และ database connections นอก handler เพื่อ reuse ใน warm environment แต่ แต่ละ concurrent environment มี pool ของตัวเองและ Lambda scale out ได้เร็ว จึงเสี่ยง connection storm สำหรับ relational database ที่มี frequent short connections/high concurrency AWS แนะนำ RDS Proxy เพื่อ multiplex/pool ฝั่งกลาง

connection idle อาจถูกปิด ใช้ driver pool/liveness behavior, lifetime/idle settings และ retry เฉพาะ operation ที่ idempotent อย่า ping ทุก invocation โดยไม่มีหลักฐาน Redis clientก็ reuse นอก handlerได้ แต่ VPC/network, cluster connection limit และ cold-start concurrency ยังต้องอยู่ใน budget

Temporal Worker

Temporal Worker เป็น long-running process ให้สร้าง heavy client/pool 1 ครั้งต่อ process และ inject เข้า Activity implementation Workflows ต้อง deterministic จึง ห้ามเข้าถึง MySQL/Redis โดยตรง; I/O อยู่ใน Activities Activity retry ต้องใช้ idempotency key/transaction contract เพราะการ retry อาจ ทำ side effect ซ้ำ และ pool size ต้องสัมพันธ์กับ Activity worker concurrency

type Activities struct {
    db    *sql.DB
    redis *redis.Client
}

Worker shutdown ต้องหยุด poll/drain ตาม SDK แล้วจึงปิด clients ที่ Activities ใช้ รายละเอียด Worker, retry และ Activity lifecycle อยู่ใน Temporal for Fintech โดยเฉพาะ Activities in Depth และ Production & Observability

Temporal Serverless Workers บน AWS Lambda ยังเป็น Public Preview ณ วันที่ตรวจ ให้ถือเป็น runtime ephemeral แยกจาก long-lived worker และใช้ default conservative จนวัด concurrency/lifecycle จริง

Query Safety และ Tests

ใช้ parameterized query, rows.Close() + rows.Err(), translate sql.ErrNoRows, ส่ง context ทุก call และวาง transaction ที่ use-case boundary หากต้อง integration test behavior ของ driver/pool คอร์สแนะนำ testcontainers-go เปิด MySQL/Redis version ใกล้ production แทน mock database/sql ทุกอย่าง

mock/stub เหมาะกับ unit test ของ service แต่ไม่พิสูจน์ placeholder syntax, scanning, constraint, transaction isolation, Redis TTL หรือ reconnection Integration suite ต้องมี isolated schema/key prefix, cleanup และ timeout พร้อมไม่แชร์ stateเมื่อรัน parallel

Production Toolbox

MySQL default: database/sql + go-sql-driver/mysql; เพิ่ม sqlx เมื่อ struct scanning/query ergonomics ลด code จริง Redis default: go-redis/v9; integration default: testcontainers-go Lambda ที่ scale เร็วให้พิจารณา RDS Proxy Pool ทุกตัวต้องมาจาก fleet budget และ runtime concurrency

Checklist ก่อนต่อ Production Data Store

  • client/pool มี owner เดียวต่อ runtime instance และถูก inject
  • pool × replicas × runtime modes ไม่เกิน dependency budget
  • lifetime/idle settings สอดคล้อง server/proxy/network
  • context deadline ถึงทุก operation และ transaction ไม่คร่อม external call
  • retry ทำเฉพาะ operation ที่ idempotent/จำแนก error ได้
  • metrics แสดง in-use, wait, timeout และ latency
  • Redis ไม่ถูกใช้เป็น authoritative source สำหรับ business decision
  • integration test ใช้ engine/version และ failure behavior ที่ใกล้จริง

อ่านเพิ่ม: Managing database connections, Opening a database handle และ Redis Go client guide