সরাসরি পর্বে যান
পর্ব ৬৩ · ৮১-এর মধ্যে · মডিউল ১১

Metric আর Prometheus — counter, gauge, histogram, RED আর USE

Metrics and Prometheus — Counters, Gauges, Histograms, RED and USE

Taught in Bangla, this lesson scrapes a small app with a real Prometheus, explains counters, gauges, histograms and summaries, writes PromQL with rate, increase, sum by and histogram_quantile, shows why rate survives counter resets and how coarse buckets distort p99, and covers RED, USE, golden signals, recording rules, label cardinality, long-term storage and useful dashboards, with real output and simulators.

এই পর্বে যা শিখবেন

  • একটি ছোট app-এ হাতে /metrics লিখে আসল Prometheus দিয়ে scrape করা
  • Counter, gauge, histogram আর summary-র পার্থক্য, আর কোনটি কখন
  • PromQL-এর rate, increase, sum by আর histogram_quantile পড়া, লেখা আর হাতে মিলিয়ে দেখা
  • RED, USE আর golden signal দিয়ে কী মাপবেন তা ঠিক করা, আর recording rule লেখা
  • Cardinality হিসাব করা, দীর্ঘমেয়াদি storage বোঝা আর কাজের dashboard বানানো

১ · মিটার পড়ার লোক: Prometheus কোথায় বসে

কোড production-এ পৌঁছানোর পথ আপনি আগের মডিউলে দেখেছেন। এবার প্রশ্ন: চলতে চলতে app কেমন আছে, সেটি বুঝবেন কীভাবে? উত্তরের প্রথম ধাপ metric: সময়ের সাথে নিয়মিত মাপা একটি সংখ্যা। Observability-র তত্ত্ব, মানে monitoring আর observability-র পার্থক্য, log আর trace-এর জায়গা, System Design কোর্সের Observability পর্বে আছে। এখানে আমরা হাতে-কলমে চালানোর দিকটি দেখব।

বাসার বিদ্যুতের মিটারের কথা ভাবুন। মিটার শুধু মোট ইউনিট গোনে, আর সংখ্যাটি কখনো কমে না। মিটার রিডার মাসে একবার এসে সংখ্যাটি খাতায় তোলেন। আগের রিডিং বিয়োগ করলেই এই মাসের খরচ পাওয়া যায়। Prometheus ঠিক এই মিটার রিডার। App মিটার রাখে, Prometheus এসে পড়ে যায়, আর হিসাবটি হয় পরে।

Pull বনাম push

মিটার রিডার নিজে আসেন, এটি pull. উল্টোটি হলো আপনি নিজে অফিসে গিয়ে রিডিং জমা দিচ্ছেন, এটি push. Prometheus মূলত pull করে। প্রতিটি target-এর ঠিকানা তার জানা থাকে, হয় config থেকে, নয়তো service discovery থেকে।

দিকPullPush
App বন্ধ হলেScrape ব্যর্থ, up হয় 0, তাই নিজেই সংকেতশুধু নীরবতা, আলাদা করে ধরতে হয়
গতির নিয়ন্ত্রণPrometheus নিজের গতিতে টানেএকসাথে বহু sender গ্রাহকের ওপর চাপ দিতে পারে
NAT-এর পেছনের appPrometheus-কে পৌঁছাতে হয়, তাই কঠিনApp বাইরে পাঠায়, তাই সহজ
স্বল্পায়ু jobScrape-এর আগেই শেষ হতে পারেশেষ হওয়ার আগে পাঠিয়ে দেয়
Debugব্রাউজারে /metrics খুলেই দেখা যায়Sender-এর log দেখতে হয়

দুই পথ মিশেও যায়। Batch job নিজের metric Pushgateway-তে push করতে পারে, আর Prometheus সেখান থেকে pull করে। Remote write দিয়ে Prometheus নিজেই metric অন্য জায়গায় push করে। OpenTelemetry-র OTLP পথও push-ভিত্তিক, যা পর্ব ৬৫-তে আসবে।

App থেকে dashboard আর alert পর্যন্ত

App → Prometheus → Grafana and Alertmanager

App থেকে Prometheus হয়ে Grafana আর Alertmanager পর্যন্ত metric-এর পথ Target: প্রতিটি /metrics দেখায় Appshop:8000 Servernode_exporter Databaseexporter GET /metrics প্রতি ৫ s অনুরোধ যায় Prometheus থেকে Prometheus server Scrapeটেনে আনা TSDBসময়-ক্রমে জমা Ruleহিসাব, alert PromQL প্রশ্ন HTTP API alert push Grafana Dashboard, মানুষ দেখে Alertmanager গ্রুপ, চুপ, পাঠানো On-call প্রকৌশলী দীর্ঘমেয়াদি storage remote write (ঐচ্ছিক)
ছবি ৬৩.১ — অনুরোধ যায় Prometheus থেকে target-এ, আর data আসে উল্টো পথে। Grafana প্রশ্ন করে, আর alert আলাদা পথে Alertmanager হয়ে মানুষের কাছে পৌঁছায়।

Time series আর label

Time series মানে একই নাম আর একই label-সমষ্টির (সময়, মান) জোড়ার তালিকা। {status="200"} আর {status="500"} দুটি আলাদা series, যদিও metric-এর নাম এক। Series জমা থাকে একটি time series database-এ, যাকে সংক্ষেপে TSDB বলে। মনে রাখুন: একটি label-এর একটি নতুন মান মানে একটি নতুন series. সব খরচের গল্প এখান থেকেই শুরু, আর সেটি আমরা অংশ ৮-এ দেখব।

Prometheus-এর কাজ শুরু হয় SoundCloud-এ, ২০১২ সালে। Matt Proud আর Julius Volz এর শুরুর দিকের প্রধান দুই নির্মাতা। অনুপ্রেরণা ছিল Google-এর ভেতরের Borgmon, যার কথা Site Reliability Engineering (২০১৬) বইয়ে আছে। ২০১৬ সালে Prometheus CNCF-তে Kubernetes-এর পরে দ্বিতীয় প্রকল্প হিসেবে যোগ দেয়, আর ২০১৮ সালে graduated হয়। সংস্করণ ৩.০ আসে ২০২৪ সালে।

২ · ছোট একটি দোকান, আসল Prometheus

একটি কাল্পনিক অনলাইন দোকানের API বানাই, শুধু Python-এর standard library দিয়ে। কোনো metric library নেই, তাই প্রতিটি লাইন আপনি নিজের চোখে দেখবেন। বাস্তবে আপনি অফিসিয়াল client library ব্যবহার করবেন, যা এই কাজগুলো নিরাপদে করে দেয়।

নিচের সব আউটপুট আসল run থেকে নেওয়া: Docker Engine 29-এ Prometheus v3.5.0, আর পর্ব ৪০-এর Docker Compose দিয়ে চালানো। সময়, সংখ্যা আর memory প্রতি run-এ আলাদা হবে। নিজের কম্পিউটারে চালালে cloud-এর কোনো খরচ নেই, আর এটি ঐচ্ছিক। Docker লাগবে, তাই ব্রাউজারের ভেতরে চলে না। কাজ শেষে docker compose down -v দিয়ে container, network আর volume মুছে ফেলবেন।

# shop.py: tiny API with hand-written Prometheus metrics (stdlib only)
import random, threading, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

lock, rng = threading.Lock(), random.Random(42)
requests, latencies, users = {}, [], 0
in_flight = 0

def histogram(name, bounds):
    out = [f"# TYPE {name} histogram"]
    for le in bounds + ["+Inf"]:
        n = sum(1 for v in latencies if le == "+Inf" or v <= le)
        out.append(f'{name}_bucket{{le="{le}"}} {n}')
    return out + [f"{name}_sum {sum(latencies):.4f}", f"{name}_count {len(latencies)}"]

def summary(name):
    out, s = [f"# TYPE {name} summary"], sorted(latencies) or [0]
    for q in (0.5, 0.95, 0.99):
        out.append(f'{name}{{quantile="{q}"}} {s[int(q * (len(s) - 1))]:.4f}')
    return out + [f"{name}_sum {sum(latencies):.4f}", f"{name}_count {len(latencies)}"]

def render():
    out = ["# HELP shop_requests_total Orders handled.", "# TYPE shop_requests_total counter"]
    out += [f'shop_requests_total{{route="/order",status="{k}"}} {v}' for k, v in sorted(requests.items())]
    out += ["# HELP shop_in_flight Requests being served now.", "# TYPE shop_in_flight gauge"]
    out += [f"shop_in_flight {in_flight}"]
    out += histogram("shop_latency_fine_seconds", [.025, .05, .1, .15, .2, .3, .5, 1])
    out += histogram("shop_latency_coarse_seconds", [.1, .5, 1]) + summary("shop_latency_true_seconds")
    if users:  # the mistake: one series per user
        out += ["# TYPE shop_orders_by_user_total counter"]
        out += [f'shop_orders_by_user_total{{user_id="u{i}"}} 1' for i in range(users)]
    return "\n".join(out) + "\n"
class Handler(BaseHTTPRequestHandler):
    def log_message(self, *args): pass
    def reply(self, code, text, ctype="text/plain"):
        self.send_response(code); self.send_header("Content-Type", ctype); self.end_headers()
        self.wfile.write(text.encode())
    def do_GET(self):
        global in_flight, users
        if self.path == "/metrics":
            with lock: text = render()
            return self.reply(200, text, "text/plain; version=0.0.4; charset=utf-8")
        if self.path.startswith("/seed-users/"):
            users = int(self.path.rsplit("/", 1)[1]); return self.reply(200, "ok\n")
        with lock:
            in_flight += 1
            delay = rng.uniform(.02, .08) if rng.random() < .95 else rng.uniform(.11, .16)
            status = "500" if rng.random() < .02 else "200"
        start = time.perf_counter(); time.sleep(delay)
        with lock:
            in_flight -= 1; latencies.append(time.perf_counter() - start)
            requests[status] = requests.get(status, 0) + 1
        self.reply(int(status), "done\n")

ThreadingHTTPServer(("0.0.0.0", 8000), Handler).serve_forever()

প্রতিটি অনুরোধ ঘুমায় সামান্য সময়: ৯৫% অনুরোধ ২০–৮০ ms, আর ৫% অনুরোধ ১১০–১৬০ ms. ২% অনুরোধ 500 ফেরত দেয়। একই latency দুটি histogram-এ গোনা হচ্ছে, একটি সরু আর একটি মোটা bucket-এর। summary সব মাপ সাজিয়ে আসল quantile দেয়, তাই পরে তুলনা করার মতো একটি সত্য মান হাতে থাকবে। /seed-users/ পথটি ইচ্ছা করে রাখা একটি ভুল, অংশ ৮-এর জন্য। Prometheus ৩-এ Content-Type ঠিক না থাকলে scrape ব্যর্থ হতে পারে, তাই header-টি বাদ দেবেন না।

name: abcl-mp
services:
  shop:
    image: python:3.12-alpine
    container_name: abcl-mp-app
    hostname: demo
    command: ["python", "-u", "/app/shop.py"]
    volumes: ["./shop.py:/app/shop.py:ro"]
    ports: ["127.0.0.1:18632:8000"]
    networks: [net]
  load:
    image: python:3.12-alpine
    container_name: abcl-mp-load
    hostname: demo
    command: ["python", "-u", "/app/load.py"]
    volumes: ["./load.py:/app/load.py:ro"]
    networks: [net]
  prometheus:
    image: prom/prometheus:v3.5.0
    container_name: abcl-mp-prom
    hostname: demo
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./rules.yml:/etc/prometheus/rules.yml:ro
    ports: ["127.0.0.1:18631:9090"]
    networks: [net]
networks:
  net:
    name: abcl-mp-net
global:
  scrape_interval: 5s
  evaluation_interval: 5s
rule_files:
  - /etc/prometheus/rules.yml
scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ["localhost:9090"]
  - job_name: shop
    static_configs:
      - targets: ["shop:8000"]
groups:
  - name: shop_red
    rules:
      - record: job:shop_requests:rate1m
        expr: sum by (job) (rate(shop_requests_total[1m]))
      - record: job:shop_errors:ratio1m
        expr: |
          sum by (job) (rate(shop_requests_total{status=~"5.."}[1m]))
          /
          sum by (job) (rate(shop_requests_total[1m]))
# load.py: three workers sending steady traffic to the shop
import threading, time, urllib.error, urllib.request

def worker():
    while True:
        try:
            urllib.request.urlopen("http://shop:8000/order", timeout=2).read()
        except urllib.error.HTTPError:
            pass  # a 500 is still an answered request
        except OSError:
            time.sleep(1)  # shop is down or restarting
        time.sleep(0.15)

for _ in range(3):
    threading.Thread(target=worker, daemon=True).start()
threading.Event().wait()

এই চারটি ফাইল আর shop.py এক ফোল্ডারে রেখে চালালে দোকান, ভিড় তৈরির প্রোগ্রাম আর Prometheus একসাথে ওঠে। ডিমোতে scrape interval ৫ সেকেন্ড রাখা হয়েছে, যাতে দ্রুত data জমে। বাস্তবে ১৫–৬০ সেকেন্ড বেশি প্রচলিত, আর Prometheus-এর নিজের ডিফল্ট ১ মিনিট।

$ docker compose up -d
…
 Container abcl-mp-prom Started
 Container abcl-mp-app Started

$ curl -s 127.0.0.1:18632/metrics
# HELP shop_requests_total Orders handled.
# TYPE shop_requests_total counter
shop_requests_total{route="/order",status="200"} 694
shop_requests_total{route="/order",status="500"} 10
# HELP shop_in_flight Requests being served now.
# TYPE shop_in_flight gauge
shop_in_flight 2
# TYPE shop_latency_fine_seconds histogram
shop_latency_fine_seconds_bucket{le="0.025"} 56
shop_latency_fine_seconds_bucket{le="0.05"} 333
shop_latency_fine_seconds_bucket{le="0.1"} 664
…
shop_latency_fine_seconds_bucket{le="+Inf"} 704
shop_latency_fine_seconds_sum 38.5635
shop_latency_fine_seconds_count 704
…

এটিই exposition format: এক লাইনে নাম, label আর মান। # TYPE বলে দেয় metric-টি কোন ধরনের। এই পাতায় ২৫টি series আছে। লক্ষ করুন, _bucket, _sum আর _count সব আলাদা series, তাই একটি histogram নিজেই অনেকগুলো series খায়।

একটি scrape-এ আসলে কী ঘটে

  1. সময় হলো5sinterval
  2. Targetshop:8000job=shop
  3. GET/metrics1,421 byte
  4. Labelinstancejob জোড়া
  5. TSDB25 samplehead, WAL
prometheus.ymlপ্রতি job-এর নিজের সময়সূচি
global:
  scrape_interval: 5s
  evaluation_interval: 5s
/api/v1/targetsআসল run (অংশ)
scrapePool: shop
scrapeUrl:  http://shop:8000/metrics
health:     up
HTTPApp পাতাটি তখনই বানায়
GET /metrics
200 OK
Content-Type: text/plain; version=0.0.4; charset=utf-8
# response body-তে 25টি sample, 1,421 byte
জমার আগেApp এই label দুটি লেখেনি
shop_in_flight 0
# হয়ে গেল
shop_in_flight{instance="shop:8000",job="shop"} 0
নিজে থেকে তৈরি seriesআসল run (অংশ)
up{job="shop"}                            1
scrape_samples_scraped{job="shop"}        25
scrape_duration_seconds{job="shop"}       0.003035817
Target বন্ধ থাকলেআসল run (অংশ)
$ docker stop -t 0 abcl-mp-app
up{job="shop"}           0
shop_in_flight           (ফল নেই)
health: down
lastError: Get "http://shop:8000/metrics": dial tcp: lookup shop … no such host
  1. ধাপ ১: সময় হলো। Prometheus প্রতিটি target-এর জন্য একটি টাইমার চালায়। এখানে ৫ সেকেন্ড পরপর। Scrape interval মানে দুটি রিডিংয়ের মাঝের সময়, তাই এর চেয়ে ছোট কোনো ঘটনা হয়তো ধরা পড়বে না।
  2. ধাপ ২: কোন ঠিকানা। এখানে ঠিকানাটি config-এ লেখা। Kubernetes-এ এটি service discovery থেকে আসে, কারণ pod-এর ঠিকানা বদলায় (পর্ব ৪৫)।
  3. ধাপ ৩: HTTP GET. App ঠিক এই মুহূর্তের সংখ্যা দিয়ে পাতাটি বানায়। এই run-এ উত্তর এলো প্রায় ৩ ms-এ। Scrape-এর সময় app-এর কোনো ইতিহাস লাগে না, শুধু চলমান সংখ্যা।
  4. ধাপ ৪: Label জোড়া। Prometheus নিজে job আর instance জুড়ে দেয়। তাই একই metric তিনটি replica থেকে এলেও আলাদা series থাকে, আর পরে sum করা যায়।
  5. ধাপ ৫: জমা। ২৫টি sample একই timestamp-এ memory-র head block আর write-ahead log-এ যায়। সাথে Prometheus নিজে কিছু series লেখে। সবচেয়ে কাজের হলো up: 1 মানে scrape সফল।
  6. ধাপ ৬: Target বন্ধ হলে। Scrape ব্যর্থ হলো, তাই up হলো 0. আগের series-গুলো stale চিহ্ন পেল, তাই shop_in_flight কোনো ফলই দিল না। Metric-এর “মান 0” আর “মান নেই” এক কথা নয়।

ধাপ ৬-এর আচরণটিকে বলে staleness. Target ব্যর্থ হলে বা কোনো series পাতা থেকে হারিয়ে গেলে Prometheus সেই series-এ একটি বিশেষ চিহ্ন লিখে দেয়। এই run-এ app বন্ধের ১৩ সেকেন্ড পরে shop_in_flight আর count(shop_requests_total), দুটোই ফাঁকা ফল দিল। কিন্তু last_over_time(shop_in_flight[1m]) শেষ মান 1 দেখাল, কারণ পুরোনো sample জমাই আছে।

Alert লেখার একটি ফাঁদ: shop_in_flight > 100 ধরনের alert app মরে গেলে চুপ হয়ে যায়, কারণ series-ই নেই। তাই app জীবিত কি না দেখতে আলাদা করে up == 0 লিখতে হয়। Stale চিহ্ন না থাকলে, যেমন timestamp-সহ metric-এ, instant query শেষ sample পেছনে lookback delta পর্যন্ত খোঁজে, যার ডিফল্ট ৫ মিনিট।

৩ · চার ধরনের metric

Counter

Counter শুধু বাড়ে, বিদ্যুতের মিটারের মতো। মোট মান প্রায় অর্থহীন, দরকার বাড়ার হার। নামের শেষে _total রাখার রীতি আছে।

shop_requests_total

Gauge

Gauge ওঠে-নামে, পানির ট্যাংকের স্তরের মতো। এই মুহূর্তের মানটিই অর্থবহ। এতে rate ব্যবহার অর্থহীন।

shop_in_flight

Histogram

Histogram মাপকে bucket-এ গোনে। পরীক্ষার ফলের A+, A, B খাতার মতো: কে কত নম্বর পেল জানা যায় না, শুধু কোন খাতায় কতজন।

_bucket, _sum, _count

Summary

Summary-তে app নিজেই p50, p99 হিসাব করে পাঠায়। হিসাব নির্ভুল, কিন্তু বহু instance-এর quantile পরে যোগ করা যায় না।

{quantile="0.99"}

Prometheus-এর bucket cumulative: le="0.1" মানে ০.১ সেকেন্ড বা তার কম সময়ের সব অনুরোধ, আগের bucket-গুলোসহ। le এসেছে “less than or equal” থেকে। নিচের ছোট প্রোগ্রাম ১০টি latency থেকে পাতাটি বানায়। মানগুলো বদলে চালিয়ে দেখুন।

# Turn raw latencies into Prometheus text format, by hand.
latencies = [0.031, 0.044, 0.052, 0.068, 0.073, 0.095, 0.121, 0.140, 0.260, 0.710]
bounds = [0.05, 0.1, 0.25, 0.5, float("inf")]

print("# TYPE shop_latency_seconds histogram")
for upper in bounds:
    count = sum(1 for v in latencies if v <= upper)
    le = "+Inf" if upper == float("inf") else upper
    print(f'shop_latency_seconds_bucket{{le="{le}"}} {count}')
