1. 项目背景详细介绍

在分布式系统和高并发架构中,如何将请求均匀且有策略地分配到后端服务器,是提升系统吞吐量与可靠性的重要手段。传统的轮询(Round Robin)算法在权重相同的场景下能够实现基本的负载分担,但面对不同性能、不同规格的服务器时,往往无法充分利用资源。

因此,引入“加权轮询”(Weighted Round Robin, WRR)算法:为每台服务器分配权重(weight),权重越高,获取请求的概率和频率越大;权重越低,则反之。这样既能保证各节点有请求落地,又能让性能更强、更空闲的节点承担更多负载,从而提升系统整体性能和稳定性。

本项目旨在使用 Java 语言,从零构建一个简单易用的 WRR 负载均衡实现,帮助大家学习并掌握该算法的原理与实现细节,便于在实际业务中进行二次开发和扩展。


2. 项目需求详细介绍

  • 功能需求

    1. 能够动态添加、删除后端服务器实例;

    2. 为每个服务器设置可调节的权重值;

    3. 根据加权轮询算法,均匀地将请求分配给后端服务器;

    4. 支持多线程并发调用,保证线程安全。

  • 非功能需求

    1. 性能:在节点数量≤20、并发线程数≤1000 的情况下,算法的调度开销应当可接受;

    2. 可扩展性:后期可接入健康检查、权重动态调整等功能;

    3. 易用性:API 简洁易懂,上手成本低;

    4. 可测试性:附带单元测试示例,验证分配结果的正确性。


3. 相关技术详细介绍

  1. Java 基础

    • 集合框架:如 ListMap 等,用于存储服务器列表与权重映射;

    • 并发包:如 ReentrantLockAtomicInteger 等,保证多线程环境下的安全。

  2. 加权轮询算法原理

    • 静态权重轮询:简单地根据固定权重计数循环分配;

    • 平滑加权轮询(Smooth WRR):维护每个节点的动态“当前权重”,每次从中选择当前权重大者,并在分配后减少总权重,实现更均匀的调度效果。

  3. 单元测试

    • 使用 JUnit 框架验证调度结果符合预期。

  4. 日志与监控(可选)

    • 日志框架(如 Log4j、SLF4J)记录调度决策过程;

    • 指标上报(如 Prometheus)监控调度性能。


4. 实现思路详细介绍

4.1 数据结构设计

  • 使用 class ServerNode 表示后端服务器,包含字段:idweight(静态权重)、currentWeight(动态权重)等。

  • 使用 List<ServerNode> 存储活跃服务器列表。

4.2 算法流程(平滑加权轮询)

  1. 初始化

    • 将所有 ServerNode.currentWeight 置零;

    • 计算所有权重之和 totalWeight

  2. 每次调度

    • 遍历所有节点,执行 node.currentWeight += node.weight

    • 从中选择 currentWeight 最大的节点 selected

    • selected.currentWeight 执行 selected.currentWeight -= totalWeight

    • 返回 selected

该算法保证在一轮调度中,高权重节点被选中的次数成比例增加,同时“平滑”地分散请求,避免权重不等时短期内请求聚集到某些节点。

4.3 并发安全

  • 读写分离:采用读写锁(ReentrantReadWriteLock)或在简单场景下使用 synchronized 对关键方法加锁;

  • 原子变量:如 AtomicInteger 管理 totalWeight,防止并发更新冲突。

4.4 扩展与优化点

  • 健康检查集成:定期探测节点健康状况,剔除失效节点;

  • 动态权重调整:根据节点负载、响应时间等动态调整 weight

  • 缓存结果:多线程高并发时,可做短期结果缓存,降低锁竞争。

5. 完整实现代码

// ==================== 文件:ServerNode.java ====================
package com.example.loadbalancer;

/**
 * 表示一个后端服务器节点
 */
public class ServerNode {
    /** 节点唯一标识 */
    private final String id;
    /** 静态权重:初始化时配置,不随调度变化 */
    private final int weight;
    /** 动态权重:每次调度时累加,用于平滑加权轮询 */
    private int currentWeight;

