L
Observability & Monitoring • Literally Web ID

Observability & Distributed Tracing di Lingkungan Microservices Berbasis Kubernetes

⏱️ 25 menit baca • 📅 16 Oct 2026 • 👁️ 1 views
Observability & Distributed Tracing di Lingkungan Microservices Berbasis Kubernetes

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap arsitektur modern, transisi dari monolit tradisional ke arsitektur microservices berbasis Kubernetes telah membawa skalabilitas, otonomi tim, dan ketahanan sistem yang luar biasa. Namun, pergeseran ini memunculkan kompleksitas operasional yang masif, terutama dalam hal visibilitas sistem. Ketika sebuah permintaan (request) HTTP masuk ke edge proxy, diteruskan melalui API Gateway, memvalidasi token di Auth Service, mengambil data profil dari User Service, memproses keranjang belanja di Cart Service, dan akhirnya memanggil Payment Gateway, eksekusi tersebut melintasi puluhan atau bahkan ratusan batas jaringan (network boundaries).

Di dunia nyata, bottleneck performa jarang terjadi karena satu kueri database yang lambat secara kasat mata. Seringkali, degradasi latensi bersifat kumulatif: network hops yang berlebihan, antrean pada thread pool di backend Node.js atau Go, lock contention pada database MySQL/MariaDB, hingga latensi I/O dari penyimpanan berbasis S3/MinIO atau pembacaan cache Redis yang tidak efisien. Tanpa mekanisme observability yang tepat, proses debugging menjadi mimpi buruk bagi SRE dan DevOps Engineer. Tim sering terjebak dalam blame game antar-tim karena tidak memiliki data empiris untuk menentukan layanan mana yang mengalami kegagalan.

Sebelum vs Sesudah Penerapan

  • Sebelum (Pendekatan Tradisional / Sileod Logging):

    • Debugging mengandalkan log teks terpusat (Elasticsearch/Fluentd/Kibana) dengan pencarian berbasis string manual.
    • Korelasi antar-layanan dilakukan secara manual menggunakan X-Request-ID yang seringkali putus di tengah jalan karena kelalaian pengembang meneruskan header HTTP.
    • MTTR (Mean Time To Resolution) memakan waktu berjam-jam atau bahkan berhari-hari hanya untuk menemukan akar masalah dari timeout.
    • Visibilitas latensi lintas jaringan bersifat buta; tidak diketahui persis di layanan mana waktu eksekusi habis.
  • Sesudah (Penerapan Distributed Tracing dengan OpenTelemetry & Grafana Tempo):

    • Setiap request diberi trace ID unik di titik masuk terluar (Edge/Ingress Nginx/Cloudflare) dan disebarkan secara otomatis ke seluruh ekosistem backend (Go, PHP, Node.js).
    • Visualisasi waterfall chart memperlihatkan durasi presisi dari setiap fungsi, kueri SQL, panggilan Redis, hingga request HTTP eksternal.
    • Metrik latensi dan error rates langsung terhubung ke Prometheus, memungkinkan alerting otomatis sebelum pengguna merasakan degradasi layanan.
    • MTTR berkurang drastis dari jam ke menit berkat kemampuan melacak root cause secara instan.

2. Konsep Inti & Deep Dive Arsitektur

Distributed tracing bekerja dengan merekam jalur perjalanan sebuah permintaan saat melintasi berbagai layanan. Komponen fundamental dalam ekosistem ini terdiri dari Trace, Span, Context Propagation, dan OpenTelemetry Collector.

  • Trace: Representasi menyeluruh dari perjalanan sebuah request dari awal hingga akhir. Sebuah trace terdiri dari kumpulan span yang membentuk struktur pohon (tree structure).
  • Span: Unit kerja tunggal yang memiliki nama, timestamp mulai, durasi, atribut key-value, dan status. Span dapat memiliki hubungan parent-child.
  • Context Propagation: Mekanisme untuk menyebarkan konteks trace (Trace ID dan Span ID) lintas batas proses dan jaringan, biasanya menggunakan header HTTP seperti traceparent (standar W3C Trace Context).
[Client Browser / CDN Cloudflare]
             │
             ▼ (HTTP Request + W3C Traceparent Header)
[Kubernetes Ingress / Nginx Proxy]
             │
             ├──────────────────────────┐
             ▼                          ▼
[API Gateway (Go)]          [Auth Service (Node.js)]
   │                                    │
   ├──> [MySQL Database]                └──> [Redis Cache]
   └──> [Payment Service (PHP-FPM)]
             │
             └──> [S3 / MinIO Storage]

Alur data di dalam kluster Kubernetes berjalan melalui OpenTelemetry (OTel) Collector yang bertindak sebagai agen perantara. Aplikasi menginstrumentasi diri menggunakan OTel SDK, mengekspor data span ke OTel Collector DaemonSet atau Deployment lokal melalui gRPC atau HTTP, yang kemudian meneruskannya ke backend penyimpanan seperti Grafana Tempo untuk trace dan Prometheus untuk metrik.


3. Perbandingan Komparatif / Analisis Pendekatan

Dalam membangun infrastruktur observability, pemilihan alat dan protokol sangat menentukan efisiensi sumber daya kluster Kubernetes. Berikut adalah perbandingan pendekatan pengumpulan dan penyimpanan data tracing.

Parameter Komparasi OpenTelemetry (OTel) Standard Jaeger (Native Protocol) Zipkin (Legacy Protocol)
Standarisasi CNCF Standard (Vendor Neutral) CNCF / Jaeger Native OpenTracing Successor
Dukungan Bahasa Hampir Semua Bahasa (Go, PHP, Node, Python, Java) Luas, tetapi fokus pada ekosistem spesifik Terbatas pada bahasa modern tertentu
Overhead CPU/RAM Rendah - Menengah (Tergantung Sampling Rate) Menengah Tinggi pada volume besar
Format Data OTLP (OpenTelemetry Protocol) Jaeger Thrift / OTLP JSON / Thrift
Integrasi Ekosistem Universal (Prometheus, Grafana, Tempo, Datadog) Fokus pada Jaeger UI Fokus pada Zipkin UI

[!TIP] Gunakan selalu protokol OTLP (OpenTelemetry Protocol) melalui gRPC untuk komunikasi antara OTel SDK, Collector, dan Backend (Grafana Tempo). OTLP jauh lebih efisien dalam penggunaan bandwidth jaringan dan CPU dibandingkan format JSON tradisional.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum melangkah ke implementasi, pastikan lingkungan Kubernetes Anda memenuhi spesifikasi berikut:

  • Kubernetes Cluster: Versi 1.25 atau lebih tinggi.
  • Resource Minimal Node: Minimal 3 Worker Node dengan spesifikasi 4 vCPU / 8 GB RAM per node untuk menjalankan aplikasi demo beserta stack observabilitas.
  • Tooling CLI: kubectl, helm (v3+).
  • Namespace Terpisah: Buat namespace khusus observability untuk mengisolasi komponen monitoring.

Jalankan perintah berikut untuk menyiapkan namespace:

kubectl create namespace observability
kubectl create namespace microservices-demo

5. Panduan Implementasi Langkah demi Langkah

Langkah 1: Deploy Prometheus dan Grafana Tempo menggunakan Helm

Kita akan menginstal kube-prometheus-stack dan Grafana Tempo di namespace observability.

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

# Install Prometheus & Grafana
helm install prometheus-stack prometheus-community/kube-prometheus-stack \
  --namespace observability \
  --set grafana.enabled=true

# Install Grafana Tempo
helm install tempo grafana/tempo \
  --namespace observability \
  --set tempo.searchEnabled=true

Langkah 2: Deploy OpenTelemetry Collector sebagai DaemonSet

