Application Metrics
log และ trace ให้รายละเอียดระดับราย request เหมาะมากตอนที่คุณรู้แล้วว่ามีอะไรผิด แล้วอยากเจาะดู request ตัวนั้น แต่ตอบไม่ได้เลยกับงานปฏิบัติการอีกครึ่งหนึ่งที่ถามว่า ตอนนี้ระบบโดยรวมยังแข็งแรงอยู่ไหม คุณไม่มีทางไล่ดู log ล้านบรรทัดต่อนาทีเพื่อสังเกตว่า error rate เพิ่งเพิ่มเป็นสองเท่า และเปิด trace ทีละอันก็ไม่ช่วยให้รู้ว่า p99 latency ทั้ง fleet ค่อย ๆ ไต่ขึ้นตลอดบ่ายหรือเปล่า สิ่งที่คุณต้องการคือบทสรุปที่พอดีหน้าจอเดียว และตัวเลขที่เครื่องเอาไปเทียบกับ threshold ได้
สัญญาณระดับรายละเอียดเอามา aggregate ได้ไม่ถูกเลย การนับ error ด้วยการ tail log ทั้งช้าและเปราะบาง การคำนวณ latency percentile ข้ามทุก replica ด้วยการอ่าน trace ก็แพงและได้คำตอบหลังเหตุการณ์จบไปแล้ว ที่สำคัญคุณต้องการมากกว่า snapshot ณ ปัจจุบัน คุณต้องการ trend ตามเวลา เพื่อให้เห็นการเสื่อมสภาพตั้งแต่ยังก่อตัว แล้ว alert ก่อน ที่จะกลายเป็นเหตุขัดข้อง ไม่ใช่มาอ่านย้อนหลังทีหลัง
แล้วคุณจะผลิตบทสรุปเชิงตัวเลขที่ราคาถูก อัปเดตอย่างต่อเนื่องของพฤติกรรมบริการของคุณได้อย่างไร — request rate, errors, latency, การใช้ทรัพยากร — ที่ dashboard สามารถวาดกราฟและระบบ alerting สามารถเฝ้าดูได้
วิธีแก้
หัวข้อที่มีชื่อว่า “วิธีแก้”Application metrics คือค่าวัดเชิงตัวเลขที่ service ของคุณเก็บไว้ในหน่วยความจำแล้วเปิดให้ระบบ monitoring มาดึงไป มีสามรูปแบบหลัก:
- Counter — ค่าที่ขึ้นได้อย่างเดียว หรือรีเซ็ตเป็นศูนย์ตอน restart เช่น จำนวน request ที่ให้บริการทั้งหมด หรือจำนวน error ทั้งหมด สิ่งที่คุณเอาไปวาดกราฟคือ อัตรา การเปลี่ยนแปลง
- Gauge — ค่าที่ขึ้นและลง: คำขอที่ in-flight ปัจจุบัน, ความลึกของ queue, ขนาดของ connection pool, หน่วยความจำที่ใช้อยู่
- Histogram — การกระจายของค่าที่วัดได้ จัดลงถังตามช่วง เช่น ระยะเวลาของ request หรือขนาด payload แล้วคำนวณ percentile อย่าง p50, p95 และ p99 ออกมา ซึ่งบอกความจริงได้ตรงกว่าค่าเฉลี่ยมาก
รูปแบบที่ใช้กันทั่วไปคือให้แต่ละ service เปิดค่า metric ปัจจุบันไว้ที่ endpoint หนึ่ง เช่น /metrics แล้วให้ scraper มา poll เป็นช่วง ๆ เก็บทุกค่าที่อ่านได้เป็น time series ลง metrics database ที่เป็นตัวป้อน dashboard และ alert อีกที ส่วนคำถามว่า metric ไหน สำคัญ มีสอง framework สั้น ๆ ช่วยตอบ ได้แก่ RED คือ Rate, Errors, Duration สำหรับ service ที่ขับเคลื่อนด้วย request และ USE คือ Utilization, Saturation, Errors สำหรับทรัพยากรอย่าง CPU, memory และ queue
flowchart LR
subgraph App[Service replica]
CODE[Request handler] -->|inc / observe| REG[In-memory registry]
REG --> EP[/metrics endpoint]
end
SCRAPER[Scraper] -->|poll every Ns| EP
SCRAPER --> TSDB[(Time-series DB)]
TSDB --> DASH[Dashboards]
TSDB --> ALERT[Alert rules] ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”สองอย่างที่คุณจะหยิบใช้บ่อยที่สุดคือ เพิ่มค่า counter เมื่อ request ทำเสร็จพร้อมติด label ตามผลลัพธ์ และบันทึก latency ของ request นั้นลง histogram ตัวอย่างข้างล่างใช้ client สไตล์ Prometheus ที่เป็น pattern มาตรฐานโดยพฤตินัยในทุกภาษา
import { Counter, Histogram } from 'prom-client';
const requests = new Counter({ name: 'http_requests_total', help: 'Total HTTP requests', labelNames: ['method', 'route', 'status'],});
const latency = new Histogram({ name: 'http_request_duration_seconds', help: 'Request latency in seconds', labelNames: ['route'], buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],});
app.use((req, res, next) => { const stop = latency.startTimer({ route: req.path }); res.on('finish', () => { requests.inc({ method: req.method, route: req.path, status: res.statusCode }); stop(); }); next();});import timefrom prometheus_client import Counter, Histogram
REQUESTS = Counter( "http_requests_total", "Total HTTP requests", ["method", "route", "status"],)LATENCY = Histogram( "http_request_duration_seconds", "Request latency in seconds", ["route"], buckets=(0.05, 0.1, 0.25, 0.5, 1, 2.5, 5),)
async def metrics_middleware(request, call_next): start = time.perf_counter() response = await call_next(request) LATENCY.labels(route=request.url.path).observe(time.perf_counter() - start) REQUESTS.labels( method=request.method, route=request.url.path, status=response.status_code ).inc() return responseimport ( "time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto")
var requests = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "http_requests_total", Help: "Total HTTP requests",}, []string{"method", "route", "status"})
var latency = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Help: "Request latency in seconds", Buckets: []float64{0.05, 0.1, 0.25, 0.5, 1, 2.5, 5},}, []string{"route"})
func instrument(route, method string, status int, start time.Time) { latency.WithLabelValues(route).Observe(time.Since(start).Seconds()) requests.WithLabelValues(method, route, statusText(status)).Inc()}use prometheus::{register_counter_vec, register_histogram_vec, CounterVec, HistogramVec};use std::time::Instant;
static REQUESTS: once_cell::sync::Lazy<CounterVec> = once_cell::sync::Lazy::new(|| { register_counter_vec!( "http_requests_total", "Total HTTP requests", &["method", "route", "status"] ).unwrap()});
static LATENCY: once_cell::sync::Lazy<HistogramVec> = once_cell::sync::Lazy::new(|| { register_histogram_vec!( "http_request_duration_seconds", "Request latency in seconds", &["route"], vec![0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0] ).unwrap()});
fn instrument(route: &str, method: &str, status: &str, start: Instant) { LATENCY.with_label_values(&[route]).observe(start.elapsed().as_secs_f64()); REQUESTS.with_label_values(&[method, route, status]).inc();}ผลลัพธ์ที่ตามมา
หัวข้อที่มีชื่อว่า “ผลลัพธ์ที่ตามมา”สิ่งที่คุณได้รับ:
- สุขภาพของ fleet บนหน้าจอเดียว กราฟ rate, errors และ duration สรุปคำขอนับพันข้ามทุก replica ลงเป็นไม่กี่เส้นที่คุณอ่านได้ในพริบตา
- alert ได้ก่อนผู้ใช้บ่น เพราะ metric เป็น time series ต่อเนื่อง คุณจึงตั้ง alert บน error rate ที่ไต่ขึ้นหรือ p99 ที่ข้าม threshold ได้ แล้วเข้าไปแก้ก่อนที่จะบานปลายเป็นเหตุขัดข้อง
- ราคาถูกและเปิดอยู่ตลอด counter และ histogram แทบไม่มีต้นทุนในการอัปเดตในหน่วยความจำ และถูกเก็บด้วยการ poll ดังนั้น overhead จึงคงที่เมื่อทราฟฟิกโตขึ้น — ไม่เหมือนการ log ทุกคำขอ
สิ่งที่คุณต้องจ่าย:
- Cardinality เป็นกับดัก ทุกการผสมที่แตกต่างกันของค่า label เป็น time series แยกต่างหาก การใส่ user id หรือ raw URL ที่มี id ลงใน label สามารถระเบิดเป็น time series นับล้านและทำให้ metrics store ล่ม ให้ label เป็น low-cardinality — method, route template, status class
- ค่า aggregate กลบรายตัว metric บอกได้ว่า error rate เพิ่มขึ้น แต่บอกไม่ได้ว่า request ไหน ล้มเหลวและเพราะอะไร คุณยังต้องใช้ log และ trace เจาะลงไปอยู่ดี พูดง่าย ๆ คือ metric ชี้จุด ส่วนสัญญาณอื่นเป็นตัวอธิบาย
- Percentile ต้องการ histogram ไม่ใช่ค่าเฉลี่ย latency เฉลี่ยที่ดูดีสามารถซ่อน p99 ที่แย่ได้ เลือก histogram bucket ที่ครอบคลุม service-level objective ของคุณ ไม่เช่นนั้น percentile ของคุณจะหยาบเกินไปจนไร้ประโยชน์
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”- Distributed Tracing — เมื่อ latency metric พุ่ง trace จะชี้ให้เห็นว่า hop ไหนเป็นต้นเหตุ
- Health Check API — health check ตอบได้แค่ผ่านหรือไม่ผ่าน ส่วน metric เผยให้เห็นการเสื่อมถอยแบบค่อยเป็นค่อยไปที่เกิดขึ้นก่อนหน้านั้น
| ข้อดี | ข้อแลกเปลี่ยน |
|---|---|
| รู้ health ของระบบแบบ real-time โดยไม่ต้อง tail log | ต้องเลือก metric ที่ใช่ — metric มากเกินทำให้ noise สูง |
| trigger alert เมื่อ metric เกิน threshold ได้อัตโนมัติ | cardinality สูง (เช่น tag ด้วย user_id) ทำให้ storage พุ่ง |
| ดู trend ข้ามเวลา — เห็น degradation ก่อนที่จะพัง | metric ไม่บอก “ทำไม” — ต้องใช้ log และ trace ช่วย |
| ใช้ตัดสินใจ capacity planning และ scaling | instrumenting code เพิ่ม complexity เล็กน้อย |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”Metric Explosion (High Cardinality) — ใช้ dynamic value เป็น label/tag อาการ:
- label ด้วย
user_id,request_id, หรือurl_pathแบบ raw - Prometheus หรือ metric store ใช้ memory พุ่งสูง
- query ช้ามากเพราะ series count ระเบิด
Vanity Metrics — track metric ที่ไม่ได้บอกอะไรเกี่ยวกับ health จริง ๆ อาการ:
- นับ request count แต่ไม่นับ error rate หรือ latency percentile
- dashboard สวยแต่ไม่ได้ช่วย debug incident
💡 ตัวอย่างจากของจริง
Netflix:
- ใช้ Atlas เป็น in-house time-series metric system
- track ล้าน metric series ต่อวินาที
- RED method: Rate, Errors, Duration สำหรับทุก service
Airbnb:
- ใช้ Prometheus + Grafana
- SLO-based alerting — alert เมื่อ error budget หมด ไม่ใช่เมื่อ metric เกิน threshold เดี่ยว ๆ