[Java微服务架构]4_服务通信之客户端负载均衡
欢迎来到啾啾的博客🐱,一个致力于构建完善的Java程序员知识体系的博客📚,记录学习的点滴,分享工作的思考、实用的技巧,偶尔分享一些杂谈💬。
欢迎评论交流,感谢您的阅读😄。
在上一篇服务通信 中我们已经了解到服务同步通信的选择,本篇继续探索服务通信的设计思想。
目录
- 引言
- 负载均衡
- 为设计算法获取信息
- 数据采集方式
- 设计流量分发算法
- 轮询
- 最小连接数
- 动态权重算法(响应时间加权)
- 为设计算法获取信息
- 总结
引言
在微服务架构中,服务通常以集群形式部署以实现高可用性和水平扩展能力。当其他服务需要调用某个服务集群时,一个关键问题随之产生:如何设计请求分发策略才能实现服务集群的最大吞吐量和最优性能?
若直接将请求固定发送至单个服务实例或简单映射,会导致以下问题:
-
负载不均:部分实例过载,其他实例闲置,资源利用率低下
-
单点故障风险:目标实例压力过大可能引发性能瓶颈甚至服务雪崩
-
架构优势浪费:集群的高可用性和弹性扩展能力无法有效发挥
解决这一问题的核心机制是负载均衡(Load Balancing)。其核心设计目标是通过智能分发请求流量至集群中的多个实例,实现以下关键收益:
-
资源最优化:充分利用集群整体计算能力,最大化吞吐量
-
性能均衡化:避免局部热点,确保请求响应延迟最优
有的资料将熔断、降级等机制放入负载均衡,本篇暂不做论述。
本篇讨论的都是客户端的负载均衡,即“从服务集群中寻找到一个合适的服务来调用”。
负载均衡
已知,负载均衡的核心设计目的是通过智能分发将请求流量均匀地分布到服务集群中,以实现服务集群的最大吞吐量,均衡服务器性能。
为此,我们需要做什么设计呢?
两步走,获取服务实例信息——>设计算法
为设计算法获取信息
在服务注册一篇中,有讲过服务实例的基本信息为:服务名、实例IP、端口,为了服务注册,注册中心一般会设计一些其他属性如:服务状态、权重、最后心跳时间、唯一ID、服务标签的等。
为了设计负载均衡,也需要设计并获取服务的一些属性。
以Ribbon的ServerStats为例,需要请求耗时、错误率、当前活跃请求数等
![![[Pasted image 20250324161358.png]]](https://i-blog.csdnimg.cn/direct/2feeeb2030f44360a9df25de2035636d.png)
| 负载均衡算法 | 依赖的关键信息 | 决策逻辑 |
|---|---|---|
| 轮询 | 实例列表 | 简单轮转选择 |
| 加权轮询 | 静态权重 + 健康状态 | 按权重比例分配 |
| 最小连接数 | 实时活跃连接数 | 选择当前负载最轻实例 |
| 一致性哈希 | 实例标识 + 请求特征(如用户ID) | 哈希值固定映射 |
| 区域感知 | 实例区域标签 + 网络延迟 | 优先选择同区域低延迟实例 |
数据采集方式
数据采集有主动探测模式、被动上报模式,以及混合模式。
设计流量分发算法
为了避免单点过载,通过算法(如轮询、随机、权重等)将请求均匀分布到多个实例,这里均匀的可以是次数,也可以是响应时间,最终目的都是最大吞吐量。
一些典型的负载均衡算法与设计如下:
| 算法 | 设计原理 | 适用场景 |
|---|---|---|
| 轮询(Round Robin) | 按顺序依次分配请求,保证绝对均衡 | 实例性能相近的集群 |
| 加权轮询/随机 | 根据实例配置(CPU、内存)分配权重,高性能实例接收更多请求 | 异构硬件环境 |
| 最小连接数 | 动态选择当前连接数最少的实例,避免过载 | 长连接或处理时间差异大的服务 |
| 一致性哈希 | 对请求特征(如用户ID)哈希计算,固定映射到特定实例,减少缓存失效 | 分布式缓存、会话保持场景 |
| 区域感知(Zone-Aware) | 优先选择同一区域的实例,降低跨区域网络延迟 | 多数据中心部署 |
轮询
最常见的负载均衡算法,通过顺序分配请求实现绝对均衡。
核心机制是通用循环索引选择目标实例,确保每个实例获得均等的请求量。
以Ribbon框架为例,其轮询实现(RoundRobinRule)大致如下:
public class RoundRobinRule extends AbstractLoadBalancerRule {private AtomicInteger nextServerCyclicCounter;public RoundRobinRule() { nextServerCyclicCounter = new AtomicInteger(0); }public Server choose(ILoadBalancer lb, Object key) {// 获取可用服务列表List<Server> allServers = lb.getAllServers();int serverCount = allServers.size();// 计算下一个索引(线程安全)int nextIndex = incrementAndGetModulo(serverCount);return allServers.get(nextIndex);}private int incrementAndGetModulo(int modulo) {// 无线循环获取下一个值for (;;) {//获取当前索引int current = nextServerCyclicCounter.get();// +1取模int next = (current + 1) % modulo;// 更新AtomicInteger值if (nextServerCyclicCounter.compareAndSet(current, next))return next;}}
}
- 使用
AtomicInteger保证索引更新的原子性,避免并发问题。 - 通过取模运算实现循环逻辑,索引超出范围时重置为0。
详细源码则如下:
public Server choose(ILoadBalancer lb, Object key) {if (lb == null) {log.warn("no load balancer");return null;}Server server = null;int count = 0;// 重试不超过10次while (server == null && count++ < 10) {// getReachableServers返回一个不可修改的List,List为从可达服务列表List<Server> reachableServers = lb.getReachableServers();// 获取所有服务器列表List<Server> allServers = lb.getAllServers();int upCount = reachableServers.size();int serverCount = allServers.size();// 服务器不可用if ((upCount == 0) || (serverCount == 0)) {log.warn("No up servers available from load balancer: " + lb);return null;}// 计算下一个服务器索引int nextServerIndex = incrementAndGetModulo(serverCount);// 从服务列表获取服务server = allServers.get(nextServerIndex);if (server == null) {/* Transient. */Thread.yield();continue;}if (server.isAlive() && (server.isReadyToServe())) {return (server);}// Next.server = null;}if (count >= 10) {log.warn("No available alive servers after 10 tries from load balancer: "+ lb);}return server;}
对Ribbon怎么获取服务感兴趣的可以评论留言。
最小连接数
最小连接数的设计思想为动态选择当前连接数最少的实例,避免过载。需实时监控实例负载状态,适合处理时间差异大的长连接场景。
设计实现需要维护每个实例的活跃连接数计数器,选择计数器值最小的实例处理新请求。
感觉适合AI应用,AI给出结果的时间都比较长且长短不一。
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;/*** 服务实例对象(记录连接数)* 这个代码demo存在当所有服务连接数都一样时都访问到同一实例的情况,可以通过锁与随机值来解决,这里没有实现*/
class AIServer {private final String endpoint; // 实例地址(如 "http://ai-node1:5000")private final AtomicInteger activeConnections = new AtomicInteger(0); // 当前活跃连接数public AIServer(String endpoint) {this.endpoint = endpoint;}// 获取当前活跃连接数(线程安全)public int getActiveConnections() {return activeConnections.get();}// 增加连接数(请求开始时调用)public void incrementConnections() {activeConnections.incrementAndGet();}// 减少连接数(请求结束时调用)public void decrementConnections() {activeConnections.decrementAndGet();}public String getEndpoint() {return endpoint;}
}/*** 最小连接数负载均衡器*/
public class LeastConnectionsLoadBalancer {private final List<AIServer> servers;public LeastConnectionsLoadBalancer(List<AIServer> servers) {this.servers = servers;}/*** 选择当前连接数最少的可用实例*/public AIServer selectServer() {AIServer selected = null;int minConnections = Integer.MAX_VALUE;// 遍历所有实例,寻找最小连接数的节点for (AIServer server : servers) {int current = server.getActiveConnections();if (current < minConnections) {minConnections = current;selected = server;}}System.out.println("Thread:"+ Thread.currentThread().getName()+",Selected server: " + selected.getEndpoint() + " ,connections : "+ selected.getActiveConnections());return selected;}/*** 示例:执行AI任务(自动管理连接数)*/public void executeAITask(String input) {AIServer targetServer = selectServer();if (targetServer == null) {throw new IllegalStateException("No available AI servers");}try {targetServer.incrementConnections(); // 增加连接计数// 模拟调用AI服务(实际替换为HTTP/gRPC调用)System.out.println("Thread:"+ Thread.currentThread().getName()+",Processing input on " + targetServer.getEndpoint());// 这里执行实际的AI推理逻辑...Thread.sleep(1000); // 模拟长时任务} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {targetServer.decrementConnections(); // 确保连接计数释放}}// 测试用例public static void main(String[] args) {List<AIServer> servers = Arrays.asList(new AIServer("http://ai-node1:5000"),new AIServer("http://ai-node2:5000"),new AIServer("http://ai-node3:5000"));LeastConnectionsLoadBalancer lb = new LeastConnectionsLoadBalancer(servers);// 模拟并发请求for (int i = 0; i < 10; i++) {new Thread(() -> lb.executeAITask("test input")).start();}}
}
### 动态权重算法(响应时间加权)
根据实时指标(如响应时间、CPU负载)动态调整权重,高性能实例自动获得更高优先级。Ribbon的 WeightedResponseTimeRule 是其典型实现。
public class WeightedResponseTimeRule extends RoundRobinRule {private volatile List<Double> accumulatedWeights;public Server choose(ILoadBalancer lb, Object key) {// 根据响应时间计算权重List<Double> weights = calculateWeights();double maxWeight = weights.get(weights.size() - 1);double randomWeight = random.nextDouble() * maxWeight;// 选择匹配的权重区间for (int i = 0; i < weights.size(); i++) {if (randomWeight < weights.get(i)) {return servers.get(i);}}return null;}
}
-
定期统计各实例的平均响应时间,响应越快权重越高。
-
通过权重区间随机选择实例,实现动态负载分配
总结
集合常用框架,负载均衡算法选型如下:
| 算法 | 依赖信息 | 适用场景 | 框架案例 |
|---|---|---|---|
| 轮询 | 实例列表 | 同构集群、简单请求分发 | Ribbon RoundRobinRule |
| 加权轮询 | 静态权重 + 健康状态 | 异构集群、静态权重分配 | Nginx、华为云SLB |
| 源地址哈希 | 客户端的源IP地址、后端服务器列表 | 会话保持、缓存亲和性 | Spring Cloud HashRule |
| 最小连接数 | 实时活跃连接数 | 长连接、处理时间差异大 | HAProxy、Envoy |
| 动态响应时间加权 | 服务器的响应时间、服务器的权重、健康状态 | 实时性能敏感型系统 | Ribbon WeightedResponseTimeRule |
| 一致性哈希 | 实例标识 + 请求特征(如用户ID) | 分布式缓存、避免雪崩 | gRPC、Redis Cluster |
需要流量分发的场景一般都需要负载均衡,其核心是算法,目的是使得集群吞吐量最大化、性能最佳。
相关文章:
[Java微服务架构]4_服务通信之客户端负载均衡
欢迎来到啾啾的博客🐱,一个致力于构建完善的Java程序员知识体系的博客📚,记录学习的点滴,分享工作的思考、实用的技巧,偶尔分享一些杂谈💬。 欢迎评论交流,感谢您的阅读😄…...
基于SpringBoot实现的高校实验室管理平台功能四
一、前言介绍: 1.1 项目摘要 随着信息技术的飞速发展,高校实验室的管理逐渐趋向于信息化、智能化。传统的实验室管理方式存在效率低下、资源浪费等问题,因此,利用现代技术手段对实验室进行高效管理显得尤为重要。 高校实验室作为…...
吴恩达深度学习复盘(1)神经网络与深度学习的发展
一、神经网络的起源与生物学动机 灵感来源 神经网络的最初动机源于对生物大脑的模仿。20 世纪 50 年代,科学家试图通过软件模拟神经元的工作机制(如树突接收信号、轴突传递信号),构建类似人类大脑的信息处理系统。 生物神经元的简…...
用Python实现资本资产定价模型(CAPM)
使用 Python 计算资本资产定价模型(CAPM)并获取贝塔系数(β)。 步骤 1:导入必要的库 import pandas as pd import yfinance as yf import statsmodels.api as sm import matplotlib.pyplot as plt 步骤 2࿱…...
Linux进程管理之子进程的创建(fork函数)、子进程与线程的区别、fork函数的简单使用例子、子进程的典型应用场景、父进程等待子进程结束后自己再结束
收尾 进程终止:子进程通过exit()或_exit()终止,父进程通过wait()或waitpid()等待子进程终止,并获取其退出状态。?其实可以考虑在另一篇博文中来写 fork函数讲解 fork函数概述 fork() 是 Linux 中用于创建新进程的系统调用。当…...
妙用《甄嬛传》中的选妃来记忆概率论中的乘法公式
强烈推荐最近在看的不错的B站概率论课程 《概率统计》正课,零废话,超精讲!【孔祥仁】 《概率统计》正课,零废话,超精讲!【孔祥仁】_哔哩哔哩_bilibili 其中概率论中的乘法公式,老师用了《甄嬛传…...
虚幻基础:UI
文章目录 控件蓝图可以装载其他控件蓝图可以安装其他蓝图接口 填充:相对于父组件填充水平框尺寸—填充—0.5:改变填充的尺寸填充—0.5:改变与父组件的距离 锚点:相对于父组件的控件坐标系原点,屏幕比例改变时ÿ…...
【MySQL篇】事务管理,事务的特性及深入理解隔离级别
目录 一,什么是事务 二,事务的版本支持 三,事务的提交方式 四,事务常见操作方式 五,隔离级别 1,理解隔离性 2,查看与设置隔离级别 3,读未提交(read uncommitted&a…...
项目实战-角色列表
抄上一次写过的代码: import React, { useState, useEffect } from "react"; import axios from axios; import { Button, Table, Modal } from antd; import { BarsOutlined, DeleteOutlined, ExclamationCircleOutlined } from ant-design/icons;const…...
fetch`的语法规则及常见用法
fetch() 是 JavaScript 用于发送 HTTP 请求的内置 API,功能强大,语法简洁。以下是 fetch 的语法规则及常见用法。 1. fetch 基本语法 fetch(url, options).then(response > response.json()) // 解析 JSON 响应体.then(data > console.log(data))…...
如何排查java程序的宕机和oom?如何解决宕机和oom?
排查oom 用jmap生成我们的堆空间的快照Heap Dump(堆转储文件),来分析我们的内存占用 用可视化工具,例如java中的jhat分析Heap Dump文件 ,它分析完会通过一个浏览器打开一个可视化页面展示分析结果 根据oom的类型来调…...
26_ajax
目录 了解 接口 前后端交互 一、安装服务器环境 nodejs ajax发起请求 渲染响应结果 get方式传递参数 post方式传递参数 封装ajax_上 封装ajax下 了解 清楚前后端交互就可以写一些后端代码了。小项目 现在写项目开发的时候都是前后端分离 之前都没有前端这个东西&a…...
代理模式(Proxy Pattern)实现与对比
代理模式(Proxy Pattern)实现与对比 1. 虚拟代理(Virtual Proxy) 定义:延迟加载对象,避免资源浪费。 适用场景:大文件或资源的加载(如图片、数据库连接)。 代码示例 /…...
MySQL - 数据库基础操作
SQL语句 结构化查询语言(Structured Query Language),在关系型数据库上执行数据操作、数据检索以及数据维护的标准语言。 分类 DDL 数据定义语言(Data Definition Language),定义对数据库对象(库、表、列、索引)的操作。 DML 数据操作语言(Data Manip…...
Spring Boot热部署插件
在实际开发中,我们修改某些代码或页面都需要重启应用后才能生效,如果每次都手动重启,会降低了开发效率;热部署是指当我们修改代码后,服务能自动重启加载新修改的内容,这样大大提高了我们开发的效率…...
pip install cryptacular卡住,卡在downloading阶段
笔者安装pip install cryptacular卡在downloading阶段,但不知道为何 Collecting cryptacularCreated temporary directory: /tmp/pip-unpack-qfbl8f08http://10.170.22.41:8082 "GET http://repo.huaweicloud.com/repository/pypi/packages/42/69/34d478310d6…...
AI大模型从0到1记录学习 day09
第 8 章 面向对象之类和对象 8.1 面向过程和面向对象 面向过程编程(Procedural Programming)和面向对象编程(OOP)是两种不同的编程范式,它们在软件开发中都有广泛的应用。 Python是一种混合型的语言,既支持…...
【FW】ADB指令分类速查清单
1. 设备管理 指令核心作用adb devices列出已连接设备adb reboot重启设备adb reboot bootloader进入Bootloader模式adb reboot recovery进入Recovery模式adb root获取Root权限(需设备支持)adb remount挂载系统分区为可读写 2. 应用管理 指令核心作用adb…...
Kafka中的消息是如何存储的?
大家好,我是锋哥。今天分享关于【Kafka中的消息是如何存储的?】面试题。希望对大家有帮助; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在 Kafka 中,消息是通过 日志(Log) 的方式进行存储的。…...
Altium Designer——同时更改多个元素的属性(名称、网络标签、字符串标识)
右键要更改的其中一个对象,选择查找相似… 进入到筛选界面,就是选择你要多选的对象的共同特点(名字、大小等等),我这里要更改的是网络标签,所以我选择Text设置为一样。 点击应用就是应用该筛选调节&#…...
当模板方法模式遇上工厂模式:一道优雅的烹饪架构设计
当模板方法模式遇上工厂模式:一道优雅的烹饪架构设计 模式交响曲的实现模板方法模式搭建烹饪骨架(抽象类)具体菜品(子类) 工厂模式 模式协作的优势呈现扩展性演示运行时流程控制 完整代码 如果在学习 设计模式的过程中…...
c++位运算总结
在C中,位运算是对二进制位进行操作的运算,主要有以下几种: 1. 按位与( & ):两个操作数对应位都为1时,结果位才为1,否则为0。例如 3 & 5 , 3 二进制是 0000 0011…...
企业级知识库建设:自建与开源产品集成的全景解析 —— 产品经理、CTO 与 CDO 的深度对话
文章目录 一、引言二、主流产品与方案对比表三、自建方案 vs. 开源产品集成:技术路径对比3.1 自建方案3.2 开源产品集成方案 四、结论与个人观点 一、引言 在当今数据驱动的商业环境中,构建高质量的知识库已成为企业数字化转型的关键一环。本博客分别从…...
Python小练习系列 Vol.6:单词搜索(网格回溯)
🧠 Python小练习系列 Vol.6:单词搜索(网格回溯) 🔍 本期我们来挑战一道 LeetCode 上经典的网格型回溯题 —— 单词搜索,考察对 DFS 状态恢复的掌握! 🧩 一、题目描述 给定一个 m x…...
shell脚本--MySQL简单调用
实现功能 增 数据库的创建,数据表的创建已经实现 创建用户 删 删除数据库, 删除库下的某个表, 删除某个用户 改 暂无 查 查看所有的数据库, 查看某个库下的所有数据表, 查看某个表的结构, 查…...
vue3项目配置别名
vue3项目配置别名 src别名的配置TypeScript 编译配置如果出现/别名引入报找不到的问题 src别名的配置 在开发项目的时候文件与文件关系可能很复杂,因此我们需要给src文件夹配置一个别名!!! // vite.config.ts import {defineCon…...
Rust 面向对象
Rust 面向对象 引言 Rust 是一种系统编程语言,以其高性能、内存安全和并发支持而受到关注。Rust 的面向对象特性是其强大功能之一,它允许开发者以面向对象的方式构建复杂的应用程序。本文将深入探讨 Rust 的面向对象编程(OOP)特性,包括类的定义、继承、封装和多态等概念…...
[ C语言 ] | 从0到1?
目录 认识计算机语言 C语言 工欲善其事必先利其器 第一个C语言代码 这一些列 [ C语言 ] ,就来分享一下 C语言 相关的知识点~ 认识计算机语言 我们说到计算机语言,语言,就是用来沟通的工具,计算机语言呢?就是我们…...
[Mac]利用Hexo+Github Pages搭建个人博客
由于我这台Mac基本没啥环境,因此需要从零开始配置,供各位参考。 注意⚠️:MacBook (M4)使用/bin/zsh作为默认Shell,其对应的配置文件为~/.zshrc 参考文档: HEXO系列教程 | 使用GitHub部署静态博客HEXO | 小白向教程 文…...
pycharm与python版本
python 3.6-3.9 pycharm 2021版本搭配最好 python 3.8 pycharm 2019版本搭配最好 pycharm各版本下载...