Buat file manifest otel-collector.yaml untuk mengumpulkan trace dari seluruh pod dalam kluster.

apiVersion: v1
kind: ConfigMap
metadata:
  name: otel-collector-conf
  namespace: observability
data:
  otel-collector-config.yaml: |
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318
    processors:
      batch:
        send_batch_size: 8192
        timeout: 1s
        send_batch_max_size: 10240
      memory_limiter:
        check_interval: 1s
        limit_percentage: 80
        spike_limit_percentage: 20
    exporters:
      otlp:
        endpoint: "tempo.observability.svc.cluster.local:4317"
        tls:
          insecure: true
    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, batch]
          exporters: [otlp]
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: otel-collector
  namespace: observability
spec:
  selector:
    matchLabels:
      app: otel-collector
  template:
    metadata:
      labels:
        app: otel-collector
    spec:
      containers:
      - name: otel-collector
        image: otel/opentelemetry-collector-contrib:0.95.0
        resources:
          limits:
            cpu: 500m
            memory: 512Mi
          requests:
            cpu: 100m
            memory: 128Mi
        args: ["--config=/conf/otel-collector-config.yaml"]
        volumeMounts:
        - name: config-volume
          mountPath: /conf
      volumes:
      - name: config-volume
        configMap:
          name: otel-collector-conf
---
apiVersion: v1
kind: Service
metadata:
  name: otel-collector
  namespace: observability
spec:
  ports:
  - name: otlp-grpc
    port: 4317
    targetPort: 4317
  - name: otlp-http
    port: 4318
    targetPort: 4318
  selector:
    app: otel-collector

Apply manifest tersebut:

kubectl apply -f otel-collector.yaml

Langkah 3: Implementasi OpenTelemetry pada Aplikasi Backend (Go)

Berikut adalah contoh kode implementasi OpenTelemetry SDK pada layanan berbasis bahasa Go yang terhubung ke MySQL dan Redis.

package main

import (
	"context"
	"database/sql"
	"fmt" 
	"log"
	"net/http"

	_ "github.com/go-sql-driver/mysql"
	"github.com/redis/go-redis/v9"
	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
	"go.opentelemetry.io/otel/propagation" 
	"go.opentelemetry.io/otel/sdk/resource"
	semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	"google.golang.org/grpc"
)

var rdb *redis.Client
var db *sql.DB

func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
	exporter, err := otlptracegrpc.New(ctx,
		otlptracegrpc.WithInsecure(),
		otlptracegrpc.WithEndpoint("otel-collector.observability.svc.cluster.local:4317"),
		otlptracegrpc.WithDialOption(grpc.WithBlock()),
	)
	if err != nil {
		return nil, fmt.Errorf("failed to create trace exporter: %w", err)
	}

	res, err := resource.New(ctx,
		resource.WithAttributes(
			semconv.ServiceNameKey.String("order-service"),
			semconv.ServiceVersionKey.String("1.0.0"),
		),
	)
	if err != nil {
		return nil, fmt.Errorf("failed to create resource: %w", err)
	}

	tp := sdktrace.NewTracerProvider(
		sdktrace.WithSampler(sdktrace.AlwaysSample()),
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(res),
	)

	otel.SetTracerProvider(tp)
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))

	return tp, nil
}

func main() {
	ctx := context.Background()
	tp, err := initTracer(ctx)
	if err != nil {
		log.Fatalf("Failed to initialize tracer: %v", err)
	}
	defer func() {
		if err := tp.Shutdown(ctx); err != nil {
			log.Printf("Error terminating TracerProvider: %v", err)
		}
	}()

	// Inisialisasi Redis
	rdb = redis.NewClient(&redis.Options{
		Addr: "redis-master.microservices-demo.svc.cluster.local:6379",
	})

	// Inisialisasi MySQL
	var errDB error
	db, errDB = sql.Open("mysql", "root:secret@tcp(mysql.microservices-demo.svc.cluster.local:3306)/orders_db")
	if errDB != nil {
		log.Fatalf("Failed to connect to MySQL: %v", errDB)
	}

	handler := http.HandlerFunc(handleOrder)
	wrappedHandler := otelhttp.NewHandler(handler, "HandleOrder")

	log.Println("Order Service starting on port 8080...")
	if err := http.ListenAndServe(":8080", wrappedHandler); err != nil {
		log.Fatalf("Server failed: %v", err)
	}
}

