├── README.md ├── .gitignore ├── simple-backend.go └── loadbalancer.go /README.md: -------------------------------------------------------------------------------- 1 | # simple-load-balancer -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | -------------------------------------------------------------------------------- /simple-backend.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "time" 10 | ) 11 | 12 | func main() { 13 | // Parse command line flags 14 | port := flag.Int("port", 8081, "Port to serve on") 15 | flag.Parse() 16 | 17 | // Create a simple HTTP server 18 | mux := http.NewServeMux() 19 | 20 | // Handler for the root path 21 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 22 | // Get hostname for identification 23 | hostname, err := os.Hostname() 24 | if err != nil { 25 | hostname = "unknown" 26 | } 27 | 28 | // Add a short delay to simulate processing time (optional) 29 | time.Sleep(100 * time.Millisecond) 30 | 31 | // Log the request 32 | log.Printf("Backend %d received request: %s %s", *port, r.Method, r.URL.Path) 33 | 34 | // Return a response that identifies this backend 35 | fmt.Fprintf(w, "Backend server on port %d\n", *port) 36 | fmt.Fprintf(w, "Host: %s\n", hostname) 37 | fmt.Fprintf(w, "Request path: %s\n", r.URL.Path) 38 | fmt.Fprintf(w, "Request method: %s\n", r.Method) 39 | fmt.Fprintf(w, "Request headers:\n") 40 | 41 | // Print all request headers 42 | for name, values := range r.Header { 43 | for _, value := range values { 44 | fmt.Fprintf(w, " %s: %s\n", name, value) 45 | } 46 | } 47 | }) 48 | 49 | // Add a health check endpoint 50 | mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { 51 | w.WriteHeader(http.StatusOK) 52 | fmt.Fprintf(w, "healthy") 53 | }) 54 | 55 | // Start the server 56 | server := &http.Server{ 57 | Addr: fmt.Sprintf(":%d", *port), 58 | Handler: mux, 59 | } 60 | 61 | log.Printf("Backend server started at :%d\n", *port) 62 | if err := server.ListenAndServe(); err != nil { 63 | log.Fatal(err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /loadbalancer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net" 8 | "net/http" 9 | "net/http/httputil" 10 | "net/url" 11 | "sync" 12 | "sync/atomic" 13 | "time" 14 | ) 15 | 16 | // Backend represents a backend server 17 | type Backend struct { 18 | URL *url.URL 19 | Alive bool 20 | mux sync.RWMutex 21 | ReverseProxy *httputil.ReverseProxy 22 | } 23 | 24 | // SetAlive updates the alive status of backend 25 | func (b *Backend) SetAlive(alive bool) { 26 | b.mux.Lock() 27 | b.Alive = alive 28 | b.mux.Unlock() 29 | } 30 | 31 | // IsAlive returns true when backend is alive 32 | func (b *Backend) IsAlive() (alive bool) { 33 | b.mux.RLock() 34 | alive = b.Alive 35 | b.mux.RUnlock() 36 | return 37 | } 38 | 39 | // LoadBalancer represents a load balancer 40 | type LoadBalancer struct { 41 | backends []*Backend 42 | current uint64 43 | } 44 | 45 | // NextBackend returns the next available backend to handle the request 46 | func (lb *LoadBalancer) NextBackend() *Backend { 47 | // Simple round-robin 48 | next := atomic.AddUint64(&lb.current, uint64(1)) % uint64(len(lb.backends)) 49 | 50 | // Find the next available backend 51 | for i := 0; i < len(lb.backends); i++ { 52 | idx := (int(next) + i) % len(lb.backends) 53 | if lb.backends[idx].IsAlive() { 54 | return lb.backends[idx] 55 | } 56 | } 57 | return nil 58 | } 59 | 60 | // isBackendAlive checks whether a backend is alive by establishing a TCP connection 61 | func isBackendAlive(u *url.URL) bool { 62 | timeout := 2 * time.Second 63 | conn, err := net.DialTimeout("tcp", u.Host, timeout) 64 | if err != nil { 65 | log.Printf("Site unreachable: %s", err) 66 | return false 67 | } 68 | defer conn.Close() 69 | return true 70 | } 71 | 72 | // HealthCheck pings the backends and updates their status 73 | func (lb *LoadBalancer) HealthCheck() { 74 | for _, b := range lb.backends { 75 | status := isBackendAlive(b.URL) 76 | b.SetAlive(status) 77 | if status { 78 | log.Printf("Backend %s is alive", b.URL) 79 | } else { 80 | log.Printf("Backend %s is dead", b.URL) 81 | } 82 | } 83 | } 84 | 85 | // HealthCheckPeriodically runs a routine health check every interval 86 | func (lb *LoadBalancer) HealthCheckPeriodically(interval time.Duration) { 87 | t := time.NewTicker(interval) 88 | for { 89 | select { 90 | case <-t.C: 91 | lb.HealthCheck() 92 | } 93 | } 94 | } 95 | 96 | // ServeHTTP implements the http.Handler interface for the LoadBalancer 97 | func (lb *LoadBalancer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 98 | backend := lb.NextBackend() 99 | if backend == nil { 100 | http.Error(w, "Service Unavailable", http.StatusServiceUnavailable) 101 | return 102 | } 103 | 104 | // Log the request 105 | log.Printf("Routing request %s %s to backend %s", r.Method, r.URL.Path, backend.URL) 106 | 107 | // Forward the request to the backend 108 | backend.ReverseProxy.ServeHTTP(w, r) 109 | } 110 | 111 | func main() { 112 | // Parse command line flags 113 | port := flag.Int("port", 8080, "Port to serve on") 114 | checkInterval := flag.Duration("check-interval", time.Minute, "Interval for health checking backends") 115 | flag.Parse() 116 | 117 | // Configure backends (in a real application, this might come from a config file) 118 | serverList := []string{ 119 | "http://localhost:8081", 120 | "http://localhost:8082", 121 | "http://localhost:8083", 122 | } 123 | 124 | // Create load balancer 125 | lb := LoadBalancer{} 126 | 127 | // Initialize backends 128 | for _, serverURL := range serverList { 129 | url, err := url.Parse(serverURL) 130 | if err != nil { 131 | log.Fatal(err) 132 | } 133 | 134 | proxy := httputil.NewSingleHostReverseProxy(url) 135 | 136 | // Customize the reverse proxy director 137 | originalDirector := proxy.Director 138 | proxy.Director = func(r *http.Request) { 139 | originalDirector(r) 140 | r.Header.Set("X-Proxy", "Simple-Load-Balancer") 141 | } 142 | 143 | // Add custom error handler 144 | proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { 145 | log.Printf("Proxy error: %v", err) 146 | http.Error(w, "Service Unavailable", http.StatusServiceUnavailable) 147 | 148 | // Mark the backend as down 149 | for _, b := range lb.backends { 150 | if b.URL.String() == url.String() { 151 | b.SetAlive(false) 152 | break 153 | } 154 | } 155 | } 156 | 157 | lb.backends = append(lb.backends, &Backend{ 158 | URL: url, 159 | Alive: true, 160 | ReverseProxy: proxy, 161 | }) 162 | log.Printf("Configured backend: %s", url) 163 | } 164 | 165 | // Initial health check 166 | lb.HealthCheck() 167 | 168 | // Start periodic health check 169 | go lb.HealthCheckPeriodically(*checkInterval) 170 | 171 | // Set up graceful shutdown signal handler 172 | // Note: In a production environment, you would implement proper 173 | // signal handling for graceful shutdown 174 | 175 | // Start the server 176 | server := http.Server{ 177 | Addr: fmt.Sprintf(":%d", *port), 178 | Handler: &lb, 179 | // Set reasonable timeouts 180 | ReadTimeout: 5 * time.Second, 181 | WriteTimeout: 10 * time.Second, 182 | IdleTimeout: 120 * time.Second, 183 | } 184 | 185 | log.Printf("Load Balancer started at :%d\n", *port) 186 | if err := server.ListenAndServe(); err != nil { 187 | log.Fatal(err) 188 | } 189 | } 190 | --------------------------------------------------------------------------------