Kegagalan upstream package registry sering memicu kegagalan beruntun pada infrastruktur CI/CD dan background worker. Saat ribuan pipeline build berjalan serentak dan registry eksternal mengalami degradasi, seluruh worker akan mengulang request secara agresif. Fenomena thundering herd ini melipatgandakan beban downstream, menghabiskan thread pool, dan memperpanjang masa pemulihan.
Anatomi Masalah: Kasus Insiden RPC AUR pada yay v13
Contoh nyata masalah ini terlihat pada rilis yay v13 yang memicu lonjakan beban masif ke RPC Arch User Repository (AUR). Perubahan logika fetching memicu ratusan ribu panggilan RPC bersamaan tanpa batasan konkurensi terdistribusi yang memadai. AUR mengalami penurunan performa ekstrem hingga downtime, yang kemudian memicu loop retry dari klien dan memperparah pemadaman.
Pola serupa terjadi pada kluster worker internal ketika NPM, PyPI, atau internal artifact store down. Worker build menghabiskan batas retry, membanjiri antrean, dan memicu resource exhaustion pada cluster compute.
Arsitektur Resiliensi Worker Antrean
Untuk menahan guncangan akibat registry outage, pipeline worker membutuhkan lapisan proteksi terstruktur:
- Rate Limiting & Jittered Backoff: Hindari interval retry statis. Terapkan full jitter exponential backoff untuk memecah gelombang request.
- Circuit Breaker: Hentikan request eksternal secara global saat rasio error melampaui ambang batas. Langsung gagalkan atau tahan job sebelum membebani network socket.
- Negative Caching: Simpan response 404 (Not Found) dan 500/503 (Server Error) dengan TTL pendek (misal: 30-60 detik) untuk menghentikan query berulang terhadap artifact yang sama.
- Single-Flight / Distributed Locking: Pastikan hanya satu worker yang mendownload atau mengompilasi package tertentu. Worker lain menunggu lock atau membaca artifact cache hasil worker pertama.
Implementasi Distributed Lock dan Fallback Cache di Go
Kode berikut mengimplementasikan deduplikasi unduhan menggunakan Redis distributed lock, negative caching, dan single-flight execution.
package main
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"github.com/redis/go-redis/v9"
)
var (
ErrPackageNotFound = errors.New("package not found in upstream")
ErrUpstreamDown = errors.New("upstream registry is unavailable")
ErrLockNotAcquired = errors.New("concurrent build in progress, lock busy")
)
type RegistryProxy struct {
rdb *redis.Client
httpClient *http.Client
}
func NewRegistryProxy(rdb *redis.Client) *RegistryProxy {
return &RegistryProxy{
rdb: rdb,
httpClient: &http.Client{
Timeout: 5 * time.Second,
},
}
}
func (p *RegistryProxy) FetchArtifact(ctx context.Context, pkg string) ([]byte, error) {
cacheKey := fmt.Sprintf("artifact:%s", pkg)
negKey := fmt.Sprintf("neg_cache:%s", pkg)
lockKey := fmt.Sprintf("lock:%s", pkg)
// 1. Cek negative cache
negVal, err := p.rdb.Get(ctx, negKey).Result()
if err == nil {
if negVal == "404" {
return nil, ErrPackageNotFound
}
return nil, ErrUpstreamDown
}
// 2. Cek cache lokal/sentral
cached, err := p.rdb.Get(ctx, cacheKey).Bytes()
if err == nil {
return cached, nil
}
// 3. Distributed Lock (Deduplikasi: hanya 1 worker memproses)
acquired, err := p.rdb.SetNX(ctx, lockKey, "worker-id", 30*time.Second).Result()
if err != nil {
return nil, err
}
if !acquired {
// Tunggu sebentar atau kembalikan antrean
return nil, ErrLockNotAcquired
}
defer p.rdb.Del(ctx, lockKey)
// 4. Request ke Upstream Registry
req, _ := http.NewRequestWithContext(ctx, "GET", "https://registry.example.internal/"+pkg, nil)
resp, err := p.httpClient.Do(req)
if err != nil {
// Simpan status registry down dengan TTL pendek
p.rdb.Set(ctx, negKey, "500", 30*time.Second)
return nil, ErrUpstreamDown
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
p.rdb.Set(ctx, negKey, "404", 60*time.Second)
return nil, ErrPackageNotFound
}
if resp.StatusCode != http.StatusOK {
p.rdb.Set(ctx, negKey, fmt.Sprintf("%d", resp.StatusCode), 20*time.Second)
return nil, ErrUpstreamDown
}
// 5. Simpan artifact valid ke storage
artifactData := []byte("artifact-binary-payload") // ganti dengan parsing io.ReadAll
p.rdb.Set(ctx, cacheKey, artifactData, 24*time.Hour)
return artifactData, nil
}
Trade-offs dan Kesalahan Implementasi Umum
Menerapkan mekanisme proteksi ini memiliki beberapa kompromi teknis yang perlu diperhatikan:
- Lock Contention: Jika ratusan worker menunggu kunci yang sama, implementasikan polling backoff dengan jitter saat mengecek ketersediaan data di cache daripada langsung melakukan fail-fast.
- TTL Negative Cache Terlalu Lama: Menyimpan error 500 selama berjam-jam dapat menghambat deployment saat upstream sudah pulih. Batasi negative cache upstream error di kisaran 15-60 detik.
- Deadlock Akibat Crash: Pastikan distributed lock selalu memiliki TTL otomatis yang realistis agar lock terlepas meski worker mengalami segfault atau OOM killed.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!