得物百万QPS高性能Netty网关架构演进路线
得物原来的网关基于 Spring Cloud Gateway(SCG)。随着接口注册数量增长到 3 万以上,SCG 在安全、性能、维护、稳定性和定制能力上都开始暴露问题,最终得物选择自研 DAG(Dewu API Gateway),并在压测中取得约 4 倍于 SCG 的吞吐表现。
一、SCG 的五个业务问题
- 泛化路由导致接口很容易被意外开放到公网,安全风险高;
- 接口注册模式推广后,路由配置由网关开发转向业务开发,协同效率下降;
- 同一套代码承载不同业务集群,内网网关也背着几十个无关 Filter;
- 双周一次发版,启动初期接口超时率明显升高;
- 原生 SCG 对灰度、接口级超时、定向节点路由等定制需求支持不足。
二、SCG 的技术痛点
SCG 基于 WebFlux、Reactor Netty、Netty 多层抽象,带来以下问题:
- 官方仓库存在多个长期未解决的内存泄漏问题,需要定期重启;
- 响应式编程复杂,修改 Request/Response Body 的代码难读;
- 调用栈过深。单个 Filter 的调用栈约 90+ 层,业务复杂时约 120 层,其中大量是框架层
subscribe()、onNext(); - 路由匹配是遍历式 O(n);
- 启动时
NamedContextFactory#getContext存在全局锁,冷启动 RT 尖刺大; - 控制流和数据流耦合,路由管理逻辑甚至混在 Filter 中。
多层抽象虽然集成友好,但在网关这种延迟敏感、流量极大的场景,非业务栈损耗会被放大。
三、自研架构
理想网关需要支持海量接口注册、简单编程范式、高吞吐、低 RT、稳定运行、强扩展能力和数据流控制流分离。
DAG 采用 request-per-thread 单请求闭环模式:
从接收请求到转发后端、接收响应、回写客户端,都在同一个 EventLoop 线程内完成,减少线程切换和数据竞争。
控制流与数据流分离:数据面板只处理代理流量,控制面监听独立端口接收管理指令。
四、请求上下文封装
DAG 保留 Netty 基础,使用ServerWebExchange封装请求上下文,接口尽量贴近 SCG,以降低迁移成本。
@GetterpublicclassDefaultServerWebExchangeimplementsServerWebExchange{privatefinalChannelclient2ProxyChannel;privatefinalChannelproxy2ClientChannel;privatefinalEventLoopexecutor;privateServerHttpRequestrequest;privateServerHttpResponseresponse;privatefinalMap<String,Object>attributes;}入口 Handler 只负责构建上下文,随后交给 FilterChain:
@OverrideprotectedvoidchannelRead0(ChannelHandlerContextctx,FullHttpRequestfullHttpRequest){try{Channelclient2ProxyChannel=ctx.channel();DefaultServerHttpRequestserverHttpRequest=newDefaultServerHttpRequest(fullHttpRequest,client2ProxyChannel);ServerWebExchangeserverWebExchange=newDefaultServerWebExchange(client2ProxyChannel,(EventLoop)ctx.executor(),serverHttpRequest,null);this.requestFilterChain.filter(serverWebExchange);}catch(Throwablet){log.error("Exception caused before filters!\n {}",ExceptionUtils.getStackTrace(t));ByteBufHelper.safeRelease(fullHttpRequest);throwt;}}五、FilterChain 与路由匹配
Filter 返回 order,但 DAG 不允许 order 重复,避免执行顺序模糊。
publicinterfaceGatewayFilterextendsOrdered{voidfilter(ServerWebExchangeexchange,GatewayFilterChainchain);}publicinterfaceResponseFilterextendsGatewayFilter{}publicinterfaceRequestFilterextendsGatewayFilter{}路由匹配从 SCG 的 O(n) 遍历改为 Hash 匹配。Path 作为 key,在同一个网关集群中唯一,匹配效率提升到 O(1)。常见的 path 参数接口不支持,但网关保留内部前缀匹配能力作为兜底。
六、单线程闭环与连接池
单线程闭环要求下游连接池的管理线程与当前 EventLoop 一致。每个线程维护一个连接池,类似 nginx 的 thread-per-core 模式。
连接获取和释放可以简化为链表操作,去掉多线程场景下的额外同步:
enumSTATE{INIT,SENDING,SEND,SEND_SUCCESS,FAILED,TIMEOUT,RECEIVED}publicclassAsyncContext<Req,Resp>implementsCloneable{STATEstate=STATE.INIT;finalChannelusedChannel;finalChannelPoolusedChannelPool;finalEventExecutorexecutor;finalAsyncClient<Req,Resp>agent;Reqrequest;Respresponse;ResponseCallback<Resp>responseCallback;ExceptionCallbackexceptionCallback;inttimeout;longdeadline;longsendTimestamp;Promise<Resp>responsePromise;}七、AsyncClient
AsyncClient 是协议无关的异步调用框架,支持超时、回调、错误报告和链式调用。HTTP、Dubbo、Redis 等协议最终都基于 TCP 连接,因此连接池可以复用。
publicAsyncContext<Req,Resp>newRequest(EventExecutorexecutor,Stringendpoint,Reqrequest){finalChannelPoolKeypoolKey=genPoolKey(endpoint);ChannelPoolusedChannelPool=channelPoolManager.acquireChannelPool(executor,poolKey);returnnewAsyncContext<>(this,executor,usedChannelPool,request,defaultTimeout,executor.newPromise());}发送时将 AsyncContext 绑定到 Channel 的 AttributeKey,响应回来时再从 Channel 中取出上下文:
publicclassAsyncClientHandlerextendsSimpleChannelInboundHandler{@OverrideprotectedvoidchannelRead0(ChannelHandlerContextctx,Objectmsg)throwsException{AsyncContextasyncContext=ctx.attr(AsyncClient.ASYNC_REQUEST_KEY).get();try{asyncContext.state=AsyncContext.STATE.RECEIVED;asyncContext.releaseChannel();asyncContext.responsePromise.setSuccess(msg);}catch(Throwablet){log.error("Exception raised when set Success callback. Exception \n: {}",ExceptionUtils.getFullStackTrace(t));ByteBufHelper.safeRelease(msg);throwt;}}}调用权限系统时可以使用链式 API:
finalFullHttpRequesthttpRequest=HttpRequestUtil.getDefaultFullHttpRequest(newAuthReq,serviceInstance,"/auth/newCheckSls");asyncClient.newRequest(exchange.getExecutor(),endPoint,httpRequest).timeout(timeout).onComplete(response->{StringcheckResultJson=response.content().toString(CharsetUtil.UTF_8);response.release();NewAuthResultresult=Jsons.parse(checkResultJson,NewAuthResult.class);TokenResulttokenResult=this.buildTokenResult(result);if(tokenResult.getUserInfoResp()!=null){UserInfoRespuserInfo=tokenResult.getUserInfoResp();headers.set("userid",userInfo.getUserid()==null?"":String.valueOf(userInfo.getUserid()));headers.set("username",StringUtils.isEmpty(userInfo.getUsername())?"":userInfo.getUsername());headers.set("name",StringUtils.isEmpty(userInfo.getName())?"":userInfo.getName());chain.filter(exchange);}else{intcode=tokenResult.getCode()!=null?tokenResult.getCode().intValue():ResultCode.UNAUTHO.code;ResponseDecorator.failResponse(exchange,code,tokenResult.getMsg());}}).onError(throwable->{log.error("Request service {},occur an exception {}",endPoint,throwable);ResponseDecorator.failResponseWithStatus(exchange,HttpResponseStatus.INTERNAL_SERVER_ERROR,"AuthFilter 验证失败");}).sendRequest();八、堆外内存与限流优化
堆外内存使用引用计数释放时,业务链路复杂后容易忘记调整引用计数。DAG 在最后一个 Handler 中调用safeRelease,只要引用计数大于 0 就持续 release,直到归零。
publicstaticvoidsafeRelease(Objectmsg){if(msginstanceofReferenceCounted){ReferenceCountedref=(ReferenceCounted)msg;intrefCount=ref.refCnt();for(inti=0;i<refCount;i++){ref.release();}}}Sentinel 原生集群限流底层存在阻塞网络 IO,如果运行在 Worker EventLoop 中会阻塞线程。DAG 将 Redis 请求 Token 改为 AsyncClient 异步非阻塞调用:
publicclassSentinelGatewayFilterimplementsRequestFilter{@ResourceRedisTokenServicetokenService;@Overridepublicvoidfilter(ServerWebExchangeexchange,GatewayFilterChainchain){ServerHttpRequestrequest=exchange.getRequest();Stringresource=request.getPath()!=null?request.getPath():"";ClusterFlowRulerule=ClusterFlowManager.getClusterFlowRule(resource);if(rule!=null){tokenService.asyncRequestToken(rule,exchange.getExecutor()).addListener(future->{TokenResulttokenResult;if(future.isSuccess()){tokenResult=(TokenResult)future.getNow();}else{tokenResult=RedisTokenService.FAIL;}ClusterFlowManager.setTokenResult(rule.getRuleId(),tokenResult);doSentinelFlowControl(exchange,chain,resource);});}else{doSentinelFlowControl(exchange,chain,resource);}}}九、压测与收益
| 指标 | SCG | DAG |
|---|---|---|
| 满负载 QPS | 约 1.1W | 约 4.5W |
| CPU 占用 | 95% | 80% |
| ART | 54.1ms | 19ms |
| 低负载 QPS 1.1W | CPU 高 | CPU 约 30%,ART 1.56ms |
DAG 投产后的主要收益:
- 接口注册模式提升安全性,未注册接口无法访问;
- 内存泄漏问题解决;
- 资源占用下降 50% 以上;
- 升级 JDK17 + ZGC 后,GC 暂停从平均 70ms 降到 1ms 内。
十、总结
得物不是一开始就自研,而是在 SCG 上逐步改造,直到安全问题、路由性能、内存泄漏和发布稳定性无法继续通过补丁解决,才选择重写。自研 DAG 的核心判断是:用单线程闭环降低并发复杂度,用 AsyncClient 统一异步 IO,用 Hash 路由解决接口规模化,用数据面控制面分离提高架构清晰度。