    public ServerNode(String id, int weight) {
        this.id = id;
        this.weight = weight;
        this.currentWeight = 0;
    }

    public String getId() {
        return id;
    }

    public int getWeight() {
        return weight;
    }

    public int getCurrentWeight() {
        return currentWeight;
    }

    public void setCurrentWeight(int currentWeight) {
        this.currentWeight = currentWeight;
    }

    @Override
    public String toString() {
        return "ServerNode{id='" + id + "', weight=" + weight + ", currentWeight=" + currentWeight + '}';
    }
}

// ==================== 文件:WeightedRoundRobinLoadBalancer.java ====================
package com.example.loadbalancer;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 平滑加权轮询负载均衡器实现
 */
public class WeightedRoundRobinLoadBalancer {

    /** 活跃的服务器列表 */
    private final List<ServerNode> nodes = new ArrayList<>();
    /** 所有节点静态权重之和 */
    private int totalWeight = 0;
    /** 读写锁保证并发安全 */
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    /**
     * 增加一个服务器节点
     * @param node 新增的节点
     */
    public void addNode(ServerNode node) {
        lock.writeLock().lock();
        try {
            nodes.add(node);
            totalWeight += node.getWeight();
        } finally {
            lock.writeLock().unlock();
        }
    }

    /**
     * 删除一个服务器节点
     * @param nodeId 要删除的节点ID
     */
    public void removeNode(String nodeId) {
        lock.writeLock().lock();
        try {
            nodes.removeIf(n -> n.getId().equals(nodeId));
            // 重算总权重
            totalWeight = nodes.stream().mapToInt(ServerNode::getWeight).sum();
        } finally {
            lock.writeLock().unlock();
        }
    }

    /**
     * 执行一次平滑加权轮询,返回被选中的节点
     * @return 本次调度选中的 ServerNode
     */
    public ServerNode selectNode() {
        lock.writeLock().lock();
        try {
            if (nodes.isEmpty()) {
                return null;
            }
            ServerNode selected = null;
            for (ServerNode node : nodes) {
                // 增加当前权重
                node.setCurrentWeight(node.getCurrentWeight() + node.getWeight());
                // 选出当前权重大者
                if (selected == null || node.getCurrentWeight() > selected.getCurrentWeight()) {
                    selected = node;
                }
            }
            // 减去总权重,实现平滑效果
            if (selected != null) {
                selected.setCurrentWeight(selected.getCurrentWeight() - totalWeight);
            }
            return selected;
        } finally {
            lock.writeLock().unlock();
        }
    }

    /**
     * 打印当前所有节点状态,用于调试
     */
    public void printStatus() {
        lock.readLock().lock();
        try {
            System.out.println("Total weight = " + totalWeight);
            for (ServerNode node : nodes) {
                System.out.println(node);
            }
        } finally {
            lock.readLock().unlock();
        }
    }
}

// ==================== 文件:TestWeightedRoundRobin.java ====================
package com.example.loadbalancer;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.util.HashMap;
import java.util.Map;

/**
 * JUnit 单元测试,验证加权轮询分布比例
 */
public class TestWeightedRoundRobin {

    private WeightedRoundRobinLoadBalancer lb;

    @Before
    public void setup() {
        lb = new WeightedRoundRobinLoadBalancer();
        // 添加三个节点,权重分别为 5、1、1
        lb.addNode(new ServerNode("A", 5));
        lb.addNode(new ServerNode("B", 1));
        lb.addNode(new ServerNode("C", 1));
    }

    @Test
    public void testDistribution() {
        Map<String, Integer> countMap = new HashMap<>();
        countMap.put("A", 0);
        countMap.put("B", 0);
        countMap.put("C", 0);

        int totalRequests = 7 * 100; // 模拟 700 次请求
        for (int i = 0; i < totalRequests; i++) {
            ServerNode node = lb.selectNode();
            countMap.put(node.getId(), countMap.get(node.getId()) + 1);
        }
        // A 应获得约 500 次,B、C 各约 100 次
        System.out.println(countMap);
        Assert.assertTrue(Math.abs(countMap.get("A") - 500) < 50);
        Assert.assertTrue(Math.abs(countMap.get("B") - 100) < 20);
        Assert.assertTrue(Math.abs(countMap.get("C") - 100) < 20);
    }
}

6. 代码详细解读

  • ServerNode.java

    • ServerNode(String id, int weight):构造方法,初始化节点 ID、静态权重,并将动态权重置零。

    • getCurrentWeight() / setCurrentWeight(int):获取和设置当前动态权重,用于轮询算法计算。

    • toString():输出节点状态,包含 ID、静态权重和当前权重,方便调试。

  • WeightedRoundRobinLoadBalancer.java

    • addNode(ServerNode):在写锁保护下将新节点添加到列表,并更新总权重。

    • removeNode(String):删除指定 ID 的节点,重算总权重,保持一致性。

    • selectNode():核心调度方法(写锁保护):

      1. 遍历所有节点,累加 currentWeight += weight

      2. 选出 currentWeight 最大的节点;

      3. 对选中节点做 currentWeight -= totalWeight

      4. 返回选中节点。

    • printStatus():读锁保护,打印所有节点及总权重信息,用于在线或本地调试。

  • TestWeightedRoundRobin.java

    • 使用 JUnit 框架进行单元测试;

    • setup() 方法中预置三节点;

    • testDistribution() 模拟大量请求,统计各节点命中次数,并断言比例在容忍范围内。


7. 项目详细总结

本项目通过 Java 语言完整实现了“平滑加权轮询”负载均衡算法,具有以下特点:

  1. 简洁明了:核心算法仅几十行,易于理解;

  2. 线程安全:利用读写锁保护,适应多线程高并发场景;

  3. 易扩展:节点管理(增删)与调度分离,后续可接入健康检查、动态权重;

  4. 可测试:单元测试覆盖,验证分配比例,增强信心。

通过本实现,读者可以深刻理解加权轮询原理,并在实际业务中快速接入或二次开发,以满足不同场景需求。


8. 项目常见问题及解答

Q1:为什么要使用平滑加权而非简单加权?
A:简单加权在短时间内可能产生请求聚集;平滑加权通过 currentWeight 动态调整,保证请求在整个调度周期内均匀分散。

Q2:多线程场景会不会影响 currentWeight 计算准确性?
A:本实现对 selectNode() 加写锁,保证一次调度完整原子;读操作仅打印状态使用读锁,不影响性能。

Q3:如何在运行时动态调整某个节点的权重?
A:可先使用 removeNode() 移除旧节点,再 addNode() 添加新权重节点;或扩展接口直接修改 ServerNode.weight 并同步更新 totalWeight

Q4:节点宕机时如何剔除?
A:可结合健康检查模块,定期探测节点可用性,在线调用 removeNode() 剔除失效节点。


9. 扩展方向与性能优化

  1. 健康检查集成

    • 定时 HTTP/TCP 探针,自动上下线节点;

    • 对失败率、延迟等指标加权,动态调整权重。

  2. 动态权重调整

    • 实时监控节点的 CPU、内存、响应时间;

    • 根据负载情况自动增减 weight,实现自适应调度。

  3. 无锁优化

    • selectNode() 使用原子变量和无锁算法(如 CAS)替代读写锁,减少线程争用;

    • 或基于 StampedLock 实现乐观读写。

  4. 高可用部署

    • 将负载均衡器自身做成集群,通过一致性协议(如 Raft)同步节点列表;

    • 支持滚动升级、灰度发布。

  5. 分布式追踪与监控

    • 集成 OpenTelemetry,采集每次请求分配路径;

    • 在 Prometheus + Grafana 查看各节点的调用次数、延迟分布。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