Microservices

Microservices adalah arsitektur yang membagi aplikasi menjadi layanan-layanan kecil yang independen. Go sangat cocok untuk membangun microservices karena performa dan concurrency-nya yang baik.

Contoh Masalah

Bagaimana cara:

  1. Design microservices
  2. Service discovery
  3. Load balancing
  4. Circuit breaking

Penyelesaian

Struktur project:

microservices/
├── api-gateway/
│   └── main.go
├── user-service/
│   └── main.go
├── order-service/
│   └── main.go
└── common/
    ├── config/
    │   └── config.go
    ├── middleware/
    │   └── middleware.go
    └── model/
        └── model.go

common/model/model.go:

package model

import "time"

type User struct {
    ID        string    `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"createdAt"`
}

type Order struct {
    ID        string    `json:"id"`
    UserID    string    `json:"userId"`
    Items     []Item    `json:"items"`
    Total     float64   `json:"total"`
    Status    string    `json:"status"`
    CreatedAt time.Time `json:"createdAt"`
}

type Item struct {
    ID       string  `json:"id"`
    Name     string  `json:"name"`
    Price    float64 `json:"price"`
    Quantity int     `json:"quantity"`
}

type ServiceHealth struct {
    Status    string `json:"status"`
    Timestamp string `json:"timestamp"`
}

common/config/config.go:

package config

import (
    "encoding/json"
    "os"
    "sync"
)

type Config struct {
    ServiceName      string   `json:"serviceName"`
    Port            int      `json:"port"`
    Dependencies    []string `json:"dependencies"`
    Database        DBConfig `json:"database"`
    CircuitBreaker  CBConfig `json:"circuitBreaker"`
}

type DBConfig struct {
    Host     string `json:"host"`
    Port     int    `json:"port"`
    User     string `json:"user"`
    Password string `json:"password"`
    DBName   string `json:"dbName"`
}

type CBConfig struct {
    Threshold   int           `json:"threshold"`
    Timeout     time.Duration `json:"timeout"`
    MaxRequests int           `json:"maxRequests"`
}

var (
    config *Config
    once   sync.Once
)

func Load(path string) (*Config, error) {
    var err error
    once.Do(func() {
        file, err := os.Open(path)
        if err != nil {
            return
        }
        defer file.Close()

        config = &Config{}
        err = json.NewDecoder(file).Decode(config)
    })
    return config, err
}

common/middleware/middleware.go:

package middleware

import (
    "context"
    "log"
    "net/http"
    "time"
)

type Middleware func(http.Handler) http.Handler

// Circuit breaker
type CircuitBreaker struct {
    threshold   int
    failures    int
    lastFailure time.Time
    timeout     time.Duration
    mu         sync.RWMutex
}

func NewCircuitBreaker(threshold int,
    timeout time.Duration) *CircuitBreaker {
    return &CircuitBreaker{
        threshold: threshold,
        timeout:   timeout,
    }
}

func (cb *CircuitBreaker) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter,
        r *http.Request) {
        if !cb.canRequest() {
            http.Error(w, "Service unavailable",
                http.StatusServiceUnavailable)
            return
        }

        next.ServeHTTP(w, r)
    })
}

func (cb *CircuitBreaker) canRequest() bool {
    cb.mu.RLock()
    defer cb.mu.RUnlock()

    if cb.failures >= cb.threshold {
        if time.Since(cb.lastFailure) > cb.timeout {
            cb.failures = 0
            return true
        }
        return false
    }
    return true
}

func (cb *CircuitBreaker) RecordFailure() {
    cb.mu.Lock()
    defer cb.mu.Unlock()

    cb.failures++
    cb.lastFailure = time.Now()
}

// Logging middleware
func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter,
        r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf(
            "%s %s %s",
            r.Method,
            r.RequestURI,
            time.Since(start),
        )
    })
}

// Tracing middleware
func Tracing(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter,
        r *http.Request) {
        traceID := r.Header.Get("X-Trace-ID")
        if traceID == "" {
            traceID = fmt.Sprintf("%d", time.Now().UnixNano())
        }

        ctx := context.WithValue(r.Context(), "traceID", traceID)
        w.Header().Set("X-Trace-ID", traceID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

user-service/main.go:

package main

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

    "microservices/common/config"
    "microservices/common/middleware"
    "microservices/common/model"
)

type UserService struct {
    users map[string]model.User
    mu    sync.RWMutex
}

func NewUserService() *UserService {
    return &UserService{
        users: make(map[string]model.User),
    }
}

func (s *UserService) GetUser(w http.ResponseWriter,
    r *http.Request) {
    id := r.URL.Query().Get("id")
    if id == "" {
        http.Error(w, "Missing user ID", http.StatusBadRequest)
        return
    }

    s.mu.RLock()
    user, exists := s.users[id]
    s.mu.RUnlock()

    if !exists {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }

    json.NewEncoder(w).Encode(user)
}

func (s *UserService) CreateUser(w http.ResponseWriter,
    r *http.Request) {
    var user model.User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    user.ID = fmt.Sprintf("user_%d", time.Now().UnixNano())
    user.CreatedAt = time.Now()

    s.mu.Lock()
    s.users[user.ID] = user
    s.mu.Unlock()

    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func (s *UserService) Health(w http.ResponseWriter,
    r *http.Request) {
    health := model.ServiceHealth{
        Status:    "UP",
        Timestamp: time.Now().Format(time.RFC3339),
    }
    json.NewEncoder(w).Encode(health)
}

func main() {
    // Load config
    cfg, err := config.Load("config.json")
    if err != nil {
        log.Fatal(err)
    }

    // Create service
    service := NewUserService()

    // Create circuit breaker
    cb := middleware.NewCircuitBreaker(
        cfg.CircuitBreaker.Threshold,
        cfg.CircuitBreaker.Timeout,
    )

    // Create router
    mux := http.NewServeMux()
    
    // Add routes
    mux.HandleFunc("/health", service.Health)
    mux.HandleFunc("/users", func(w http.ResponseWriter,
        r *http.Request) {
        switch r.Method {
        case http.MethodGet:
            service.GetUser(w, r)
        case http.MethodPost:
            service.CreateUser(w, r)
        default:
            http.Error(w, "Method not allowed",
                http.StatusMethodNotAllowed)
        }
    })

    // Add middleware
    handler := middleware.Logging(
        middleware.Tracing(
            cb.Middleware(mux),
        ),
    )

    // Start server
    addr := fmt.Sprintf(":%d", cfg.Port)
    log.Printf("User service starting on %s", addr)
    log.Fatal(http.ListenAndServe(addr, handler))
}

order-service/main.go:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "sync"
    "time"

    "microservices/common/config"
    "microservices/common/middleware"
    "microservices/common/model"
)

type OrderService struct {
    orders    map[string]model.Order
    userSvcURL string
    client    *http.Client
    mu        sync.RWMutex
}

func NewOrderService(userSvcURL string) *OrderService {
    return &OrderService{
        orders:    make(map[string]model.Order),
        userSvcURL: userSvcURL,
        client:    &http.Client{Timeout: 5 * time.Second},
    }
}

func (s *OrderService) validateUser(userID string) error {
    resp, err := s.client.Get(fmt.Sprintf("%s/users?id=%s",
        s.userSvcURL, userID))
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("user validation failed: %s",
            resp.Status)
    }

    return nil
}

func (s *OrderService) CreateOrder(w http.ResponseWriter,
    r *http.Request) {
    var order model.Order
    if err := json.NewDecoder(r.Body).Decode(&order); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // Validate user
    if err := s.validateUser(order.UserID); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    order.ID = fmt.Sprintf("order_%d", time.Now().UnixNano())
    order.CreatedAt = time.Now()
    order.Status = "pending"

    // Calculate total
    var total float64
    for _, item := range order.Items {
        total += item.Price * float64(item.Quantity)
    }
    order.Total = total

    s.mu.Lock()
    s.orders[order.ID] = order
    s.mu.Unlock()

    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(order)
}

func (s *OrderService) GetOrder(w http.ResponseWriter,
    r *http.Request) {
    id := r.URL.Query().Get("id")
    if id == "" {
        http.Error(w, "Missing order ID", http.StatusBadRequest)
        return
    }

    s.mu.RLock()
    order, exists := s.orders[id]
    s.mu.RUnlock()

    if !exists {
        http.Error(w, "Order not found", http.StatusNotFound)
        return
    }

    json.NewEncoder(w).Encode(order)
}

func (s *OrderService) Health(w http.ResponseWriter,
    r *http.Request) {
    health := model.ServiceHealth{
        Status:    "UP",
        Timestamp: time.Now().Format(time.RFC3339),
    }
    json.NewEncoder(w).Encode(health)
}

func main() {
    // Load config
    cfg, err := config.Load("config.json")
    if err != nil {
        log.Fatal(err)
    }

    // Create service
    service := NewOrderService("http://localhost:8081")

    // Create circuit breaker
    cb := middleware.NewCircuitBreaker(
        cfg.CircuitBreaker.Threshold,
        cfg.CircuitBreaker.Timeout,
    )

    // Create router
    mux := http.NewServeMux()
    
    // Add routes
    mux.HandleFunc("/health", service.Health)
    mux.HandleFunc("/orders", func(w http.ResponseWriter,
        r *http.Request) {
        switch r.Method {
        case http.MethodGet:
            service.GetOrder(w, r)
        case http.MethodPost:
            service.CreateOrder(w, r)
        default:
            http.Error(w, "Method not allowed",
                http.StatusMethodNotAllowed)
        }
    })

    // Add middleware
    handler := middleware.Logging(
        middleware.Tracing(
            cb.Middleware(mux),
        ),
    )

    // Start server
    addr := fmt.Sprintf(":%d", cfg.Port)
    log.Printf("Order service starting on %s", addr)
    log.Fatal(http.ListenAndServe(addr, handler))
}

api-gateway/main.go:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
    "sync"
    "time"

    "microservices/common/config"
    "microservices/common/middleware"
)

