Designing Load Balancing and Keep‑Alive Strategies in Go
This article walks through implementing three load‑balancing algorithms (random, round‑robin, weighted round‑robin) in Go, explains the weighted selection math with step‑by‑step examples, and presents two service‑liveness solutions—heartbeat via Docker/Kafka and HTTP health checks—complete with code snippets and deployment commands.
Through this project you can learn how to implement load‑balancing algorithms in Go and how a scheduler can maintain live nodes.
Load‑balancing algorithms: random, round‑robin, weighted round‑robin.
loadbalance
Load‑balancing algorithms: random algorithm, round‑robin algorithm, weighted round‑robin algorithm.
Random algorithm
// Random
type Rand struct {
addrs []string
}
func (r *Rand) Add(param []string) error {
if len(param) != 1 {
return ErrParam
}
r.addrs = append(r.addrs, param[0])
return nil
}
// Random seed, return one address each time
func (r *Rand) Get() (string, error) {
if len(r.addrs) == 0 {
return "", ErrNoAddr
}
rand.Seed(time.Now().UnixNano())
idx := rand.Intn(len(r.addrs))
return r.addrs[idx], nil
}Round‑robin algorithm
type RoundRobin struct {
addrs []string
curIdx int
}
func (r *RoundRobin) Add(param []string) error {
if len(param) != 1 {
return ErrParam
}
r.addrs = append(r.addrs, param[0])
return nil
}
func (r *RoundRobin) Get() (string, error) {
if len(r.addrs) == 0 || r.curIdx >= len(r.addrs) {
return "", ErrNoAddr
}
addr := r.addrs[r.curIdx]
r.curIdx = (r.curIdx + 1) % len(r.addrs) // curIdx +1 each time
return addr, nil
}Weighted round‑robin algorithm
Implementation principle: each node has a weight; the higher the weight, the more often it is selected. The expected selection count ≈ (node weight / total weight) × total requests.
Example: weights weight[a=1,b=2,c=5]; initial curWeight[a=0,b=0,c=0]; sumWeight = 8.
First request: curWeight becomes [a=1,b=2,c=5]; select c; then c.curWeight -= sumWeight → [a=1,b=2,c=-3].
Second request: curWeight → [a=2,b=4,c=2]; select b; then b.curWeight -= sumWeight → [a=2,b=-4,c=2].
Third request: curWeight → [a=3,b=-2,c=7]; select c; then c.curWeight -= sumWeight → [a=3,b=-2,c=-1].
Fourth request: curWeight → [a=4,b=0,c=4]; a and c tie, prefer a.
type WeigthRoundRobin struct {
weightAddrs []*weightAddr
}
type weightAddr struct {
addr string // address
weight int // weight
curWeight int // used for calculation
}
func (w *WeigthRoundRobin) Add(param []string) error {
if len(param) != 2 {
return ErrParam
}
weight, err := strconv.Atoi(param[1])
if err != nil {
return err
}
w.weightAddrs = append(w.weightAddrs, &weightAddr{addr: param[0], weight: weight, curWeight: 0})
return nil
}
func (w *WeigthRoundRobin) Get() (string, error) {
if len(w.weightAddrs) == 0 {
return "", ErrNoAddr
}
maxWeight := math.MinInt
idx := 0
sumWeight := 0
for k, weightAddr := range w.weightAddrs {
sumWeight += weightAddr.weight // total weight
weightAddr.curWeight += weightAddr.weight // add weight
if weightAddr.curWeight > maxWeight { // record max
maxWeight = weightAddr.curWeight
idx = k
}
}
w.weightAddrs[idx].curWeight -= sumWeight // subtract total weight
return w.weightAddrs[idx].addr, nil // return address with max weight
}Effective Service Maintenance Scheme (Reference)
Scheme 1: Heartbeat
Prerequisite configuration:
1. Start Docker
docker-compose -f docker-compose-env.yml up -d zookeeper
docker-compose -f docker-compose-env.yml up -d kafka2. Modify local hosts
vim /etc/hosts
# add line
127.0.0.1 kafka3. Optional Kafka scripts (located in /opt/kafka/bin inside the container)
# Create topic
./kafka-topics.sh --create --zookeeper zookeeper:2181 --replication-factor 1 --partitions 3 --topic easy_topic
# List topics
./kafka-topics.sh --list --zookeeper zookeeper:2181
# Consume from beginning
./kafka-console-consumer.sh --bootstrap-server kafka:9092 --from-beginning --topic easy_topic
# Consumer group (from latest)
./kafka-console-consumer.sh --bootstrap-server kafka:9092 --consumer-property group.id=testGroup --topic easy_topic
# Produce messages
./kafka-console-producer.sh --broker-list kafka:9092 --topic easy_topicThe scheduler subscribes to Kafka messages and maintains a list of alive services , then distributes requests according to the load‑balancing strategy.
func main() {
// Service sends heartbeat
go heart.RunHeartBeat()
// Scheduler receives heartbeat
go heart.ListenHeartbeat()
// Use load balancer to get address
lb := loadbalance.LoadBalanceFactory(loadbalance.BalanceTypeRand)
go func() {
for {
time.Sleep(5 * time.Second)
for _, v := range heart.GetAddrList() {
lb.Add([]string{v})
}
fmt.Println(lb.Get())
}
}()
sigusr1 := make(chan os.Signal, 1)
signal.Notify(sigusr1, syscall.SIGTERM)
<-sigusr1
}Scheme 2: Health Check
Maintain active nodes by sending HTTP requests to services.
func main() {
health.AddAddr("https://www.sina.com.cn/", "https://www.baidu.com/", "http://www.aajklsdfjklsd")
go health.HealthCheck()
time.Sleep(50 * time.Second)
alist := health.GetAliveAddrList()
for i := 0; i < len(alist); i++ {
fmt.Println(alist[i])
}
var block = make(chan bool)
<-block
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Nullbody Notes
Go backend development, learning open-source project source code together, focusing on simplicity and practicality.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
