บทที่ 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
| Runtime | Client lifecycle | Pool/concurrency default | จุดเสี่ยง |
|---|---|---|---|
| Long-lived HTTP service | สร้างใน main, inject, ปิดตอน shutdown | อิง max in-flight DB work ต่อ replicaและ fleet budget | rolling deploy คูณ pool, long transaction, queue ซ้อน |
| AWS Lambda | สร้างนอก handlerเพื่อ warm reuse | pool เล็กและคิดจำนวน execution environments; ใช้ RDS Proxy เมื่อ short/high concurrency | cold-start burst, idle stale connection, environment scale-out |
| Temporal Worker | สร้าง 1 ครั้งต่อ worker process แล้ว inject เข้า Activity struct | อิง Activity concurrency/task queues ไม่ใช่ Workflow count | retry ทำ 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