type Service struct {
    Name     string
    URL      string
    Health   bool
    LastPing time.Time
}

type Gateway struct {
    services map[string]*Service
    mu       sync.RWMutex
}

func NewGateway() *Gateway {
    return &Gateway{
        services: make(map[string]*Service),
    }
}

func (g *Gateway) RegisterService(name, urlStr string) error {
    url, err := url.Parse(urlStr)
    if err != nil {
        return err
    }

    service := &Service{
        Name: name,
        URL:  urlStr,
    }

    g.mu.Lock()
    g.services[name] = service
    g.mu.Unlock()

    // Start health check
    go g.healthCheck(service)

    return nil
}

func (g *Gateway) healthCheck(service *Service) {
    ticker := time.NewTicker(10 * time.Second)
    client := &http.Client{Timeout: 5 * time.Second}

    for range ticker.C {
        resp, err := client.Get(fmt.Sprintf("%s/health",
            service.URL))
        
        g.mu.Lock()
        if err != nil {
            service.Health = false
            log.Printf("Service %s is down: %v", service.Name, err)
        } else {
            service.Health = resp.StatusCode == http.StatusOK
            service.LastPing = time.Now()
            resp.Body.Close()
        }
        g.mu.Unlock()
    }
}

func (g *Gateway) ProxyHandler(w http.ResponseWriter,
    r *http.Request) {
    // Extract service name from path
    parts := strings.SplitN(r.URL.Path[1:], "/", 2)
    if len(parts) < 2 {
        http.Error(w, "Invalid path", http.StatusBadRequest)
        return
    }

    serviceName := parts[0]
    
    g.mu.RLock()
    service, exists := g.services[serviceName]
    g.mu.RUnlock()

    if !exists {
        http.Error(w, "Service not found", http.StatusNotFound)
        return
    }

    if !service.Health {
        http.Error(w, "Service unavailable",
            http.StatusServiceUnavailable)
        return
    }

    // Create reverse proxy
    target, _ := url.Parse(service.URL)
    proxy := httputil.NewSingleHostReverseProxy(target)

    // Update request path
    r.URL.Path = "/" + parts[1]
    r.URL.Host = target.Host
    r.URL.Scheme = target.Scheme
    r.Host = target.Host

    proxy.ServeHTTP(w, r)
}

func main() {
    // Load config
    cfg, err := config.Load("config.json")
    if err != nil {
        log.Fatal(err)
    }

    // Create gateway
    gateway := NewGateway()

    // Register services
    gateway.RegisterService("users", "http://localhost:8081")
    gateway.RegisterService("orders", "http://localhost:8082")

    // Create router
    mux := http.NewServeMux()
    
    // Add routes
    mux.HandleFunc("/", gateway.ProxyHandler)

    // Add middleware
    handler := middleware.Logging(
        middleware.Tracing(mux),
    )

    // Start server
    addr := fmt.Sprintf(":%d", cfg.Port)
    log.Printf("API Gateway starting on %s", addr)
    log.Fatal(http.ListenAndServe(addr, handler))
}

Penjelasan Kode

  1. Service Components

    • User service
    • Order service
    • API Gateway
  2. Features

    • Service discovery
    • Health checking
    • Circuit breaking
    • Load balancing
  3. Best Practices

    • Error handling
    • Logging
    • Monitoring
    • Configuration

Setup

  1. Create config files:

config.json for User Service:

{
    "serviceName": "user-service",
    "port": 8081,
    "circuitBreaker": {
        "threshold": 5,
        "timeout": "1m",
        "maxRequests": 100
    }
}

config.json for Order Service:

{
    "serviceName": "order-service",
    "port": 8082,
    "dependencies": ["user-service"],
    "circuitBreaker": {
        "threshold": 5,
        "timeout": "1m",
        "maxRequests": 100
    }
}

config.json for API Gateway:

{
    "serviceName": "api-gateway",
    "port": 8080,
    "dependencies": ["user-service", "order-service"]
}
  1. Run services:
# Terminal 1
go run user-service/main.go

# Terminal 2
go run order-service/main.go

# Terminal 3
go run api-gateway/main.go

Test API

  1. Create user:
curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name":"John Doe","email":"john@example.com"}'
  1. Create order:
curl -X POST http://localhost:8080/orders \
  -H "Content-Type: application/json" \
  -d '{
    "userId":"user_1234567890",
    "items":[
      {"id":"item1","name":"Product 1","price":10.99,"quantity":2}
    ]
  }'

Output

2024/01/21 11:57:22 API Gateway starting on :8080
2024/01/21 11:57:22 User service starting on :8081
2024/01/21 11:57:22 Order service starting on :8082
2024/01/21 11:57:23 POST /users 234.56µs
2024/01/21 11:57:24 GET /users?id=user_1234567890 123.45µs
2024/01/21 11:57:24 POST /orders 345.67µs

Tips

  • Use service discovery
  • Implement circuit breaker
  • Monitor health
  • Handle failures gracefully
  • Scale horizontally