به گزارش از وبسایت cncf 📰 منتشرشده در ۳۱ اوت ۲۰۲۶، نوشته نیل شاه، Stackgen

پروژه‌های CNCF که در این مطلب به آن‌ها اشاره شده است

Kubernetes زیرساخت را قابل‌برنامه‌ریزی‌تر، مقیاس‌پذیرتر و مقاوم‌تر کرد؛ اما در عین حال، فهمیدن رفتار سیستم‌های production را هم دشوارتر ساخت. بارهای کاری جابه‌جا می‌شوند، replicaها مدام تغییر می‌کنند، وابستگی‌ها بیشتر می‌شوند و یک درخواست ساده کاربر ممکن است پیش از تکمیل شدن از ingress، services، queueها، storage و workerهای پس‌زمینه عبور کند. همین پیچیدگی باعث شده که dashboardها به‌تنهایی کافی نباشند. metrics می‌توانند نشان دهند مشکلی وجود دارد، اما معمولاً توضیح نمی‌دهند چرا این مشکل رخ داده، از کجا شروع شده یا دامنه اثر آن تا کجاست. در Kubernetes، observability با telemetry آغاز می‌شود، اما تنها زمانی ارزشمند است که این telemetry به operatorها کمک کند از نشانه‌ها به درک برسند. 👀

monitoring نشانه‌ها را نشان می‌دهد

monitoring سنتی برای پاسخ دادن به پرسش‌های از پیش تعریف‌شده ساخته شده است. CPU از یک آستانه بالاتر رفته؟ memory در حال افزایش است؟ نرخ خطاها بیشتر شده؟ این پرسش‌ها ضروری‌اند، اما فرض می‌کنند تیم از قبل می‌داند باید دنبال چه چیزی بگردد. Kubernetes این فرض را به چالش می‌کشد. incidentهای مدرن اغلب از تعامل میان اجزا شکل می‌گیرند، نه از خراب شدن واضح یک host. ممکن است یک rollout در سطح deployment سالم به نظر برسد، اما هم‌زمان به‌دلیل یک dependency پایین‌دستی، یک retry loop پر سروصدا یا یک مسیر overloaded در control-plane، تأخیر ایجاد کند. monitoring همچنان مهم است، اما تمام ماجرا نیست. این ابزار به تیم می‌گوید مشکلی وجود دارد؛ observability کمک می‌کند مشکلی را که از قبل پیش‌بینی نشده بود بررسی کنند. 🔍

observability فهم ایجاد می‌کند

از نگاه CNCF، observability شامل instrumentation، collection، processing، storage، querying، curation و correlation داده‌های telemetry مانند metrics، logs، traces و profiling data برای workloadهای cloud-native است. این گستره مهم است، چون observability فقط یک ابزار یا یک dashboard نیست؛ بلکه یک ویژگی طراحی برای سیستم و یک مدل عملیاتی برای تیم محسوب می‌شود.

تعریف کاربردی observability این است: یک سیستم زمانی observable است که سیگنال‌های باکیفیت کافی ارائه دهد تا مهندسان بتوانند از خروجی‌های بیرونی، رفتار داخلی را حدس بزنند. در عمل، این یعنی پاسخ‌گویی به incident از حالت حدس‌زدن خارج شده و به یک بررسی هدایت‌شده تبدیل می‌شود. برای تیم‌های Kubernetes، این تغییر بسیار مهم است. به‌جای جابه‌جا شدن بین chartهای نامرتبط و فرمان‌های مختلف terminal، می‌توانند شواهد را در میان وضعیت زیرساخت، رفتار workload و جریان درخواست دنبال کنند. این تفاوت میان «دیدن» یک cluster و «فهمیدن» آن است. 🌐

metrics نقطه شروع هستند

metrics معمولاً نخستین سیگنالی هستند که تیم‌ها به سراغشان می‌روند، چون کارآمد، عددی و برای alerting و تحلیل روند کاملاً مناسب‌اند. آن‌ها رفتار پیچیده را به time seriesهایی تبدیل می‌کنند که جمع‌آوری، ذخیره و query کردنشان نسبت به سیگنال‌های جزئی‌تر، هزینه کمتری دارد.

