Server-Side Discovery Pattern
In server-side discovery, clients make requests to a router or load balancer, which:
- Queries the service registry
- Selects a service instance
- Routes the request to the selected instance
This pattern simplifies client code but requires an additional infrastructure component.
Here’s an implementation of a server-side discovery router in Go:
package main
import (
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"time"
)
// ServiceRegistry from previous example...
// DiscoveryRouter routes requests to services using service discovery
type DiscoveryRouter struct {
registry *ServiceRegistry
proxies map[string][]*httputil.ReverseProxy
mutex sync.RWMutex
}
// NewDiscoveryRouter creates a new discovery router
func NewDiscoveryRouter(registry *ServiceRegistry) *DiscoveryRouter {
return &DiscoveryRouter{
registry: registry,
proxies: make(map[string][]*httputil.ReverseProxy),
}
}
// updateProxies updates the reverse proxies for a service
func (dr *DiscoveryRouter) updateProxies(serviceName string) error {
instances, exists := dr.registry.GetService(serviceName)
if !exists || len(instances) == 0 {
return fmt.Errorf("no instances available for service: %s", serviceName)
}
var proxies []*httputil.ReverseProxy
for _, instance := range instances {
if instance.Status != "UP" {
continue
}
target, err := url.Parse(fmt.Sprintf("http://%s:%d", instance.Address, instance.Port))
if err != nil {
log.Printf("Error parsing URL for instance %s: %v", instance.ID, err)
continue
}
proxy := httputil.NewSingleHostReverseProxy(target)
// Add custom error handler
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("Proxy error: %v", err)
w.WriteHeader(http.StatusBadGateway)
w.Write([]byte("Service unavailable"))
}
proxies = append(proxies, proxy)
}
if len(proxies) == 0 {
return fmt.Errorf("no healthy instances available for service: %s", serviceName)
}
dr.mutex.Lock()
dr.proxies[serviceName] = proxies
dr.mutex.Unlock()
return nil
}
// ServeHTTP implements the http.Handler interface
func (dr *DiscoveryRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Extract service name from the request path
// In a real implementation, you'd use a more sophisticated routing mechanism
serviceName := extractServiceName(r.URL.Path)
dr.mutex.RLock()
proxies, exists := dr.proxies[serviceName]
dr.mutex.RUnlock()
// If no proxies exist or they need to be refreshed
if !exists || len(proxies) == 0 {
err := dr.updateProxies(serviceName)
if err != nil {
log.Printf("Error updating proxies: %v", err)
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("Service unavailable"))
return
}
dr.mutex.RLock()
proxies = dr.proxies[serviceName]
dr.mutex.RUnlock()
}
// Simple round-robin selection
proxy := proxies[time.Now().UnixNano()%int64(len(proxies))]
// Forward the request
proxy.ServeHTTP(w, r)
}
// extractServiceName extracts the service name from the request path
// In a real implementation, you'd use a more sophisticated routing mechanism
func extractServiceName(path string) string {
// This is a simplified example
// In practice, you might use a routing table or path-based convention
if len(path) > 1 && path[0] == '/' {
parts := strings.Split(path[1:], "/")
if len(parts) > 0 {
return parts[0]
}
}
return "default"
}
func main() {
// Create a service registry
registry := NewServiceRegistry()
// Register some service instances
registry.Register(ServiceInstance{
ID: "payment-service-1",
Name: "payment",
Address: "10.0.0.1",
Port: 8080,
Status: "UP",
})
registry.Register(ServiceInstance{
ID: "payment-service-2",
Name: "payment",
Address: "10.0.0.2",
Port: 8080,
Status: "UP",
})
registry.Register(ServiceInstance{
ID: "order-service-1",
Name: "order",
Address: "10.0.0.3",
Port: 8080,
Status: "UP",
})
// Create a discovery router
router := NewDiscoveryRouter(registry)
// Start the router
log.Println("Starting discovery router on :8000")
log.Fatal(http.ListenAndServe(":8000", router))
}
This implementation demonstrates the core components of server-side discovery:
- A router that intercepts client requests
- Integration with the service registry to discover service instances
- Dynamic proxy creation to route requests to the appropriate service
- Load balancing across multiple instances of the same service
Comparing the Approaches
Both client-side and server-side discovery have advantages and disadvantages:
Client-Side Discovery:
- Advantages:
- Fewer network hops (direct client-to-service communication)
- More control over instance selection and load balancing
- No single point of failure in the request path
- Disadvantages:
- More complex client code
- Registry client library needed for each language/framework
- Clients need to implement service selection logic
Server-Side Discovery:
- Advantages:
- Simpler client code
- Clients don’t need to be aware of the discovery mechanism
- Centralized load balancing and routing policies
- Disadvantages:
- Additional network hop
- Router can become a bottleneck or single point of failure
- More complex infrastructure
The choice between these patterns depends on your specific requirements, but many production systems use a combination of both approaches.