资讯详情

资讯详情

微服务架构下Gateway与Dubbo集成实战:路由、泛化调用与限流熔断

1. 为什么要在微服务架构里引入网关层1.1 从一次服务调用混乱说起早些年做微服务改造的时候很多团队都会经历一个相似的阶段服务拆得挺开心注册中心里躺着几十个服务名前端同学却开始骂人了。原因很简单原来一个单体应用对外只有一个域名现在拆成十几个服务每个服务都有自己的地址和端口前端要调一个订单详情得先问用户服务拿用户信息再问商品服务拿商品信息再问订单服务拿订单信息跨域、鉴权、限流、日志这些事在每个服务里重复写一遍改一个规则要改十几个地方。这个阶段最典型的特征就是调用关系网状化。服务A调服务B服务B调服务C服务C又回调服务A链路一长排查问题就像在迷宫里找出口。更麻烦的是外部请求直接打到具体服务上服务地址一旦暴露安全边界就模糊了任何一个服务被扫到整个系统都可能被拖下水。网关层就是在这个背景下被引入的。它的核心思路很朴素在外部请求和内部服务之间加一道统一的门。所有外部流量先到网关由网关完成鉴权、限流、路由、日志、协议转换这些横切关注点然后再转发到具体的内部服务。内部服务之间怎么调是内部的事外部只认网关这一个入口。1.2 Dubbo与Gateway各自扮演什么角色这里要先厘清一个容易混淆的点。Dubbo和Spring Cloud Gateway不是竞争关系它们解决的是不同层面的问题。Dubbo是RPC框架负责服务之间的远程调用。它关注的是服务怎么注册、怎么发现、怎么负载均衡、怎么序列化、怎么容错。Dubbo的调用是面向接口的消费者拿到的是接口的代理对象调用起来像调本地方法一样。它天然适合内部服务之间的高频、低延迟通信。Spring Cloud Gateway是API网关负责外部流量的入口管理。它关注的是请求怎么进来、怎么鉴权、怎么限流、怎么路由到后端、怎么改写请求和响应。它基于WebFlux响应式模型吞吐量比传统的Servlet网关要高不少。把这两个东西结合起来就形成了一个很清晰的架构分层外部流量走Gateway内部调用走Dubbo。Gateway作为系统的统一入口接收到外部HTTP请求后通过某种方式调用后端的Dubbo服务把结果再以HTTP形式返回给调用方。这样既保留了Dubbo在内部调用上的高性能和治理能力又让外部调用方不需要关心内部的服务拓扑。1.3 这套组合适合什么样的团队不是所有项目都需要这套架构。如果你的系统只有三五个服务调用关系简单直接让前端调各个服务也不是不行加网关反而多了一层跳转。但如果你的系统满足下面几个条件中的两个以上就值得考虑引入Gateway加Dubbo的组合服务数量超过十个调用关系开始变得复杂需要统一入口收敛流量有多个客户端Web、App、小程序、第三方每个客户端的鉴权、限流策略不同内部服务需要高性能RPC调用但外部只接受HTTP协议需要对所有外部请求做统一的日志、监控、链路追踪团队里已经有Dubbo的使用经验不想把内部调用全部改成HTTP我见过不少团队一开始用Spring Cloud全家桶内部调用走Feign后来发现Feign的HTTP调用在高压场景下性能吃紧又慢慢把内部调用换回Dubbo但网关层保留Gateway。这种混合架构在实际生产里跑得挺稳关键是边界要划清楚。2. 整体架构设计与核心思路拆解2.1 分层架构的边界划分这套架构的核心在于分层每一层只做自己该做的事。我把它拆成四层来看第一层是客户端层包括浏览器、移动端、第三方系统。这一层只认网关的地址不关心后端有多少个服务、服务叫什么名字、部署在哪里。第二层是网关层由Spring Cloud Gateway承担。它负责统一鉴权、限流、路由转发、请求日志、跨域处理、协议转换。网关层不写业务逻辑只做流量治理。第三层是服务层由Dubbo服务组成。每个服务暴露的是Dubbo接口内部通过注册中心互相发现。服务层不关心请求是从哪来的只关心接口调用是否正常。第四层是基础设施层包括注册中心如Nacos、Zookeeper、配置中心、监控系统。Dubbo服务注册到注册中心Gateway从注册中心获取服务列表或者通过固定配置找到后端。这个分层的关键在于Gateway不直接依赖Dubbo的注册中心来做路由。虽然理论上可以让Gateway从注册中心拉取Dubbo服务列表但实践中更常见的做法是Gateway路由到一个HTTP到Dubbo的桥接层或者Gateway本身通过Dubbo的泛化调用直接调后端服务。两种方式各有取舍后面会详细说。2.2 两种主流集成方式的取舍把Gateway和Dubbo接起来业内主要有两种做法我分别说说它们的适用场景和坑。方式一Gateway通过HTTP转发到独立的桥接服务桥接服务再用Dubbo调后端。这种方式的好处是职责清晰。Gateway只管HTTP路由桥接服务可以理解为一个BFF层负责把HTTP请求翻译成Dubbo调用。桥接服务可以用Spring MVC写开发调试都方便团队里不熟悉Dubbo泛化调用的人也能快速上手。缺点是链路多了一跳延迟会增加而且桥接服务本身可能成为瓶颈需要单独做集群和限流。方式二Gateway直接通过Dubbo泛化调用后端服务。这种方式链路最短Gateway收到HTTP请求后直接构造Dubbo泛化调用参数调完后端服务再把结果序列化成JSON返回。性能上更有优势少了一次网络跳转。但缺点是Gateway里要引入Dubbo的依赖泛化调用的代码写起来比较绕参数类型转换、异常处理、超时控制都需要自己封装。而且Gateway是基于WebFlux的响应式模型Dubbo的调用是阻塞的直接混用会阻塞事件循环线程必须把Dubbo调用放到单独的线程池里执行否则网关的吞吐量会断崖式下跌。我个人的选择是如果团队规模不大、服务数量中等优先用方式一开发效率高出问题好排查。如果对延迟极其敏感、团队有足够的Dubbo功底再考虑方式二但一定要处理好阻塞调用和响应式模型的冲突。2.3 路由策略与负载均衡的配合Gateway的路由配置决定了请求怎么落到后端。在Dubbo场景下路由策略要考虑两个层面Gateway层面的路由和Dubbo层面的负载均衡。Gateway层面通常按路径前缀来路由。比如/api/user/**路由到用户服务/api/order/**路由到订单服务。这种配置直观运维好理解。也可以用请求头或查询参数来做路由适合灰度发布或者多版本并存的场景。Dubbo层面负载均衡策略有随机、轮询、最少活跃调用、一致性哈希几种。默认是随机大部分场景够用。如果后端服务有状态比如某些缓存服务可能要用一致性哈希来保证同一类请求落到同一个节点。如果各节点性能差异大用最少活跃调用更合适。这里有个容易踩的坑Gateway的路由和Dubbo的负载均衡是两层独立的决策。Gateway把请求转发到桥接服务或者发起泛化调用时选的是桥接服务的实例桥接服务再调Dubbo时Dubbo自己又选一次后端实例。如果桥接服务是多实例部署实际上做了两次负载均衡。这本身不是问题但排查问题时要注意一次请求可能经过两次实例选择日志里要能串起来。2.4 鉴权与限流的落点选择鉴权和限流放在哪一层是个需要提前想清楚的问题。鉴权通常放在Gateway层。因为Gateway是所有外部请求的必经之路在这里做统一鉴权后端服务就不用各自实现一套。常见的做法是Gateway校验JWT或者Token校验通过后把用户信息放到请求头里传给后端。后端服务信任网关传来的用户信息不再重复校验。这样做的前提是内部网络是可信的外部请求无法绕过网关直接访问后端服务。如果内部网络不完全可信后端服务还需要做二次校验。限流可以放在Gateway层也可以放在Dubbo层或者两层都放。Gateway层限流针对的是外部流量比如按IP、按用户、按接口维度限制QPS。Dubbo层限流针对的是内部调用防止某个服务被上游打垮。两层限流的维度不同不冲突。Gateway层可以用Redis加令牌桶算法Dubbo层可以用Sentinel或者Dubbo自带的限流功能。我一般建议Gateway层做粗粒度限流Dubbo层做细粒度限流。Gateway层挡住明显的恶意流量和突发流量Dubbo层保护核心服务不被内部的其他服务拖垮。3. 核心细节解析与实操要点3.1 Gateway路由配置的关键参数Gateway的路由配置有两种方式配置文件YAML和代码配置Java DSL。生产环境我更推荐用配置文件改路由不用重新打包配合配置中心还能动态刷新。一个典型的路由配置长这样spring: cloud: gateway: routes: - id: user-service-route uri: http://user-bridge-service:8080 predicates: - Path/api/user/** filters: - StripPrefix1 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 100 redis-rate-limiter.burstCapacity: 200这里有几个参数值得展开说。uri指向的是桥接服务的地址如果是方式二泛化调用这里可能指向一个本地的Dubbo调用处理器。predicates里的Path是匹配规则/api/user/**表示匹配这个前缀下的所有路径。filters里的StripPrefix1表示转发时去掉第一层路径这样后端服务收到的路径就不带/api前缀了。RequestRateLimiter是限流过滤器replenishRate是令牌桶的填充速率burstCapacity是桶的容量。这两个值要根据实际压测结果来定拍脑袋设很容易要么限不住要么误伤。注意Gateway的路由匹配是有顺序的越靠前的路由优先级越高。如果两条路由的匹配规则有重叠要确保更具体的规则排在前面否则请求可能落到错误的路由上。3.2 Dubbo泛化调用的封装要点如果选择方式二泛化调用的封装是核心难点。Dubbo的泛化调用不需要依赖服务方的接口jar包通过GenericService接口就能调用但参数和返回值的处理比较繁琐。核心代码大概是这样ReferenceConfigGenericService reference new ReferenceConfig(); reference.setInterface(com.example.UserService); reference.setGeneric(true); reference.setUrl(dubbo://user-service:20880); GenericService genericService reference.get(); Object result genericService.$invoke( getUserById, new String[]{java.lang.Long}, new Object[]{userId} );这段代码看起来简单但实际用起来有几个坑。第一ReferenceConfig的创建和销毁是有成本的不能每次请求都新建一个要用缓存池管理。第二泛化调用的返回值是Map或者Object需要根据实际类型做转换转换逻辑要健壮否则遇到复杂对象容易出错。第三Dubbo调用是阻塞的在Gateway的响应式线程里直接调会阻塞事件循环必须用Mono.fromCallable()包一层并指定单独的调度器。return Mono.fromCallable(() - genericService.$invoke(...)) .subscribeOn(Schedulers.boundedElastic());Schedulers.boundedElastic()是专门为阻塞任务准备的调度器它会用独立的线程池执行不会占用Netty的事件循环线程。这个细节如果不注意压测的时候网关的QPS会低得离谱而且很难排查原因。3.3 请求参数与返回值的类型映射HTTP请求的参数是字符串或者JSONDubbo接口的参数是Java类型中间要做映射。简单类型String、Integer、Long、Boolean还好办复杂对象就麻烦了。我的做法是在网关层维护一份接口元数据记录每个Dubbo接口的方法名、参数类型、返回值类型。请求进来后根据路径和方法名找到对应的元数据把JSON参数反序列化成对应的Java对象再传给泛化调用。返回值反过来把Java对象序列化成JSON返回给调用方。这份元数据可以手工维护也可以从Dubbo的注册中心或者接口文档里自动生成。手工维护的缺点是接口一变就要改容易漏自动生成的好处是省事但需要额外的工具支持。中小团队我建议先手工维护接口稳定后再考虑自动化。类型映射里最容易出问题的是日期类型和枚举类型。日期在不同序列化方式下的格式不一样枚举在泛化调用里可能变成字符串或者数字需要统一约定。我的经验是网关层统一用ISO 8601格式处理日期枚举统一用字符串传输这样前后端和Dubbo服务之间的约定最清晰。3.4 超时与重试的策略设计超时和重试是网关层必须处理的问题处理不好会导致雪崩。超时要分层设置。Gateway到桥接服务的超时、桥接服务到Dubbo服务的超时、Dubbo服务内部的超时每一层都要设而且外层超时要大于内层超时否则外层已经超时返回了内层还在执行白白浪费资源。比如Dubbo调用超时设3秒桥接服务的HTTP超时设5秒Gateway的路由超时设8秒这样逐层放大给内层留出处理时间。重试要谨慎。不是所有接口都适合重试。查询类接口重试相对安全写入类接口重试可能导致重复下单、重复扣款。如果一定要重试要保证接口的幂等性或者用唯一的请求ID做去重。Gateway的Retry过滤器可以配置重试次数和重试条件但默认只对特定异常重试配置的时候要看清楚。提示Dubbo本身也有重试机制默认重试2次。如果Gateway层也配了重试实际重试次数是两层相乘的。比如Gateway重试2次、Dubbo重试2次最坏情况下一个请求会打到后端6次。这个坑我在生产环境踩过一个慢查询接口因为重试叠加把数据库连接池打满了。4. 完整实操流程与核心环节实现4.1 环境准备与依赖梳理假设我们要搭一套最小可用的环境包含一个Gateway、一个桥接服务、两个Dubbo服务、一个注册中心。注册中心用NacosDubbo版本用3.xSpring Cloud版本用2022.xSpring Boot用3.x。依赖清单如下组件依赖版本参考Gatewayspring-cloud-starter-gateway4.x桥接服务spring-boot-starter-web3.x桥接服务dubbo-spring-boot-starter3.xDubbo服务dubbo-spring-boot-starter3.x注册中心nacos-discovery2.x限流spring-boot-starter-data-redis-reactive3.x版本对齐很重要。Dubbo 3.x和Spring Boot 3.x的兼容性在早期版本里有些问题建议用Dubbo 3.2以上的版本。Spring Cloud Gateway 4.x要求Spring Boot 3.x如果还在用Spring Boot 2.x要用Gateway 3.x。4.2 桥接服务的接口设计与实现桥接服务的职责很明确接收HTTP请求转成Dubbo调用返回HTTP响应。接口设计上我建议按业务域划分Controller每个Controller对应一个Dubbo服务。RestController RequestMapping(/user) public class UserBridgeController { DubboReference private UserService userService; GetMapping(/{id}) public ResultUserDTO getUser(PathVariable Long id) { return Result.success(userService.getUserById(id)); } }这里用DubboReference注入Dubbo服务调用起来和本地方法一样。Result是统一的返回包装类包含code、message、data三个字段。这样前端拿到的响应格式是统一的不用每个接口单独处理。桥接服务的参数校验也要做。HTTP请求的参数可能不合法比如ID是负数、分页参数超范围这些在桥接服务里就要拦住不要传到Dubbo服务里。用Valid注解加JSR 303校验配合全局异常处理器把校验失败的响应统一返回。4.3 Gateway路由与过滤器的配置落地Gateway的配置文件里除了路由规则还要配一些全局过滤器。我常用的几个鉴权过滤器校验请求头里的Token解析出用户信息放到新的请求头里传给下游。校验失败直接返回401不往后转发。日志过滤器记录请求的路径、方法、参数、响应状态、耗时。日志里带上请求ID方便和下游服务的日志串联。跨域过滤器处理CORS预检请求配置允许的来源、方法、请求头。Gateway有内置的CORS配置但有时候和自定义过滤器冲突需要调整顺序。Bean public GlobalFilter authFilter() { return (exchange, chain) - { String token exchange.getRequest().getHeaders().getFirst(Authorization); if (token null || !validateToken(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } ServerHttpRequest request exchange.getRequest().mutate() .header(X-User-Id, getUserId(token)) .build(); return chain.filter(exchange.mutate().request(request).build()); }; }这个过滤器里mutate()方法用来修改请求把解析出的用户ID放到新的请求头里。下游服务直接从请求头里拿用户ID不用再解析Token。4.4 限流与熔断的配置实操限流用Redis加令牌桶Gateway的RequestRateLimiter过滤器已经封装好了。需要提供一个KeyResolver来决定限流的维度。Bean public KeyResolver ipKeyResolver() { return exchange - Mono.just( exchange.getRequest().getRemoteAddress().getAddress().getHostAddress() ); }这个KeyResolver按IP限流。如果要按用户限流就从请求头里拿用户ID要按接口限流就从路径里提取接口名。可以组合多个维度比如每个用户每个接口每秒最多10次。熔断用Resilience4j或者Sentinel。Gateway层做熔断的意义在于当后端服务响应变慢或者大量报错时网关快速失败不再把请求转发过去给后端服务喘息的时间。熔断的阈值要根据实际压测来定一般错误率超过50%、且请求数超过一定量时才触发。注意限流和熔断的配置要配合监控一起上。没有监控你不知道限流是不是限得太狠、熔断是不是误触发。Gateway可以暴露Actuator端点把限流和熔断的指标打到Prometheus里配Grafana看板。4.5 链路追踪的串联微服务架构下一个请求经过Gateway、桥接服务、Dubbo服务链路追踪是排查问题的刚需。用Sleuth或者Micrometer Tracing加Zipkin可以在日志里带上TraceId和SpanId。Gateway层生成TraceId通过请求头传给桥接服务桥接服务再通过Dubbo的隐式参数传给Dubbo服务。Dubbo 3.x支持通过RpcContext传递隐式参数把TraceId放进去下游服务从RpcContext里取出来放到MDC里日志就能串起来了。// 桥接服务发送时 RpcContext.getClientAttachment().setAttachment(traceId, traceId); // Dubbo服务接收时 String traceId RpcContext.getServerAttachment().getAttachment(traceId); MDC.put(traceId, traceId);这个细节看起来小但生产环境排查问题时价值极大。没有TraceId你只能靠时间戳去猜哪个日志对应哪个请求效率极低。5. 常见问题与排查技巧实录5.1 网关转发后服务收到的路径不对这是最常见的配置问题。Gateway的Path断言匹配的是原始路径转发时如果不做处理后端服务收到的路径会带上匹配的前缀。比如/api/user/123转发到用户服务用户服务收到的可能是/api/user/123但用户服务的接口定义是/user/123就会404。解决办法是用StripPrefix过滤器去掉前缀。StripPrefix1去掉一层StripPrefix2去掉两层。具体去几层要看你的路径设计。我一般建议网关路由前缀和后端接口路径分离网关用/api/xxx后端用/xxx统一去掉一层。5.2 Dubbo调用超时但HTTP响应已经返回这个问题的现象是前端收到了超时错误但后端服务的日志显示调用还在执行甚至最终执行成功了。原因是Gateway或者桥接服务的超时时间设得比Dubbo的超时时间短外层已经超时返回了内层还在跑。解决方法是逐层放大超时时间。Dubbo超时3秒桥接服务HTTP超时5秒Gateway路由超时8秒。同时对于写操作要考虑超时后的补偿机制不能因为超时就认为操作失败了。5.3 泛化调用返回的Map转对象失败泛化调用的返回值是Map字段名和类型可能和实际的对象不一致。比如日期字段返回的是时间戳枚举字段返回的是字符串嵌套对象返回的是Map的Map。直接强转成目标对象会报ClassCastException。解决办法是用Jackson或者Gson做转换先把Map转成JSON字符串再反序列化成目标对象。转换的时候要配置好日期格式和枚举处理策略。如果字段名不一致用JsonProperty注解做映射。5.4 限流规则不生效限流的坑比较多。首先检查Redis连接是否正常RequestRateLimiter依赖Redis存储令牌桶状态Redis挂了限流就失效了。其次检查KeyResolver是否返回了空值如果返回Mono.empty()限流器会跳过限流。最后检查限流的维度和预期是否一致按IP限流和按用户限流的效果完全不同。还有一个隐蔽的坑Gateway集群部署时限流是分布式的还是单机的。用Redis做存储是分布式的所有Gateway实例共享令牌桶如果用内存做存储每个实例独立限流实际限流阈值是配置值乘以实例数。生产环境一定要用Redis做分布式限流。5.5 常见问题速查表问题现象可能原因排查方向解决办法后端服务404路径前缀未去除检查StripPrefix配置调整StripPrefix层数前端超时但后端成功超时时间倒挂检查各层超时配置逐层放大超时时间泛化调用类型转换异常Map转对象失败检查字段类型和名称用JSON中转转换限流不生效Redis异常或KeyResolver为空检查Redis连接和KeyResolver修复Redis或调整KeyResolver网关QPS低阻塞调用占用事件循环检查是否有阻塞操作用boundedElastic调度器重试次数过多多层重试叠加检查Gateway和Dubbo重试配置只在一层配置重试5.6 几个压箱底的实操心得心得一网关层不要做业务逻辑。我见过有人在Gateway的过滤器里查数据库、做复杂计算结果网关成了瓶颈。网关只做流量治理业务逻辑放到桥接服务或者Dubbo服务里。心得二Dubbo的泛化调用缓存要设上限。ReferenceConfig的缓存如果不设上限服务多了会内存泄漏。用ConcurrentHashMap加LRU策略或者用Guava Cache设置合理的过期时间和最大容量。心得三日志里一定要带请求ID。从Gateway生成一路透传到Dubbo服务每个环节的日志都带上。排查问题时一个请求ID就能把所有相关日志串起来效率提升十倍不止。心得四压测要覆盖网关层。很多人压测只压后端服务忽略了网关。实际上网关的线程模型、限流配置、连接池大小都会影响整体吞吐量。压测的时候要从Gateway入口压观察网关的CPU、内存、GC情况。心得五灰度发布用请求头做路由。Gateway支持按请求头路由灰度发布的时候给测试用户的请求打上特定请求头路由到新版本服务其他用户走老版本。这样不用改配置灵活可控。这套Gateway加Dubbo的组合我在几个项目里都用过整体跑下来比较稳。关键是把边界划清楚Gateway管入口Dubbo管内部调用桥接层做协议转换各司其职。配置上注意超时、重试、限流的细节监控和链路追踪跟上基本不会出大问题。真正难的不是搭起来而是搭起来之后怎么调优、怎么排查、怎么随着业务变化演进。这些经验都是一次次踩坑踩出来的希望能帮到正在做类似架构的同行。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →