├── README.md ├── bin ├── loadbal └── server ├── go.mod ├── loadbal └── main.go └── server ├── main.go └── server.go /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Load Balancer 3 | 4 | A Load balancer which distributes requests to many servers, implemented from scratch in go lang. 5 | 6 | ## Features 7 | 8 | - `Static load Balacing`: Acheived using Round robin assumes optimistic requests completion. 9 | 10 | - `Periodic Health Checks`: Hits health check end points after a specific timeout can be tuned according to use case. 11 | 12 | - `Active Passive Server Management`: Requests wont be redirected to failing servers or servers that are shutdown. 13 | 14 | 15 | 16 | 17 | ## API Reference 18 | 19 | ### Loadbalancer 20 | 21 | ```http 22 | GET / 23 | ``` 24 | 25 | Redirects the request to server according to algorithmn, copies the response and sends back to client. 26 | ### Servers 27 | 28 | #### Default endpoint 29 | 30 | ```http 31 | GET / 32 | ``` 33 | 34 | Returns the message from servers. For easier identification I explicitly specified Server Id. 35 | 36 | #### Healthcheck endpoint 37 | 38 | ```http 39 | GET /healthcheck 40 | ``` 41 | 42 | Returns health message with 200 status code. 43 | 44 | 45 | ## Run Locally 46 | 47 | Clone the project 48 | 49 | ```bash 50 | git clone https://github.com/adityadafe/load-balancer 51 | ``` 52 | 53 | Go to the server directory 54 | 55 | ```bash 56 | cd server 57 | ``` 58 | 59 | Run server 60 | 61 | ```bash 62 | go run . 63 | ``` 64 | 65 | Go to loadbal directory 66 | 67 | ```bash 68 | cd loadbal 69 | ``` 70 | 71 | Run loadbalancer 72 | 73 | ```bash 74 | go run . 75 | ``` 76 | 77 | 78 | 79 | 80 | ## Benefits 81 | 82 | - **Improved Availability**: Distributes incoming traffic across multiple servers to ensure no single server becomes a bottleneck, enhancing overall system uptime. 83 | - **Scalability**: Easily scales application infrastructure by adding or removing servers as demand changes, ensuring consistent performance. 84 | - **Enhanced Performance**: Balances the load to prevent any single server from being overwhelmed, improving response times and throughput. 85 | - **Fault Tolerance**: Automatically detects server failures and reroutes traffic to healthy servers, maintaining service continuity. 86 | - **Efficient Resource Utilization**: Maximizes the utilization of available server resources, reducing idle time and optimizing cost-efficiency. 87 | - **Security**: Provides an additional layer of security by masking the backend server infrastructure, and can also help mitigate DDoS attacks. 88 | - **Simplified Maintenance**: Allows for maintenance or upgrades on individual servers without disrupting the overall service, ensuring continuous availability. 89 | - **Geographic Distribution**: Directs traffic to servers located closest to the user, reducing latency and improving user experience. 90 | 91 | 92 | ## Caution 93 | 94 | This is not recommended to put in production enviornment use battle tested lb like [fabio](https://github.com/fabiolb/fabio) 95 | ## Future Work 96 | 97 | I have created branches with different versions and implementation. In coming time i will add registry endpoint for service registry. -------------------------------------------------------------------------------- /bin/loadbal: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadafe/load-balancer/429877073705697af88ec2856134c4a4b2ecce79/bin/loadbal -------------------------------------------------------------------------------- /bin/server: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadafe/load-balancer/429877073705697af88ec2856134c4a4b2ecce79/bin/server -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kakashi1210/load-balancer 2 | 3 | go 1.22.2 4 | -------------------------------------------------------------------------------- /loadbal/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "slices" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | type Server struct { 14 | url string 15 | isActive bool 16 | } 17 | 18 | type Servers []*Server 19 | 20 | var ( 21 | servers Servers = []*Server{ 22 | &Server{url: "http://localhost:3000", isActive: true}, 23 | &Server{url: "http://localhost:3001", isActive: true}, 24 | &Server{url: "http://localhost:3002", isActive: true}, 25 | } 26 | 27 | timeout time.Duration = time.Second * 2 28 | 29 | activeSvr []*Server 30 | 31 | mu sync.Mutex 32 | 33 | curr int = -1 34 | ) 35 | 36 | func healthChecks() { 37 | for { 38 | for i, svr := range servers { 39 | _, err := http.Get(svr.url + "/health") 40 | if err != nil { 41 | svr.isActive = false 42 | if slices.Contains(activeSvr, svr) { 43 | fs := servers[:i] 44 | ss := servers[i+1:] 45 | 46 | activeSvr = []*Server{} 47 | mu.Lock() 48 | 49 | for _, f := range fs { 50 | activeSvr = append(activeSvr, f) 51 | } 52 | 53 | for _, f := range ss { 54 | activeSvr = append(activeSvr, f) 55 | } 56 | mu.Unlock() 57 | } 58 | continue 59 | } 60 | mu.Lock() 61 | svr.isActive = true 62 | if slices.Contains(activeSvr, svr) { 63 | mu.Unlock() 64 | continue 65 | } 66 | activeSvr = append(activeSvr, svr) 67 | mu.Unlock() 68 | } 69 | time.Sleep(timeout) 70 | } 71 | } 72 | 73 | func GetCurrentRobin() (*Server, error) { 74 | if len(activeSvr) == 0 { 75 | return nil, errors.New("No current active servers") 76 | } 77 | mu.Lock() 78 | curr++ 79 | fmt.Println("Request forwarded to Server ", curr) 80 | if curr >= len(activeSvr) { 81 | curr = 0 82 | } 83 | robin := activeSvr[curr] 84 | mu.Unlock() 85 | return robin, nil 86 | } 87 | 88 | func ForwardRequest(w http.ResponseWriter, r *http.Request, str string) { 89 | proxyReq, err := http.NewRequest(r.Method, str, r.Body) 90 | if err != nil { 91 | http.Error(w, "Error creating request", http.StatusInternalServerError) 92 | } 93 | proxyReq.Header = r.Header.Clone() 94 | proxyReq.Header.Set("X-Forwarded-For", r.RemoteAddr) 95 | 96 | client := &http.Client{} 97 | res, err := client.Do(proxyReq) 98 | 99 | if err != nil { 100 | http.Error(w, "Error forwarding request", http.StatusBadGateway) 101 | } 102 | defer res.Body.Close() 103 | 104 | for header, values := range r.Header { 105 | for _, value := range values { 106 | proxyReq.Header.Add(header, value) 107 | } 108 | } 109 | 110 | w.WriteHeader(res.StatusCode) 111 | io.Copy(w, res.Body) 112 | 113 | } 114 | 115 | func main() { 116 | 117 | go healthChecks() 118 | 119 | fmt.Println("Load Balancer is ready to serve") 120 | 121 | http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { 122 | w.Header().Add("Content-Type", "application/json") 123 | robin, err := GetCurrentRobin() 124 | if err != nil { 125 | http.Error(w, err.Error(), http.StatusInternalServerError) 126 | } 127 | w.WriteHeader(http.StatusOK) 128 | ForwardRequest(w, r, robin.url) 129 | }) 130 | 131 | http.ListenAndServe(":5173", nil) 132 | } 133 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | s := NewServer(":3000") 5 | s.Run() 6 | } 7 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | ) 8 | 9 | type Server struct { 10 | Addr string 11 | } 12 | 13 | func NewServer(addr string) *Server { 14 | return &Server{ 15 | Addr: addr, 16 | } 17 | } 18 | 19 | func (s *Server) Run() { 20 | port := s.Addr 21 | sm := http.NewServeMux() 22 | 23 | sm.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { 24 | w.Header().Add("Content-Type", "application/json") 25 | w.WriteHeader(http.StatusOK) 26 | w.Write([]byte("Server is healthy")) 27 | }) 28 | 29 | sm.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { 30 | w.Header().Add("Content-Type", "application/json") 31 | w.WriteHeader(http.StatusOK) 32 | fmt.Println("Server hosted at ", port, " served request to ", r.UserAgent()) 33 | // converting port to int 34 | pti, _ := strconv.Atoi(port[1:]) 35 | w.Write([]byte("Hello from server " + strconv.Itoa(pti-2999))) 36 | }) 37 | 38 | fmt.Println("Starting server ... ") 39 | fmt.Println("Server is ready to serve") 40 | 41 | err := http.ListenAndServe(s.Addr, sm) 42 | 43 | for err != nil { 44 | port = fmt.Sprintf(":%d", HandlePortError(port[1:])) 45 | err = http.ListenAndServe(port, sm) 46 | } 47 | } 48 | 49 | func HandlePortError(pp string) int { 50 | // pp is previous port :) 51 | p, _ := strconv.Atoi(pp) 52 | return p + 1 53 | } 54 | --------------------------------------------------------------------------------