Роутер построен на radix tree для O(k) поиска маршрута, где k — длина пути. Конфигурация маршрутов загружается из YAML и может обновляться hot-reload через SIGHUP:
# config/routes.yaml
routes:
- path: /api/v1/orders
methods: [GET, POST]
upstream:
service: order-service
endpoints:
- addr: order-svc-1:8080
weight: 70
- addr: order-svc-2:8080
weight: 30
middlewares:
- auth: { roles: [seller, admin] }
- rate_limit: { key: seller_id, limit: 100, window: 60s }
- transform: { request: order_normalize }
- path: /api/v1/products/{marketplace_id}
methods: [GET]
upstream:
service: catalog-service
endpoints:
- addr: catalog-svc:8080
weight: 100
middlewares:
- cache: { ttl: 30s, vary: [marketplace_id, Accept-Language] }
- path: /api/v1/webhooks/{provider}
methods: [POST]
upstream:
service: webhook-processor
route_by_body: true # Маршрут зависит от содержимого тела
body_routing_field: event_type
body_routes:
order.created: webhook-orders:8080
order.updated: webhook-orders:8080
payment.received: webhook-payments:8080
stock.changed: webhook-inventory:8080
Реализация роутера:
// router/router.go
package router
import (
"net/http"
"sync/atomic"
)
type Router struct {
tree atomic.Pointer[radixTree]
config atomic.Pointer[Config]
}
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
tree := r.tree.Load()
route, params := tree.Find(req.Method, req.URL.Path)
if route == nil {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
// Inject path params into context
ctx := withParams(req.Context(), params)
req = req.WithContext(ctx)
// Build and execute middleware chain
handler := route.BuildChain(route.UpstreamHandler())
handler.ServeHTTP(w, req)
}
// HotReload перезагружает конфигурацию без даунтайма
func (r *Router) HotReload(newConfig *Config) error {
newTree, err := buildRadixTree(newConfig.Routes)
if err != nil {
return fmt.Errorf("invalid config: %w", err)
}
r.tree.Store(newTree)
r.config.Store(newConfig)
log.Info("routes reloaded", "count", len(newConfig.Routes))
return nil
}
Балансировка — weighted round-robin с health check. Каждые 5 секунд gateway проверяет /healthz каждого upstream и исключает нездоровые ноды из ротации:
// router/balancer.go
type WeightedEndpoint struct {
Addr string
Weight int
currentWeight int
alive atomic.Bool
}
func (b *Balancer) Next() *WeightedEndpoint {
b.mu.Lock()
defer b.mu.Unlock()
totalWeight := 0
var best *WeightedEndpoint
for _, ep := range b.endpoints {
if !ep.alive.Load() {
continue
}
ep.currentWeight += ep.Weight
totalWeight += ep.Weight
if best == nil || ep.currentWeight > best.currentWeight {
best = ep
}
}
if best == nil {
return nil // Все upstream мертвы
}
best.currentWeight -= totalWeight
return best
}
Оставить комментарий