Kronologi Insiden: Worker Pool Starvation pada fasthttp
Rilis integrasi layanan downstream pada microservice gateway berbasis Go Fiber memicu lonjakan p99 latency dari 15ms menjadi 30 detik dalam kurun waktu 3 menit pasca-deployment. Go Fiber berjalan di atas engine fasthttp yang mengalokasikan goroutine dari worker pool internal (secara default menampung hingga 256.000 worker aktif). Karakteristik model konkurensi ini sangat efisien untuk operasi I/O non-blocking, tetapi rentan terhadap fenomena worker starvation.
Ketika layanan downstream mengalami degradasi response time, koneksi HTTP client outbound tertahan tanpa timeout yang membatasi. Setiap incoming request ke Go Fiber menahan satu worker thread dari pool untuk menunggu respons downstream. Dalam hitungan detik, seluruh worker pool terisi penuh oleh koneksi macet. Akibatnya, runtime fasthttp menolak koneksi TCP baru, menyebabkan klien eksternal menerima HTTP 503 Service Unavailable dan lonjakan drastis pada connection reset.
Deteksi Masalah: Observabilitas OTel dan Metrik Prometheus
Analisis kegagalan dilakukan dengan mengkorelasikan metrik HTTP duration pada Prometheus dan distributed tracing OpenTelemetry (OTel). Dashboard Grafana mendeteksi anomali melalui query latensi p99:
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{app="fiber-gateway"}[1m])) by (le))Grafik menunjukkan kurva eksponensial bersamaan dengan drop-off metrik fasthttp_open_connections yang menyentuh batas saturasi. Untuk mengidentifikasi titik kemacetan, trace context diekstraksi menggunakan tracing middleware OpenTelemetry.
// Contoh trace context propagation pada fasthttp request
func callDownstream(ctx context.Context, c *fiber.Ctx, url string) ([]byte, error) {
tr := otel.Tracer("fiber-gateway")
ctx, span := tr.Start(ctx, "callDownstream")
defer span.End()
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
req.SetRequestURI(url)
req.Header.SetMethod(fiber.MethodGet)
// Inject OTel trace headers ke downstream call
otel.GetTextMapPropagator().Inject(ctx, &fasthttpHeaderCarrier{h: &req.Header})
err := client.Do(req, resp)
if err != nil {
span.RecordError(err)
return nil, err
}
return resp.Body(), nil
}Flamegraph OpenTelemetry membuktikan bahwa 99.4% durasi request tertahan pada span downstream HTTP invocation. Gateway menunggu response payload dari service downstream yang mengalami lockup database.
Mitigasi Darurat: Automated Rollback via Kubernetes
Langkah pertama mitigasi kegagalan masif adalah menghentikan traffic ke pod yang bermasalah. Intervensi manual via terminal:
kubectl rollout undo deployment/fiber-gateway -n productionStatus rollback dipantau via kubectl rollout status. Untuk mencegah eskalasi manual di masa depan, trigger mitigasi diintegrasikan ke dalam analisis Prometheus SLI menggunakan resource Argo Rollouts:
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: p99-latency-check
spec:
metrics:
- name: p99-latency
interval: 30s
failureLimit: 1
provider:
prometheus:
address: http://prometheus-server.monitoring:9090
query: |
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{app="fiber-gateway"}[1m])) by (le)) < 2.0Jika p99 latency melebihi 2 detik selama interval 30 detik pada deployment canary, controller langsung mengaborsi rilis dan mengembalikan traffic ke versi stabil secara otomatis.
Postmortem: Akar Masalah
Investigasi pasca-insiden menyoroti dua kelemahan mendasar pada layer kode:
- Default Client Timeout Tak Terbatas: Instansiasi
fasthttp.Clientdibuat secara implisit menggunakan instance global bawaan (fasthttp.Do) yang tidak menetapkanReadTimeoutdanWriteTimeout. Request downstream dapat tertahan selama durasi OS-level TCP keep-alive (jamak mencapai puluhan menit). - Absensi Circuit Breaker: Tidak ada isolasi dependensi. Kegagalan atau perlambatan pada satu microservice downstream langsung menjalar ke upstream gateway hingga melumpuhkan seluruh rute endpoint lain yang tidak saling terkait.
Pencegahan: Circuit Breaker dan Konfigurasi fasthttp
Langkah pencegahan mutlak memerlukan konfigurasi instance fasthttp.Client secara eksplisit serta pembungkusan remote call ke dalam pola Circuit Breaker menggunakan pustaka sony/gobreaker.
1. Konfigurasi Ketat fasthttp.Client
var client = &fasthttp.Client{
ReadTimeout: 1500 * time.Millisecond,
WriteTimeout: 1500 * time.Millisecond,
MaxIdleConnDuration: 30 * time.Second,
MaxConnsPerHost: 1000,
DisableHeaderNamesNormalizing: true,
}2. Implementasi Middleware / Wrapper sony/gobreaker
package main
import (
"errors"
"time"
"github.com/sony/gobreaker"
"github.com/valyala/fasthttp"
)
var cb *gobreaker.CircuitBreaker
func init() {
settings := gobreaker.Settings{
Name: "DownstreamServiceBreaker",
MaxRequests: 5,
Interval: 10 * time.Second,
Timeout: 5 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= 20 && failureRatio >= 0.5
},
}
cb = gobreaker.NewCircuitBreaker(settings)
}
func ExecuteProtectedCall(req *fasthttp.Request, resp *fasthttp.Response) error {
_, err := cb.Execute(func() (interface{}, error) {
err := client.Do(req, resp)
if err != nil {
return nil, err
}
if resp.StatusCode() >= fasthttp.StatusInternalServerError {
return nil, errors.New("downstream returned 5xx")
}
return nil, nil
})
return err
}3. Definisi Alerting Prometheus
Untuk mendeteksi degradasi downstream sebelum saturasi total pada worker pool terulang, terapkan alert rule berikut:
- alert: DownstreamCircuitBreakerOpen
expr: rate(circuit_breaker_tripped_total{app="fiber-gateway"}[1m]) > 0
for: 30s
labels:
severity: warning
annotations:
summary: "Circuit breaker terbuka pada rute gateway, traffic downstream diputus otomatis."Pola ini mencegah worker pool starvation pada fasthttp: ketika downstream gagal memenuhi batas SLA, breaker langsung mengembalikan fallback error dalam hitungan mikrodetik tanpa menahan resource pool goroutine.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!