یکی از پروژههایی که روش کار میکنم پردازشهای زمانبر زیادی داره که بر حسب نیاز لازم شد که این زمان رو کاهش بدیم. ولی مشکلی که باهاش روبرو هستیم اینه که نمیدونیم کدوم فرایندها زمانبرتر و وضعیت وخیم تری دارند. به همین دلیل تصمیم بر این شد که فرایندها رو ردیابی کنیم تا دید بهتری به سیستم داشته باشیم. Jaeger ابزاری هست که دقیقا برای همین کار طراحی شده، برای opentracing. ولی با توجه به اینکه مدتی بود ازش استفاده نکرده بودم تصمیم گرفتم یه کد تستی روی گولنگ باهاش پیاده کنم و یه مقاله هم ازش بنوسیم که هم اشتراک دانشی شده باشه و هم کمکی باشه در کمبود منابع یادگیری این ابزار باحال.
Jaeger چی هست اصلا؟
خب Jaeger یک ابزار متن-باز برای مانیتورینگ و ردیابی فرایندها در سیستمهای توزیعشده (و همچنین متمرکز) است که این امکان رو بهتون میده که بتونید مسیر عبور درخواستها/فراخوانیها داخل توابع، کلاسها و حتی سرویسهای مختلفتون رو باهاش رصد کنید. همچنین Jaeger بهتون زمان دقیق اجرای هر کدوم از پردازشها از جمله پردازشهای فرزند (مثل فراخوانی یک تابع دیگه) رو نشون میده که میتونه در بهینه کردن زمان پردازش سیستم خیلی کمکتون کنه. یکی دیگه از تواناییهای مهم این ابزار نمایش محل وقوع خطا در سیستمتون هست که پیدا کردنشون با ابزارهای لاگینگ میتونه وقت گیر باشه.
کلمات کلیدی
موقع استفاده از Jaeger با واژههایی روبرو میشید که لازمه باهاشون اشنایی داشته باشید:
Span: هر اتفاقی از جمله یک درخواست http، فراخوانی یک تابع یا اجرای یک کوئری دیتابیس یک span محسوب میشه که کوچکترین واحد کاری Jaeger هست. یک span از یک ایدی ویژه، نام، زمان شروع و طول زمان اجرا تشکیل شده.
Trace: یک مجموعه از spanهای متصل به هم است که رابطه پدر/فرزندی با هم دارند.
معماری Jaeger
Jaeger از چند ماژول اصلی تشکیل شده:
Client Library: این ماژول کتابخونههایی هستند که برای زبانهای برنامهنویسی پیادهسازی شدهاند تا شما بتونید در برنامهتون ازش استفاده کنید و از طریق فراخوانی توابع اونها ایجاد یا اتمام پروسه یک تابع رو برای Jaeger ارسال کنید.
Agent: یک پروسه که به صورت دائم منتظر دریافت فراخوانی ایجاد و اتمام span ها از client library هستند تا اونها رو به صورت دستهای (batch) به سرور Jaeger ارسال کنه.
Collector: مسئول دریافت spanها از برنامههای مختلف هست تا بعد از احراز هویت و تغیر شکل ساختاریشون، اونها رو برای ماژول ذخیرهسازی ارسال کنه.
Storage: تمامی span و trace های ایجاد شده در این واحد ذخیره میشوند تا بعدا توسط ابزار مانیتورینگ استفاده شوند. Jaeger از پایگاهدادههای مختلف مثل Elastic Search و Cassandra پشتیبانی میکند.
UI Dashboard: یک داشبورد تحت وب که امکان مانیتور کردن و انالیز Trace هاتون رو بهتون میده. این داشبورد درخواستهای شما رو در قالب Query به ماژول Qeury ارسال میکنه.
Query: مسئول واکشی دادهها از پایگاهداده بنابر در درخواست کاربر است.
راهاندازی Jaeger
برای استفاده از Jaeger لازمه اول سرورش رو راهاندازی کنید که طبق معمول علاوه بر اجرا روی کلاسترهای orchestracte شده، میتونید روی سیستم لوکالتون هم داشته باشیدش. برای اجرای سرور میتونید فایل نصبی Jaeger رو از سایت رسمیش در این ادرس دانلود کنید و یا اینکه از ایمیج داکرش استفاده کنید که صرفا لازمه این دستور رو اجرا کنید:
docker run -d --name jaeger \
-e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
-e COLLECTOR_OTLP_ENABLED=true \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
-p 14250:14250 \
-p 14268:14268 \
-p 14269:14269 \
-p 9411:9411 \
jaegertracing/all-in-one:1.46
این دستور داکر که از ایمیج jaegertracing/all-in-one استفاده میکنه، هر اونچه که برای کار با Jaeger لازم دارید رو براتون اجرا میکنه. بعد از اجرا شما میتونید با ورود به ادرس http://localhost:۱۶۶۸۶ ، داشبود مانیتورینگ Jaeger رو ببینید.
استفاده در برنامه
بعد از راهاندازی سرور Jaeger، میتونید با استفاده از کتابخونههای مخصوص زبان پروژهتون، فرایندها رو ثبت کنید که ما در این اموزش از زبان Go استفاده میکنیم. یک پروژه ساده هم که صرفا برای این مقاله پیادهکردهام رو در این ادرس https://github.com/faramarzQ/jaeger_golang قرار دادم که با هم بررسی میکنیم.
خب برای استفاده از Jaeger لازمه شما کتابخونه رو به روش زیر initialize کنید. این کتابخونه یک مجموعه از متغیرهای محلی مثل اسم سرویس (JAEGER_SERVICE_NAME) رو از سیستمتون میخونه و استفاده میکنه.
package jaeger
import (
"fmt"
"io"
"os"
"github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/config"
)
// Jaeger open-tracing instance
var Tracer opentracing.Tracer
// InitializeJaeger Initializes jaeger tracing variable
func InitializeJaeger() (opentracing.Tracer, io.Closer) {
cfg, err := config.FromEnv()
if err != nil {
panic(fmt.Sprintf("Failed reading Jaeger env vars: %v\n", err))
}
tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger))
if err != nil {
panic(fmt.Sprintf("Cannot initialize Jaeger: %v\n", err))
}
Tracer = tracer
return tracer, closer
}
اینجا داخل پکیج jaeger، بعد از خوندن متغیرهای محلی از سیستم، یک نمونه از Jaeger tracer ایجاد میکنیم و اونو به صورت global در دسترس برنامه قرار میدیم. بعد از پیادهسازی initializer، اون رو داخل برنامه استفاده میکنیم :
tracer, closer := jaeger.InitializeJaeger()
defer closer.Close()
opentracing.SetGlobalTracer(tracer)
تابع InitializeJaeger نمونه ساخته شده Jeager و یک closer بهمون برمیگردونه که برای بستن ارتباط با jaeger بعد از اتمام پروسه اصلی برنامه فراخوانی میشه. متد SetGlobalTracer هم نمونه ساخته شده رو به صورت singleton در دسترس کل برنامه قرار میده.
در این پروژه من از یک API ساده برای نمایش tracing استفاده کردم که چند تابع با پردازش مصنوعی زمانبر رو فراخوانی میکنه که اجرای هر کدوم در قالب یک span برای Jaeger ارسال میشن. handler این API که در کد زیر نشون داده شده، در ابتدای کار یک span پدر به اسم process-function ایجاد میکنه:
// An API handler executing a multi second task
func processHandler(w http.ResponseWriter, r *http.Request) {
span := jaeger.Tracer.StartSpan("process-function")
defer span.Finish()
ctx := opentracing.ContextWithSpan(context.Background(), span)
// a time consuming operation
time.Sleep(1 * time.Second)
internal.Foo(ctx)
// a time consuming operation
time.Sleep(1 * time.Second)
w.Write([]byte("Request processed successfully."))
}
بعد از ایجاد span پدر، اتمام اون رو به صورت defer شده اجرا میکنیم. اساس کار Jaeger ها روی context هایی هستند که داخل رگ سیستم جریان دارند.Jaeger با ContextWithSpan یک context جدید از نمونه context موجود و span ایجاد شده برامون تولید میکنه که در فراخوانی های اینده، برای ایجاد span فرزند استفاده میکنیم. ایجا صرفا برای سادهسازی نمایش اجرای یک تسک زمانبر، از time.Sleep() استفاده کردم.
بعد از ایجاد span پدر، با فراخوانی هر تابع/متد و ارسال context با ان، پروسههای جدید رو لاگ کنیم:
// Foo processes a 6 seconds task
func Foo(ctx context.Context) {
span, _ := opentracing.StartSpanFromContext(ctx, "bar-function")
defer span.Finish()
// a time consuming operation
time.Sleep(2 * time.Second)
Bar(ctx)
// a time consuming operation
time.Sleep(1 * time.Second)
}
در ابتدای تابع Foo، یک span جدید از context داده شده ایجاد میکنیم که اتمام اون رو defer میکنیم. فراخوانیهای اینده هم به همین شکل قابل رصد هستند.
اجرای برنامه، ۸۰۸۰ لوکالتون رو برای دریافت درخواست اماده میکنه که با هر درخواست و اتمام اون، spanها برای Jaeger ارسال شده و قابل مانیتورینگ هستند. در پنل Jaeger هم با فیلتر کردن اطلاعات در سمت چپ، میتونید trace هایی که ایجاد شدهاند رو لیست کنید.
با کلیک روی هر trace وارد صفحه اون trace شده و لیست span هاش رو میتونید ببینید.
همونطور که داخل تصویر مشخصه، trace با نام process-function از چند span تشکیل شده که هر کدوم برای چند ثانیه طول کشیدند. با کلیک روی هر span اطلاعات بیشتر اون span رو میتونید ببینید.
همونطور که اول مقاله توضیح دادم، Jaeger برای ردیابی کردن زمان اجرای توابعی که وضعیت وخیمی دارند و شما ممکنه نسبت بهشون اطلاع نداشته باشین هم مفید هست مثل توابع goroutine که ممکنه به هر دلیل با راحتی توابع معمولی قابل ردیابی نباشن. برای نمایش این موضوع بعد از اجرای تابع Foo، دوباره اون رو در قالب یک goroutine اجرا میکنیم:
طبق تصویر با وجود اتمام اجرای process-function، تابع bar دوم همچنان در حال اجرا هست.
در این مثال ما صرفا از یک سرویس با چند تابع استفاده کردیم که نمونه یک سیستم متمرکز و واحد بود. Jaeger میتونه سیستمها و فرایندهای با پیچیدگی بالاتر مثل سیستمهای توزیعشده هم ردیابی کنه.