设计模式与负载均衡:策略模式选算法、代理模式转请求、观察者监控节点
负载均衡这东西,说白了就是「把活儿分给兄弟们干」。我做了十几年后端系统,几乎每个高并发项目都离不开它。但很多人只停留在配配 Nginx 或者用用 Ribbon 的层面,一旦要自己实现一套轻量级的负载均衡器,就抓瞎了。
今天咱们就用三个经典设计模式,把负载均衡的核心逻辑拆开揉碎。你想想看,一个负载均衡器无非就三件事:选谁干活、怎么转发、如何知道谁还活着。正好对应策略模式、代理模式和观察者模式。
1. 策略模式:负载算法怎么选?
我记得刚入行那会儿,公司有个老系统用的轮询算法。结果有一台机器配置高、一台配置低,轮询下去低配机经常被打满。后来我改成加权轮询,问题才解决。你看,算法选不对,系统就遭罪。
策略模式在这里的用法很自然:把「选择服务器」这个行为抽象成接口,每种算法一个实现类。调用方只需要传入策略对象,不用关心具体怎么选的。
// 策略接口
public interface LoadBalanceStrategy {
Server select(List<Server> servers, String clientIp);
}
// 具体策略:轮询
public class RoundRobinStrategy implements LoadBalanceStrategy {
private int index = 0;
@Override
public Server select(List<Server> servers, String clientIp) {
if (servers == null || servers.isEmpty()) return null;
index = (index + 1) % servers.size();
return servers.get(index);
}
}
// 具体策略:加权随机
public class WeightedRandomStrategy implements LoadBalanceStrategy {
@Override
public Server select(List<Server> servers, String clientIp) {
int totalWeight = servers.stream().mapToInt(Server::getWeight).sum();
int random = new Random().nextInt(totalWeight);
int cumulative = 0;
for (Server server : servers) {
cumulative += server.getWeight();
if (random < cumulative) return server;
}
return servers.get(0);
}
}
// 具体策略:一致性哈希
public class ConsistentHashStrategy implements LoadBalanceStrategy {
private final TreeMap<Integer, Server> hashRing = new TreeMap<>();
private static final int VIRTUAL_NODES = 160;
public ConsistentHashStrategy(List<Server> servers) {
for (Server server : servers) {
for (int i = 0; i < VIRTUAL_NODES; i++) {
int hash = (server.getIp() + "#" + i).hashCode();
hashRing.put(hash, server);
}
}
}
@Override
public Server select(List<Server> servers, String clientIp) {
int hash = clientIp.hashCode();
Map.Entry<Integer, Server> entry = hashRing.ceilingEntry(hash);
if (entry == null) entry = hashRing.firstEntry();
return entry.getValue();
}
}
2. 代理模式:请求怎么转发?
代理模式在负载均衡里扮演的角色,就是「中间人」。客户端不直接访问后端服务器,而是通过代理对象转发。这样做的好处是:可以在转发前后做统一处理,比如记录日志、限流、熔断。
我个人习惯把代理模式分为静态代理和动态代理。静态代理就是手写一个转发类,动态代理可以用 Java 的 InvocationHandler 或者 C++ 的虚函数表拦截。不过在实际项目中,我更推荐用静态代理,因为逻辑清晰,出了问题好排查。
// 真实主题:后端服务
public interface BackendService {
Response handleRequest(Request request);
}
public class RealBackendService implements BackendService {
private final String serverId;
public RealBackendService(String serverId) {
this.serverId = serverId;
}
@Override
public Response handleRequest(Request request) {
// 实际处理业务逻辑
return new Response(200, "Handled by " + serverId);
}
}
// 代理类:负责转发请求
public class LoadBalanceProxy implements BackendService {
private final List<Server> servers;
private final LoadBalanceStrategy strategy;
public LoadBalanceProxy(List<Server> servers, LoadBalanceStrategy strategy) {
this.servers = servers;
this.strategy = strategy;
}
@Override
public Response handleRequest(Request request) {
// 前置处理:记录请求开始时间
long start = System.currentTimeMillis();
// 策略模式选择目标服务器
Server target = strategy.select(servers, request.getClientIp());
if (target == null) {
return new Response(503, "No available server");
}
// 转发请求到真实服务
RealBackendService realService = new RealBackendService(target.getId());
Response response = realService.handleRequest(request);
// 后置处理:记录耗时
long elapsed = System.currentTimeMillis() - start;
System.out.println("Request to " + target.getId() + " took " + elapsed + "ms");
return response;
}
}
3. 观察者模式:节点状态怎么监控?
负载均衡最怕什么?怕把请求转发给已经挂掉的节点。观察者模式正好解决这个问题:让每个服务器节点作为被观察者,负载均衡器作为观察者。节点状态变化时主动通知,负载均衡器及时更新可用列表。
我见过很多团队用定时轮询来检测节点健康,其实效率很低。观察者模式能做到「有变化才通知」,比轮询省资源得多。
// 观察者接口
public interface ServerObserver {
void onServerStatusChanged(Server server, ServerStatus newStatus);
}
// 被观察者:服务器节点
public class Server {
private String id;
private String ip;
private int port;
private int weight;
private ServerStatus status;
private List<ServerObserver> observers = new ArrayList<>();
public void addObserver(ServerObserver observer) {
observers.add(observer);
}
public void removeObserver(ServerObserver observer) {
observers.remove(observer);
}
public void setStatus(ServerStatus newStatus) {
if (this.status != newStatus) {
this.status = newStatus;
// 状态变化时通知所有观察者
for (ServerObserver observer : observers) {
observer.onServerStatusChanged(this, newStatus);
}
}
}
}
// 具体观察者:负载均衡器
public class LoadBalancer implements ServerObserver {
private List<Server> availableServers = new CopyOnWriteArrayList<>();
@Override
public void onServerStatusChanged(Server server, ServerStatus newStatus) {
if (newStatus == ServerStatus.UP) {
if (!availableServers.contains(server)) {
availableServers.add(server);
System.out.println("Server " + server.getId() + " is UP, added to pool");
}
} else if (newStatus == ServerStatus.DOWN) {
availableServers.remove(server);
System.out.println("Server " + server.getId() + " is DOWN, removed from pool");
}
}
public List<Server> getAvailableServers() {
return availableServers;
}
}
// 状态枚举
public enum ServerStatus {
UP, DOWN, DEGRADED
}
4. 三者如何协同工作?
光讲单个模式没意思,咱们看看它们怎么配合。下面这张图展示了完整的请求处理流程:
流程其实很简单:
- 客户端请求到达代理(负载均衡器)
- 代理调用策略模式选择目标服务器
- 代理将请求转发给选中的服务器
- 服务器状态变化时,通过观察者模式通知代理
- 代理更新可用服务器列表,下次选择时跳过不可用节点
5. 实战中的组合技巧
这三个模式在实际项目中不是孤立的。我分享几个组合使用的经验:
| 场景 | 模式组合 | 说明 |
|---|---|---|
| 灰度发布 | 策略 + 代理 | 策略模式根据用户ID或IP选择灰度版本,代理模式负责转发到对应集群 |
| 动态扩缩容 | 观察者 + 策略 | 观察者检测到新节点加入,通知策略模式重新计算权重或哈希环 |
| 熔断降级 | 观察者 + 代理 | 观察者发现节点响应变慢,代理模式自动降级到备用节点 |