print(f"shop_latency_seconds_sum {sum(latencies):.3f}")
print(f"shop_latency_seconds_count {len(latencies)}")

average = sum(latencies) / len(latencies)
print(f"\n# average = _sum / _count = {average:.4f} s")
ones = [sum(1 for v in latencies if v <= b) for b in bounds]
each = [b - a for a, b in zip([0] + ones, ones)]
print("# non-cumulative counts per bucket:", each)

আউটপুটে le="0.05" 2, le="0.1" 6, le="0.25" 8, le="0.5" 9 আর le="+Inf" 10. সংখ্যা শুধু বাড়ছে, কারণ bucket cumulative. শেষের লাইনে [2, 4, 2, 1, 1] হলো প্রতিটি খোপের নিজের সংখ্যা। _sum / _count থেকে গড় আসে 0.1594 s, অথচ ১০টির মধ্যে ৮টি অনুরোধ ০.১৪ সেকেন্ড বা তার কম। একটি ০.৭১ সেকেন্ডের অনুরোধ গড়কে টেনে তুলেছে, তাই latency-তে একা গড় দেখা বিপজ্জনক।

প্রশ্নHistogramSummary
Quantile কোথায় হিসাব হয়Prometheus-এ, query-র সময়App-এ, মাপার সময়
বহু instance মেলানোহ্যাঁ, bucket যোগ করা যায়না, quantile যোগ করা অর্থহীন
নির্ভুলতাBucket-এর প্রস্থের ওপর নির্ভর করেনির্দিষ্ট ত্রুটি-সীমার মধ্যে
Query-র সময় নতুন quantileযেকোনো quantileApp-এ আগে থেকে ঠিক করা
সাধারণ পরামর্শService latency-র জন্য প্রথম পছন্দএকটিমাত্র instance-এর সূক্ষ্ম মাপে

Summary-র একটি বাস্তব পার্থক্য: বাস্তব client library সাধারণত সাম্প্রতিক কিছু সময়ের জানালায় quantile হিসাব করে। আমাদের ছোট shop.py সব মাপ রেখে দেয়, যা production-এ করা যায় না, কারণ memory ফুরিয়ে যাবে। এটি শুধু তুলনার জন্য সত্য মান রাখছে।

৪ · PromQL: rate, increase আর sum by

PromQL-এর ফলাফল বেশিরভাগ সময় একটি instant vector: এক মুহূর্তে প্রতিটি series-এর একটি মান। [1m] লিখলে পাওয়া যায় range vector: গত ১ মিনিটের সব sample. rate() range vector নেয় আর প্রতি সেকেন্ডের গড় বৃদ্ধি দেয়। increase() দেয় পুরো জানালায় মোট বৃদ্ধি। Prometheus-এর HTTP API-তে আসল উত্তর এমন:

$ curl -sG 127.0.0.1:18631/api/v1/query \
    --data-urlencode 'query=rate(shop_requests_total[1m])'
{status="200"}  13.948020022445089
{status="500"}  0.3028305751999572

$ … 'query=increase(shop_requests_total[1m])'
{status="200"}  836.8812013467053
{status="500"}  18.169834511997433

$ … 'query=sum(rate(shop_requests_total[1m]))'
{}  14.250850597645046

$ … 'query=rate(shop_requests_total{status="200"}[5s])'
(ফল নেই)

$ … 'query=rate(shop_in_flight)'
{"status":"error","errorType":"bad_data","error":"invalid parameter
\"query\": 1:6: parse error: expected type range vector in call to
function \"rate\", got instant vector"}

JSON-কে পড়ার সুবিধার জন্য ছোট করে দেখানো হয়েছে। increase ভগ্নাংশ দিল, যদিও অনুরোধ পূর্ণসংখ্যা। কারণ এটি আসলে rate গুণ ৬০, আর rate জানালার প্রান্ত পর্যন্ত অনুমান করে (extrapolation). [5s] ফাঁকা ফল দিল, কারণ ৫ সেকেন্ডের জানালায় একটিই sample পড়ে, আর হার বের করতে অন্তত দুটি লাগে। প্রচলিত নিয়ম: জানালা অন্তত scrape interval-এর ৪ গুণ রাখুন।

এবার একটি পূর্ণ query ধাপে ধাপে দেখি। কোন অংশ আগে চলে, সেটি বোঝাই PromQL বোঝার চাবি।

sum by (status) ( rate( shop_requests_total [1m] ) )
Parseভেতর থেকে বাইরে চলবে
sum by (status)
 └─ rate()
     └─ shop_requests_total[1m]
Series খোঁজাIndex থেকে ২টি পাওয়া গেল
{instance="shop:8000",job="shop",route="/order",status="200"}
{instance="shop:8000",job="shop",route="/order",status="500"}
Sample পড়াপ্রতিটি series-এর গত ৬০ সেকেন্ড
status="200"  প্রায় 12টি sample (৫ সেকেন্ড পরপর)
status="500"  প্রায় 12টি sample
rate()প্রতি series-এ আলাদাভাবে
{…,status="200"}  13.957852758337568
{…,status="500"}  0.27368338741838366
sum by (status)অন্য label ঝরে গেল
{status="200"}  13.957852758337568
{status="500"}  0.27368338741838366
# instance, job, route আর ফলাফলে নেই
HTTP উত্তরআসল JSON
{"status":"success","data":{"resultType":"vector","result":[
 {"metric":{"status":"200"},"value":[…,"13.957852758337568"]},
 {"metric":{"status":"500"},"value":[…,"0.27368338741838366"]}]}}
  1. ধাপ ১: Parse. Query একটি গাছে ভাঙে। সবচেয়ে ভেতরের অংশ আগে চলে, বাইরের sum সবার শেষে।
  2. ধাপ ২: Series বাছাই। Metric-এর নাম ধরে index থেকে series পাওয়া গেল। এখানে দুটি, status ভিন্ন। Grafana-তে ধীর query-র একটি বড় কারণ এই ধাপে অসংখ্য series মিলে যাওয়া।
  3. ধাপ ৩: জানালা পড়া। [1m] প্রতিটি series-এর গত ৬০ সেকেন্ডের sample আনল। ৫ সেকেন্ড পরপর scrape, তাই প্রায় ১২টি।
  4. ধাপ ৪: rate. প্রতিটি series-এ আলাদা করে reset সামলে হার বের হলো: সফল প্রায় ১৩.৯৬, আর ব্যর্থ প্রায় ০.২৭ প্রতি সেকেন্ডে। এটি ঘটে যোগের আগে।
  5. ধাপ ৫: sum by. একই status-এর series যোগ হলো, আর বাকি label বাদ গেল। এখানে একটিই instance, তাই সংখ্যা বদলায়নি। তিনটি replica থাকলে তিনটির হার যোগ হতো।
  6. ধাপ ৬: উত্তর। Prometheus JSON ফেরত দিল, আর মানগুলো string আকারে আসে। Grafana এই উত্তরই আঁকে। সময় দেওয়া থাকলে একই হিসাব প্রতিটি বিন্দুতে হয়, যাকে range query বলে।
মূল নিয়ম

সবসময় আগে rate, তারপর sum. উল্টোটি, rate(sum(x)[1m:]), ভুল: একটি replica restart হলে যোগফল সামান্য কমে, আর Prometheus সেটিকে পুরো counter-এর reset ভেবে নকল spike দেখায়।

৫ · Counter reset: কেন rate ভুল করে না

পুরোনো বিদ্যুতের মিটার নষ্ট হলে নতুন মিটার বসে, আর রিডিং শুরু হয় 0 থেকে। শুধু বিয়োগ করলে মাসের বিল ঋণাত্মক আসবে। অফিস তখন পুরোনো মিটারের শেষ রিডিংটিও হিসাবে ধরে। App restart হলেও memory-র counter 0-তে ফেরে, যাকে বলে counter reset. Prometheus-এর নিয়মও একই: আগের মানের চেয়ে নতুন মান ছোট হলে সেটিকে reset ধরা হয়, আর আগের মানটি হিসাবে যোগ করা হয়।

আমরা চলন্ত দোকানটিকে docker restart করেছিলাম। সেই জানালার আসল sample:

$ … 'query=shop_requests_total{status="200"}[1m]'
সময় (জানালা খোলার পর, s)   মান
 1.278    675
 6.277    744
11.278    814
16.277    884     <- restart এখানে
37.145    195
42.145    267
47.146    338
52.152    410
57.152    482

rate(...[1m])       12.367111715645917
increase(...[1m])   742.026702938755
delta(...[1m])      -207.25203135626586
resets(...[1m])     1

delta gauge-এর জন্য তৈরি, তাই reset বোঝে না আর ঋণাত্মক ফল দিল। resets বলল একবার reset হয়েছে। rate ঠিক আছে। মাঝের ২১ সেকেন্ডের ফাঁকে যা ঘটেছে তা হারিয়ে গেছে, তাই rate-ও অনুমান। ৮৮৪ পড়ার পরের কিছু অনুরোধ আর restart-এর মুহূর্ত কোনো metric-এ ধরা পড়েনি।

নিচের সিমুলেটরে Prometheus-এর হিসাবটি লেখা আছে, ঠিক এই sample দিয়ে। ফল উপরের আসল উত্তরের সাথে মিলিয়ে দেখুন। তারপর একটি মান বদলে দেখুন, যেমন শেষ দিকে আরেকটি reset বসিয়ে।

// rate() and increase() the Prometheus way, on samples from a real run.
// Each pair: [seconds after the window opened, counter value].
// The app restarted between 884 and 195. Try editing the numbers.
const RANGE = 60;
const samples = [[1.278, 675], [6.277, 744], [11.278, 814], [16.277, 884],
  [37.145, 195], [42.145, 267], [47.146, 338], [52.152, 410], [57.152, 482]];

