Skip to content

About

HTTP load balancer in Go — round-robin & random routing, health checks, runtime config reload via SIGHUP

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

8 Commits

Folders and files

Repository files navigation

go-load-balancer

A configurable Layer-7 HTTP reverse-proxy load balancer written in Go. It fronts a pool of backend servers, forwards incoming HTTP requests to a backend chosen by a pluggable selection strategy, and periodically health-checks the backends so traffic is only sent to reachable ones. The backend list is loaded from a config file and hot-reloaded when that file changes on disk, so servers can be added or removed without restarting the balancer.

Features

  • HTTP reverse proxy built on the standard library's httputil.NewSingleHostReverseProxy.
  • Eight selection strategies, chosen by the strategy config field:
    • roundRobin — cycles through alive backends.
    • weightedRoundRobin — distributes proportionally to per-server weight.
    • leastConnection — picks the alive backend with the fewest active connections.
    • weightedLeastConnection — picks by lowest connection/weight ratio; increments/decrements the connection counter around the request.
    • leastResponseTime — picks the backend with the lowest exponentially-smoothed average response time.
    • weightedResponseTime — picks by lowest avg-time/weight score.
    • IpHash — hashes the client IP (FNV-32a) to consistently map a client to a backend.
    • random — uniformly random alive backend (seeded RNG behind a mutex).
  • Active health checking on a fixed interval: each backend is probed with a TCP dial (2s timeout) and marked alive/dead; strategies skip dead backends.
  • Config hot-reload via filesystem watching (fsnotify): on a write/create event to the config file, the config is re-parsed and the backend pool is rebuilt live.
  • JSON or YAML config, selected automatically by file extension.
  • Thread-safe config and server state using sync.RWMutex/sync.Mutex and atomics.
  • Bundled test backends (Bun + Hono TypeScript apps) for exercising the balancer locally.

How it works

Request flow

Client ──► http.ServeMux "/" handler (cmd/main.go)
             • extract client IP (host part of RemoteAddr) into request context
             • serverManager.GetServer(ctx, pool.Servers)  → chosen backend URL
             • (strategy-specific connection / avg-time bookkeeping)
             • httputil.NewSingleHostReverseProxy(backend).ServeHTTP(w, r)
           ──► backend server

On startup main loads the config, seeds the ServerPool, selects a ServerManager implementation from the strategy string via GetServerManger, starts the health-check goroutine, starts the config watcher goroutine, and serves HTTP on the configured port. Each strategy implements the ServerManager interface (GetServer(ctx, []*Server) (url.URL, error)); some also implement optional UpdateServerStats (connection decrement) and UpdateAvgTime (response-time update) interfaces that the handler invokes when supported.

Health checking

healthCheck runs a time.Ticker at healthCheckInterval seconds. On each tick it calls ServerPool.HealthCheck, which for every backend performs a net.DialTimeout("tcp", host, 2s); success marks the server alive, failure marks it dead. Selection strategies filter on this IsAlive flag, so only reachable backends receive traffic.

Config hot-reload (fsnotify, not signals)

Reload is driven by filesystem events, not a SIGHUP handler. config.WatchConfig creates an fsnotify watcher on the config file path; when a Write or Create event fires it calls Config.ReloadFromFile, then clears and repopulates the ServerPool from the new server list under lock. Config guards its fields with a sync.RWMutex, and SafeGet returns a deep copy of the current config for safe concurrent reads.

Concurrency model

  • HTTP requests are served concurrently by net/http; each strategy is safe for concurrent GetServer calls (round-robin uses an atomic counter, weighted round-robin and random use mutexes).
  • Health checking and config watching each run in their own goroutine.
  • Server state (alive flag, connection count) is protected by a per-server sync.Mutex; ServerPool mutations are guarded by its own mutex.

Data model

  • models.Server — backend URL, connection count, smoothed average time, alive flag, weight, and a mutex.
  • models.ServerPool — the slice of *Server, with AddServer, ClearServers, and HealthCheck.

Tech stack

  • Go (module targets go 1.23.4), using only the standard library for HTTP proxying (net/http, net/http/httputil), hashing (hash/fnv), and concurrency primitives.
  • github.com/fsnotify/fsnotify v1.9.0 — filesystem watching for config hot-reload.
  • gopkg.in/yaml.v3 v3.0.1 — YAML config parsing (JSON handled by encoding/json).
  • Test backends: TypeScript apps using the Hono framework, run with Bun (test/servers/server_1..4).

Getting started

Prerequisites: Go 1.23+. (Optional: Bun to run the sample backends.)

# From the repo root — the app reads ./config.yaml by default
go run ./cmd

# Or build a binary
go build -o load-balancer ./cmd
./load-balancer

The config path is config.yaml (hard-coded in cmd/main.go); a config.json with the same schema is also included and would be parsed if that path were used (parsing is chosen by file extension).

Running the sample backends

Each test/servers/server_N is a Bun + Hono app listening on ports 7891–7894 (matching the default config). For example:

cd test/servers/server_1
bun install
bun run src/index.ts   # serves on :7891

Start all four, then run the balancer on :8080 and send requests to http://localhost:8080/.

Usage

Configuration

config.yaml (or config.json):

port: "8080"                 # balancer listen port
strategy: "leastConnection"  # one of the eight strategy names below
healthCheckInterval: 15      # seconds between health checks
servers:
  - "http://localhost:7891"
  - "http://localhost:7892"
  - "http://localhost:7893"
  - "http://localhost:7894"

Valid strategy values: roundRobin, weightedRoundRobin, leastConnection, weightedLeastConnection, leastResponseTime, weightedResponseTime, IpHash, random.

Sending traffic

curl http://localhost:8080/

Every request to any path is proxied to a backend selected by the configured strategy. Edit config.yaml while the balancer is running to add or remove backends; the watcher reloads the server pool automatically.

Notes:

  • The servers config carries only URLs. Server.Weight is a per-server field that is not populated from this config schema, so the weighted strategies (which require Weight > 0) depend on weights being set for backends to be selectable.
  • The active strategy, port, and health-check interval are read once at startup; hot-reload updates the backend list.

Project structure

go-load-balancer/
├── cmd/main.go                      # entrypoint: load config, wire strategy, HTTP proxy, health check, watcher
├── config.yaml                      # default config (read at startup)
├── config.json                      # equivalent JSON config
├── internals/
│   ├── algo/                        # selection strategies (one file each)
│   │   ├── RoundRobin.go
│   │   ├── WeightedRoundRobin.go
│   │   ├── LeastConnection.go
│   │   ├── WeightedLeastConnection.go
│   │   ├── LeastResponseTime.go
│   │   ├── WeightedResponseTime.go
│   │   ├── IpHash.go
│   │   └── Random.go
│   ├── config/
│   │   ├── config.go                # Config struct, ReloadFromFile (JSON/YAML), SafeGet
│   │   └── watcher.go               # fsnotify watcher → reload + rebuild pool
│   └── models/
│       ├── Server.go                # backend state + TCP health probe
│       └── ServerPool.go            # pool management + HealthCheck
└── test/servers/server_1..4/        # Bun + Hono sample backends (ports 7891–7894)

About

HTTP load balancer in Go — round-robin & random routing, health checks, runtime config reload via SIGHUP

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages