ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

网关架构:得物百万QPS高性能Netty网关架构演进路线

网关架构:得物百万QPS高性能Netty网关架构演进路线

得物百万QPS高性能Netty网关架构演进路线

得物原来的网关基于 Spring Cloud Gateway(SCG)。随着接口注册数量增长到 3 万以上,SCG 在安全、性能、维护、稳定性和定制能力上都开始暴露问题,最终得物选择自研 DAG(Dewu API Gateway),并在压测中取得约 4 倍于 SCG 的吞吐表现。

一、SCG 的五个业务问题

  1. 泛化路由导致接口很容易被意外开放到公网,安全风险高;
  2. 接口注册模式推广后,路由配置由网关开发转向业务开发,协同效率下降;
  3. 同一套代码承载不同业务集群,内网网关也背着几十个无关 Filter;
  4. 双周一次发版,启动初期接口超时率明显升高;
  5. 原生 SCG 对灰度、接口级超时、定向节点路由等定制需求支持不足。

二、SCG 的技术痛点

SCG 基于 WebFlux、Reactor Netty、Netty 多层抽象,带来以下问题:

  • 官方仓库存在多个长期未解决的内存泄漏问题,需要定期重启;
  • 响应式编程复杂,修改 Request/Response Body 的代码难读;
  • 调用栈过深。单个 Filter 的调用栈约 90+ 层,业务复杂时约 120 层,其中大量是框架层subscribe()、onNext();
  • 路由匹配是遍历式 O(n);
  • 启动时NamedContextFactory#getContext存在全局锁,冷启动 RT 尖刺大;
  • 控制流和数据流耦合,路由管理逻辑甚至混在 Filter 中。

WebFlux

Reactor Netty

Netty

底层网络

多层抽象虽然集成友好,但在网关这种延迟敏感、流量极大的场景,非业务栈损耗会被放大。

三、自研架构

理想网关需要支持海量接口注册、简单编程范式、高吞吐、低 RT、稳定运行、强扩展能力和数据流控制流分离。

DAG 采用 request-per-thread 单请求闭环模式:

BackendFilterChainWorker EventLoopClientBackendFilterChainWorker EventLoopClient请求构建 ServerWebExchange异步转发响应回写Response

从接收请求到转发后端、接收响应、回写客户端,都在同一个 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);}}}

九、压测与收益

指标SCGDAG
满负载 QPS约 1.1W约 4.5W
CPU 占用95%80%
ART54.1ms19ms
低负载 QPS 1.1WCPU 高CPU 约 30%,ART 1.56ms

DAG 投产后的主要收益:

  • 接口注册模式提升安全性,未注册接口无法访问;
  • 内存泄漏问题解决;
  • 资源占用下降 50% 以上;
  • 升级 JDK17 + ZGC 后,GC 暂停从平均 70ms 降到 1ms 内。

十、总结

得物不是一开始就自研,而是在 SCG 上逐步改造,直到安全问题、路由性能、内存泄漏和发布稳定性无法继续通过补丁解决,才选择重写。自研 DAG 的核心判断是:用单线程闭环降低并发复杂度,用 AsyncClient 统一异步 IO,用 Hash 路由解决接口规模化,用数据面控制面分离提高架构清晰度。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进