资讯动态

API网关重构实战:连接池陷阱、分布式限流与事件驱动改造

发布时间:2026/10/17 11:18:12 来源:尧图企业网站定制
1. 本周核心一个差点翻车的网关重构20260302到20260308这周我大部分时间都耗在一个内部系统的API网关重构上。不说虚的这周的目标很明确——把原来单体服务里耦合得乱七八糟的鉴权、限流、路由逻辑拆出来放到独立网关层同时把数据同步模块从定时轮询改成事件驱动。为什么做这件事原架构扛不住了。每天早上八点半到九点这个高峰段接口平均响应时间能从平时的120ms飙到600ms偶尔还出现雪崩式的超时。问题就出在鉴权逻辑每次都要查一次数据库限流又是单机内存版的水平扩展等于白扩展。网关重构这个方案我们内部讨论了两周最后选定的是基于OpenResty做流量入口配合集中式配置中心管理路由规则理由很实在能用Nginx的并发模型扛住流量又能用Lua脚本灵活处理业务逻辑不需要额外引入一套Java技术栈的网关组件。这周的产出可以归纳成三条完成网关核心链路开发包括路由转发、JWT鉴权、分布式限流三个模块代码全部过完CR并合入主干数据同步模块完成事件化改造的第一阶段订阅消息队列的消费程序已经跑通单条消息处理耗时从原来的平均45ms降到了18ms周四压测时踩了一个连接池配置的坑排查了整整一个下午这个后面单独说值得所有做服务化改造的人记一笔。2. 逐日推进从需求评审到联调混乱2.1 周一需求评审和接口梳理周一上午开需求评审会我没法回避的一个问题是原来的网关逻辑散落在三个服务里每个服务都有自己的鉴权代码有的用Redis存token有的直接用数据库session表。这次要统一先把所有接口列出来按路径前缀、鉴权方式、是否限流三个维度做了一张表。这是整个重构里最基础也最容易被忽略的环节。接口梳理这件事看着简单实际做起来完全不是那么回事。我们最终整理了83个接口其中有7个接口连负责人都不确定还在不在用还有3个接口存在一个路径匹配两种业务语义的情况。这周我用了一个下午把这些不确定项全部标记成待确认在周报里也单独列了一栏。下午做的事是确定技术选型。网关层用OpenResty这个没什么争议因为团队里几个老成员都有Nginx模块开发经验。配置中心选的是内部自研的老系统虽然界面不好看但胜在稳定而且权限体系和公司账号系统已经是打通的。这里我想多说一句很多团队在做技术选型的时候容易冲着最好去选但真实项目里最熟悉往往比最好更靠谱。2.2 周二核心链路开发和第一个设计决策周二开始写代码。我按照拆好的模块把工作量分成了两块路由转发和鉴权自己负责限流模块交给了另一位同事做。这里有一个关键设计决策就是网关和后端服务的通信协议。最初方案是走HTTP/1.1但考虑到部分接口一次调用涉及多个内部服务最终改成了HTTP/1.1 keep-alive同时在后端服务前面保留一层内网DNS做负载均衡。为什么不直接上HTTP/2因为后端服务里有两个老系统用的是JDK 8的旧HTTP客户端改造成本太大性价比不高。鉴权这边的实现我用了JWT Redis黑名单的双层方案。签发token时把token ID存到Redis设置过期时间与服务端一致。每次请求进来先验JWT签名然后拿着token ID去Redis查一下是否在黑名单里。黑名单用来处理用户修改密码后让旧token立即失效的场景。这个方案的优点是大部分请求不需要碰数据库Redis的读性能足够扛住峰值。代码写完后我自己先在本地用docker起了一套完整环境包括网关、两个模拟后端服务、Redis和消息队列。这一步省了后面联调至少半天的时间因为很多低级错误比如路径匹配前缀写错、参数传递类型对不上这类问题本地就能发现。2.3 周三联调一场预料之中的混乱周三进入联调阶段混乱是意料之中的。两个后端团队加上前端三个同学一起在联调环境里测。联调刚一开始就发现了一个接口网关转发出去后后端收到的请求头里丢了原始的客户端IP。原因是我们的网关在发起上游请求时用了自定义的header构造逻辑漏掉了X-Forwarded-For的拼接。这个问题说大不大但直接影响后端的风控逻辑排查了大概四十分钟。这种联调期的问题本质上都是设计阶段考虑不周导致的。后来我在代码里加了一个统一的header处理工具函数所有请求出口强制经过这个函数把X-Forwarded-For、X-Request-Id、Authorization这些公共header统一处理才算把这一类问题从根上解决。下午还处理了一个更隐蔽的问题——POST请求的body重放。在一次超时重试的测试里发现网关自动重试时把上一个请求的body原样发给了后端导致用户在下单接口提交了两次同样的数据。这其实是重试策略设计的问题。我们的网关重试里只对GET请求做了自动重试POST请求一律不重试直接返回502让客户端自己决定。这个规则也在周报里明确写了出来避免后续有人踩坑。2.4 周五压测连接池引发的性能血案周五压测问题集中爆发。我们预期网关单机能扛2000 QPS但实际压到800 QPS的时候后端服务的Tomcat线程池就已经全部占满新的请求排队等线程导致响应时间直线上升。最开始我以为是后端服务本身的问题结果看监控曲线发现Tomcat的线程数跟网关发过来的并发连接数完全同步。再往网关这边查Nginx的upstream keepalive设置为0等于每个请求都新建一条TCP连接到后端。TCP握手开销加上TIME_WAIT状态的堆积直接把后端的端口资源耗光了。这个问题的根因说透了就是一句话网关层的连接池配置没有跟后端的处理能力做匹配。我们最后把keepalive设成了128同时在压测脚本里把请求的并发数控制在合理的范围再进行了一轮测试数据立刻不一样了800 QPS下后端线程池占用下降到原来的四分之一响应时间也回到了200ms以内。3. 关键技术复盘从数据同步事件化到限流降级策略3.1 数据同步模块从轮询到事件驱动数据同步这块原来是一个定时任务每30秒扫描一次订单表的更新时间字段把增量数据拉出来同步到报表库。这个方案的痛点是数据量小的时候没事一旦订单量上来每30秒一次的全表扫描让数据库的IO压力越来越大。而且实时性太差业务方想在报表里看到当日订单数据得等最多30秒。这周做的事件化改造方案思路其实不复杂业务服务在订单创建和更新的时候往消息队列里发一条领域事件消费程序收到事件后把相关数据写入报表库。为了兼容一些历史数据启动时做一次全量初始化之后只靠事件驱动。消费程序这里有一个值得注意的细节我们用了手动ACK 重试队列的方式。消费端拿到消息先做业务处理成功之后才ACK。如果处理失败不ACK消息会重新进入队列。但这里有个陷阱如果某条消息一直处理失败就会被反复投递导致后续消息被阻塞。我们的解决办法是单独配置了一个重试队列失败超过三次的消息转到重试队列重试队列里的消息延迟十分钟再消费连续失败超过五次就进入死信队列由人工介入处理。这套机制上线第一天就发挥了作用。有两条消息因为报表库字段长度限制导致消费失败如果没有死信队列这两条消息会一直卡在队列头部后面几万条消息都别想消费。死信队列救了我们一次。3.2 分布式限流算法选型和降级方案限流模块最初讨论了两个方案固定窗口和滑动窗口。固定窗口的实现最简单在Redis里以秒为key存一个计数器超过阈值就拒绝请求。但这个方案有个已知缺陷就是临界点问题假设限制是100次/秒用户在第一个窗口的最后100ms里发了100次又在下一个窗口的前100ms里发了100次实际上200ms内就放过了200个请求。滑动窗口的实现要稍微复杂一点需要记录窗口内每个请求的时间戳或者用更省内存的算法近似实现。我们最终选了滑动窗口的简化版用Redis的ZSET每个请求的时间戳作为score窗口大小1秒每次请求先删除窗口外的记录再统计窗口内的数量。ZSET在key的数量大的时候比较耗内存但我们的接口维度最多几百个完全扛得住。再往下走是降级策略。限流触发后我们不是直接返回错误而是把多余的请求转发到一个集群的专属降级池来处理。降级池里的请求只返回热门缓存数据不走完整业务逻辑。这样在极端流量下用户看到的只是页面上的部分数据不更新而不是直接报错。这周调整了限流阈值从初始的每秒500调整到每秒800是基于压测数据来的同时也配置了限流触发后的降级策略避免服务雪崩。4. 数据与结果这周的各项指标变化这周结束后我对各项数据做了一个统计这里放出来方便大家对比参考指标改造前改造后变化网关层平均响应时间560ms213ms降低62%P95响应时间1100ms429ms降低61%每日暴露的5xx错误数日均37次日均4次降低89%后端服务CPU占用高峰84%56%降低28%数据同步延时最长30秒平均1.2秒降低96%数据同步成功率99.2%99.97%提升0.77%这些数字看着挺提气的但其中有一定客观因素压测环境和自己搭的测试环境本身比生产环境简单。很多同学容易掉进一个误区就是一看到改造后的数据变好就觉得改造成功了。看数据要综合考虑。拿同步延时来说30秒到1.2秒的变化主要是架构调整带来的但如果消息队列中间件本身抖动延时还是会上去这个不是一劳永逸的。再说一个让我比较在意的数值P95从1100ms降到了429ms但P99只从1800ms降到了1350ms。这说明长尾请求的优化效果还不够。排查下来这部分请求多数是第一次调用某个后端服务时的冷启动开销包括DNS解析、连接建立、类加载。下一步可以考虑在网关层做upstream的预热或者在压测的时候覆盖这个场景。5. 问题排查实录与避坑指南5.1 连接池问题一个下午换来的教训上面提到的keepalive连接池问题值得展开讲一下因为这是很多做网关改造的团队都会遇到的问题。现象是这样的压测到800 QPS时后端服务日志开始疯狂报错报的是Connection reset by peer。我一开始以为是后端服务的线程池满了把Tomcat的maxThreads从200调到了400重启后再压错误消失了一段时间但继续压到1200 QPS时又出来了。后来我抓了一下网络连接状态网关所在机器的TIME_WAIT连接数达到38000多个后端服务机器的ESTABLISHED连接数也在持续增长用netstat检查发现后端服务有大量来自网关不同端口的新连接。问题就很明显了。网关每个请求都新建TCP连接请求结束后连接进入TIME_WAIT而新的请求又在不断发起新的连接导致后端的文件描述符和端口资源被大量占用。最终在Nginx的upstream配置里加上keepalive 128并在location块里设置proxy_http_version 1.1和Connection空头之后问题解决了。这中间还有一个细节改完配置以后压测前几次的数据还是不太稳定。后来发现是keepalive连接池的冷启动问题连接池是慢慢被填满的前几百个请求还是会走新建连接的老路。所以我们压测脚本里加了30秒的预热阶段等连接池稳定之后再统计正式数据。5.2 消息队列消费阻塞一次半夜告警周四晚上十点半告警群里突然弹出一条消息说数据同步消费积压超过两万条。我第一反应是消费程序挂了结果登上去看进程还在日志也没报错。再往下查才发现是消费程序里有一个数据库操作没有设置超时时间而当时数据库刚好在做备份导致这个操作的连接被阻塞消费线程全部卡住。这个问题的根因说白了消费程序里不应该直接同步调用数据库操作必须要设置超时和重试机制。我们的消费逻辑里有一个写报表库的操作这个操作没有走我们统一封装的DAO层而是用了最原始的JDBC连接连接不设置超时。数据库正常的时候没事一抖动就是灾难。修复方案有两步第一步立刻停了消费程序手动把积压的消息写了一个补偿脚本处理掉第二步在消费逻辑里加入超时控制和失败重试超时时间设置为3秒超过3秒直接放弃本次写入把消息重新放回队列等下一轮再处理。这样数据库抖动只会影响到当条消息的消费不会把整个消费者拖死代价只是消费延时稍微变长。5.3 联合排查联调环境里的请求头丢失联调阶段还遇到一个请求头丢失的问题具体表现是前端调用网关的接口时在header里放了一个traceId后端服务收到之后发现traceId变成了空。排查链路先用curl直连网关发现有同样的现象在网关代码里加了日志发现请求进来的时候header里的traceId是正常的转发出去之后丢了检查HttpClient的header构造逻辑发现自定义header需要从原始请求里显式拷贝而我们的代码只拷贝了默认的几个header。修复很简单把所有需要透传的header列入一个白名单在统一的转发函数里做拷贝。这里想提醒大家一句网关类的项目header透传一定提前列好规则不要让每个开发按自己的需求往代码里加header最后会乱得没法维护。5.4 常见问题速查表问题现象可能原因解决方案后端连接被重置网关未启用keepalive配置Nginx upstream keepalive设置合理的数量POST接口数据重复自动重试机制导致POST请求禁止自动重试只允许GET重试消费积压但进程正常数据库连接无超时统一DAO层必须设置连接超时请求头字段丢失header拷贝逻辑不全使用header白名单统一透传限流触发后无提示未配置降级响应返回降级缓存数据或明确错误码6. 下周计划与风险预警下周的事情已经在周报里列好了我在这里也同步出来方便各位对照第一完成网关与剩余未迁移接口的切换。目前83个接口里还有14个老接口没有走到新网关计划下周三之前全部切完。切换的时候会采用灰度策略先切流量占比5%的低风险接口观察半天再逐步放量。第二把压测环境扩展到三台网关节点做一次集群级别的压测。这里的重点是验证网关横向扩展后限流是否还能正常工作。我们的分布式限流依赖Redis当网关节点变多之后Redis的写入量会成倍增加要提前关注Redis的性能瓶颈。第三是数据同步事件化的第二阶段把订单之外的另外两类业务数据也切到事件驱动。第二阶段涉及的改造范围更大需要业务团队的配合沟通成本会是一个风险点。风险方面我在这周的周报里明确写了一个Redis的可用性已经成为单一故障点。网关的限流、鉴权的黑名单、数据同步的消息队列中间件都依赖Redis如果Redis集群出现问题影响面会非常大。这个需要运维团队支持计划下周把Redis的主从切换演练做一次验证故障转移的时间窗口。另外一个预警是联调环境的稳定性堪忧。这周联调时联调环境的后端服务重启了三次每次重启都会导致网关到后端的连接池清空短时间内请求大量超时。下周会跟运维确认联调环境的服务重启流程能不能加一个优雅下线机制给网关一个连接池主动重建的时间窗口。7. 写周报背后的几点个人体会周报这个东西很多团队写成了流水账我自己以前也这样。但干了几年之后我越来越觉得一份好的周报不只是给领导看的更是给自己做项目复盘用的。这周的周报我实际是按目标-进展-问题-数据-计划这个框架来写的。为什么坚持写问题和数据这两个板块因为很多项目的价值恰恰体现在解决问题和指标变化上如果不记录下来过两周自己都会忘掉当初为什么做这个决策。就拿连接池这个坑来说如果这周周报里不记录故障现象和排查过程下次换个人踩到同一个坑又得花一个下午去查。写周报的时候还有一个小建议把下一步计划写得具体不要只写优化网关性能这六个字写成优化网关连接池配置将keepalive从128调整到256并压测验证P99降幅这样下周自己拿到周报就能直接开工。这周的情况大概就是这样。有一个改动虽然还没完全收尾但方向是对的。下周的计划已经列好按照节奏推进就行。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