L
Artificial Intelligence & LLM • Literally Web ID

Integrasi Enterprise Large Language Models (LLM) dengan Arsitektur Microservices Go

⏱️ 15 menit baca • 📅 14 Oct 2026 • 👁️ 1 views
Integrasi Enterprise Large Language Models (LLM) dengan Arsitektur Microservices Go

1. Konteks Industri & Analisis Akar Masalah

Integrasi Large Language Models (LLM) ke dalam arsitektur microservices berbasis Go telah menjadi standar emas bagi perusahaan teknologi modern yang ingin menghadirkan fitur-fitur kecerdasan buatan skala enterprise. Namun, tantangan mendasar muncul ketika sistem backend harus berinteraksi dengan API LLM eksternal (seperti OpenAI, Anthropic, atau model open-source lokal seperti Llama 3 via Ollama) di bawah beban konkurensi tinggi. API LLM terkenal memiliki karakteristik latensi yang sangat tinggi (time-to-first-token bisa memakan waktu ratusan milidetik hingga beberapa detik), rentan terhadap rate limiting dari provider, serta mengonsumsi alokasi memori dan bandwidth yang masif jika tidak dikelola dengan benar.

Di dunia nyata, arsitektur yang naif—seperti melakukan synchronous HTTP call dari handler web Go langsung ke endpoint LLM—akan dengan cepat menghabiskan kuota goroutine, menyebabkan connection pool exhaustion, dan memicu efek riak (cascading failure) yang melumpuhkan seluruh infrastruktur microservices. Ketika lonjakan traffic terjadi (misalnya ribuan pengguna mengirimkan prompt bersamaan), server web akan mengalami thread/goroutine pile-up, memori melonjak drastis, dan aplikasi mengalami Out-Of-Memory (OOM) Killer.

Kondisi Sebelum Penerapan (Naive Approach):

  • Pemanggilan API LLM dilakukan secara synchronous di dalam siklus permintaan HTTP (HTTP request lifecycle).
  • Klien mengalami timeout karena latensi LLM yang lambat, menyebabkan pemborosan sumber daya server web.
  • Tidak ada mekanisme proteksi saat penyedia LLM mengalami down atau rate limit, mengakibatkan kegagalan total.
  • Respons yang identik diproses berulang kali oleh LLM, menghabiskan biaya token dan latensi yang tidak perlu.

Kondisi Sesudah Penerapan (Enterprise Microservices Approach):

  • Arsitektur berbasis Asynchronous Worker Pool menggunakan channel Go untuk memisahkan penerimaan request dari eksekusi model.
  • Implementasi Circuit Breaker dan Exponential Backoff Retry untuk memastikan ketahanan sistem (resilience) terhadap gangguan eksternal.
  • Lapisan Redis Caching terdistribusi untuk menyimpan dan melayani kembali respons prompt yang identik secara instan.
  • Manajemen Rate Limiting berbasis token bucket untuk mengontrol penggunaan API secara presisi.

2. Konteks Inti & Deep Dive Arsitektur

Untuk merancang sistem yang handal, kita memerlukan arsitektur berlapis yang memisahkan tanggung jawab (separation of concerns) antara API Gateway, Worker Pool, Caching Layer, dan External LLM Provider. Diagram alur di bawah ini mengilustrasikan bagaimana sebuah request diproses secara asinkron.

+-----------------------------------------------------------------------------------+
|                           GO MICROSERVICES ARCHITECTURE                           |
|                                                                                   |
|  +------------+      +------------------+      +-------------------------------+  |
|  | HTTP Client| ---> |  API Gateway/Gin | ---> | Redis Cache Layer (Check Hit) |  |
|  +------------+      +------------------+      +-------------------------------+  |
|                               |                                |                  |
|                               | (Miss)                         | (Hit)            |
|                               v                                v                  |
|                      +------------------+             +-----------------+         |
|                      | Worker Pool &    |             | Return Cached   |         |
|                      | Job Queue        |             | Response        |         |
|                      +------------------+             +-----------------+         |
|                               |                                                   |
|                               v                                                   |
|                      +------------------+                                         |
|                      | Circuit Breaker  |                                         |
|                      +------------------+                                         |
|                               |                                                   |
|                               v                                                   |
|                      +------------------+                                         |
|                      | External LLM API |                                         |
|                      +------------------+                                         |
+-----------------------------------------------------------------------------------+

Alur Eksekusi (Execution Flow):

  1. Inbound Request: Klien mengirimkan HTTP POST berisi prompt ke endpoint Go microservice.
  2. Cache Verification: Middleware atau handler memeriksa apakah hash dari prompt tersebut sudah ada di Redis. Jika ada (Cache Hit), respons dikembalikan seketika dengan latensi < 5ms.
  3. Asynchronous Dispatching: Jika Cache Miss, job dimasukkan ke dalam antrean Worker Pool berbasis channel Go, dan klien menerima respons status Accepted (202) dengan ID Job untuk polling atau WebSocket stream.
  4. Circuit Breaker & Rate Limiter: Worker mengambil job dari antrean, melewati pemeriksaan Rate Limiter internal dan Circuit Breaker sebelum memanggil API LLM.
  5. Persistence & Caching: Setelah respons diterima dari LLM, sistem menyimpannya ke Redis dan memperbarui status job di database atau memory store.

3. Perbandingan Komparatif / Analisis Pendekatan

Parameter / Aspek Pendekatan Synchronous Naif Pendekatan Asinkron + Worker Pool + Redis Dampak Infrastruktur
Latensi Respons Klien Tinggi (> 3000ms, terikat LLM) Cepat (202 Accepted) atau Instan (Cache) Mengurangi beban timeout HTTP server
Konsumsi Memori (RAM) Sangat Tinggi (Goroutine menumpuk) Stabil & Terkontrol (Fixed Worker Size) Mencegah OOM Killer pada container Docker
Resilience / Ketahanan Rendah (Gagal jika LLM down) Tinggi (Circuit Breaker + Retry) Menjamin ketersediaan sistem 99.9%
Biaya API LLM Boros (Setiap prompt dihitung ulang) Efisien (Reduksi hingga 40% berkat Redis) Penghematan operasional cloud yang signifikan
Kompleksitas Kode Rendah / Sederhana Menengah - Tinggi (Butuh pemahaman Concurrency) Memerlukan testing integrasi yang matang

[!WARNING] Jangan pernah membiarkan goroutine spawning tak terbatas (go processLLM()) untuk setiap request masuk. Ini adalah jebakan klasik yang akan meruntuhkan aplikasi Go Anda akibat kehabisan memori RAM saat traffic melonjak.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum masuk ke implementasi kode, pastikan environment pengembangan Anda memenuhi spesifikasi berikut:

  • Go Version: Go 1.21 atau lebih tinggi.
  • Redis Server: Redis 7.0+ (mendukung ACL dan JSON module jika diperlukan).
  • OS: Linux (Ubuntu 22.04 LTS / Debian 12) atau macOS untuk development, dengan dukungan systemd.
  • Dependencies Go Utama:
    • github.com/gin-gonic/gin (Web Framework)
    • github.com/redis/go-redis/v9 (Redis Client)
    • github.com/sony/gobreaker (Circuit Breaker)
    • golang.org/x/time/rate (Rate Limiter)

Inisialisasi modul Go baru di terminal Anda:

mkdir go-llm-microservice
cd go-llm-microservice
go mod init github.com/enterprise/go-llm-microservice
go get github.com/gin-gonic/gin
go get github.com/redis/go-redis/v9
go get github.com/sony/gobreaker
go get golang.org/x/time/rate

5. Panduan Implementasi Langkah demi Langkah

Berikut adalah implementasi kode lengkap yang siap produksi, mencakup manajemen worker pool, integrasi Redis caching, circuit breaker, dan rate limiting.

Struktur File Proyek

.|
+-- go.mod
+-- go.sum
+-- main.go
+-- worker/pool.go
+-- cache/redis.go
+-- llm/client.go

1. cache/redis.go - Manajemen Caching Redis

package cache

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"time"

	"github.com/redis/go-redis/v9"
)

type RedisCache struct {
	Client *redis.Client
	TTL    time.Duration
}

func NewRedisCache(addr string, ttl time.Duration) *RedisCache {
	rdb := redis.NewClient(&redis.Options{
		Addr:     addr,
		Password: "", // Gunakan password di produksi
		DB:       0,
	})
	return &RedisCache{
		Client: rdb,
		TTL:    ttl,
	}
}

func (r *RedisCache) HashKey(prompt string) string {
	hasher := sha256.New()
	hasher.Write([]byte(prompt))
	return hex.EncodeToString(hasher.Sum(nil))
}

func (r *RedisCache) Get(ctx context.Context, prompt string) (string, error) {
	key := r.HashKey(prompt)
	val, err := r.Client.Get(ctx, key).Result()
	return val, err
}

func (r *RedisCache) Set(ctx context.Context, prompt string, response string) error {
	key := r.HashKey(prompt)
	return r.Client.Set(ctx, key, response, r.TTL).Err()
}

2. llm/client.go - Klien LLM dengan Circuit Breaker

package llm

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"time"

	"github.com/sony/gobreaker"
)

type LLMClient struct {
	APIKey     string
	Endpoint   string
	HTTPClient *http.Client
	Breaker    *gobreaker.CircuitBreaker
}

type RequestPayload struct {
	Model  string `json:"model"`
	Prompt string `json:"prompt"`
}

type ResponsePayload struct {
	Response string `json:"response"`
}