در Kubernetes، metrics به پرسش‌های اولیه عملیاتی پاسخ می‌دهند: آیا node تحت فشار است؟ آیا podها دوباره راه‌اندازی می‌شوند؟ آیا latency درخواست‌ها در حال افزایش است؟ آیا API server کند شده؟ آیا work queueها در حال انباشته شدن‌اند؟ این‌ها همان سیگنال‌هایی هستند که نخستین نشانه‌های مشکل را آشکار می‌کنند.

metrics به‌ویژه برای دو الگوی رایج در عملیات بسیار مفیدند: RED برای services یعنی rate، errors و duration؛ و USE برای infrastructure یعنی utilization، saturation و errors. این الگوها مؤثرند، چون telemetry را با پرسش‌هایی که هنگام incident اهمیت دارند هماهنگ می‌کنند. افزایش request rate همراه با latency پایدار، یک روایت را نشان می‌دهد. افزایش duration و saturation با traffic ثابت، روایتی دیگر است. metrics نخستین تصویر از وضعیت را به تیم می‌دهند. 📈

نمونه metrics برای service

یک application ساده می‌تواند شمارش درخواست‌ها و latency histogramها را expose کند تا هم از SLOها پشتیبانی شود و هم triage incident ساده‌تر شود.

from prometheus_client import Counter, Histogram, start_http_server
from flask import Flask, request
import time

app = Flask(__name__)

REQUESTS_TOTAL = Counter(
    "http_requests_total",
    "Total HTTP requests",
    ["method", "route", "status_code"],
)

REQUEST_DURATION = Histogram(
    "http_request_duration_seconds",
    "HTTP request latency",
    ["method", "route", "status_code"],
    buckets=[0.05, 0.1, 0.25, 0.5, 1, 2, 5],
)

@app.route("/checkout", methods=["POST"])
def checkout():
    start = time.time()
    status_code = 200
    try:
        time.sleep(0.12)
        return {"status": "ok"}, status_code
    except Exception:
        status_code = 500
        raise
    finally:
        duration = time.time() - start
        REQUESTS_TOTAL.labels(request.method, request.path, str(status_code)).inc()
        REQUEST_DURATION.labels(request.method, request.path, str(status_code)).observe(duration)

if __name__ == "__main__":
    start_http_server(8000)
    app.run(host="0.0.0.0", port=8080)

این نوع instrumentation ارزشمند است، چون counterها و histogramها برای پایش traffic، نرخ خطا و latency در طول زمان بسیار مناسب‌اند. histogramها به‌طور ویژه اهمیت دارند، چون تحلیل بر پایه percentile را ممکن می‌کنند؛ چیزی که اغلب به تجربه کاربر نزدیک‌تر از میانگین ساده است. ⏱️

metrics به‌تنهایی کافی نیستند

metrics در نشان دادن تغییر رفتار بسیار خوب عمل می‌کنند، اما در حفظ زمینه رخدادهای فردی ضعیف‌ترند. یک spike در latency ممکن است در نمودار دیده شود، در حالی که علت اصلی آن در یک مسیر خاص، یک dependency مشخص یا یک دسته درخواست غیرمعمول پنهان مانده باشد.

این محدودیت در Kubernetes پررنگ‌تر می‌شود، چون labelها ممکن است تیم‌ها را وسوسه کنند که همه جزئیات را داخل metrics قرار دهند. این کار معمولاً به مشکل cardinality منجر می‌شود؛ جایی که ترکیب‌های بیش‌ازحد یکتا از labelها، هزینه را بالا می‌برند و performance query را کاهش می‌دهند. whitepaper observability در CNCF در این‌باره صریح است: metrics به این دلیل کارآمدند که dimensions آن‌ها نسبتاً پایدار می‌ماند، و وقتی برای نمایش رخدادهای بسیار یکتا استفاده شوند، کارایی خود را از دست می‌دهند. قرار دادن request ID، user ID یا سایر مقادیر تقریباً یکتا در metrics معمولاً نشانه آن است که باید آن جزئیات را به سیگنال دیگری سپرد. ⚠️

نمونه alert بر پایه کیفیت service

alertها باید ریسک service را بازتاب دهند، نه فقط فشار منابع را. یک latency objective معمولاً از یک threshold عمومی برای CPU معنادارتر است، چون telemetry را به اثر روی کاربر وصل می‌کند.

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: checkout-alerts
  namespace: observability
