Prometheus Recording Rules: Pre-compute Metrics เพื่อ Performance

เมื่อ Prometheus ต้องประมวลผลคำสั่งที่ซับซ้อนหรือรวมข้อมูลจากเมตริกจำนวนมากซ้ำ ๆ ใน Dashboard และ Alert การเรียก PromQL แบบสดทุกครั้งจะเปลือง CPU และทำให้ Grafana ค่อย ๆ ช้าลงอย่างเห็นได้ชัด บทความนี้จะอธิบาย Recording Rules — กลไกการ pre-compute เมตริกเพื่อให้ query ที่มีต้นทุนสูงถูกคำนวณล่วงหน้าและเก็บเป็นเมตริกใหม่ พร้อมตัวอย่าง rule file, naming convention, การวางแผน evaluation interval และ anti-pattern ที่ควรหลีกเลี่ยง

หากคุณมี Dashboard ที่ใช้เวลาโหลดหลายวินาที หรือ Alert ที่ประมวลผลช้าจนทริกเกอร์สาย Recording Rules คือเครื่องมือที่ควรรู้จักเป็นอันดับต้น ๆ ก่อนจะไปแก้ด้วยการเพิ่ม hardware

Recording Rules คืออะไร

Recording Rules คือกฎที่บอกให้ Prometheus ประมวลผล PromQL expression ตามรอบเวลาที่กำหนด (โดยปกติ 15-60 วินาที) แล้วบันทึกผลลัพธ์เป็นเมตริกใหม่ใน TSDB ทำให้ query ที่ใช้เมตริกใหม่นี้ทำงานเหมือนกับการอ่านเมตริกธรรมดา ไม่ต้องคำนวณซ้ำทุกครั้งที่ Dashboard หรือ Alert เรียกใช้

ประโยชน์หลักมี 3 ประการ ประการแรกคือลดเวลา query ของ Dashboard จากวินาทีเหลือมิลลิวินาที ประการที่สองคือลดภาระของ Prometheus เอง เพราะคำสั่งที่ต้องอ่าน series หลายหมื่นตัวจะถูกคำนวณครั้งเดียวแล้วแชร์ทุกคน ประการที่สามคือทำให้ Alert Rule มีความเสถียรเพราะคำนวณจากข้อมูลที่ pre-compute แล้ว ไม่ขึ้นกับโหลดของเครื่องตอนที่ query

เมื่อไหร่ควรใช้ Recording Rules

  • Query ที่ใช้ใน Dashboard หลายแผง แต่ละแผงคำนวณเหมือนกันซ้ำ ๆ
  • Aggregation ที่ครอบคลุม series จำนวนมาก (หลักพันขึ้นไป) เช่น sum(rate(http_requests_total[5m]))
  • Query ที่ใช้ใน Alert Rule หลายตัว และต้องการให้คำนวณจากค่าชุดเดียวกัน
  • Query ที่ประกอบด้วย histogram_quantile บน histogram ที่มี bucket เยอะ
  • Expression ซับซ้อนที่ใช้ subquery หรือ label_replace หลายชั้น

ในทางกลับกัน query ที่เรียกใช้เพียงครั้งเดียวหรือนาน ๆ ครั้ง ไม่ควร pre-compute เพราะจะเพิ่มภาระการเขียน TSDB โดยไม่จำเป็นและสร้างเมตริกที่ไม่มีใครใช้

โครงสร้างไฟล์ Recording Rules

Recording Rules ถูกเก็บในไฟล์ YAML แยกจาก prometheus.yml และอ้างถึงผ่านคีย์ rule_files โครงสร้างพื้นฐานประกอบด้วย groups ที่ห่อ rules หลายกฎ แต่ละ group มี interval กำหนดรอบประมวลผลของตัวเอง

groups:
  - name: http_recording_rules
    interval: 30s
    rules:
      - record: job:http_requests:rate5m
        expr: sum by (job) (rate(http_requests_total[5m]))

      - record: job:http_request_duration:p95
        expr: histogram_quantile(0.95,
                sum by (job, le) (
                  rate(http_request_duration_seconds_bucket[5m])))

      - record: job:http_errors:ratio_rate5m
        expr: |
          sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
            /
          sum by (job) (rate(http_requests_total[5m]))

ในไฟล์ prometheus.yml เพิ่มบรรทัด rule_files ให้ชี้ไปที่ไฟล์ดังกล่าว Prometheus จะโหลดและประมวลผลตาม interval ที่กำหนดทันทีหลัง reload config