func NewLLMClient(apiKey, endpoint string) *LLMClient {
	var st gobreaker.Settings

st.Name = "LLM-API-Breaker"
st.MaxRequests = 3
st.Interval = 10 * time.Second
st.Timeout = 30 * time.Second
st.ReadyToTrip = func(counts gobreaker.Counts) bool {
	failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
	return counts.Requests >= 3 && failureRatio >= 0.6
}

	client := &LLMClient{
		APIKey:   apiKey,
		Endpoint: endpoint,
		HTTPClient: &http.Client{
			Timeout: 20 * time.Second,
		},
		Breaker: gobreaker.NewCircuitBreaker(st),
	}

	return client
}

func (c *LLMClient) CallLLM(ctx context.Context, prompt string) (string, error) {
	body, err := json.Marshal(RequestPayload{
		Model:  "llama3",
		Prompt: prompt,
	})
	if err != nil {
		return "", err
	}

	result, err := c.Breaker.Execute(func() (interface{}, error) {
		req, err := http.NewRequestWithContext(ctx, "POST", c.Endpoint, bytes.NewBuffer(body))
		if err != nil {
			return "", err
		}
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.APIKey))

		resp, err := c.HTTPClient.Do(req)
		if err != nil {
			return "", err
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			return "", fmt.Errorf("llm api error status: %d", resp.StatusCode)
		}

		var resPayload ResponsePayload
		if err := json.NewDecoder(resp.Body).Decode(&resPayload); err != nil {
			return "", err
		}

		return resPayload.Response, nil
	})

	if err != nil {
		return "", err
	}

	return result.(string), nil
}

3. worker/pool.go - Implementasi Worker Pool Asinkron

package worker

import (
	"context"
	"fmt"
	"log"
	"sync"

	"github.com/enterprise/go-llm-microservice/cache"
	"github.com/enterprise/go-llm-microservice/llm"
)

type Job struct {
	ID     string
	Prompt string
	Result chan JobResult
}

type JobResult struct {
	Response string
	Error    error
}

type WorkerPool struct {
	WorkerCount int
	JobQueue    chan Job
	LLMClient   *llm.LLMClient
	Cache       *cache.RedisCache
	Wg          sync.WaitGroup
}

func NewWorkerPool(workerCount int, queueSize int, llmClient *llm.LLMClient, cache *cache.RedisCache) *WorkerPool {
	return &WorkerPool{
		WorkerCount: workerCount,
		JobQueue:    make(chan Job, queueSize),
		LLMClient:   llmClient,
		Cache:       cache,
	}
}

func (wp *WorkerPool) Start(ctx context.Context) {
	for i := 1; i <= wp.WorkerCount; i++ {
		wp.Wg.Add(1)
		go wp.worker(ctx, i)
	}
}

func (wp *WorkerPool) worker(ctx context.Context, id int) {
	defer wp.Wg.Done()
	for {
		select {
		case <-ctx.Done():
			log.Printf("Worker %d shutting down...", id)
			return
		case job, ok := <-wp.JobQueue:
			if !ok {
				return
			}
			
			// 1. Cek Redis Cache terlebih dahulu
			cachedResp, err := wp.Cache.Get(ctx, job.Prompt)
			if err == nil && cachedResp != "" {
				job.Result <- JobResult{Response: cachedResp, Error: nil}
				continue
			}

			// 2. Panggil LLM jika Cache Miss
			resp, err := wp.LLMClient.CallLLM(ctx, job.Prompt)
			if err != nil {
				job.Result <- JobResult{Response: "", Error: err}
				continue
			}

			// 3. Simpan ke Redis Cache
			_ = wp.Cache.Set(ctx, job.Prompt, resp)

			job.Result <- JobResult{Response: resp, Error: nil}
		}
	}
}

func (wp *WorkerPool) Stop() {
	close(wp.JobQueue)
	wp.Wg.Wait()
}

4. main.go - Entrypoint & Web Server (Gin Framework)

package main

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

	"github.com/enterprise/go-llm-microservice/cache"
	"github.com/enterprise/go-llm-microservice/llm"
	"github.com/enterprise/go-llm-microservice/worker"
	"github.com/gin-gonic/gin"
	"golang.org/x/time/rate"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Inisialisasi Redis Cache (TTL 24 Jam)
	redisCache := cache.NewRedisCache("localhost:6379", 24*time.Hour)

	// Inisialisasi LLM Client
	llmClient := llm.NewLLMClient("dummy-api-key", "http://localhost:11434/api/generate")

	// Inisialisasi Worker Pool (10 Workers, Queue Size 100)
	wp := worker.NewWorkerPool(10, 100, llmClient, redisCache)
	wp.Start(ctx)
	defer wp.Stop()

	r := gin.Default()

	// Rate Limiter Middleware (10 request per detik per IP, Burst 20)
	limiter := rate.NewLimiter(10, 20)
	r.Use(func(c *gin.Context) {
		if !limiter.Allow() {
			c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded\
🏷️ Tags: #Nginx #High Concurrency #Performance #Sysadmin #Web Server