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:
- Design microservices
- Service discovery
- Load balancing
- 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
Service Components
- User service
- Order service
- API Gateway
Features
- Service discovery
- Health checking
- Circuit breaking
- Load balancing
Best Practices
- Error handling
- Logging
- Monitoring
- Configuration
Setup
- 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"]
}
- 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
- Create user:
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","email":"john@example.com"}'
- 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