برنامه نویسی

[Bahasa] Tracer: Open Telemetry، Golang و Jagger Simple Implementation

ردیاب

Tracer بخشی از Observability است که نقش مهمی در پیاده سازی Microservices Architecture ایفا می کند و یک نمای کلی “Trace” از فرآیندهای در حال اجرا در یک منطق برنامه ارائه می دهد.

به زبان ساده، در Webservice، Tracer یک نمای کلی از مدت زمان اجرای یک منطق را با انتشار یک سیگنال Trace ارائه می دهد. بعداً، این ردیاب پس از ارسال سیگنال از طریق صادر کننده به جمع کننده، به صورت یک دهانه تودرتو دیده می شود. [OpenTelemetry: Traces]

OpenTelemetry

برای اینکه بتواند سیگنال‌های Traces را که بعداً می‌تواند توسط گردآورنده جمع‌آوری شود، منتقل کند، وب‌سرویس به OpenTelemetry به عنوان کتابخانه‌ای نیاز دارد که به یک پروتکل استاندارد مشاهده‌پذیری تبدیل شده است که معمولاً پروتکل OpenTelemetry (OTLP) نامیده می‌شود. [OpenTelemetry: Language – Go]

جیگر

تجسم ردیابی سیگنال واقعاً برای ارائه یک نمای کلی از فرآیندهای رخ داده در وب سرویس مورد نیاز است. Jaeger یک پلت فرم منبع باز است که با استفاده از پروتکل ارتباطی HTTP یا gRPC از OTLP پشتیبانی می کند. [Jaeger]

اجرا در Golang

پیاده‌سازی Tracer در زبان برنامه‌نویسی Golang یک مورد ساده را پیاده‌سازی می‌کند که در آن وب‌سرویس فقط داده‌هایی را با مدت زمان پاسخ متفاوت برمی‌گرداند. کتابخانه های مورد استفاده عبارتند از:

  • چی: چارچوب HTTP
  • OpenTelemetry: سیگنالینگ تله متری

OpenTelemetry را به عنوان یک ماژول تله متری راه اندازی کنید

پیاده سازی ماژول تله متری در دایرکتوری pkg/telemetry/telemetry.go:

package telemetry

import (
    "context"
    "errors"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
    "go.opentelemetry.io/otel/sdk/resource"
    "go.opentelemetry.io/otel/sdk/trace"
)

// enumeration constant for which protocol used
const (
    HTTP uint8 = iota
    GRPC
)

// setup client to connect web-service with Jaeger
func SetupTraceClient(ctx context.Context, protocol uint8, endpoint string) otlptrace.Client {
    switch protocol {
    case GRPC:
        return otlptracegrpc.NewClient(otlptracegrpc.WithEndpoint(endpoint), otlptracegrpc.WithInsecure(), otlptracegrpc.WithCompressor("gzip"))
    default:
        return otlptracehttp.NewClient(otlptracehttp.WithEndpoint(endpoint), otlptracehttp.WithInsecure(), otlptracehttp.WithCompression(otlptracehttp.NoCompression))
    }
}

func setupTraceProvider(ctx context.Context, traceClient otlptrace.Client) (*trace.TracerProvider, error) {
    // set resource
    res, err := resource.New(
        ctx,
        resource.WithFromEnv(),
    )
    if err != nil {
        return nil, err
    }

    // init trace exporter
    traceExporter, err := otlptrace.New(ctx, traceClient)
    if err != nil {
        return nil, err
    }

    // init trace exporter
    traceProvider := trace.NewTracerProvider(
        trace.WithBatcher(
            traceExporter,
            trace.WithBatchTimeout(time.Duration(time.Second*3)),
        ),
        trace.WithResource(res), // Discover and provide attributes from OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME environment variables.
    )

    return traceProvider, nil
}

func SetupTelemetrySDK(ctx context.Context, traceClient otlptrace.Client) (func(context.Context) error, error) {
    var err error
    var shutdownFuncs []func(context.Context) error

    shutdown := func(ctx context.Context) error {
        var err error
        for _, fn := range shutdownFuncs {
            err = errors.Join(err, fn(ctx))
        }
        shutdownFuncs = nil
        return err
    }

    handleErr := func(inErr error) {
        err = errors.Join(inErr, shutdown(ctx))
    }

    traceProvider, err := setupTraceProvider(ctx, traceClient)
    if err != nil {
        handleErr(err)
        return shutdown, err
    }

    shutdownFuncs = append(shutdownFuncs, traceProvider.Shutdown)
    otel.SetTracerProvider(traceProvider)

    return shutdown, nil
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

سپس، پیکربندی Telemetry را در تابع اصلی تنظیم کنید main.go:

package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/go-chi/chi/v5"
    "github.com/wahyurudiyan/medium/otel-jaeger/config"
    "github.com/wahyurudiyan/medium/otel-jaeger/pkg/telemetry"
    "github.com/wahyurudiyan/medium/otel-jaeger/router"
)

func SetupTelemetry(ctx context.Context, config *config.Config) (func(context.Context) error, error) {
    otlpCli := telemetry.SetupTraceClient(ctx, telemetry.GRPC, config.JaegerGRPCEndpoint)
    shutdownFn, err := telemetry.SetupTelemetrySDK(ctx, otlpCli)
    return shutdownFn, err
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    cfg := config.Get()

    shutdownFn, err := SetupTelemetry(ctx, cfg)
    if err != nil {
        shutdownFn(ctx)
        panic(err)
    }

    r := chi.NewRouter()
    r.Route("/api", func(r chi.Router) {
        router.Router(r)
    })

    srv := http.Server{
        Addr:    "0.0.0.0:8080",
        Handler: r,
    }
    go func() {
        fmt.Println("Server running at port:", srv.Addr)
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            fmt.Printf("listen: %s\n", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    defer shutdownFn(ctx)
    fmt.Println("Server is shutting down...")
    if err := srv.Shutdown(context.Background()); err != nil {
        fmt.Println("Server forced to shutdown:", err)
    }

    fmt.Println("Server exiting")
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

از Tracer در مدیریت فایل استفاده کنید router/router.go برای اینکه بتوانید سیگنال های Traces را ساطع کنید:

package router

import (
    "encoding/json"
    "net/http"
    "time"

    "github.com/go-chi/chi/v5"
    "github.com/wahyurudiyan/medium/otel-jaeger/pkg/random"
    "go.opentelemetry.io/otel"
)

var (
    tracer = otel.Tracer("WebServer-Otel-Jaeger")
)

func getUserHandler(w http.ResponseWriter, r *http.Request) {
    _, span := tracer.Start(r.Context(), "GetUser")
    defer span.End()

    user := struct {
        Name     string
        Email    string
        Password string
    }{
        Name:     "John Doe",
        Email:    "john@email.com",
        Password: "Super5ecr3t!",
    }
    blob, _ := json.Marshal(&user)

    sleepDuration := time.Duration(time.Millisecond * time.Duration(random.GenerateRandNum()))
    time.Sleep(sleepDuration)

    w.Header().Add("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write(blob)
}

func Router(router chi.Router) {
    router.Get("/user", getUserHandler)
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

استقرار

پیکربندی Docker برای این ساخت وب سرویس، از مکانیسم تصویر ساخت چند مرحله ای در Dockerfile:

FROM golang:1.23.4 AS build
WORKDIR /src
COPY . .
RUN go get -v
RUN CGO_ENABLED=0 go build -o /bin/service .

FROM alpine:latest
WORKDIR /app
COPY --from=build /bin/service /bin/service
CMD ["/bin/service"]
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در مرحله بعد، تصویر ساخت از طریق فایل انجام می شود docker-compose.yaml با پیکربندی زیر:

services:
  web-service:
    container_name: service
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      OTEL_SERVICE_NAME: service-otel-jaeger
      JAEGER_GRPC_ENDPOINT: jaeger:4317
    entrypoint: ["service"]
    ports:
      - 8080:8080

  jaeger:
    container_name: jaeger
    image: jaegertracing/all-in-one:latest
    environment:
      COLLECTOR_ZIPKIN_HOST_PORT: :9411
    ports:
      - 16686:16686 
      - 4317:4317 
      - 4318:4318 
      - 9411:9411
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

روشن service.jaeger.ports، درگاهی که در معرض دید قرار می گیرد پورتی است برای:

  • 16686: داشبورد جیگر
  • 4317: Jaeger OTLP Protobuf با پروتکل gRPC
  • 4318: Jaeger OTLP Protobuf/JSON با پروتکل HTTP
  • 9411: زیپکین کلکسیونر

برنامه ای را که روی آن تعریف شده است اجرا کنید docker-compose.yaml، می توان از دستور استفاده کرد:

docker compose up --build
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پس از اجرا شدن برنامه، می توانید برنامه را در نقطه پایانی ضربه بزنید http://127.0.0.1:8080/api/user، اگر وب سرویس و برنامه متصل باشند، نام سرویس مانند تصویر ظاهر می شود.

service_connected

span ظاهر می شود تا مدت زمان لازم برای اجرای یک فرآیند را مشخص کند.

span_result

تست بارگذاری

حالا بیایید از ابزار CLI استفاده کنیم hey [https://github.com/rakyll/hey] برای اجرای تست بارگذاری برای انجام یک تست بارگذاری ساده می توان از دستور زیر استفاده کرد:

hey -c 100 -z 10m http://127.0.0.1:8080/api/user
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این دستور یک بار تست را برای 100 درخواست در ثانیه (RPS) به مدت 10 دقیقه اجرا می کند. نتایجی که در صفحه Jaeger UI ظاهر می شود مانند زیر خواهد بود.

ردیابی_نتیجه

اگر آزمایش بار کامل شده باشد، گزارشی از نتایج آزمایش بار ارائه می شود.

Summary:
  Total:    600.9545 secs
  Slowest:  1.2674 secs
  Fastest:  0.1005 secs
  Average:  0.5553 secs
  Requests/sec: 179.9071

  Total data:   7568120 bytes
  Size/request: 70 bytes

Response time histogram:
  0.101 [1] |
  0.217 [21210] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.334 [10993] |■■■■■■■■■■■■■■■■■■■■
  0.451 [10719] |■■■■■■■■■■■■■■■■■■■■
  0.567 [10919] |■■■■■■■■■■■■■■■■■■■■
  0.684 [10830] |■■■■■■■■■■■■■■■■■■■■
  0.801 [10749] |■■■■■■■■■■■■■■■■■■■■
  0.917 [21675] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  1.034 [10902] |■■■■■■■■■■■■■■■■■■■■
  1.151 [113]   |
  1.267 [5] |


Latency distribution:
  10% in 0.2009 secs
  25% in 0.3027 secs
  50% in 0.6010 secs
  75% in 0.8028 secs
  90% in 0.9604 secs
  95% in 1.0028 secs
  99% in 1.0069 secs

Details (average, fastest, slowest):
  DNS+dialup:   0.0000 secs, 0.1005 secs, 1.2674 secs
  DNS-lookup:   0.0000 secs, 0.0000 secs, 0.0000 secs
  req write:    0.0000 secs, 0.0000 secs, 0.0237 secs
  resp wait:    0.5552 secs, 0.1005 secs, 1.2660 secs
  resp read:    0.0001 secs, 0.0000 secs, 0.0216 secs

Status code distribution:
  [200] 108116 responses

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پروژه Github

برای کسانی که می خواهند کد کامل را امتحان کنند یا ببینند، می توانند مخزن زیر را شبیه سازی کنند: https://github.com/wahyurudiyan/otel-jaeger.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا