당신이 높은 양의 트래픽을 처리 할 수 있어야하는 서비스를 실행하는 경우, 당신은 당신의 백엔드 서버 사이에 그 트래픽의 많은 균형을 로드 할 수 있습니다.There are a lot of production-grade load balancer on the market (NGINX, HAProxy, etc.) but knowing how they work behind the scene is good knowledge.
A Simple HTTP Load Balancer in Go Using Standard Library, 이 구현에서 우리는 라운드 로빈 알고리즘을 사용하여 백엔드 서버의 집합 사이에 들어오는 요청을 균등하게 배포할 것입니다.
기본 구조
첫째, 우리는 우리의 핵심 데이터 구조를 정의해야 합니다.Our load balancer will track multiple backend servers and their health:
package main
import (
"flag"
"fmt"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"sync/atomic"
"time"
)
// Backend represents a backend server
type Backend struct {
URL *url.URL
Alive bool
mux sync.RWMutex
ReverseProxy *httputil.ReverseProxy
}
// SetAlive updates the alive status of backend
func (b *Backend) SetAlive(alive bool) {
b.mux.Lock()
b.Alive = alive
b.mux.Unlock()
}
// IsAlive returns true when backend is alive
func (b *Backend) IsAlive() (alive bool) {
b.mux.RLock()
alive = b.Alive
b.mux.RUnlock()
return
}
// LoadBalancer represents a load balancer
type LoadBalancer struct {
backends []*Backend
current uint64
}
// NextBackend returns the next available backend to handle the request
func (lb *LoadBalancer) NextBackend() *Backend {
// Simple round-robin
next := atomic.AddUint64(&lb.current, uint64(1)) % uint64(len(lb.backends))
// Find the next available backend
for i := 0; i < len(lb.backends); i++ {
idx := (int(next) + i) % len(lb.backends)
if lb.backends[idx].IsAlive() {
return lb.backends[idx]
}
}
return nil
}
이제 여기에서 기억해야 할 몇 가지 핵심 요소 :
- Backend struct는 URL 및 라이브 상태를 가진 단일 백엔드 서버를 나타냅니다.The backend struct represents a single backend server with its URL and live status.
- 우리는 안전하게 업데이트하고 동시 환경에서 각 백엔드의 라이브 상태를 확인하기 위해 mutex를 사용하고 있습니다.
- LoadBalancer는 백엔드 목록을 추적하고 라운드 로빈 알고리즘에 대한 카운터를 유지합니다.
- 우리는 원자 작동을 사용하여 동시 환경에서 카운터를 안전하게 증가시킵니다.
- NextBackend 방법은 round-robin 알고리즘을 구현하고, 건강에 좋지 않은 백엔드를 건너니다.
건강검진
백엔드의 사용할 수 없음을 감지하는 것은 모든 부하 밸런서의 중요한 기능 중 하나입니다.
// isBackendAlive checks whether a backend is alive by establishing a TCP connection
func isBackendAlive(u *url.URL) bool {
timeout := 2 * time.Second
conn, err := net.DialTimeout("tcp", u.Host, timeout)
if err != nil {
log.Printf("Site unreachable: %s", err)
return false
}
defer conn.Close()
return true
}
// HealthCheck pings the backends and updates their status
func (lb *LoadBalancer) HealthCheck() {
for _, b := range lb.backends {
status := isBackendAlive(b.URL)
b.SetAlive(status)
if status {
log.Printf("Backend %s is alive", b.URL)
} else {
log.Printf("Backend %s is dead", b.URL)
}
}
}
// HealthCheckPeriodically runs a routine health check every interval
func (lb *LoadBalancer) HealthCheckPeriodically(interval time.Duration) {
t := time.NewTicker(interval)
for {
select {
case <-t.C:
lb.HealthCheck()
}
}
}
이 건강 검사는 간단합니다 :
- 우리는 백엔드와 TCP 연결을 시작하려고합니다.
- 이 연결이 성공하면 백엔드는 살아있다; 그렇지 않으면 죽는다.
- 체크는 HealthCheckPeriodically 함수에서 지정한 간격으로 실행됩니다.
생산 환경에서 실제로 HTTP 요청을 지정된 엔드포인트로 만드는 더 고급 건강 검사를 원할 수도 있지만 이것은 우리를 시작하게합니다.
HTTP Handler 사용
앞으로 가서 요청을 수신하고 우리의 백엔드로 라우팅 할 HTTP 트레이너를 구현하자 :
// ServeHTTP implements the http.Handler interface for the LoadBalancer
func (lb *LoadBalancer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
backend := lb.NextBackend()
if backend == nil {
http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
return
}
// Forward the request to the backend
backend.ReverseProxy.ServeHTTP(w, r)
}
그리고 이것은 마법이 일어나는 곳입니다 :
- 우리의 라운드 로빈 알고리즘은 우리가 사용할 수있는 다음 백엔드를 제공합니다.
- 백엔드가 없는 경우에는 503 Service Unavailable 오류를 반환합니다.
- 그렇지 않으면 Go의 내부 반대 프록시를 통해 선택한 백엔드에 요청을 프록시합니다.
어떻게 하는지 알아보세요~net/http/httputil
패키지는 그ReverseProxy
우리를 위해 HTTP 요청을 프록시하는 모든 복잡성을 처리하는 유형입니다.
모든 것을 함께 넣어주는
마지막으로, 우리는 그것을 실행하자main
우리의 부하 밸런서를 구성하고 시작하는 기능:
func main() {
// Parse command line flags
port := flag.Int("port", 8080, "Port to serve on")
flag.Parse()
// Configure backends
serverList := []string{
"http://localhost:8081",
"http://localhost:8082",
"http://localhost:8083",
}
// Create load balancer
lb := LoadBalancer{}
// Initialize backends
for _, serverURL := range serverList {
url, err := url.Parse(serverURL)
if err != nil {
log.Fatal(err)
}
proxy := httputil.NewSingleHostReverseProxy(url)
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("Error: %v", err)
http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
}
lb.backends = append(lb.backends, &Backend{
URL: url,
Alive: true,
ReverseProxy: proxy,
})
log.Printf("Configured backend: %s", url)
}
// Initial health check
lb.HealthCheck()
// Start periodic health check
go lb.HealthCheckPeriodically(time.Minute)
// Start server
server := http.Server{
Addr: fmt.Sprintf(":%d", *port),
Handler: &lb,
}
log.Printf("Load Balancer started at :%d\n", *port)
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
이제, 주요 기능에서 일어나는 일은 다음과 같습니다 :
- 우리는 우리가 파스팅 한 명령줄 깃발을 기반으로 포트를 구성합니다.We configure the port based on the command line flags we parse.
- 우리는 백엔드 서버의 목록을 만들었습니다.
- For each backend server, we:
- Parse the URL
- Create a reverse proxy for that backend
- Add error handling for when a backend fails
- Add the backend to our load balancer
- 초기 건강검진을 실시합니다.
- 우리는 정기적 인 건강 검사를위한 goroutine을 시작합니다.
- 마지막으로, 우리는 HTTP 서버를 관리자로서 우리의 로드 밸런서로 시작합니다.
Load Balancer 테스트
그래서 다시 한 번 우리의 로드 밸런서를 테스트하려면 몇 가지 백엔드 서버가 필요합니다.A naive implementation of a backend server might look like this:
// Save this to backend.go
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
)
func main() {
port := flag.Int("port", 8081, "Port to serve on")
flag.Parse()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
hostname, _ := os.Hostname()
fmt.Fprintf(w, "Backend server on port %d, host: %s, Request path: %s\n", *port, hostname, r.URL.Path)
})
log.Printf("Backend started at :%d\n", *port)
if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), nil); err != nil {
log.Fatal(err)
}
}
여러 포트에서 여러 인스턴스를 실행합니다.Run multiple instances of this backend on different ports:
go build -o backend backend.go
./backend -port 8081 &
./backend -port 8082 &
./backend -port 8083 &
그런 다음 부하 밸런서를 구축하고 실행하십시오 :
go build -o load-balancer main.go
./load-balancer
이제 그것을 테스트하기 위해 몇 가지 요청을하십시오 :
curl http://localhost:8080/test
여러 요청을 작성하면 백엔드 서버에 라운드 루빈 방식으로 배포됩니다.Make multiple requests and you will see them being distributed across your backend servers in a round-robin mode.
잠재적 개선
이것은 최소한의 구현이지만, 그것을 개선하기 위해 할 수있는 것이 많습니다.
-
Different balancing algorithms: Implement weighted round-robin, least connections, or IP hash-based selection.
-
Better health checking: Make full HTTP requests to a health endpoint instead of just checking TCP connectivity.
-
Metrics collection: Track request counts, response times, and error rates for each backend.
-
Sticky sessions: Ensure requests from the same client always go to the same backend.
-
TLS support: Add HTTPS for secure connections.
-
Dynamic configuration: Allow updating the backend list without restarting.
Go의 표준 라이브러리 외에는 아무것도 사용하지 않고 간단하지만 효과적인 HTTP 로드 밸런서를 구축했습니다.This example illustrates important concepts in network programming and the power of Go's built-in networking features.
이것은 생산 준비가되어 있지 않지만 로드 밸런서가 어떻게 작동하는지 이해하기위한 견고한 기초를 제공합니다.The complete solution is around 150 lines of code - a testament to the expressiveness of Go and the strength of its standard library.
생산 사용을 위해서는 더 많은 기능을 구축하고 강력한 오류 처리 기능을 추가하고 싶지만 핵심 개념은 모두 동일합니다.이 기본 원칙을 이해하면 도로에서 사용할 모든 부하 밸런서를 더 잘 구성하고 디버그 할 수 있습니다.
여기서 소스 코드를 찾을 수 있습니다.https://github.com/rezmoss/simple-load-balancer