spec:
  groups:
    - name: checkout-slo
      rules:
        - alert: CheckoutHighLatency
          expr: |
            histogram_quantile(
              0.99,
              sum by (le) (
                rate(http_request_duration_seconds_bucket{route="/checkout"}[10m])
              )
            ) > 1
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: "Checkout p99 latency is above 1s"
            description: "The checkout path is exceeding its latency objective for 10 minutes."

alerting معنادار یکی از محورهای اصلی راهنمای CNCF درباره observability است؛ راهنمایی که بر best practiceها در thresholdها، ruleها، dashboardها و policyها تأکید دارد، نه بر انبوه alertهای بی‌هدف. 🚨

logs زمینه محلی را توضیح می‌دهند

logs روایتی را ارائه می‌کنند که metrics فاقد آن هستند. آن‌ها رخدادها را با جزئیات حفظ می‌کنند و همین باعث می‌شود برای فهمیدن اینکه یک service، component یا process در یک لحظه مشخص چه می‌کرده، بسیار مفید باشند.

در Kubernetes، logs زمانی بسیار قدرتمندتر می‌شوند که ساختاریافته باشند. فیلدهای یکنواخت مانند timestamp، severity، service name، namespace، هویت pod و request path، به‌همراه trace context، امکان جست‌وجو و correlation میان workloadها را فراهم می‌کنند. این همان نقطه‌ای است که بسیاری از تیم‌ها نخستین گام را از data collection به observability واقعی برمی‌دارند. یک metric سرویس درگیر را مشخص می‌کند، اما یک log line علت واقعی مشکل را آشکار می‌سازد؛ مثلاً timeout، exception، مشکل configuration یا failure در dependency. 🧾

نمونه log ساختاریافته application

import json
import logging
import sys
from datetime import datetime, timezone

logger = logging.getLogger("checkout")
handler = logging.StreamHandler(sys.stdout)
logger.addHandler(handler)
logger.setLevel(logging.INFO)

def log_event(level, message, **fields):
    payload = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "level": level,
        "service.name": "checkout",
        "k8s.namespace.name": "production",
        "message": message,
        **fields,
    }
    logger.info(json.dumps(payload))

log_event(
    "error",
    "payment authorization failed",
    route="/checkout",
    http_status_code=502,
    trace_id="4f8b9c1d3a2e7f10",
    error_type="upstream_timeout",
)

ارزش این الگو در یکپارچگی آن است. استفاده دوباره از یک ساختار metadata مشترک میان سیگنال‌ها، correlation را بهتر می‌کند و در زمان incident response اصطکاک را کاهش می‌دهد.

traces مسیر درخواست را آشکار می‌کنند

distributed traceها به پرسشی پاسخ می‌دهند که با metrics و logs متفاوت است. آن‌ها نشان می‌دهند یک درخواست واحد چگونه در سیستم حرکت کرده و در هر مرحله چه مقدار زمان صرف شده است. این موضوع در Kubernetes اهمیت زیادی دارد، چون failureهای production اغلب failureهای توزیع‌شده هستند. کاربر فقط یک درخواست کند یا ناموفق را می‌بیند، اما مسئله اصلی ممکن است چند service، retry، مرز queueها یا callهای database را درگیر کرده باشد.

propagationِ trace context همان چیزی است که این امکان را فراهم می‌کند. whitepaper CNCF بر propagation استاندارد به‌عنوان سازوکاری تأکید دارد که رابطه میان services را حفظ می‌کند و spanهای بازیگران مختلف را زیر یک request context واحد به هم وصل می‌سازد.

نمونه tracing در Python

from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter

resource = Resource.create({
    "service.name": "checkout",
    "service.namespace": "storefront",
    "deployment.environment": "prod",
})

provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)

def process_checkout(order_id, cart_total):
    with tracer.start_as_current_span("checkout.request") as span:
        span.set_attribute("order.id", order_id)
        span.set_attribute("cart.total", cart_total)
        reserve_inventory(order_id)
        authorize_payment(order_id)
        return {"status": "ok"}

def reserve_inventory(order_id):
    with tracer.start_as_current_span("inventory.reserve"):
        pass

def authorize_payment(order_id):
    with tracer.start_as_current_span("payment.authorize"):
        pass

استفاده از semantic conventions مشترک این موضوع را حتی بهتر می‌کند، چون نام‌ها و attributes را استاندارد می‌سازد و correlation میان ابزارها را آسان‌تر می‌کند. ✨