rule_files:
  - /etc/prometheus/rules/*.yml

# reload config โดยไม่ต้อง restart
curl -X POST http://localhost:9090/-/reload

Naming Convention ที่แนะนำ

Prometheus แนะนำรูปแบบ level:metric:operations เพื่อให้ผู้อ่านรู้ได้ทันทีว่าเมตริกใหม่ผ่านการรวมด้วย label ระดับไหน และดำเนินการอะไรบ้าง รูปแบบนี้ช่วยให้ทีมที่ร่วมกันดูแลหลายสิบ rule ไม่เขียนทับกันและค้นหาเมตริกในระบบได้ง่ายขึ้น

องค์ประกอบความหมายตัวอย่าง
levellabel ที่เก็บไว้หลัง aggregationjob, instance, namespace
metricชื่อเมตริกต้นทาง (ตัด suffix)http_requests, node_cpu
operationsการดำเนินการที่ทำrate5m, sum_irate, p95

ตัวอย่างชื่อที่ดีเช่น job:http_requests:rate5m หมายถึง rate 5 นาทีของ http_requests รวมให้เหลือเฉพาะ label job และ instance:node_cpu_usage:avg1m หมายถึง CPU usage เฉลี่ย 1 นาที แยกตาม instance

Evaluation Interval กับต้นทุนการประมวลผล

ทุก group ประมวลผลตาม interval ของตัวเอง (หากไม่ระบุจะใช้ global.evaluation_interval ใน prometheus.yml) ตั้งค่าถี่เกินไปจะเปลือง CPU ส่วนตั้งห่างเกินไปจะทำให้ alert ตอบสนองช้า หลักคิดที่ใช้จริงคือปรับให้สอดคล้องกับ scrape interval และความไวที่ต้องการ

  • Scrape interval 15 วินาที → ตั้ง evaluation interval 15-30 วินาที
  • Rule ที่ใช้สำหรับ Alert ต้อง <= for duration ของ alert
  • Rule ที่ใช้ทำ Dashboard 5m+ สามารถตั้ง 60 วินาทีได้เพื่อประหยัด
  • Rule หนัก ๆ ที่รวมหลาย series ให้แยกเป็น group ที่มี interval ของตัวเอง

การเรียงลำดับและพึ่งพาระหว่าง Rule

Rule ใน group เดียวกันจะถูกประมวลผลตามลำดับบนลงล่างแบบ sequential ทำให้ rule ล่าง ๆ สามารถอ้างผลลัพธ์ของ rule ก่อนหน้าได้ แต่ rule ข้าม group ไม่รับประกันลำดับ — Prometheus 2.x ขึ้นไปประมวลผล group แบบขนาน หากจำเป็นต้องอ้าง rule ข้าม group ต้องยอมรับว่าอาจได้ข้อมูลจากรอบก่อน

groups:
  - name: slo_calculations
    interval: 30s
    rules:
      # rule แรก: error rate
      - record: job:http_errors:rate5m
        expr: sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))

      # rule ที่สอง: total rate
      - record: job:http_requests:rate5m
        expr: sum by (job) (rate(http_requests_total[5m]))

      # rule ที่สาม: error ratio (อ้างสองตัวบน)
      - record: job:http_errors:ratio5m
        expr: job:http_errors:rate5m / job:http_requests:rate5m

ตรวจสอบประสิทธิภาพของ Rule

Prometheus เปิดเผยเมตริกภายในเกี่ยวกับ rule ที่ endpoint /metrics ของตัวเอง ใช้ตรวจสอบว่า rule ตัวไหนช้าหรือใช้ทรัพยากรเกินควร

เมตริกความหมาย
prometheus_rule_evaluation_duration_secondsเวลาที่ใช้ประมวลผล rule แต่ละรอบ
prometheus_rule_group_duration_secondsเวลารวมของทั้ง group ต่อรอบ
prometheus_rule_group_iterations_missed_totalจำนวนรอบที่ข้ามไป — ถ้าเพิ่มเรื่อย ๆ แปลว่า group ช้าเกิน interval
prometheus_rule_evaluation_failures_totalจำนวนครั้งที่ rule ประมวลผลไม่สำเร็จ

หาก iterations_missed_total โตต่อเนื่อง ให้พิจารณาเพิ่ม interval ของ group แยก rule หนักออก หรือปรับ expression ให้มี label น้อยลง

ใช้คู่กับ Alerting Rules

วิธีที่ทีม SRE นิยมใช้คือ pre-compute ค่าที่ alert ต้องใช้ไว้ก่อน แล้วให้ Alert Rule อ้าง recording rule แทนการคำนวณใหม่ ทำให้ alert คำนวณเร็ว ประหยัดทรัพยากร และถ้าเปลี่ยน threshold ในอนาคตก็แก้ที่ alert เพียงที่เดียว

# Recording Rule (pre-compute)
- record: job:http_errors:ratio5m
  expr: sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
      / sum by (job) (rate(http_requests_total[5m]))

# Alert Rule (อ้างค่าที่ pre-compute)
- alert: HighErrorRate
  expr: job:http_errors:ratio5m > 0.05
  for: 5m
  labels:
    severity: critical
  annotations:
    summary: "Error rate ของ {{ $labels.job }} สูงเกิน 5%"

Anti-patterns ที่ควรหลีกเลี่ยง

  • สร้าง recording rule สำหรับ query ที่เรียกใช้วันละไม่กี่ครั้ง — เสียพื้นที่เก็บข้อมูลโดยไม่คุ้ม
  • ใส่ rule นับร้อยใน group เดียว — ประมวลผลแบบ sequential ทำให้ rule ตัวท้าย ๆ ได้ข้อมูลช้า
  • Pre-compute rate บน counter ที่มี cardinality สูงโดยไม่ลด label ก่อน — ยังคงมี series เยอะเท่าเดิม
  • ใช้ชื่อเหมือนเมตริกต้นทาง เช่น http_requests_total — สับสนกับของจริง ควรใช้รูปแบบ level:metric:operations
  • ไม่เก็บ rule file ไว้ใน Git — เมื่อแก้ rule แล้วผิด จะย้อนกลับไม่ได้

ทดสอบ Rule ก่อนใช้งานจริง

Prometheus มีคำสั่ง promtool สำหรับตรวจสอบไวยากรณ์ของ rule file และรัน unit test กับข้อมูลจำลอง ใช้ควบคู่กับ CI pipeline เพื่อไม่ให้ rule ที่ผิดเข้า production

# ตรวจไวยากรณ์
promtool check rules /etc/prometheus/rules/*.yml

# รัน unit test (ต้องเขียน test file แยก)
promtool test rules tests/recording_rules_test.yml

ไฟล์ test แบบพื้นฐานมีโครงสร้างดังนี้ กำหนด input series พร้อมค่าที่ทำการ simulate ตามเวลา จากนั้นระบุค่า expected ที่ rule ควรคำนวณออกมา

rule_files:
  - recording_rules.yml

tests:
  - interval: 15s
    input_series:
      - series: 'http_requests_total{job="api",status="200"}'
        values: '0 10 20 30 40 50'
      - series: 'http_requests_total{job="api",status="500"}'
        values: '0 1 2 3 4 5'

    promql_expr_test:
      - expr: job:http_errors:ratio5m
        eval_time: 5m
        exp_samples:
          - labels: 'job:http_errors:ratio5m{job="api"}'
            value: 0.1

สรุป

Recording Rules เป็นเครื่องมือคลาสสิกของ Prometheus ที่ให้ผลตอบแทนสูงต่อความพยายาม เพียงย้าย query ที่ใช้บ่อยและหนักมาเป็น rule แยก Dashboard จะโหลดเร็วขึ้น Alert คำนวณเร็วขึ้น และทรัพยากรของ Prometheus ก็ถูกใช้อย่างมีประสิทธิภาพมากขึ้น กุญแจสำคัญคือเลือก query ที่คุ้มค่าต่อการ pre-compute วางแผน naming convention ให้เข้าใจตรงกัน ตั้ง interval ให้เหมาะกับการใช้งาน และตรวจ iterations_missed_total เป็นระยะ

สำหรับทีมที่เพิ่งเริ่มใช้ Prometheus ในขนาด production ขอแนะนำให้ตั้ง rule ชุดแรกกับ 3 สิ่งที่ได้ผลแน่นอน: error rate ratio, request rate แยกตาม service, และ p95 latency จาก histogram — สามตัวนี้ครอบคลุม Dashboard และ SLO ส่วนใหญ่ที่องค์กรต้องการดูทุกวัน