func handleOrder(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	tr := otel.Tracer("order-service")
	_, span := tr.Start(ctx, "ProcessOrderLogic")
	defer span.End()

	// Simulasi kueri database
	_, err := db.ExecContext(ctx, "INSERT INTO orders (status) VALUES ('pending')")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Simulasi simpan cache Redis
	err = rdb.Set(ctx, "last_order", "pending", 0).Err()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"status":"success"}`)) 
}

6. Bedah Kode & Penjelasan Detail Baris Kunci

  1. otlptracegrpc.New(...): Menginisialisasi koneksi gRPC ke OpenTelemetry Collector. Penggunaan WithInsecure() sah dilakukan di dalam jaringan privat Kubernetes (ClusterIP), menghindari overhead manajemen sertifikat TLS antar-service mesh.
  2. propagation.NewCompositeTextMapPropagator(...): Memastikan header W3C (traceparent) otomatis dibaca dari request HTTP masuk dan diteruskan ke panggilan HTTP/gRPC keluar berikutnya. Tanpa baris ini, rantai tracing akan terputus (broken trace).
  3. otelhttp.NewHandler(...): Middleware otomatis dari OpenTelemetry yang mencatat metrik HTTP standar (durasi request, status code, ukuran payload) sekaligus mengekstrak konteks trace dari header HTTP klien.
  4. sdktrace.WithSampler(sdktrace.AlwaysSample()): Menentukan kebijakan pengambilan sampel (sampling rate). Di lingkungan produksi dengan beban tinggi (ribuan RPS), AlwaysSample() harus diganti dengan TraceIDRatioBased(0.1) (10%) untuk menghemat kapasitas penyimpanan Tempo dan beban CPU.

7. Skenario Produksi & Studi Kasus Dunia Nyata

Dalam sebuah platform e-commerce berskala besar yang menangani 20.000 Request Per Second (RPS) di Kubernetes, lonjakan traffic saat flash sale sering memicu kegagalan sistem yang sulit diisolasi.

Studi Kasus: Pengguna melaporkan kegagalan checkout (HTTP 504 Gateway Timeout).

  • Investigasi Grafana Tempo & Prometheus:
    1. SRE membuka Grafana dan mencari trace dengan error status 504.
    2. Melalui waterfall view, terlihat bahwa request masuk ke Cart Service (Node.js), diteruskan ke Payment Service (Go), yang kemudian memanggil layanan eksternal Payment Gateway pihak ketiga.
    3. Span pada panggilan HTTP ke Payment Gateway menunjukkan durasi memakan waktu persis 30.000ms (timeout limit).
    4. Namun, ditemukan anomali: Payment Service melakukan retrying secara tidak sengaja sebanyak 3 kali secara sekuensial sebelum gagal, memperparah beban antrean thread pool.
  • Solusi Penerapan:
    • Menerapkan circuit breaker menggunakan pustaka seperti Gobreaker pada integrasi pihak ketiga.
    • Menambahkan batas retry timeout yang lebih agresif.
    • Menggunakan Redis sebagai pembatas rate limiter di API Gateway.

8. Common Pitfalls, Anti-Patterns & Solusi

  • Anti-Pattern 1: Terlalu Banyak Sampling (AlwaysSample di Produksi)
    • Dampak: Konsumsi memori OTel Collector melonjak, penyimpanan Grafana Tempo membengkak dengan cepat, dan terjadi network saturation.
    • Solusi: Terapkan probabilistic sampling (misal 5% hingga 10%) untuk traffic normal, dan gunakan tail-based sampling di OTel Collector untuk memastikan trace yang menghasilkan error (HTTP 5xx) tetap disimpan 100%.
  • Anti-Pattern 2: Tidak Menggunakan Context Propagation pada Antrean Asinkron (RabbitMQ / Kafka)
    • Dampak: Trace terputus saat request dimasukkan ke dalam antrean (message broker), sehingga konsumen (consumer) dianggap sebagai awal trace baru.
    • Solusi: Inject konteks OpenTelemetry ke dalam metadata/headers pesan saat produsen mengirim pesan, dan ekstrak kembali saat konsumen memproses pesan tersebut.
  • Anti-Pattern 3: Kardinalitas Atribut Span yang Terlalu Tinggi
    • Dampak: Memasukkan nilai unik seperti User Email atau Transaction UUID langsung ke dalam metrik Prometheus (metric cardinality explosion), menyebabkan memori Prometheus jebol.
    • Solusi: Simpan atribut bernilai kardinalitas tinggi di Grafana Tempo (Spans/Traces), bukan di metrik Prometheus.

9. Benchmark, Metrik Kinerja & Tuning Performa

Untuk mengukur overhead dari agen OpenTelemetry pada aplikasi backend, dilakukan pengujian beban (load testing) menggunakan k6 pada pod Go Service dengan skenario 5.000 Virtual Users (VUs) selama 10 menit.

Hasil Benchmark:

  • Tanpa OpenTelemetry SDK:
    • Throughput: 12,450 req/sec
    • P99 Latency: 12ms
    • CPU Usage Pod: 1.2 vCPU
  • Dengan OpenTelemetry SDK (AlwaysSample, gRPC Exporter ke Collector Lokal):
    • Throughput: 11,980 req/sec (-3.7% penurunan kinerja)
    • P99 Latency: 14ms
    • CPU Usage Pod: 1.4 vCPU (+0.2 vCPU overhead)
  • Dengan OpenTelemetry SDK (10% Probabilistic Sampling):
    • Throughput: 12,350 req/sec
    • P99 Latency: 12.5ms
    • CPU Usage Pod: 1.25 vCPU

[!WARNING] Hindari menggunakan HTTP JSON Exporter di dalam container produksi karena proses serialisasi JSON memakan CPU jauh lebih besar dibandingkan protokol biner gRPC (OTLP).


10. Checklist Penerapan Produksi & Kesimpulan

Sebelum meluncurkan arsitektur distributed tracing ke lingkungan produksi, pastikan seluruh item berikut telah terpenuhi:

  • OpenTelemetry Collector dikonfigurasi menggunakan DaemonSet agar tidak menjadi titik kegagalan tunggal (single point of failure).
  • Kebijakan Sampling Rate telah disesuaikan (tidak menggunakan AlwaysSample pada traffic tinggi).
  • W3C Trace Context propagation diaktifkan pada seluruh HTTP client/server middleware di semua bahasa pemrograman layanan.
  • Penyimpanan Grafana Tempo telah dikonfigurasi dengan Retention Policy yang sesuai (misal: 7 hari) dan terhubung ke S3/MinIO compatible object storage.
  • Metrik latensi (P50, P95, P99) dari Prometheus terintegrasi dengan sistem alerting (Alertmanager / PagerDuty).
  • Uji beban (load test) telah dilakukan untuk memvalidasi bahwa overhead OTel SDK tidak melebihi ambang batas performa aplikasi.

Dengan mengintegrasikan OpenTelemetry, Prometheus, dan Grafana Tempo secara tepat, kompleksitas arsitektur microservices dapat dijinakkan, memberikan tim kejelasan operasional yang akurat, cepat, dan terukur.

🏷️ Tags: #Microservices #Kubernetes #Opentelemetry #Prometheus #Grafana Tempo