function evaluate(s, range) {
  const first = s[0], last = s[s.length - 1];
  const naive = last[1] - first[1];
  let growth = naive;
  for (let i = 1; i < s.length; i++) {
    if (s[i][1] < s[i - 1][1]) {
      growth += s[i - 1][1];
      console.log(`reset: ${s[i - 1][1]} -> ${s[i][1]}, add back ${s[i - 1][1]}`);
    }
  }
  const covered = last[0] - first[0];
  const avgGap = covered / (s.length - 1);
  let toStart = first[0], toEnd = range - last[0];
  if (toStart >= avgGap * 1.1) toStart = avgGap / 2;
  if (toEnd >= avgGap * 1.1) toEnd = avgGap / 2;
  // A counter cannot go below zero, so do not extrapolate past that point.
  if (growth > 0) toStart = Math.min(toStart, covered * first[1] / growth);
  const factor = (covered + toStart + toEnd) / covered;
  return { naive, growth, factor, increase: growth * factor };
}

const r = evaluate(samples, RANGE);
console.log("last - first (wrong):", r.naive);
console.log("growth after reset fix:", r.growth);
console.log("extrapolation factor:", r.factor.toFixed(5));
console.log("increase[1m]:", r.increase.toFixed(3));
console.log("rate[1m]:", (r.increase / RANGE).toFixed(3), "per second");

আউটপুট: reset: 884 -> 195, add back 884, তারপর last - first (wrong): -193, growth after reset fix: 691, extrapolation factor: 1.07384, increase[1m]: 742.027 আর rate[1m]: 12.367 per second. হিসাব: ৪৮২ − ৬৭৫ = −১৯৩, তাতে ৮৮৪ যোগ করে ৬৯১. Sample-গুলো জানালার ৫৫.৮৭৪ সেকেন্ড ঢেকেছে, তাই ৬০ ÷ ৫৫.৮৭৪ = ১.০৭৩৮৪ গুণ করে প্রান্ত পর্যন্ত টানা হলো। ফল ৭৪২.০২৭, যা Prometheus-এর আসল increase-এর সাথে মিলে গেছে।

৬ · Histogram থেকে quantile, আর bucket-এর ভুল

Percentile হিসাব করতে সব মাপ লাগে, কিন্তু histogram-এ আছে শুধু খোপের সংখ্যা। histogram_quantile() তাই খোপের ভেতরে মানগুলো সমানভাবে ছড়ানো ধরে নেয়। এই অনুমানের নাম linear interpolation. সূত্রটি:

quantile ≈ lower + (upper − lower) × (rank − below) ÷ in_bucket
rank = q × মোট, below = নিচের খোপগুলোর মোট, in_bucket = এই খোপের সংখ্যা

দোকানের ৩,২২৬টি অনুরোধ নিয়ে হিসাব করি। মোটা histogram বলছে le="0.1"-এ ৩,০৫১ আর le="0.5"-এ ৩,২২৬. p99-এর rank = ০.৯৯ × ৩,২২৬ = ৩,১৯৩.৭৪. এটি (০.১, ০.৫] খোপে পড়ে। তাই ০.১ + ০.৪ × (৩,১৯৩.৭৪ − ৩,০৫১) ÷ ১৭৫ = ০.৪২৬৩ সেকেন্ড. Prometheus কী বলল, দেখুন:

$ … 'query=histogram_quantile(0.99, sum by (le) (shop_latency_coarse_seconds_bucket))'
{}  0.4262628571428567     # 3 bucket

$ … 'query=histogram_quantile(0.99, sum by (le) (shop_latency_fine_seconds_bucket))'
{}  0.16065853658536558    # 8 bucket

$ … 'query=shop_latency_true_seconds'
{quantile="0.5"}   0.051
{quantile="0.95"}  0.1149
{quantile="0.99"}  0.1516   # সব মাপ সাজিয়ে পাওয়া সত্য মান

$ curl -s 127.0.0.1:18632/metrics | grep -E 'shop_latency_fine'
…
shop_latency_fine_seconds_bucket{le="0.2"} 3258
…
shop_latency_fine_seconds_count 3258

শেষের দুই লাইন সবচেয়ে কড়া প্রমাণ: সব অনুরোধ ০.২ সেকেন্ডের মধ্যে শেষ। অথচ মোটা histogram p99 বলছে ৪২৬ ms! সত্য মানের চেয়ে প্রায় ১৮১% বেশি। আসলে ১১০–১৬০ ms-এর একগুচ্ছ অনুরোধ ০.১–০.৫ খোপের নিচের দিকে জড়ো, কিন্তু সূত্রটি ধরে নেয় তারা পুরো খোপে ছড়ানো। সরু bucket-এ একই হিসাবে এলো ১৬১ ms, ভুল প্রায় ৬%. আপনার SLO ৩০০ ms হলে মোটা histogram দেখে আপনি ভুল করে ভাববেন SLO ভেঙেছে।

Quantileসত্য মান (ms)৮ bucket (ms)৩ bucket (ms)
p50৫১.০৫১.৮৫২.৯
p95১১৪.৯১০৫.১১৩১.৩
p99১৫১.৬১৬০.৭৪২৬.৩

নিচের সিমুলেটরে একই আকারের ২০,০০০টি মাপ তৈরি হয়, নির্দিষ্ট seed দিয়ে। BOUNDS_LIST বদলে দেখুন কোথায় bucket বসালে p99 ঠিক আসে। TAIL_FROM আর TAIL_TO বদলে দেখুন লেজ সরে গেলে কী হয়।

// Histogram quantile by linear interpolation vs the true percentile.
const BOUNDS_LIST = [[0.1, 0.5, 1], [0.025, 0.05, 0.1, 0.15, 0.2, 0.3, 0.5, 1]];
const Q = 0.99, N = 20000, TAIL_SHARE = 0.05, TAIL_FROM = 0.11, TAIL_TO = 0.16;

function mulberry32(a) {
  return () => { a |= 0; a = a + 0x6D2B79F5 | 0;
    let t = Math.imul(a ^ a >>> 15, 1 | a);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296; };
}
const rnd = mulberry32(42), values = [];
for (let i = 0; i < N; i++) {
  const [lo, hi] = rnd() < TAIL_SHARE ? [TAIL_FROM, TAIL_TO] : [0.02, 0.08];
  values.push(lo + (hi - lo) * rnd());
}
values.sort((a, b) => a - b);
const truth = values[Math.ceil(Q * N) - 1];

function quantile(bounds) {
  const rank = Q * N; let below = 0, lower = 0;
  for (const upper of bounds) {
    const count = values.filter(v => v <= upper).length;
    if (count >= rank) {
      const inBucket = count - below;
      return { lower, upper, inBucket, est: lower + (upper - lower) * (rank - below) / inBucket };
    }
    below = count; lower = upper;
  }
  return { lower, upper: Infinity, inBucket: N - below, est: lower };
}
console.log("true value:", (truth * 1000).toFixed(1), "ms");
for (const b of BOUNDS_LIST) {
  const r = quantile(b);
  console.log(`${b.length} buckets: (${r.lower}, ${r.upper}] holds ${r.inBucket}, estimate ${(r.est * 1000).toFixed(1)} ms, error ${((r.est / truth - 1) * 100).toFixed(1)}%`);
}

আউটপুট: true value: 149.9 ms, 3 buckets: (0.1, 0.5] holds 989, estimate 419.1 ms, error 179.6% আর 8 buckets: (0.1, 0.15] holds 792, estimate 149.8 ms, error -0.1%. এখানে সরু bucket-এর ভুল প্রায় শূন্য, কারণ লেজটি খোপের ভেতরে সত্যিই সমানভাবে ছড়ানো। আসল run-এ ভুল ছিল ৬%, কারণ বাস্তব মাপ অত পরিপাটি নয়। Q বদলে ০.৫ করলে দেখবেন মাঝের quantile-এ দুই histogram-ই প্রায় ঠিক থাকে, কারণ ভুলটি মূলত লেজে।

Bucket বাছার নিয়ম

আপনার SLO-র সীমাটি bucket-এর একটি প্রান্ত হোক। ৩০০ ms SLO হলে le="0.3" রাখুন। তাহলে “কত ভাগ অনুরোধ ৩০০ ms-এর মধ্যে” প্রশ্নের উত্তর সরাসরি আসে, কোনো অনুমান ছাড়াই। আর quantile যদি শেষ সসীম bucket পেরিয়ে +Inf-এ পড়ে, Prometheus সেই সীমাটিই ফেরত দেয়, তাই আসল latency আরও খারাপ হলেও গ্রাফে ধরা পড়ে না।

৭ · কী মাপবেন: RED, USE, golden signal আর recording rule

Metric বানানো সহজ, কিন্তু কোনগুলো দেখবেন? তিনটি পরিচিত কাঠামো আছে। রেস্তোরাঁর কথা ভাবুন। RED খদ্দেরের চোখে দেখে: কতজন এলেন, কতজন ভুল খাবার পেলেন, কতক্ষণ বসে থাকলেন। USE রান্নাঘরের চোখে দেখে: চুলা কতটা ব্যস্ত, কত হাঁড়ি অপেক্ষায়, কোনো চুলা নষ্ট কি না।

কাঠামোকী নিয়েযা মাপেউৎস
REDService, অনুরোধ-চালিতRate, Errors, DurationTom Wilkie (Weaveworks-এ থাকাকালীন)
USEResource: CPU, disk, memory, networkUtilization, Saturation, ErrorsBrendan Gregg
Golden signalsUser-মুখী সিস্টেমLatency, traffic, errors, saturationGoogle-এর Site Reliability Engineering (২০১৬)

দোকানের RED, একই মুহূর্তের আসল query থেকে:

# Rate: requests per second
sum(rate(shop_requests_total[1m]))
{}  14.250850597645046

# Errors: share of requests that failed
sum(rate(shop_requests_total{status=~"5.."}[1m])) / sum(rate(shop_requests_total[1m]))
{}  0.021249999999999998

# Duration: p95 over the last minute
histogram_quantile(0.95, sum by (le) (rate(shop_latency_fine_seconds_bucket[1m])))
{}  0.11060606060606068

সেকেন্ডে ১৪.২৫ অনুরোধ, ২.১২% ব্যর্থ, আর p95 প্রায় ১১১ ms. তিনটি সংখ্যায় service-এর অবস্থা বলা গেল। Error-এর ক্ষেত্রে হারের চেয়ে অনুপাত বেশি কাজের, কারণ ভিড় বাড়লে শুধু ব্যর্থ অনুরোধের হারও বাড়ে। এখানে rate(bucket) ব্যবহার করে শুধু গত এক মিনিট দেখা হলো। অংশ ৬-এর query সব সময়ের জমা সংখ্যা দেখছিল, যা সাম্প্রতিক সমস্যা ঢেকে দিতে পারে।

USE-এর metric আসে OS থেকে, app থেকে নয়। Linux server-এ এই কাজ করে node_exporter, একটি exporter. PostgreSQL, Redis, Nginx-এর জন্যও এমন exporter আছে।

# CPU utilization (share of time not idle), per server
1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))

# CPU saturation: run queue compared with the core count
node_load1 / count without (cpu, mode) (node_cpu_seconds_total{mode="idle"})

# Disk utilization: share of time the device was busy
rate(node_disk_io_time_seconds_total[5m])

# Network errors per second
rate(node_network_receive_errs_total[5m])

এই লাইনগুলো node_exporter-এর প্রচলিত metric নাম দিয়ে লেখা, কিন্তু এই পর্বের run-এ চালানো হয়নি। Version-ভেদে নাম মিলিয়ে নেবেন। Saturation আর utilization আলাদা: CPU ১০০% ব্যস্ত থাকা মানেই সমস্যা নয়, কিন্তু run queue লম্বা হওয়া মানে কাজ অপেক্ষায় আছে। পর্ব ৪৮-এর CPU limit-ও এখানে কাজে লাগে।

Recording rule: দামি হিসাব আগে করে রাখা

একটি dashboard ২০ জন খুলে রাখলে একই ভারী query বারবার চলে। Recording rule সেই query নির্দিষ্ট সময় পরপর চালায়, আর ফলকে নতুন series হিসেবে জমায়। রান্নাঘরে আগে থেকে বাটা মসলা রাখার মতো: দরকারের সময় শুধু তুলে নিলেই হয়। নামের প্রচলিত রীতি level:metric:operations, যেমন job:shop_requests:rate1m.

$ docker exec abcl-mp-prom promtool check rules /etc/prometheus/rules.yml
Checking /etc/prometheus/rules.yml
  SUCCESS: 2 rules found

$ … 'query=job:shop_requests:rate1m'
{job="shop"}   14.2167119698066
$ … 'query=job:shop_errors:ratio1m'
{job="shop"}   0.017881705639614855

# /api/v1/rules, shortened
group shop_red: interval 5, evaluationTime 0.0003613

দুটি rule মিলিয়ে ০.৩৬ ms লেগেছে, কারণ এখানে series মাত্র কয়েকটি। মান সরাসরি query-র ১৪.২৫-এর সাথে হুবহু মেলে না, কারণ rule সর্বশেষ evaluation-এর মান রাখে, যা কয়েক সেকেন্ড পুরোনো হতে পারে। মনে রাখবেন, rule-এর জমা ফলও নতুন series, তাই সেগুলোরও খরচ আছে। Alert rule একই ইঞ্জিনে চলে, আর শর্ত সত্য হলে alert যায় Alertmanager-এ। Burn rate আর ভালো alert-এর নকশা পর্ব ৬৬-তে।

৮ · Cardinality: একটি label-এ series-এর বিস্ফোরণ

Cardinality মানে আলাদা series-এর সংখ্যা। Series-এর সংখ্যা হলো প্রতিটি label-এর মানের সংখ্যার গুণফল। দোকানি প্রতিটি পণ্যের জন্য একটি হিসাবের খাতা রাখতে পারেন। কিন্তু প্রতিটি ক্রেতা আর প্রতিটি পণ্যের জোড়ার জন্য আলাদা খাতা রাখলে ঘরে আর জায়গা থাকবে না। আমরা দোকানে সেই ভুলটিই করলাম, ১,০০,০০০ ব্যবহারকারীর জন্য।

Prometheus-এর আসল সংখ্যা
shop_* series
head block-এ মোট series
/metrics পাতা (byte)
Scrape সময় (ms)
Prometheus RSS (MB)
  1. ধাপ ১: ভুলের আগে। দোকানের ২৫টি series, মোট ৬৮৪টি (Prometheus নিজের metric-সহ)। পাতা ১.৪ KB, scrape ৩ ms, আর memory ৯৮.৫ MB.
  2. ধাপ ২: user_id জুড়ল, ২৬ সেকেন্ড পরে। shop_orders_by_user_total{user_id="u0"} থেকে শুরু করে ১,০০,০০০টি নতুন series. পাতা হলো ৪.৬ MB, প্রায় ৩,২০০ গুণ। প্রতিটি series-এ এখনো মাত্র একটি মান, 1, অথচ খরচ পুরোটাই দিতে হচ্ছে।
  3. ধাপ ৩: আরও ৪০ সেকেন্ড পরে। Series আর বাড়েনি, কিন্তু memory ৩২৫.৮ MB-তে উঠল, তিন গুণের বেশি। Scrape ৯০.৭ ms-এ পৌঁছাল, আর শুধু এই পাতাটি প্রতি ৫ সেকেন্ডে ৪.৬ MB টানছে।
  4. ধাপ ৪: Label তুলে নেওয়া হলো। Query-তে series ২৫-এ ফিরল, কারণ সেগুলো stale. কিন্তু head block-এ এখনো ১,০০,৭০৭, আর memory কমেনি, বরং ৩৫৪.১ MB. ভুল ঠিক করলেই খরচ সাথে সাথে ফেরে না।
  5. ধাপ ৫: শিক্ষা। Head block থেকে পুরোনো series সরে পরের compaction-এ, সাধারণত কয়েক ঘণ্টার ব্যবধানে। আর আগের block-এর data retention শেষ না হওয়া পর্যন্ত disk-এ থাকে। তাই এই ভুল merge-এর আগেই ধরা সবচেয়ে সস্তা।

এই ছোট run-এ অতিরিক্ত memory ভাগ করলে series প্রতি প্রায় ২.৩ KB পড়ে: (৩২৫.৮ − ৯৮.৫) MB ÷ ১,০০,০২৩. এটি একটি ছোট পরীক্ষার মাপ, নিয়ম নয়। Label বেশি হলে বা series-এর ঘন পরিবর্তন (churn) হলে খরচ আরও বাড়ে। এবার গুণের হিসাবটি নিজে করুন:

// Series count is the product of label value counts. Change the numbers.
const BASE = { route: 20, method: 3, status: 5, instance: 50 };
const BUCKETS = 10;               // histogram: buckets + +Inf + _sum + _count
const KB_PER_SERIES = 3;          // assumed; one small run measured about 2.3
const ADD = { user_id: 100000 };  // the label someone wants to add

const product = o => Object.values(o).reduce((a, b) => a * b, 1);
const show = (name, n) => {
  const gb = n * KB_PER_SERIES * 1000 / 1e9;
  console.log(name.padEnd(26), String(n).padStart(16), "series", gb.toFixed(2).padStart(12), "GB");
};
const seriesPerHistogram = BUCKETS + 3;
const counter = product(BASE);
show("counter", counter);
show("histogram", counter * seriesPerHistogram);
show("counter + user_id", counter * product(ADD));
show("histogram + user_id", counter * seriesPerHistogram * product(ADD));

// Worst case assumes every combination occurs. Real traffic fills a share.
const SEEN = 0.02;
show("hist + user_id, 2% seen", Math.round(counter * seriesPerHistogram * product(ADD) * SEEN));

আউটপুটে counter ১৫,০০০ series, histogram ১,৯৫,০০০ series আর প্রায় 0.58 GB. user_id জুড়লে worst case-এ histogram দাঁড়ায় 19500000000 series, মানে 58500.00 GB memory. বাস্তবে সব সমন্বয় ঘটে না, তাই ২% ধরে শেষ লাইনে 390000000 series আর 1170.00 GB. ২% সংখ্যাটি একটি অনুমান, আর তবুও এটি কোনো একক Prometheus-এর সামর্থ্যের অনেক বাইরে।

Label-এর নিয়ম

Label-এর মান হবে সীমিত আর আগে থেকে জানা। user_id, request_id, email, IP, session আর ID-সহ পূর্ণ URL কখনো label নয়। /users/8812/orders না লিখে route-এর ছাঁচ /users/:id/orders লিখুন। প্রতিটি ব্যবহারকারীর গল্প দরকার হলে সেটি log (পর্ব ৬৪) বা trace (পর্ব ৬৫)-এর কাজ। এছাড়া /api/v1/status/tsdb দেখায় কোন metric আর কোন label সবচেয়ে বেশি series খাচ্ছে। এই run-এ প্রথম সারিতে ছিল shop_orders_by_user_total: 100000.

আরও গভীরে: Prometheus-এর TSDB-র compression-এর ধারণা এসেছে Facebook-এর Gorilla পেপার (VLDB, ২০১৫) থেকে। সেখানে timestamp আর মানের শুধু পরপর পার্থক্যটুকু রেখে ডেটা ছোট করা হয়। Prometheus-এর document অনুযায়ী এতে sample প্রতি সাধারণত ১–২ byte লাগে, তাই লম্বা ইতিহাসের চেয়ে active series-এর সংখ্যাই memory-র আসল চালক। Native histogram bucket হাতে বাছার ঝামেলা কমায় আর একটি series-এই সব রাখে। এটি কোন version-এ কোন flag-এ চলে তা বদলাচ্ছে, তাই বর্তমান document দেখে নেবেন।

৯ · দীর্ঘমেয়াদি storage, Grafana আর ভালো dashboard

একটি Prometheus একটি server-এই থাকে, আর ডিফল্ট retention ১৫ দিন। বছরের পর বছরের ইতিহাস, বহু cluster-এর একসাথে ছবি বা server নষ্ট হলে data রক্ষা, এসব এর একার কাজ নয়। Disk-এর আন্দাজ করার সূত্রটি সহজ:

Disk ≈ retention (s) × প্রতি সেকেন্ডে sample × প্রতি sample-এ byte

ধরুন ১,০০,০০০ active series, scrape interval ১৫ সেকেন্ড, আর sample প্রতি ১.৫ byte. প্রতি সেকেন্ডে ১,০০,০০০ ÷ ১৫ ≈ ৬,৬৬৭ sample. দিনে ৮৬,৪০০ ÷ ১৫ = ৫,৭৬০টি scrape, মানে ৫৭.৬ কোটি sample. Disk দিনে প্রায় ৮৬৪ MB, আর ১৫ দিনে প্রায় ১৩ GB. Managed সেবায় দাম সাধারণত sample সংখ্যায় ধরা হয়, তাই scrape interval ১৫ থেকে ৬০ সেকেন্ড করলে sample চার ভাগের এক ভাগ হয়।

