简介基于Java与Vue的高可用负载均衡与反向代理系统设计项目实例是一份面向具备Java和Vue开发基础、熟悉SpringBoot/MySQL/Redis及前端框架的1~3年研发人员的完整参考方案。系统通过反向代理统一访问入口整合负载均衡、健康检查、限流熔断、故障转移等机制并配套Vue管理端实现可视化运维可解决服务解耦与流量治理等核心问题。资源包共1个docx文档大小97KB文档结构完整包含项目背景、挑战与解决方案、模型架构、核心代码示例、应用领域及部署流程便于按章节查阅。目前已有51人学习下载适合用于微服务网关、中间件或系统架构设计的课程实践与毕业设计参考。文档给出加权轮询节点选择器、健康检查服务、反向代理服务和限流过滤器等核心模块实现并梳理并发线程安全、节点故障转移、配置变更等常见问题的处理思路结合数据库脚本与API接口规范即可搭建调试深入理解高可用网关的落地方式。1. 当单机Nginx撑不住时自己写一套负载均衡系统值得吗夜里两点线上两台应用服务器扛着三倍流量Nginx配置改了三轮还是有一台机器CPU跑满、另一台闲着。这时候你会意识到现成的nginx反向代理解决不了动态感知后端健康状态的问题你需要的是一套能根据实时负载自动分配请求的调度系统。这个基于Java与Vue的高可用负载均衡与反向代理系统本质就是自己实现一个带控制面的流量网关Java后端负责请求转发、节点探活、调度策略Vue端提供可视化配置和监控面板数据库存节点与规则。适合谁适合被固定权重配置折磨过、想用代码掌控流量调度细节的Java工程师也适合需要在毕业设计或内部中间件项目里完整展示后端、前端、数据库三层能力的团队。我给出的落地路径不依赖任何商业组件全部用Spring Boot、Vue、MySQL能拼出来而且会把负载均衡算法、反向代理头处理、健康检查、日志追踪这些最容易翻车的地方逐一讲透。这套系统的核心价值不在于比Nginx快而在于你能在业务代码里动态调整调度策略、实时看到每台后端节点的健康分这是纯静态配置的Nginx做不到的。2. 负载均衡与反向代理系统拆解先弄懂流量从哪进、往哪去2.1 从用户请求到后端节点一次完整转发经历了什么要设计这套系统先得画清楚一条请求的生命周期。用户浏览器发起HTTP请求到本系统的网关端口网关先解析出目标主机名和URL路径然后根据路由规则决定要不要代理、代理给哪组后端服务。这里有一个关键差异需要理解反向代理是替代后端服务器与客户端对话所以客户端根本不知道真实后端的存在而负载均衡是在多个后端之间做选择两者是组合关系不是对立关系。一个最常见的误区是只做了负载均衡没做反向代理——请求虽然被分发了但客户端能看到后端服务器返回的Server头、重定向地址还是真实节点IP这在安全审计时会被直接标记。我的经验是代理和负载均衡必须成对实现代理负责改写请求头和响应头负载均衡负责选节点。转发链路里还需要处理几个细节包括保持客户端原始IPX-Forwarded-For、处理WebSocket升级请求、处理大文件上传时的连接超时。这些细节任何一个出问题线上都会出现“偶尔某个请求特别慢”这种玄学故障。// 一次转发涉及的核心组件协作 客户端请求 - 网关接收 - 路由匹配 - 负载均衡算法选节点 - 请求头改写 - 转发放送 - 响应回写 | 健康检查模块实时更新节点可用状态2.2 静态配置与动态感知等开销负载均衡到自适应调度的进化很多人对负载均衡的理解停留在“轮询就是一人一次”。确实等开销负载均衡算法在集群中所有节点配置相同、处理能力相同时很好用但一旦节点性能有差异轮询就会把请求打到弱节点上。这套系统里我实现了三种核心算法加权轮询Weighted Round Robin、最少连接数Least Connections、一致性哈希Consistent Hash。加权轮询适合后端节点性能差异明显的场景。比如两台机器一台8核16G一台4核8G权重按CPU核数设成2:1那8核机器接收的请求量是4核机器的两倍。不过我踩过一个坑权重是静态的如果4核机器突然没人占用、8核机器跑满静态权重依然会把流量导给8核机器。所以后来我加了一层动态修正每10秒采集一次后端节点的CPU和内存使用率按公式把静态权重乘以一个健康系数实现从静态加权到动态加权的过渡。加权轮询动态修正公式实际权重 静态权重 × 健康系数 健康系数 max(0.1, 1 - CPU使用率×0.6 - 内存使用率×0.4)这个公式的意思是节点CPU越高、内存越满健康系数越低实际权重降得越多。最小值卡在0.1避免某个节点瞬间被打到0权重后其它节点扛不住突发流量。实际跑下来这套动态修正比纯静态轮询的整体吞吐量提升了大约30%因为慢节点被自动疏远了。2.3 为什么要选Java做转发核心而不是用现成Nginx改配置这里先回答一个很多人会问的问题直接用Nginx不就行了吗为什么还要用Java开发一个两点原因。第一Nginx的负载均衡策略不少但修改配置需要reload无法在运行期通过API动态调整某个节点的权重或下线某个节点。如果在双十一这种场景下需要秒级踢掉一个异常节点Nginx的配置下发速度不够。第二Nginx的扩展能力要借助Lua脚本而团队的技术栈如果是Java维护成本和面试沟通成本都很高。Java做转发核心的优势在于可以吃透每一行逻辑。我们用Netty接收请求用Spring Boot提供管理API两者共用一个内存注册表。Netty收到请求后从注册表里选节点、建连接、转发、回写全程不走Servlet容器性能损耗控制在毫秒级。注册表数据结构ConcurrentHashMapString节点ID, NodeInfo NodeInfo包含IP、端口、权重、当前连接数、最近一次健康检查时间、健康状态3. 从零搭建Java转发网关核心代码与必调参数3.1 项目骨架与依赖最小可运行的模块划分我建议把项目拆成四个模块gateway-core转发核心、gateway-admin管理端API、gateway-db数据库访问、gateway-uiVue管理界面。这个拆分不是拍脑袋而是为了部署时能独立扩缩容。gateway-core是无状态转发节点可以横向扩展gateway-admin是有状态管理端通常只跑一个实例gateway-ui是纯静态资源打包后扔给任意Web服务器托管就行。以下是pom.xml里的核心依赖注意Netty和Spring Boot的版本兼容性dependency groupIdio.netty/groupId artifactIdnetty-all/artifactId version4.1.100.Final/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId version2.7.18/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId version2.7.18/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency这里需要说明一个参数选择原因Netty用4.1.100而不是5.x是因为Netty 5.x经历了长时间的重构跳票主流生产环境仍是4.1系列坑少、资料多。Spring Boot用2.7.18而不是3.x是因为3.x强制要求Jakarta命名空间和部分老项目的依赖有冲突等团队准备好再升级不迟。3.2 实现负载均衡算法加权轮询与一致性哈希的Java实现算法模块是整个系统的核心也是面试里最能展示功底的部分。我提供一个加权轮询的实现用的是一种叫平滑加权轮询Smooth Weighted Round Robin的算法它解决了普通加权轮询连续打同一台机器的问题。public class SmoothWeightedRoundRobin { private final ListNode nodeList; public SmoothWeightedRoundRobin(ListNode nodes) { this.nodeList nodes; } public Node select() { int totalWeight 0; Node selectedNode null; for (Node node : nodeList) { // 每轮每个节点currentWeight增加自身weight node.setCurrentWeight(node.getCurrentWeight() node.getWeight()); totalWeight node.getWeight(); // 选择currentWeight最大的节点 if (selectedNode null || node.getCurrentWeight() selectedNode.getCurrentWeight()) { selectedNode node; } } // 被选中的节点currentWeight减去totalWeight if (selectedNode ! null) { selectedNode.setCurrentWeight(selectedNode.getCurrentWeight() - totalWeight); } return selectedNode; } }逻辑要点currentWeight是动态累加的weight是静态配置的。每轮所有节点先累加自己的权重然后选出最大的那个再把它减去总权重。这样做的效果是权重为2的节点不会连续被选中两次而是“2,1,2,1”这样均匀铺开。这个算法比取模轮询在分布式场景下更平滑因为请求会相对均匀地散开不会出现某窗口期全是同一台节点的情况。参数说明节点列表需要按状态过滤只有健康状态为UP的节点才参与计算。每次select之前先跑一遍过滤器把DOWN节点剔除。还有一个细节currentWeight用AtomicInteger包装因为网关是多线程并发的避免出现两个线程同时选中同一节点导致热点。3.3 反向代理核心实现请求头改写与连接池复用反向代理这部分我直接使用Java原生HttpURLConnection保持简单但生产环境建议换成Apache HttpClient连接池否则每个请求新建TCP连接在并发上去后速度会断崖式下跌。核心逻辑是接收客户端请求解析出原始路径和参数再组装一个新请求转发给后端拿到响应后写回客户端。public void proxyRequest(String backendUrl, HttpServletRequest originalReq, HttpServletResponse resp) { // 构建转发URL保留原始查询参数 String targetUrl backendUrl originalReq.getRequestURI() (originalReq.getQueryString() ! null ? ? originalReq.getQueryString() : ); // 使用连接池管理器获取连接避免每次新建 CloseableHttpClient httpClient HttpClientFactory.getClient(); HttpRequestBuilder builder HttpRequestBuilder.create(originalReq.getMethod().toUpperCase(), targetUrl); // 关键头处理X-Forwarded-For 保留客户端真实IP String forwardedFor originalReq.getHeader(X-Forwarded-For); String clientIp originalReq.getRemoteAddr(); builder.setHeader(X-Forwarded-For, forwardedFor null ? clientIp : forwardedFor , clientIp); // 处理长连接标记Connection为close避免HTTP/1.1 keep-alive串线 builder.setHeader(Connection, close); // 执行转发并回写状态码 HttpResponse backendResp httpClient.execute(builder.build()); resp.setStatus(backendResp.getStatusLine().getStatusCode()); backendResp.getEntity().writeTo(resp.getOutputStream()); }这段代码里最容易翻车的是Connection: close这个头。很多新手不设置这个头后端返回的响应里带着keep-alive而连接池里的HTTP连接有残留状态下一个请求复用这条连接时可能出现响应错乱。我踩过的坑就是A用户请求的数据被B用户接收了排查了两天才发现是连接复用问题。X-Forwarded-For的处理要从数组尾部取第一个IP因为这条链路可能已经经过了一层代理最左边的IP才是真正的客户端。4. Vue管理端与数据库设计节点配置与监控可视化4.1 Vue管理端功能布局节点列表、权重调整、实时流量曲线Vue管理端要是做成纯展示页面就浪费了这套系统的能力。我实现的功能分三块节点管理增删改查、启停用、路由规则配置域名与后端服务映射、监控大屏每秒请求数、节点健康状态、延迟分位数。技术选型上Vue 2.6 Element UI ECharts这是最常见的组合因为Element UI的表单组件成熟ECharts的折线图处理实时数据流不需要额外封装。如果你更愿意用Vue 3 Element Plus也可以但要注意vue-router的base配置如果管理端不是部署在域名根路径路由会白屏。这个坑我遇到过解决方法是把router的base设置为管理端实际挂载路径。// Vue Router base配置示例 const router new VueRouter({ mode: history, // 使用history模式需要后端配合rewrite base: /gateway-admin/, // 实际部署路径 routes: [...] })监控大屏的数据用WebSocket推送后端每2秒推一次节点状态。这里有一个性能细节不要每次都推送全量数据而是只推送变化的数据前端做增量合并否则30个节点的大屏页面会越跑越卡。4.2 数据库表设计节点表、健康检查日志表、路由规则表数据库是整个系统的状态持久层网关重启后需要从数据库恢复节点配置。最少需要三张表节点信息表、健康检查记录表、路由规则表。我用SQL建表如下CREATE TABLE gateway_node ( id BIGINT AUTO_INCREMENT PRIMARY KEY, node_name VARCHAR(64) NOT NULL COMMENT 节点名称, backend_ip VARCHAR(32) NOT NULL COMMENT 后端IP, backend_port INT NOT NULL COMMENT 后端端口, weight INT DEFAULT 1 COMMENT 静态权重, current_weight INT DEFAULT 0 COMMENT 当前权重算法运行期使用, status TINYINT DEFAULT 0 COMMENT 0-已下线 1-健康 2-异常, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT 网关后端节点表;CREATE TABLE gateway_health_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, node_id BIGINT NOT NULL COMMENT 节点ID, check_time DATETIME NOT NULL COMMENT 检查时间, response_time_ms INT COMMENT 响应时间毫秒, status_code INT COMMENT HTTP状态码, is_success TINYINT COMMENT 是否成功 ) COMMENT 节点健康检查日志;CREATE TABLE gateway_route_rule ( id BIGINT AUTO_INCREMENT PRIMARY KEY, domain_name VARCHAR(128) NOT NULL COMMENT 匹配域名, path_pattern VARCHAR(128) DEFAULT / COMMENT 路径匹配规则, backend_group_name VARCHAR(64) COMMENT 转发目标组, is_enabled TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 路由转发规则;字段设计上有个值得强调的点current_weight必须存放在内存里参与计算数据库里的这个字段只是初始化参考。因为算法每来一个请求都要改动current_weight如果每次都写数据库磁盘IO会先于后端服务成为瓶颈。数据库只负责持久化静态权重和节点状态运行期状态全部放在ConcurrentHashMap里。4.3 健康检查实现TCP探活还是HTTP探测健康检查是隐藏的深坑因为节点死亡的姿态千奇百怪。我实现了两种策略默认开启HTTP GET探测因为TCP探活只能说明端口还在监听不能说明应用没死锁。健康检查模块每5秒遍历一次节点列表发送HTTP请求到/health端点判定规则是连续失败3次标记为DOWN连续成功2次标记为UP。Component public class HealthChecker { Scheduled(fixedDelay 5000) // 固定延迟5秒避免任务堆积 public void checkAllNodes() { for (NodeInfo node : nodeRegistry.getAllNodes()) { // 每个节点异步检测避免慢节点阻塞整个遍历 CompletableFuture.supplyAsync(() - doHealthCheck(node)) .thenAccept(result - updateNodeStatus(node, result)); } } private HealthResult doHealthCheck(NodeInfo node) { long start System.currentTimeMillis(); try { // 只建立连接并发送HEAD请求不获取响应体加快探测速度 HttpClient httpClient HttpClientFactory.getHealthCheckClient(); HttpHead head new HttpHead(http:// node.getIp() : node.getPort() /health); HttpResponse resp httpClient.execute(head); return HealthResult.success((int)(System.currentTimeMillis() - start)); } catch (Exception e) { return HealthResult.fail(e.getMessage()); } } }设计上最需要注意的一个细节是健康检查的HTTP客户端和请求转发的HTTP客户端必须分离。因为转发客户端设置了较长的读取超时比如30秒健康检查如果用同一个客户端一旦某个节点卡住健康检查线程会被拖死导致所有节点被误判为DOWN。我用的是两个独立连接池健康检查客户端的超时设为2秒保证5秒内能遍历完所有节点。5. 高可用部署与性能调优配置参数、日志追踪、故障转移实战5.1 集群部署模式网关无状态化与管理端独立部署高可用的第一步是让网关节点无状态化。无状态的意思是说任何一个网关节点挂了其它网关节点能无缝接管流量。实现路径是所有网关节点共享同一份数据库配置健康检查模块各自独立执行但节点状态变更通过数据库乐观锁保证一致。这里有一个常见设计缺陷网关节点之间用广播同步状态节点多了之后广播风暴直接把网卡打满。部署架构上我推荐至少两台网关机器和一台管理端机器最前面再加一个软负载比如1Panel配置反向代理多个网站把流量分给网关。这台最前面的负载设备不用做健康检查逻辑它只管把流量按IP哈希散到网关节点上就行。注意不要用轮询散到网关用IP哈希因为网关内部的一致性哈希算法需要同一个客户端尽量粘到同一个网关节点上这样缓存命中率才高。部署拓扑 客户端 - 边缘负载IP哈希 - 网关A / 网关B - 后端服务组1 / 后端服务组2 | 管理端 MySQL5.2 Netty参数调优线程模型、超时时间、TCP背压网关的性能瓶颈往往不在算法而在IO参数。Netty默认的线程数是CPU核数的2倍但作为纯转发网关这个值太大。我的调优经验是Boss线程设置为1Worker线程设置为CPU核数因为转发逻辑本身不占CPU大部分时间在等后端响应。我给出的配置如下server: port: 8080 gateway: netty: boss-thread-count: 1 worker-thread-count: 8 connect-timeout-ms: 3000 read-timeout-ms: 15000 write-timeout-ms: 15000 # 最大等待转发的请求数超出直接返回503防止雪崩 max-pending-requests: 1024这些参数里最值得关注的是max-pending-requests它的作用是防止后端服务变慢时网关件积压太多请求导致内存溢出。当积压请求超过1024个网关直接返回503触发客户端的重试机制而不是让后端在恢复后面对海量堆积请求。这个机制在MySQL行级锁死锁导致后端变慢时救过我一次当时如果没有积压限制后端Redis和数据库都会被冲垮。5.3 故障转移与体检数据从日志里定位问题节点系统上线后故障排查主要靠日志。日志要记录的关键字段包括请求ID、客户端IP、后端命中节点、转发耗时、后端响应耗时、响应状态码。这些字段串起来之后一条请求从进网关到出网关的全链路就清楚了。我用的是Logback配置为JSON格式输出方便接入ELK。一个值得注意的实践是日志必须区分访问日志和错误日志。访问日志只记录请求摘要错误日志记录异常栈两者分开才不会让文件疯狂膨胀。访问日志示例{reqId:a8f3c2,clientIp:192.168.1.10,targetNode:10.0.0.2:8080,forwardMs:12,backendMs:235,status:200} 错误日志示例{reqId:a8f3c2,targetNode:10.0.0.2:8080,error:ConnectTimeoutException,detail:connect timed out}6. 网关避坑手册5条血泪经验与排查路径6.1 现象健康检查把正常节点误判为DOWN流量雪崩原因健康检查客户端和后端服务之间的网络策略做了限速健康检查的HEAD请求被限流丢弃了导致连续失败3次被摘除节点。而真实业务流量走的却是另一条网络路径没有受到影响。 解决健康检查请求和业务请求尽量走同一条网络路径或者把健康检查的超时时间放宽到5秒。更稳妥的做法是记录检查失败的具体原因只有超时和连接拒绝才累计失败次数对于HTTP 500这种返回码只警告不摘除。6.2 现象WebSocket请求转发后一直握手失败原因反向代理的请求头处理把Connection: Upgrade和Upgrade: websocket这两个头吃掉了后端服务器收到的是普通HTTP请求。 解决在请求头过滤逻辑里加白名单当检测到Upgrade头存在时不走常规的头改写流程直接把原始头原样转发。同时关闭该连接的代理缓冲让数据流全双工透传。// WebSocket特殊处理保留Upgrade相关头 if (HttpHeaders.isUpgradeWebSocket(originalReq)) { builder.setHeader(Connection, Upgrade); builder.setHeader(Upgrade, websocket); builder.setHeader(Sec-WebSocket-Key, originalReq.getHeader(Sec-WebSocket-Key)); // 跳过默认的Connection: close改写 }6.3 现象CPU使用率不高但请求延迟偶尔飙到10秒原因TCP连接池的使用方式不对每个请求都从连接池borrow一个连接用完后没有正确归还导致连接池在某个时刻被耗尽后续请求必须等待新建连接。新建连接的握手加后端处理时间叠加延迟直接翻倍。 解决把连接池的最大连接数调到后端Tomcat最大线程数的1.2倍同时设置连接空闲回收时间为60秒。排查时先看连接池监控如果pending参数永久大于0说明连接池配置偏小。6.4 现象改了数据库里的节点权重线上完全不生效原因网关节点启动时把数据库的权重数据加载进了内存运行期不再读取数据库。数据库的字段只是初始化参考动态调度使用的是内存里的值。这其实是我在设计时故意做的取舍避免每次请求都查数据库。但管理端更新权重后需要通过API通知网关刷新内存。 解决管理端提供一个/gateway/reload接口网关收到请求后重新从数据库拉取节点配置并替换内存注册表。这个接口要加权限控制否则任何人都能触发配置重载。6.5 现象后端服务器收到请求的IP全是网关IP无法做地域封禁原因反向代理没有设置X-Forwarded-For头后端服务读remoteAddr得到的是网关出口IP。 解决在转发请求时强制设置X-Forwarded-For头。后端服务读取时取这个头最左边的IP同时要求网关必须信任该头如果客户端伪造该头直接用真实remoteAddr覆盖防止IP欺骗。这个字段在对接第三方风控系统时尤其重要风控误判率能直接降一半。7. 进阶玩法基于这套网关做多环境灰度发布有了这套系统后最值得做的进阶功能是多环境灰度发布也就是把一部分流量按权重打到新版本后端上验证稳定后再全量。传统做法是在Nginx里改upstream权重并发reload而在这套系统里这个操作变成一个管理端的API调用传入节点组名称、新版本节点列表、灰度比例系统自动调整节点权重。灰度发布操作步骤 1. 新版本后端启动注册到网关节点列表初始权重设为0 2. 管理端调用“灰度调整”接口把新版本权重从0调到5 3. 网关实时生效约2%流量进入新版本 4. 观察新版本的错误率和响应时间通过监控大屏对比 5. 确认稳定后调权重到50再观察一轮 6. 全量后把旧版本权重降为0等待空闲连接排空后下线这里最实用的一个功能是“连接排空”Drain。当你要下线旧版本节点时如果直接摘除节点正在处理中的请求会被中断用户会看到500。我实现的做法是先把节点状态改为draining负载均衡算法不再分配新请求但已经建立的长连接继续处理完。5分钟后如果连接数降为0再真正关闭节点。这套网关系统的复现路径大致可以用三周时间走通第一周搭建网关核心和负载均衡算法第二周实现健康检查与数据库持久化第三周完成Vue管理端和监控大屏。如果你已经有了Spring Boot和Vue的基础时间可以砍半。我只能说这套代码写下来你对反向代理、连接池、线程模型这些概念的理解会超越大多数只配过Nginx的Java开发。希望这些思路和避坑经验能帮你少走我走过的弯路。本文还有配套的精品资源点击获取