راه‌اندازی Observability با LGTM و Pyroscope، بخش ۵: ردیابی با Tempo

تا اینجا متریک‌ها گفتند «چیزی کند یا خراب است» و لاگ‌ها گفتند «چه پیامی ثبت شده است». اما وقتی یک درخواست از چند مرحله عبور می‌کند، هنوز یک پرسش می‌ماند: زمان دقیقاً کجا صرف شد؟ سیگنال سوم، یعنی ردیابی (tracing)، دقیقاً همین را نشان می‌دهد. در این بخش ردیابی توزیع‌شده را با OpenTelemetry و Tempo راه می‌اندازیم.

ردیابی، span و trace

در ردیابی، هر کار کوچک یک span است؛ مثلاً «پردازش درخواست HTTP» یا «اجرای یک پرس‌وجوی پایگاه‌داده». مجموعه‌ی span هایی که به یک درخواست تعلق دارند، با هم یک trace می‌سازند. هر trace نشان می‌دهد کار از کجا شروع شد، به چه بخش‌هایی رفت و هر بخش چقدر طول کشید.

ابزارگذاری اپلیکیشن با OpenTelemetry

برای تولید span، از OpenTelemetry استفاده می‌کنیم؛ یک استاندارد باز برای تولید و ارسال داده‌های ردیابی. در اپلیکیشن Django دو ابزارگذاری خودکار را فعال می‌کنیم: یکی برای درخواست‌های HTTP و دیگری برای پرس‌وجوهای پایگاه‌داده. span ها با پروتکل OTLP به جمع‌کننده فرستاده می‌شوند:

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.django import DjangoInstrumentor
from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor

provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(
    OTLPSpanExporter(endpoint="alloy.observability.svc:4317", insecure=True)))
trace.set_tracer_provider(provider)

DjangoInstrumentor().instrument()
SQLite3Instrumentor().instrument()

نکته درباره‌ی gunicorn

چون gunicorn برای هر worker یک پردازه‌ی جداگانه ایجاد می‌کند، این ابزارگذاری را در قلاب post_fork انجام می‌دهیم تا هر worker پس از ساخته‌شدن، ابزارگذاری خودش را داشته باشد.

جمع‌آوری و ذخیره با Alloy و Tempo

باز هم به همان جمع‌کننده‌ی واحد، Alloy، تکیه می‌کنیم؛ فقط این‌بار یک گیرنده‌ی OTLP و یک خروجی به Tempo به آن اضافه می‌کنیم:

otelcol.receiver.otlp "default" {
  grpc { endpoint = "0.0.0.0:4317" }
  http { endpoint = "0.0.0.0:4318" }
  output {
    traces = [otelcol.exporter.otlp.tempo.input]
  }
}

otelcol.exporter.otlp "tempo" {
  client {
    endpoint = "tempo.observability.svc:4317"
    tls { insecure = true }
  }
}

Tempo هم مانند Loki و Mimir به‌شکل مانیفست در همان مخزن قرار می‌گیرد و Argo CD آن را مستقر می‌کند. پس از تولید ترافیک، می‌توانیم از Tempo فهرست آخرین trace ها را بگیریم:

فهرست آخرین trace ها از Tempo
آخرین trace های ذخیره‌شده در Tempo. هر ردیف یک درخواست است، با نام سرویس، نام عملیات و مدت‌زمان کل.

خواندن یک trace در Grafana

Tempo را هم به‌عنوان datasource به Grafana اضافه می‌کنیم و یک trace را باز می‌کنیم. نمای آبشاری (waterfall) span ها را روی محور زمان نشان می‌دهد. اینجا به‌روشنی دیده می‌شود که درخواست GET index یک span فرزند دارد: پرس‌وجوی SELECT روی پایگاه‌داده که بخشی از زمان را گرفته است:

نمای آبشاری یک trace در Grafana با span درخواست و پرس‌وجوی پایگاه‌داده
نمای آبشاری یک trace: span والد GET index و span فرزند SELECT (پرس‌وجوی پایگاه‌داده). مدت‌زمان هر span نشان می‌دهد زمان کجا صرف شده است.

چون span ها همان برچسب‌های سرویس را دارند و trace_id در دسترس است، می‌توان از یک درخواست کند در متریک‌ها، به trace همان درخواست و از آنجا به لاگ‌های مربوطه رسید. این پیوند میان سه سیگنال، همان چیزی است که Observability را کاربردی می‌کند.

جمع‌بندی و بخش بعد

در این بخش سیگنال سوم را اضافه کردیم: با OpenTelemetry در اپلیکیشن span تولید کردیم، آن‌ها را با Alloy جمع کردیم، در Tempo ذخیره کردیم و در Grafana به‌شکل آبشاری دیدیم. حالا هر سه سیگنال اصلی را داریم.

در بخش پایانی یک لایه‌ی دیگر اضافه می‌کنیم: با Pyroscope پروفایلینگ پیوسته را راه می‌اندازیم تا در سطح کد ببینیم زمان پردازنده دقیقاً کجا مصرف می‌شود.

بخش‌های این مجموعه

← بازگشت به وبلاگ