পথমূল ধারণাকখন ভাববেন
Thanosপ্রতিটি Prometheus-এর পাশে sidecar, block যায় object storage-এ, ওপরে একটি query স্তরPrometheus রাখতে চান, শুধু দীর্ঘ ইতিহাস আর global view দরকার
Grafana MimirPrometheus remote write করে, একটি বড় clustered সিস্টেম গ্রহণ করেবহু দল, tenant আর বিশাল series-এর জন্য কেন্দ্রীয় সেবা চান
Cloud-এর সেবাAmazon Managed Service for Prometheus, Azure Monitor managed service for Prometheus, Google Cloud Managed Service for Prometheusনিজে চালাতে চান না, আর sample-ভিত্তিক বিল মেনে নিতে পারেন

এই সেবাগুলোর নাম, সীমা আর দাম বদলায়, তাই provider-এর পাতায় মিলিয়ে নেবেন। প্রায় সব দীর্ঘমেয়াদি সিস্টেমে downsampling থাকে বা থাকতে পারে: পুরোনো data ৫ মিনিট বা ১ ঘণ্টার সারাংশে রাখা, যাতে বছরের গ্রাফ দ্রুত খোলে। মনে রাখবেন, downsampling ক্ষণস্থায়ী spike মুছে দিতে পারে।

Grafana: ছবি আঁকে, data রাখে না

Grafana নিজে metric জমায় না। সে Prometheus-কে PromQL পাঠায়, উত্তর এনে panel আঁকে। Prometheus-এর নিজের UI দ্রুত query পরীক্ষার জন্য ভালো, আর Grafana দলের স্থায়ী dashboard-এর জন্য। Dashboard JSON-কে Git-এ রাখা ভালো অভ্যাস, ঠিক যেমন পর্ব ৫১-এ infrastructure-কে রাখা হয়। Grafana-র লাইসেন্স ২০২১ সালে AGPLv3 হয়েছে, তাই নিজের পণ্যের ভেতরে জুড়তে চাইলে শর্ত পড়ে নেবেন।

ভালো dashboard-এর নকশা

  • ওপরে উত্তর, নিচে কারণ: প্রথম সারিতে RED আর SLO. নিচে USE, তারপর dependency. রাত দুইটায় ৩০ সেকেন্ডে বোঝা চাই সমস্যা আছে কি না।
  • একটি প্যানেল, একটি প্রশ্ন: শিরোনামে প্রশ্নটি লিখুন, যেমন “কত ভাগ অনুরোধ ব্যর্থ”. একই গ্রাফে ২০টি রেখা কেউ পড়ে না।
  • গড় নয়, percentile: p50, p95, p99 পাশাপাশি। আর percentile-এর গড় কখনো নয়।
  • একক আর সীমারেখা: ms, req/s, % স্পষ্ট লিখুন। SLO-র সীমাটি রেখা হিসেবে আঁকুন।
  • Deployment চিহ্ন: Release-এর সময় annotation বসান, যাতে “কখন থেকে” প্রশ্নের উত্তর মেলে (পর্ব ৫৯)।
  • জানালা মিলিয়ে নিন: rate(x[1m]) ৫ মিনিটের গ্রাফে আর ৩০ দিনের গ্রাফে আলাদা ছবি দেয়। Grafana-র $__rate_interval এই সমস্যা কমায়।
  • Log আর trace-এ লাফ: Exemplar বা link দিয়ে গ্রাফ থেকে সরাসরি trace-এ যাওয়ার পথ রাখুন।
  • মুছে ফেলার সাহস: তিন মাসে কেউ না খোলা dashboard মুছুন। কম dashboard মানে কম fatigue.

১০ · AI-এর যুগে metric যাচাই

AI খুব দ্রুত PromQL আর prometheus.yml লিখে দেয়। কিন্তু ভুল query-ও একটি সুন্দর গ্রাফ আঁকে, তাই ভুল সহজে চোখে পড়ে না। Metric-এ AI-এর পরিচিত ভুল:

AI যা লেখেসমস্যাযাচাই
user_id, পূর্ণ URL label-এCardinality বিস্ফোরণ, বিল বাড়েসম্ভাব্য মানের সংখ্যা গুনুন
rate(sum(x)[5m:])নকল spikeআগে rate, পরে sum
Gauge-এ rateঅর্থহীন সংখ্যাMetric-এর # TYPE দেখুন
Quantile-এর avgগাণিতিকভাবে ভুলBucket যোগ করে histogram_quantile
নেই এমন metric নাম বা functionফাঁকা গ্রাফ বা errorনিজের /metrics আর docs মিলিয়ে দেখুন
9090 port 0.0.0.0/0-তে খোলাMetric-এ ভেতরের তথ্য ফাঁস হয়Private network, auth আর পর্ব ২৭-এর নিয়ম
prom/prometheus:latestনিয়ন্ত্রণ ছাড়া version বদলনির্দিষ্ট version লিখুন
Rule বা flag হাতে বদলের পরামর্শConfig ভেঙে Prometheus ওঠে নাআগে promtool check config

AI-কে কাজ দেওয়ার সময় metric-এর আসল ধরন আর label জানিয়ে দিন, আর হাতে মেলানোর মতো ব্যাখ্যা চান:

আমার service থেকে এই metric পাই: [এখানে /metrics-এর ১০–১৫ লাইন পেস্ট করব। কোনো secret, ব্যবহারকারীর তথ্য বা আসল ঠিকানা থাকবে না] scrape interval 15s. Prometheus version লিখে দিচ্ছি: [version] তুমি আমাকে RED method-এর তিনটি PromQL query লিখে দাও। প্রতিটি query-র জন্য: ১. কোন লাইনে কী হচ্ছে সহজ বাংলায় বলো। ২. কোন label-এ যোগ হচ্ছে আর কেন বলো। ৩. শুধু আমার দেওয়া metric নাম ব্যবহার করো। নাম না থাকলে অনুমান না করে আমাকে জিজ্ঞেস করো। ৪. একটি উদাহরণ সংখ্যা দিয়ে হাতে হিসাব দেখাও, যাতে আমি মিলিয়ে দেখতে পারি। ৫. নতুন label বা recording rule প্রস্তাব করলে series কত বাড়বে তা হিসাব করে দেখাও।

AI-এর লেখা metric-কাজ হাতে পেলে

পরীক্ষা ও ইন্টারভিউতে:

  • Counter আর gauge-এর পার্থক্য? Counter শুধু বাড়ে আর reset হয়, gauge ওঠে-নামে।
  • rate আর irate-এর পার্থক্য? rate পুরো জানালার গড়, irate শেষ দুটি sample-এর হার।
  • Histogram না summary? বহু instance মেলাতে হলে histogram, কারণ bucket যোগ করা যায়।
  • Pull-এর সবচেয়ে বড় সুবিধা? Target না পেলে up 0 হয়, তাই মৃত্যুও একটি সংকেত।
  • RED আর USE-এর পার্থক্য? RED service-এর চোখে, USE resource-এর চোখে।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১Prometheus কেন pull বেছে নিল, যখন প্রায় সব পুরোনো সিস্টেম push করত? আজ এই সিদ্ধান্ত কতটা টেকে?

Pull-এর পেছনে তিনটি বাস্তব যুক্তি ছিল, আর Prometheus-এর নকশা Google-এর Borgmon-এর ধারণা থেকে প্রভাবিত।

  • জীবিত থাকার সংকেত বিনা খরচে: Prometheus জানে কার কাছে যাওয়ার কথা। না পেলে up 0. Push-এ নীরবতা মানে app মৃত, network ভাঙা, না শুধু কোনো অনুরোধ নেই, তা আলাদা করে বলা যায় না।
  • চাপের নিয়ন্ত্রণ: Scrape-এর গতি Prometheus নিজে ঠিক করে। হঠাৎ হাজার instance উঠলেও তারা Prometheus-কে একসাথে ডুবিয়ে দেয় না।
  • সহজ debug: curl দিয়ে যে পাতা আপনি দেখেন, Prometheus-ও ঠিক সেটিই দেখে। আমরা এই পর্বে সেটিই করেছি।

তবে pull-এর সীমাও আছে। Serverless function আর স্বল্পায়ু job scrape-এর আগেই শেষ হতে পারে। NAT বা firewall-এর পেছনের app-এ Prometheus পৌঁছাতে পারে না, আর multi-account পরিবেশে network খোলা কঠিন। Prometheus-এর নিজের document-ই বলে, pull বনাম push খুব বড় ব্যাপার নয়, বেশিরভাগ ক্ষেত্রে দুটোই চলে।

বাস্তব উত্তর তাই মিশ্র। Cluster-এর ভেতরে pull, আর cluster থেকে কেন্দ্রীয় সেবায় remote write-এ push. Prometheus-এর agent mode এই ধারণা থেকেই এসেছে: শুধু scrape আর পাঠানো, query নেই। OpenTelemetry Collector-ও উভয় দিকে কাজ করতে পারে। Serverless function-এর metric সাধারণত provider-এর monitoring সেবায় যায় (পর্ব ৬৯)।

মিটার রিডারের উদাহরণে ফিরি। তিনি কোনো বাসায় গিয়ে দরজা বন্ধ পেলে খাতায় লেখেন “পড়া যায়নি”, আর এটি নিজেই একটি তথ্য। কিন্তু গ্রাহক নিজে রিডিং পাঠানোর নিয়ম হলে, কেউ না পাঠালে অফিস বুঝবে না তিনি ভুলে গেছেন, বাসা ছেড়েছেন, না মিটার নষ্ট। আবার pull-এর একটি দামও আছে: Prometheus-কে সব target-এর ঠিকানা জানতে হয়, আর network অনুমতি আগে থেকে ঠিক থাকতে হয়। Autoscaling-এ নতুন instance এলে discovery সেটি খুঁজে পায়, কিন্তু security group-এ port খোলা না থাকলে scrape ব্যর্থ হয়।

মূল উপলব্ধি: Pull বনাম push ধর্মযুদ্ধ নয়। আসল প্রশ্ন হলো, কে কার ঠিকানা জানে, আর target চুপ হলে সেটি কে ধরবে।

প্র ০২একটি deployment-এ সব pod একসাথে restart হলো। গ্রাফে কোনো গর্ত আসবে কি? rate কতটা সত্য বলবে?

rate reset ধরে নেয়, কিন্তু যা মাপা হয়নি তা ফিরিয়ে আনতে পারে না। ধরুন একটি pod শেষবার scrape হলো ৮৮৪ মানে। এরপর পরের scrape-এর আগে আরও ৫০টি অনুরোধ এলো, তারপর pod মরে গেল। ওই ৫০টির কোনো হিসাব Prometheus-এর কাছে নেই। আমাদের আসল run-এ এই কারণেই reset-এর জানালায় হিসাবটি একটি অনুমান।

Rolling update-এ (পর্ব ৫৯) বিষয়টি আরও মজার। প্রতিটি নতুন pod-এর নাম আলাদা, তাই pod বা instance label-ও নতুন। অর্থাৎ এটি reset নয়, একেবারে নতুন series. পুরোনো series stale হয়, আর নতুনটি শুরু হয় ছোট মান থেকে। নতুন series-এর প্রথম মানের আগে কোনো sample নেই, তাই শুরুর ওই মানটি বৃদ্ধির হিসাবে আসে না। ঘন ঘন deployment মানে series churn, যা cardinality-কেও বাড়ায়, যদিও এক মুহূর্তে active series একই থাকে।

আরেকটি ফাঁদ: কম ট্রাফিকের error counter. ধরুন একটি counter নতুন তৈরি হলো error ঘটার মুহূর্তে, মান 1. আগে কোনো 0 ছিল না, তাই rate সেটি বৃদ্ধি হিসেবে দেখে না, আর আপনার প্রথম error গ্রাফে আসে না। এজন্যই পরামর্শ হলো জানা label-সমন্বয়ের counter শুরুতেই 0 দিয়ে দেখানো। আমাদের shop.py এই কাজ করে না, আপনি চাইলে সেটিই ঠিক করতে পারেন।

Prometheus-এর নতুন সংস্করণে created timestamp নিয়ে কাজ চলছে, যাতে শুরু-বিন্দু ধরা সহজ হয়। এটি version-নির্ভর, তাই document দেখে নেবেন।

টাকার হিসাবের মতো কাজ তাই database-এর লেনদেনের রেকর্ড থেকে করা হয়, কারণ সেখানে প্রতিটি ঘটনা আলাদা করে জমা থাকে। Metric প্রবণতা দেখার হাতিয়ার, যেখানে সামান্য ফাঁক মেনে নেওয়া যায়।

মূল উপলব্ধি: Metric হলো নির্দিষ্ট সময় পরপর তোলা ছবি, ভিডিও নয়। Scrape-এর ফাঁকের ঘটনা, আর restart-এর সাথে হারিয়ে যাওয়া সংখ্যা, হিসাবে থাকে না। তাই খুব নির্ভুল গণনা, যেমন বিল, metric দিয়ে করবেন না।

প্র ০৩“Percentile-এর গড় হয় না”: কেন? আর tail latency নিয়ে এত মাথাব্যথা কেন?

দুটি server ভাবুন। Server A প্রতি সেকেন্ডে ১,০০০ অনুরোধ নেয়, আর p99 ১০ ms. Server B নেয় ১০টি, p99 ১,০০০ ms. দুটির p99-এর গড় ৫০৫ ms, যা কোনো কিছুর সাথেই মেলে না। মোট ১,০১০ অনুরোধের আসল p99 Server A-র কাছাকাছি, কারণ প্রায় সব অনুরোধই সেখানে। Percentile হলো একটি অবস্থান, পরিমাণ নয়। অবস্থানের গড় নেওয়া যায় না, কিন্তু খোপের সংখ্যা যোগ করা যায়। তাই sum by (le) ঠিক, আর avg(p99) ভুল।

Tail কেন গুরুত্বপূর্ণ? Jeffrey Dean আর Luiz André Barroso-র “The Tail at Scale” (Communications of the ACM, ২০১৩) দেখায়, একটি user-অনুরোধ অনেক ছোট service-কে ডাকলে সবচেয়ে ধীরটির জন্যই অপেক্ষা করতে হয়। ধরুন প্রতিটি service-এর ১% অনুরোধ ধীর, আর একটি পাতা ১০০টি service ডাকে। অন্তত একটি ধীর পাওয়ার সম্ভাবনা ১ − ০.৯৯১০০ ≈ ৬৩%. p99 তখন আর “বিরল” থাকে না।

গড়ের আরেকটি দোষ হলো এটি দুটি দল মিশিয়ে দেয়। ক্যাশ-হিট ৫ ms আর ক্যাশ-মিস ৫০০ ms হলে গড় ৫০ ms হতে পারে, যা কোনো একজন ব্যবহারকারীরও অভিজ্ঞতা নয়। আমাদের expo.py-তে এক ০.৭১ সেকেন্ডের অনুরোধ গড়কে ১৫৯ ms-এ তুলেছিল।

আবার percentile-ও পুরো সত্য নয়। p99 বলে না সবচেয়ে খারাপ ১% কতটা খারাপ। সেজন্য সর্বোচ্চ মান, আর SLO-র জন্য “কত ভাগ অনুরোধ সীমার মধ্যে” দেখা ভালো, কারণ ওই অনুপাত bucket থেকে সরাসরি আসে, আর সব instance মিলিয়েও সঠিক থাকে (পর্ব ৬৬)।

Bucket-ভিত্তিক হিসাবের আরও একটি লাভ আছে: নতুন instance যোগ হলেও পুরোনো query-ই চলে, কিছু বদলাতে হয় না। Summary-তে সেই সুবিধা নেই, আর সেখানে প্রতিটি instance আলাদা করেই দেখতে হয়।

মূল উপলব্ধি: যা যোগ করা যায় (সংখ্যা) সেটি জমান, আর যা যোগ করা যায় না (quantile) সেটি শেষে হিসাব করুন। ব্যবহারকারীরা গড় নয়, tail অনুভব করেন।

প্র ০৪Cardinality সীমিত রাখলে “কোন ব্যবহারকারীর সমস্যা” কীভাবে ধরবেন? Metric কি ভুল হাতিয়ার?

হ্যাঁ, এই প্রশ্নে metric ভুল হাতিয়ার। Metric আগে থেকে একত্র করা সংখ্যা, তাই অল্প খরচে বিশাল ট্রাফিক ধরে। দাম হলো খুঁটিনাটি হারানো। Label বাড়ালে সেই সুবিধা নষ্ট হয়, যেমনটি আমরা ১,০০,০২৫ series-এ দেখেছি।

কাজের পথ হলো তিন সংকেত মিলিয়ে ব্যবহার। Metric বলে “কিছু একটা ভুল, এখন, এই route-এ”। Exemplar সেই গ্রাফের একটি বিন্দু থেকে একটি trace id-তে নিয়ে যায়। Trace দেখায় কোন service-এ দেরি (পর্ব ৬৫)। Structured log জানায় কোন ব্যবহারকারী, কোন অনুরোধ (পর্ব ৬৪)। সেখানে user_id রাখা স্বাভাবিক, কারণ log-এর খরচ আয়তনে, series-এ নয়।

বড় প্রতিষ্ঠান কিছু বাড়তি কৌশল নেয়। Tier বা plan-এর মতো ছোট শ্রেণি label হতে পারে: plan="free", plan="pro". বড় গ্রাহকের ছোট তালিকা আলাদা রাখা যায়, আর বাকিরা “other”. “কতজন আলাদা ব্যবহারকারী” জানতে HyperLogLog-এর মতো কাঠামো কাজে লাগে। আর “wide event” ধারার সিস্টেম, যেমন column store-এ প্রতিটি অনুরোধের সব তথ্য রাখা, high-cardinality প্রশ্ন করতে দেয়, তবে খরচ আর sampling-এর হিসাব আলাদা।

সংগঠনের দিকটিও আছে। Prometheus-এ sample_limit দিয়ে একটি target-এর সীমা বেঁধে দেওয়া যায়, যাতে সীমা পেরোলে scrape ব্যর্থ হয় আর একজনের ভুলে সবার সিস্টেম না পড়ে। Relabeling দিয়ে বিপজ্জনক label বা metric scrape-এর সময়ই ফেলে দেওয়া যায়। CI-তে নতুন metric-এর নাম আর label-এর review রাখা যায়। Platform দল প্রতিটি দলের series-এর হিসাব (showback) দেখাতে পারে (পর্ব ৬২)।

মনে রাখবেন, এই সীমাগুলো শুরুতে বসানো সহজ, কিন্তু বিস্ফোরণের পরে ঠিক করা কঠিন আর দামি, যেমনটি আমাদের আসল run-এ memory-র সংখ্যায় দেখা গেছে।

মূল উপলব্ধি: Metric বলে কী আর কখন। Log আর trace বলে কে আর কেন। এক হাতিয়ারে সব প্রশ্নের উত্তর চাইলে বিলটি আপনাকেই দিতে হবে।

প্র ০৫Prometheus নিজেই বন্ধ হয়ে গেলে কে জানাবে? Monitoring-এর monitoring কীভাবে করবেন?

এটি একটি পুরোনো সমস্যা। ব্যাংকের অ্যালার্মের তার কেটে দিলে অ্যালার্ম বাজে না। Prometheus পড়ে গেলে কোনো alert আসে না, আর আপনি শান্তিতে ঘুমান। এই নীরবতাই সবচেয়ে বিপজ্জনক।

প্রথম ধাপ দুটি একই রকম Prometheus. প্রচলিত HA ব্যবস্থায় দুটি server একই target scrape করে, একে অপরকে চেনে না। দুজনই alert পাঠায়, আর Alertmanager cluster duplicate বাদ দেয়। তাই data হুবহু এক হয় না, কিন্তু একটি পড়লেও alert থামে না। Grafana-তে এতে সামান্য অমিল দেখা যায়, যা Thanos-ধরনের query স্তর মিলিয়ে নিতে পারে।

দ্বিতীয় ধাপ dead man's switch. রেলের ইঞ্জিনে চালককে নিয়মিত একটি বোতাম চাপতে হয়, না চাপলে গাড়ি থামে। Prometheus-এ একই ভাবে একটি alert রাখা হয় যা সবসময় জ্বলে, যেমন vector(1). Alertmanager সেটি একটি বাইরের সেবায় পাঠায়। সংকেত বন্ধ হলেই বাইরের সেবা আপনাকে ডাকে। ধারণাটি জরুরি: নজরদারির নজরদারি অবশ্যই সেই একই failure domain-এর বাইরে থাকতে হবে। একই region, একই cluster বা একই account-এ থাকলে একসাথেই পড়বে (পর্ব ৫)।

তৃতীয় ধাপ Prometheus-এর নিজের metric দেখা: up, scrape ব্যর্থতা, rule-এর দেরি, prometheus_tsdb_head_series, আর Alertmanager-এ notification ব্যর্থতা। আমাদের config-এ job: prometheus নিজেকেই scrape করছিল, এজন্যই তার memory আমরা মাপতে পেরেছিলাম।

কিছু public postmortem-এও দেখা যায়, বড় outage-এ status পাতা বা monitoring সেই একই ভাঙা ব্যবস্থার ওপর নির্ভর করছিল। ফেব্রুয়ারি ২০১৭-এর S3 outage-এর পর AWS জানিয়েছিল, তাদের status dashboard-ও S3-এর ওপর নির্ভরশীল থাকায় ঠিকমতো হালনাগাদ হয়নি।

ছোট দলের জন্য সহজ শুরু হলো অন্য region-এ চলা একটি বাইরের uptime পরীক্ষা, যা নিয়মিত আপনার মূল সাইট আর Prometheus-এ অনুরোধ পাঠায়, আর সাড়া না পেলে জানায়।

মূল উপলব্ধি: Alert না আসা মানে সব ঠিক আছে, এটি প্রমাণ নয়। নীরবতাকে সংকেতে বদলাতে হয়, আর সেই সংকেত থাকতে হয় ভিন্ন জায়গায়।

অনুশীলন

খাতা-কলমে হিসাব করুন, তারপর উত্তর মিলিয়ে নিন। কাজ ১-এ rate_reset.js-ও ব্যবহার করতে পারেন।

  1. Reset-সহ rate (হিসাব): ৬০ সেকেন্ডের জানালায় একটি counter-এর ৪টি sample: ৭.৫ সেকেন্ডে ১০০, ২২.৫ সেকেন্ডে ১৩০, ৩৭.৫ সেকেন্ডে ৪০, ৫২.৫ সেকেন্ডে ৭০. increase আর rate কত? আর সাধারণ বিয়োগ কত দেয়?

    সাধারণ বিয়োগ: ৭০ − ১০০ = −৩০. ১৩০ → ৪০-এ reset, তাই ১৩০ যোগ করে বৃদ্ধি = ১০০. ঢাকা সময় ৪৫ সেকেন্ড, গড় ফাঁক ১৫ সেকেন্ড, আর সীমা ১৬.৫ সেকেন্ড। দুই প্রান্তে ৭.৫ সেকেন্ড করে বাকি, যা সীমার নিচে, তাই পুরোটাই টানা হয়। শূন্য-সীমা: ৪৫ × ১০০ ÷ ১০০ = ৪৫ সেকেন্ড, ৭.৫-এর চেয়ে বড়, তাই বাধা নেই। গুণক ৬০ ÷ ৪৫ = ১.৩৩৩. increase ≈ ১৩৩.৩৩, আর rate ≈ ২.২২২ প্রতি সেকেন্ডে।

  2. হাতে histogram_quantile: Cumulative bucket: le="0.1" 900, le="0.25" 950, le="0.5" 990, le="1" 1000, le="+Inf" 1000. p98 আর p99.5 কত আসবে? ৩০০ ms SLO-র জন্য এই bucket-গুলো কি ভালো?

    p98: rank ৯৮০, পড়ে (০.২৫, ০.৫] খোপে, যেখানে ৪০টি। ০.২৫ + ০.২৫ × (৯৮০ − ৯৫০) ÷ ৪০ = ০.৪৩৭৫ s.

    p99.5: rank ৯৯৫, পড়ে (০.৫, ১] খোপে, যেখানে ১০টি। ০.৫ + ০.৫ × (৯৯৫ − ৯৯০) ÷ ১০ = ০.৭৫ s.

    ৩০০ ms SLO-র জন্য ভালো নয়। ০.৩-এ কোনো প্রান্ত নেই, তাই “কত ভাগ ৩০০ ms-এর মধ্যে” সরাসরি পাওয়া যায় না। le="0.3" যোগ করা উচিত।

  3. ফল আগে বলুন: ৫ সেকেন্ড scrape interval-এর একটি Prometheus একটি app scrape করছে। আপনি app-টির container বন্ধ করে ১৫ সেকেন্ড অপেক্ষা করলেন। এই চারটি query কী দেবে? (ক) up{job="shop"} (খ) shop_in_flight (গ) count(shop_requests_total) (ঘ) last_over_time(shop_in_flight[1m])

    (ক) 0, কারণ scrape ব্যর্থ, আর up Prometheus নিজেই লেখে। (খ) ফাঁকা ফল, কারণ series stale. (গ) ফাঁকাই, 0 নয়: count ফাঁকা ভেক্টরে কিছুই ফেরত দেয় না। সংখ্যা চাইলে or vector(0) লিখতে হয়। (ঘ) শেষ জানা মান, কারণ জানালায় পুরোনো sample আছে। আমাদের আসল run-এর ফলও এমনই ছিল: 0, ফাঁকা, ফাঁকা, 1.

  4. সমালোচনা ও নকশা: একটি দল তাদের payment API-র জন্য এই পরিকল্পনা দিল: metric http_requests_total{request_id, user_email, full_url, status}, latency-র জন্য শুধু avg_latency_ms নামের gauge, একটি dashboard-এ ৪০টি panel, আর ১ সেকেন্ড scrape interval. ভুলগুলো ধরুন আর ভালো নকশা দিন।
    • Label: request_id প্রায় প্রতিটি অনুরোধে নতুন, তাই প্রতি অনুরোধে একটি series. user_email ব্যক্তিগত তথ্যও। full_url-এ ID ঢোকে। রাখুন route ছাঁচ, method আর status. বাকিগুলো trace আর log-এ, আর email log-এও মাস্ক করুন।
    • Latency: গড়ের gauge দিয়ে tail দেখা যায় না, আর একাধিক instance মেলানো যায় না। http_request_duration_seconds histogram নিন, আর SLO-র সীমায় bucket রাখুন।
    • Dashboard: ওপরে RED আর SLO, নিচে USE আর dependency. ৪০টি panel একসাথে নয়, বিস্তারিত আলাদা dashboard-এ।
    • Scrape: ১ সেকেন্ড মানে ১৫ সেকেন্ডের ১৫ গুণ sample. কারণ না থাকলে ১৫–৩০ সেকেন্ড যথেষ্ট।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি latency সমস্যা app থেকে প্রকৌশলীর কাছে পৌঁছানোর ধাপগুলো সাজান (▲▼ চেপে সরান)

  1. App নিজের histogram bucket-এর সংখ্যা বাড়ায়
  2. Prometheus /metrics scrape করে TSDB-তে জমায়
  3. Alert rule PromQL চালিয়ে শর্ত সত্য পায়
  4. Prometheus alert পাঠায় Alertmanager-এ
  5. Alertmanager গ্রুপ করে on-call প্রকৌশলীকে জানায়

Prometheus-এ queue_depth নামের metric-টি একবার বাড়ে, একবার কমে। এটি কোন ধরনের, আর কোন query অর্থবহ?

  • Counter, তাই rate(queue_depth[5m])
  • Gauge, তাই queue_depth বা avg_over_time(queue_depth[5m])
  • Histogram, তাই histogram_quantile(0.9, queue_depth)
  • Summary, তাই increase(queue_depth[5m])

একটি counter-এর পরপর sample: 100, 150, 20, 70. কোনো extrapolation ধরা না হলে মোট কত বৃদ্ধি গণ্য হবে?

  • −30
  • 50
  • 120
  • 230

Histogram bucket: le="0.1" = 80, le="0.5" = 100, +Inf = 100. Linear interpolation-এ p90 কত?

  • 0.3 s
  • 0.5 s
  • 0.1 s
  • 0.09 s

৫টি service, প্রতিটিতে ৩টি route, ৪টি status আর ১০টি instance. একটি histogram metric-এ প্রতিটি label-সমন্বয়ের জন্য ১২টি series লাগে। সব সমন্বয় ঘটলে মোট series কত?

  • ৩৪টি
  • ৬০০টি
  • ১,৪৪০টি
  • ৭,২০০টি

একটি database server-এর disk কতটা ব্যস্ত, কত I/O অপেক্ষায় আর কত device error হচ্ছে, তা জানতে কোন পদ্ধতি সবচেয়ে মানানসই?

  • RED method
  • USE method
  • SLA
  • Histogram bucket

৫ সেকেন্ড scrape interval-এ rate(x[5s]) ফাঁকা ফল দিল, যদিও metric আছে। কারণ কী?

  • Counter-এর মান খুব ছোট
  • Query-র syntax ভুল
  • জানালায় দুটি sample পড়েনি
  • Target বন্ধ হয়ে গেছে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের শব্দগুলো এখানে একসাথে:

মনে রাখুন

  1. Prometheus pull করে: /metrics scrape, আর ব্যর্থ হলে up 0.
  2. Counter-এ সবসময় rate আগে, sum পরে. কমে যাওয়া মান মানে reset.
  3. histogram_quantile একটি আন্দাজ, আর SLO-র সীমায় bucket রাখলে আন্দাজের দরকার কমে।
  4. Series হলো label-এর গুণফল. অসীম-ধরনের মান label নয়, সেটি log আর trace-এর কাজ।
  5. Service-এ RED, resource-এ USE, আর dashboard-এ আগে উত্তর, পরে কারণ।

পরের পর্বে: Metric বলল কিছু একটা ভুল। কিন্তু কোন অনুরোধে, আর ঠিক কী ঘটেছিল? পর্ব ৬৪, Centralized logging-এ সব server-এর log এক জায়গায় আনব, আর সেই log-এর খরচের হিসাব করব।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান