尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java+Vue自研负载均衡系统:从算法到实现与高可用保障

Java+Vue自研负载均衡系统:从算法到实现与高可用保障 简介面向具备Java与Vue开发基础、熟悉Spring Boot、MySQL、Redis的1-3年研发人员这份资源以完整项目实例的形式展示了基于Java与Vue的高可用负载均衡与反向代理系统的设计与实现。系统围绕统一访问入口、负载均衡、健康检查、限流熔断、故障转移等核心机制展开并通过Vue管理端实现可视化运维可支撑微服务网关原型开发、课程设计或毕业设计参考。资源共1个文件为docx格式文档压缩包大小约97KB内容精炼集中便于系统阅读。文档包含项目背景与目标、项目挑战及解决方案、架构模型、加权轮询节点选择器、健康检查服务、反向代理服务、限流过滤器等核心代码示例并涵盖数据库设计、API接口规范与部署流程同时针对并发请求线程安全、后端节点故障转发异常、配置变更与运维等关键难点给出了具体处理思路配合代码示例与数据库脚本便于读者动手搭建并调试验证。目前已有51人学习下载适合需要掌握高可用网关核心机制并快速搭建原型的研发者。1. 用 Java 与 Vue 自研负载均衡系统到底在解决什么问题大多数团队在流量入口处会直接选择 Nginx 或 HAProxy很少有人会想到用 Java 与 Vue 从零搭建一套负载均衡与反向代理系统。但当你需要动态调整上游节点权重、在管理界面上实时查看每个后端实例的连接数、或者把路由规则持久化到数据库而不是改完配置文件再 reload 时通用型代理的短板就暴露出来了。这套系统的核心价值是把 L7 层的请求转发逻辑从静态配置中解耦变成一组可编程的服务Java 负责建立 TCP 连接、解析 HTTP 请求、根据算法挑选上游节点Vue 则提供可视化面板去查看集群状态和调整转发策略。本文默认你已经熟悉 Spring Boot 的基本用法了解 HTTP 协议状态码含义并知道 Nginx 的 location 匹配规则大概是什么样。下面从负载均衡算法选型讲起逐步落到可运行的代码、数据库表设计和 Vue 管理端的关键实现最后给出高可用验证和性能调优的具体手段。整个方案不需要额外的网关中间件你只需要一个 Java 服务和一个 Vue 工程就能跑通。2. 负载均衡算法选型与 Java 实现从轮询到一致性哈希2.1 静态算法与动态算法的适用边界写负载均衡系统的第一步不是写转发代码而是确定你要用哪些调度算法。常见的选择有轮询、加权轮询、最少连接数、一致性哈希这四种。轮询实现最简适合所有上游节点处理能力一致的场景。加权轮询在轮询基础上增加 weight 字段解决服务器配置不一致的问题。最少连接数属于动态算法需要代理层持续跟踪每个上游节点的活跃连接数量对长连接和请求耗时波动大的服务更友好。一致性哈希解决的是会话保持问题把请求的某个特征值映射到哈希环上保证同一用户的请求落到同一个节点。在 Java 生态里权重算法最容易被写错的地方是加权轮询的平滑性。如果不做平滑处理直接用权重比例一次性分配请求短时间内某个节点会被连续打到多次。Nginx 采用的平滑加权轮询算法用 currentWeight 动态调整每次选择 currentWeight 最大的节点然后减去总权重。这个算法实现简单且效果远好于随机取模是自研系统的首选。public class SmoothWeightedRoundRobin { private final ListNode nodes; private final int totalWeight; public SmoothWeightedRoundRobin(ListNode nodes) { this.nodes nodes; this.totalWeight nodes.stream().mapToInt(n - n.weight).sum(); } public synchronized Node next() { Node selected null; for (Node node : nodes) { node.currentWeight node.weight; if (selected null || node.currentWeight selected.currentWeight) { selected node; } } assert selected ! null; selected.currentWeight - totalWeight; return selected; } public static class Node { private final String host; private final int port; private final int weight; private int currentWeight; public Node(String host, int port, int weight) { this.host host; this.port port; this.weight weight; this.currentWeight 0; } } }这段代码的核心逻辑在 next 方法中。每次调用时先累加所有节点的 currentWeight相当于让权重大的节点在竞争中更快到达峰值选中后把 currentWeight 减去总权重使其在后续几轮中降低优先级。synchronized 保证多线程环境下调度状态不被破坏。实际项目中节点列表应该是动态的增加节点时需要重新计算 totalWeight删除节点时要注意当前选中节点可能已被移除需要保证此时返回一个可用节点而不是直接抛异常。建议把节点列表放在一个支持并发读写的容器中例如使用 CopyOnWriteArrayList避免在每次请求转发时对列表加锁。2.2 一致性哈希与最小连接数的 Java 实现要点一致性哈希并不是必须实现的算法但如果你的系统要接入 Websocket 长连接或需要基于 Session 的应用它就变得不可绕过。Java 实现一致性哈希的常见做法是用 TreeMap 模拟哈希环每个真实节点根据 IP 加端口做 160 次虚拟节点映射。取节点时找到第一个大于等于请求哈希值的 entry如果没有则取第一个形成环状查找。public class ConsistentHashRouter { private final TreeMapLong, String ring new TreeMap(); private final int virtualNodeCount; public ConsistentHashRouter(ListString realNodes, int virtualNodeCount) { this.virtualNodeCount virtualNodeCount; for (String node : realNodes) { addNode(node); } } private void addNode(String node) { for (int i 0; i virtualNodeCount; i) { long hash hash(node # i); ring.put(hash, node); } } public String route(String key) { long hash hash(key); Map.EntryLong, String entry ring.ceilingEntry(hash); if (entry null) { entry ring.firstEntry(); } return entry.getValue(); } private long hash(String key) { return Math.abs(key.hashCode()); } }虚拟节点数量直接决定负载均衡的均匀程度。节点只有三台时160 个虚拟节点可以把请求分布做得比较平滑节点数增多时每个节点的虚拟节点数量不需要线性增长50 到 100 之间通常就够用。hash 方法使用系统默认的 String.hashCode虽然性能好但分布质量一般如果请求量很大建议改用 MurmurHash 或 FNV 算法减少哈希碰撞带来的热点问题。一致性哈希在节点增减时只会影响哈希环上邻近的少量请求这是它相比普通取模的最大优势代价是当节点数很少且虚拟节点分配不均时可能产生倾斜推荐至少三个真实节点。最少连接数的实现则需要在每个节点对象上维护一个 AtomicInteger。转发请求前比较所有节点的当前连接数选择最小的那个转发成功后计数加一连接关闭时减一。这里需要注意的是HTTP 请求是否复用连接会直接影响计数逻辑如果代理与上游之间建立了 keep-alive 连接池计数对象应该以连接而不是请求为单位。public class LeastConnectionScheduler { private final ConcurrentHashMapString, AtomicInteger connections new ConcurrentHashMap(); public String select() { return connections.entrySet().stream() .min(Comparator.comparingInt(e - e.getValue().get())) .map(Map.Entry::getKey) .orElseThrow(() - new IllegalStateException(no available node)); } public void increment(String node) { connections.computeIfAbsent(node, k - new AtomicInteger()).incrementAndGet(); } public void decrement(String node) { connections.computeIfAbsent(node, k - new AtomicInteger()).decrementAndGet(); } }stream 的 min 操作在节点数量较少时性能没问题但节点数量超过一百之后每次请求都遍历一遍会产生明显开销。更高效的做法是用一个小顶堆维护节点连接数排序但节点连接数频繁变化时堆的调整成本也很高。工程上一般会限制单个代理的后端节点数量不超过三十个此时遍历就是最稳妥的方案。算法是否需要感知连接状态会话保持支持时间复杂度典型场景轮询否否O(1)同配置无状态服务平滑加权轮询否否O(n)异构集群最少连接数是否O(n)请求耗时差异大一致性哈希否是O(logn)需要会话保持3. 反向代理核心Spring Boot 结合 Netty 实现 HTTP 流式转发3.1 为什么不用 Spring MVC 做转发而选择 Netty如果你只会 Spring Boot 的同步模型你会发现实现反向代理很别扭用 RestTemplate 或 WebClient 去请求上游再把响应体拼装出来返回给客户端。这种做法对小型系统可行但请求体稍微大一点就面临内存压力长连接场景下更是难以维持。反向代理的本质是原样搬运字节流而不是做协议转换或内容改写所以最合适的模型是 NIO 事件驱动。Netty 提供了完整的 HTTP 编解码器和流式处理能力。代理接收到客户端请求时不需要把整个 body 读完再转发可以在 decode 完成后立即向上游发起连接把已经读到的内容先写过去后续到达的数据继续转发。这个过程叫流式代理或管道转发。另一个选择是使用 JDK 内置的 HttpServer 搭配 CompletableFuture 做异步处理但 HttpServer 对 HTTP 协议的支持不够完整比如缺少对 chunked 编码的细致控制遇到 WebSocket 升级请求更是难以处理。Netty 是更可靠的方案。public class ProxyFrontendHandler extends ChannelInboundHandlerAdapter { private final BackendRegistry registry; private Channel outboundChannel; public ProxyFrontendHandler(BackendRegistry registry) { this.registry registry; } Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (outboundChannel null) { String backendNode registry.next(); outboundChannel registry.connect(backendNode); outboundChannel.pipeline().addLast(new ProxyBackendHandler(ctx.channel())); } outboundChannel.writeAndFlush(msg); } Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { if (outboundChannel ! null) { outboundChannel.close(); } super.channelInactive(ctx); } }这段代码展示的是正向代理模式客户端连接到 Netty 服务端口后数据读取事件触发时选择一个后端节点并建立连接随后把客户端发来的数据原样转发。实际反向代理需要先解析 HTTP 请求头中的 Host 字段来决定路由到哪个后端集群或者根据路径前缀转发给不同的服务这里的 BackendRegistry 可以封装路由表和负载均衡算法两个职责。建立连接时要注意超时处理connect操作需要指定超时时间避免后端节点宕机时客户端一直等待。3.2 HTTP 头部改写与连接池复用的关键细节反向代理转发请求时HTTP 头必须做三处修改Host 头改为上游真实地址、X-Forwarded-For 追加客户端 IP、Connection 头根据是否复用连接做清除或保留。如果不改写 Host 头上游如果是虚拟主机托管的服务会返回 404 或者错误站点内容。X-Forwarded-For 头在多层代理下需要从原有值后面追加而不是覆盖否则上游只能看到代理地址。X-Real-IP 头也建议一并追加很多 Java Web 框架读取客户端 IP 时只认这个头。private static void rewriteHeaders(HttpHeaders headers, String clientIp) { String host headers.get(Host); headers.set(Host, host); headers.set(X-Forwarded-For, headers.get(X-Forwarded-For, ) , clientIp); headers.set(X-Real-IP, clientIp); headers.remove(Connection); headers.remove(Proxy-Connection); }Netty 的 HttpHeaders 对象在请求解码后可以直接 set 和 remove。Connection 头必须清除因为代理与上游之间是否使用持久连接由连接池管理器决定而不是由客户端的 Connection 头决定保留它可能导致上游误解连接语义。另一个容易遗忘的操作是清除 Accept-Encoding 头中的 gzip 标记。典型的代理服务器不应该让上游做压缩因为压缩会加大代理的 CPU 开销而且经过代理后再压缩才能对不同的客户端分别优化。连接复用对反向代理的性能影响极大。每转发一个请求就新建一条 TCP 连接在高并发下会迅速耗尽文件描述符并且增加三次握手延迟。常见做法是用一个连接池按上游地址缓存 ChannelFuture比如使用ConnectionPool维护每个地址的空闲连接队列。从池里取连接时要校验连接是否仍然活跃Netty 的 Channel 可以通过isActive判断但更可靠的做法是检查 ChannelFuture 是否 successfully 执行完毕。3.3 路由规则持久化到 MySQL 的数据库表设计负载均衡器既然要做成带管理界面的系统路由规则就不能写在 Java 代码里。至少需要三张表backend_group 存储后端服务组名和负载均衡算法类型backend_node 存储具体节点地址、权重、当前状态route_rule 存储路径匹配规则与后端组的映射关系。CREATE TABLE backend_group ( id BIGINT PRIMARY KEY AUTO_INCREMENT, group_name VARCHAR(64) NOT NULL UNIQUE, lb_algorithm VARCHAR(32) NOT NULL DEFAULT weighted_round_robin, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE backend_node ( id BIGINT PRIMARY KEY AUTO_INCREMENT, group_id BIGINT NOT NULL, host VARCHAR(128) NOT NULL, port INT NOT NULL, weight INT NOT NULL DEFAULT 1, enabled TINYINT NOT NULL DEFAULT 1, health_check_url VARCHAR(255), last_checked_at TIMESTAMP, last_success_at TIMESTAMP, INDEX idx_group_id (group_id) ); CREATE TABLE route_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, path_pattern VARCHAR(255) NOT NULL, group_id BIGINT NOT NULL, rule_order INT NOT NULL DEFAULT 100, enabled TINYINT NOT NULL DEFAULT 1, INDEX idx_path_pattern (path_pattern) );这张表设计的核心意图是 route_rule 的 path_pattern 前缀匹配策略。查询时根据请求的 URI 去匹配最长的前缀规则类似于 Nginx 的 location 前缀匹配规则。如果有多条规则命中rule_order 字段用来决定优先级数值小的优先。匹配过程在 Java 代码里遍历规则列表并用 String.startsWith 判断规则数量不多时效率足够。路由表变化后需要通知代理服务重新加载可以定期扫描或通过管理接口触发刷新推荐后者因为主动刷新可以基于版本号避免并发问题。4. 高可用保障健康检查与故障转移的实现4.1 主动健康检查定时探测而不是依赖请求失败没有健康检查的负载均衡器只是一个转发器只能坐等请求失败后被动摘除节点。主动健康检查要求代理定期探测后端节点的可用性探测方式可以是 TCP 端口探测、HTTP 请求探测或自定义指令。TCP 探测只能确认端口在监听不能确认业务可用HTTP 探测可以配置一个轻量级的 health 接口返回 200 即认为节点健康。Component public class HealthCheckScheduler { private final ScheduledExecutorService scheduler Executors.newScheduledThreadPool(2); private final BackendRegistry registry; private final RestTemplate restTemplate new RestTemplate(); PostConstruct public void start() { scheduler.scheduleWithFixedDelay(this::runCheck, 1, 5, TimeUnit.SECONDS); } private void runCheck() { registry.getAllNodes().parallelStream().forEach(node - { String url http:// node.getHost() : node.getPort() /health; try { ResponseEntityString response restTemplate.getForEntity(url, String.class); boolean healthy response.getStatusCode().is2xxSuccessful(); registry.updateHealth(node, healthy); } catch (Exception ex) { registry.updateHealth(node, false); } }); } }scheduleWithFixedDelay 保证上一次探测结束后等待固定间隔再开始下一次这样不会因为上游响应慢导致探测任务堆积堆积是普通 scheduleAtFixedRate 的常见问题。pool 大小设置为 2 意味着默认串行执行探测任务如果集群规模增大可以调高或者改用线程池中更多的线程。parallelStream 适合 CPU 密集的并发 IO 操作但注意后续节点的健康状态写回时要保证线程安全BackendRegistry 内部必须使用 ConcurrentHashMap 或在更新节点状态的方法上加锁。实际项目中还有一个细节值得注意健康检查的返回内容不能只看状态码。如果上游的数据库连接已经耗尽虽然 health 接口能响应但不是正常的业务状态。可以把健康检查接口设计成返回 JSON里面包含 CPU 负载、活跃线程数等指标由代理根据阈值判断节点是否健康。这不属于反向代理的必须功能但对高可用系统来说是很有区分度的增强。4.2 被动健康检查与熔断降级策略主动探测存在一个盲区探测间隔时间内节点可能已经出现故障此时请求还是会打过去。被动健康检查通过观察实时请求的成功率来弥补这个盲区。每个节点维护一个滑动窗口记录最近 100 次请求中成功和失败的数量失败率超过 50% 时把节点标记为熔断状态直接不再分配新的请求。public class CircuitBreaker { private final int windowSize 100; private final ConcurrentLinkedDequeBoolean results new ConcurrentLinkedDeque(); private final AtomicInteger failureCount new AtomicInteger(); private final AtomicBoolean open new AtomicBoolean(false); public synchronized void record(boolean success) { results.addLast(success); if (results.size() windowSize) { Boolean removed results.pollFirst(); if (removed ! null !removed) { failureCount.decrementAndGet(); } } if (success) { if (failureCount.get() 0) { failureCount.decrementAndGet(); } } else { failureCount.incrementAndGet(); } updateState(); } private void updateState() { double failureRate (double) failureCount.get() / results.size(); if (failureRate 0.5 !open.get()) { open.compareAndSet(false, true); } if (failureRate 0.2 open.get()) { open.compareAndSet(true, false); } } public boolean isOpen() { return open.get(); } }熔断器最关键的两个参数是失败率阈值和恢复阈值它们不能相同。如果恢复阈值和熔断阈值一样节点会在临界点来回抖动导致大量请求在尝试和拒绝之间反复切换这种现象叫做熔断抖动。恢复阈值调低到 0.2意味着节点必须表现稳定一段时间才能重新上线给后端留出充分的恢复时间。滑动窗口用 Deque 实现请求量大的时候每次 record 都在队头插入、队尾删除时间复杂度 O(1)。4.3 请求失败时的自动重试与幂等性判断健康检查和熔断解决了请求分配的问题但请求已经发出且上游在响应前宕机客户端仍会收到超时错误。一个高质量的反向代理会在这时自动重试把失败请求转发到其他健康节点。重试机制有一个重大陷阱如果请求不是幂等的重试可能导致数据重复写入。判断幂等性的标准方式是把 HTTP 方法分成两类GET、HEAD、OPTIONS 安全方法可以自动重试POST、PUT、DELETE 需要谨慎处理PUT 和 DELETE 在 HTTP 协议语义上通常是幂等的但 POST 不具备幂等性保证。自定义实现时务必要在代理层保存请求体用于重试。如果使用流式转发请求体已经被转发出去一部分后就不能重试了否则上游 B 会收到一个不完整的请求体出现不可预期的后果。因此可重试请求需要先把 body 完整缓存到内存或磁盘再开始转发。public class RetryableRequestWrapper { private final byte[] body; private final AtomicInteger attemptCount new AtomicInteger(0); private final int maxAttempts; public RetryableRequestWrapper(byte[] body, int maxAttempts) { this.body body; this.maxAttempts maxAttempts; } public boolean shouldRetry(int statusCode) { return attemptCount.incrementAndGet() maxAttempts (statusCode 500); } public ByteBuf toByteBuf() { return Unpooled.wrappedBuffer(body); } }实现重试时连接超时应立即重试后续节点读超时则需要等待更长时间再重试。读超时可能是上游在处理慢请求而不是宕机立刻重试会把并发压力全部转移到下游节点。常见的做法是连接超时设置 500ms 内立即重试读超时设置 5 到 10 秒之后重试且最多重试一次。另一种方案是给客户端返回 503 并让客户端自行决定是否重试这样更安全但用户体验稍差。5. Vue 管理界面用 WebSocket 实时展示集群状态5.1 Vue 项目结构与路由规则管理页面Vue 在这个系统里负责两部分工作展示负载均衡器的运行状态以及修改路由规则和后端节点配置。技术栈选择 Vue 3 加 Element Plus 是常见做法表格、表单、标签页这些中后台组件开箱即用。工程结构上分成三个核心页面节点管理页面、路由规则页面、监控大屏页面。路由管理页面的核心是一张表格展示 path_pattern、目标后端分组、规则优先级、是否启用并支持新增和编辑操作。template el-table :datarules v-loadingloading el-table-column proppathPattern label路径匹配模式 / el-table-column propgroupName label后端分组 / el-table-column propruleOrder label优先级 width80 / el-table-column propenabled label状态 width80 template #defaultscope el-tag :typescope.row.enabled ? success : info {{ scope.row.enabled ? 启用 : 停用 }} /el-tag /template /el-table-column el-table-column label操作 width150 template #defaultscope el-button sizesmall clickeditRule(scope.row)编辑/el-button el-button sizesmall typedanger clickdeleteRule(scope.row.id)删除/el-button /template /el-table-column /el-table /template表格列和 API 返回字段一一对应后端返回的 groupId 需要在前端映射成 groupName因为在 route_rule 表中只存储外键 group_id页面展示时必须关联查一次表。编辑和删除操作直接调用后端 API接口成功后重新 load 表格数据。这里需要特别注意并发一致性问题如果两个管理员同时修改路由规则后提交的会覆盖先提交的工程上可以在每个规则上加 version 字段做乐观锁更新时校验 version 是否一致。5.2 实时监控面板的 WebSocket 推送机制节点状态的变化和请求监控数据需要实时展示。两种实现方式一种是前端轮询接口每隔几秒拉取最新数据实现简单但会有一到两秒的延迟而且空转请求浪费资源另一种是建立 WebSocket 长连接后端实时推送。推荐后者因为负载均衡器的节点状态变化本身就是事件驱动的不需要定时拉取。const socket new WebSocket(ws://${window.location.host}/ws/monitor); socket.onmessage (event) { const data JSON.parse(event.data); if (data.type nodeUpdate) { updateNodeStatus(data.node); } else if (data.type trafficStats) { updateTrafficChart(data.stats); } }; socket.onclose () { setTimeout(() initWebSocket(), 3000); }; function updateNodeStatus(node) { const index tableData.findIndex(item item.id node.id); if (index ! -1) { tableData[index].health node.healthy ? healthy : unhealthy; tableData[index].activeConnections node.activeConnections; } }初始化连接时要注意处理连接失败的情况onclose 回调中不能立即重连否则后端重启时前端会进入快速重连循环每次失败又触发下一次重连产生大量无效请求。设置 3 秒的间隔是在网络抖动和响应速度之间取的一个常见折中值。前端组件卸载时还需要调用 socket.close否则页面切换后连接仍然存在造成资源泄漏。WebSocket 推送的数据包要小巧建议只传节点 ID 和状态变化字段不要每次推送一个完整节点对象。Vue 的响应式系统在处理这种高频数据更新时需要额外注意性能把更新的对象放在深层嵌套结构中会触发大量依赖重新收集。建议把监控数据放在独立的状态管理模块中在组件中用 computed 按需取值避免整个页面因为一个连接数变化而全量刷新。6. 压测与内核调优验证高可用能力的三个必做操作系统上线前需要验证三个指标转发性能、故障转移时间、连接数限制。转发性能够不够用直接做压测用 wrk 或 Apache Bench 发请求观察吞吐量上不去时的瓶颈是在 CPU 还是文件描述符。wrk -t4 -c100 -d30s http://localhost:8080/test这条命令会开启 4 个线程 100 个连接持续压测 30 秒。如果吞吐量远低于预期先用top看进程 CPU 占用再用netstat -ant | grep TIME_WAIT | wc -l检查 socket 状态数量。TIME_WAIT 过多是代理服务器的典型问题几乎每次请求失败排查都会先看到它。Time_wait 本身是 TCP 正常关闭的状态但数量超过几万说明连接的创建和关闭过于频繁连接复用的配置没有起作用。在 Java 的 Netty 层面确保所有请求都从连接池获取连接而不是每次都新建。在 Linux 系统层面修改两个内核参数后可以用 sysctl -p 生效net.ipv4.tcp_tw_reuse1让内核复用 TIME_WAIT 状态的连接net.ipv4.tcp_fin_timeout15缩短连接释放的等待时间。故障转移时间验证方法很直接在后端节点上停掉一个业务进程观察管理界面的健康状态变化时间。从进程停止到界面标记不健康的时间差就是健康检查间隔加探测超时时间的总和。如果健康检查间隔是 5 秒探测超时时长也是 5 秒那么最坏情况下要 10 秒才能摘除故障节点。实际业务上 10 秒偏长可以把检查间隔降到 2 秒超时降到 1 秒但要注意频繁探测会占用后端资源。这里的经验值是检查间隔不要小于 1 秒超时不要大于间隔的一半。文件描述符限制是另一个容易被低估的问题。Linux 默认的 ulimit 是 1024也就是单个进程最多同时打开 1024 个文件描述符每秒几百个请求就能把这个数撑满。用ulimit -n 65535临时调整永久修改需要编辑/etc/security/limits.conf添加* soft nofile 65535和* hard nofile 65535两行。Java 进程启动参数中还要加上-XX:UseG1GC和-Xms2g -Xmx2g固定堆大小避免运行时频繁扩容导致 GC 停顿这些参数对 Netty 这类高吞吐 IO 密集型应用的影响非常明显。线上出现问题时的排错顺序建议是先看监控面板确认节点是否全部健康再看内核参数确认文件描述符是否耗尽再用 tcpdump 抓包确认三次握手和四次挥手是否有异常最后才去看 Java 日志。tcpdump 抓包命令示例tcpdump -i eth0 port 8080 -w proxy.pcap抓完用 Wireshark 打开重点看是否有大量 TCP Retransmission如果有很可能是网络层丢包而不是应用层的问题。写日志时每个转发请求记录一行包含请求路径、上游节点、响应状态码、耗时四个字段这会对接下来的问题定位带来非常大的价值。本文还有配套的精品资源点击获取
返回列表