多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Spring Cloud整合Dubbo实战:从原理到踩坑调优

Spring Cloud整合Dubbo实战:从原理到踩坑调优 1. Spring Cloud项目里为什么还要引入Dubbo很多人问我一个问题项目里已经上了Spring Cloud服务之间都用Feign走HTTP为什么还要把Dubbo拉进来说实话我在真实业务里遇到过太多次这种场景——系统不是从零设计的老团队长期用Dubbo沉淀了大量核心服务新团队又基于Spring Cloud在搭新的应用两边要互相调用硬用RestTemplate去写HTTP客户端适配Dubbo服务代码丑陋不说性能和治理能力都打折扣。Spring Cloud拿手的是配置管理、网关、服务发现、熔断这些微服务治理能力但它默认的HTTP调用在内部高频RPC场景下存在连接开销大、序列化效率低的问题。Dubbo恰恰相反它对内网服务调用做了深度优化基于TCP的长连接复用、高效的Hessian2序列化、内置的负载均衡和集群容错在低延迟高吞吐的场景下优势非常明显。所以理想的做法是保留Spring Cloud作为微服务治理底座同时引入Dubbo作为高性能RPC调用通道。这就是Spring Cloud整合Dubbo的意义所在。这篇文章适合谁看一种是正在做技术栈融合评估的架构师一种是项目里被Feign调不通Dubbo服务折磨的开发者还有一种是刚接触微服务想搞明白这两套东西怎么配合的新人。我会从选型原理讲到完整落地代码再到高频踩坑实录和调优方案尽量少讲虚的多给能直接抄的东西。2. 整合前的认知准备与版本选型2.1 核心角色划分服务发现归Spring CloudRPC归Dubbo整合的第一步不是写代码而是想清楚谁干什么。我的建议是服务注册与发现、配置中心、网关、链路追踪这些横切能力统一走Spring Cloud体系而服务之间的同步调用尤其是要求低延迟的内部核心链路走Dubbo协议。两者共用一个注册中心实践中用得最多的是Nacos就能实现服务实例的互联互通。这里有个关键认知Dubbo本身不依赖Spring Cloud它完全可以独立使用有自己的注册中心抽象和配置体系。整合的本质是让Dubbo的注册中心指向和Spring Cloud同一个Nacos同时把Dubbo的Bean管理和配置注入交给Spring Boot容器来管。这样你在一个应用里既可以用Spring Cloud的Autowired注入组件也可以用Dubbo的DubboReference注入远程服务。两者互不干扰但注册数据落在同一个服务列表里。2.2 版本兼容矩阵与踩过的坑版本选择是整合过程中最容易出事的环节。Dubbo、Spring Boot、Spring Cloud Alibaba三者必须落在兼容区间内否则启动时各种NoSuchMethodError和ClassNotFoundException会让人怀疑人生。我整理了一份自己用过的兼容组合基本踩不出大坑。Spring Boot版本Spring Cloud版本Spring Cloud Alibaba版本Dubbo版本说明2.2.xHoxton.RELEASE2.2.x.RELEASE2.7.5老项目常见稳定但偏旧2.3.xHoxton.SR82.2.7.RELEASE2.7.8较稳的组合文档多2.6.x2021.0.x2021.0.1.03.0.x引入Dubbo 3接口级到实例级的过渡2.7.x2021.0.52021.0.5.03.2.x当前比较主流推荐3.2.x2023.0.x2023.0.1.03.3.x新项目可选注意javax换成jakarta我实际项目中最稳的组合是Spring Boot 2.7.x加Spring Cloud Alibaba 2021.0.5.0加Dubbo 3.2.x这个组合对Nacos 2.x支持好官方文档齐全社区踩坑记录多出问题好查。如果你还在用Spring Boot 2.2.x那套建议尽快升级否则后面引入新组件时依赖冲突会越来越难解。2.3 注册中心双注册的工作原理Nacos在做Spring Cloud服务发现时注册的是应用名实例IP端口Spring Cloud的DiscoveryClient通过这个数据拿到可用实例列表。而Dubbo注册时因为接口是多对一的它会同时注册实例信息和接口元数据包括com.example.api.UserService这个接口名、版本号、分组号以及协议类型。所以你在Nacos服务列表里会看到一个奇怪的现象同样一台机器既有order-service这种Spring Cloud应用名服务又有带providers:com.example.api.UserService:前缀的Dubbo服务。这点必须提前跟团队讲清楚不然Nacos里看到一堆不认识的带前缀服务容易误以为是脏数据而手动删除删完就真的调不通了。3. 实操搭建一个可运行的Spring CloudDubbo工程3.1 工程结构与依赖配置我这次演示的是一个迷你但完整的场景一个user-service提供Dubbo接口一个order-service作为Spring Cloud应用消费这个接口。工程用Maven多模块组织从上到下依次是父工程、API模块、提供方模块、消费方模块。父工程的pom.xml里用dependencyManagement统一管理版本。我给出一段核心配置。properties spring.boot.version2.7.18/spring.boot.version spring.cloud.version2021.0.5/spring.cloud.version spring.cloud.alibaba.version2021.0.5.0/spring.cloud.alibaba.version dubbo.version3.2.9/dubbo.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version${spring.boot.version}/version typepom/type scopeimport/scope /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-dependencies/artifactId version${spring.cloud.version}/version typepom/type scopeimport/scope /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-alibaba-dependencies/artifactId version${spring.cloud.alibaba.version}/version typepom/type scopeimport/scope /dependency dependency groupIdorg.apache.dubbo/groupId artifactIddubbo-bom/artifactId version${dubbo.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement注意这里用的是dubbo-bom它能统一管理Dubbo相关子模块的版本号避免每个依赖单独写版本导致冲突。整合时不要再用老的spring-cloud-starter-dubbo那种引入方式了新版Dubbo用dubbo-spring-boot-starter更直接后续升级也干净。3.2 服务提供方改造API模块里定义一个普通接口注意这里只放接口和DTO不引入任何Dubbo或Spring Cloud的依赖。public interface UserService { UserDTO getUserById(Long id); }DTO必须实现java.io.Serializable这个我后面会专门讲为什么。提供方模块的依赖里引入dubbo-spring-boot-starter、spring-cloud-starter-alibaba-nacos-discovery以及API模块本身。核心配置在application.yml里。spring: application: name: user-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 dubbo: application: name: user-service qos-enable: false protocol: name: dubbo port: -1 registry: address: nacos://127.0.0.1:8848 scan: base-packages: com.example.provider.service这里有几个细节需要解释。port: -1表示让Dubbo自动分配端口避免多个服务在同一台机器部署时产生端口冲突。qos-enable: false关闭Dubbo自带的QoS端口否则默认会在22222端口起一个telnet服务在容器环境里容易端口冲突。scan.base-packages指定扫描Dubbo服务的包路径漏掉这个配置会让DubboService注解失效服务注册不上注册中心。实现类用DubboService注解暴露服务。这里有个容易混淆的点如果你的项目同时引了Spring的Service千万别写混了Dubbo的注解是org.apache.dubbo.config.annotation.DubboService。DubboService public class UserServiceImpl implements UserService { Override public UserDTO getUserById(Long id) { return new UserDTO(id, 用户- id); } }启动类上加上EnableDubbo让Dubbo的自动配置生效。3.3 服务消费方接入消费方看起来更简单依赖里除了Spring Cloud Alibaba Nacos Discovery之外也要有API模块和Dubbo的starter。配置如下。spring: application: name: order-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 dubbo: application: name: order-service qos-enable: false registry: address: nacos://127.0.0.1:8848 consumer: check: false在ConsumerConfig里通过DubboReference注入远程服务。check: false表示消费方启动时不去强行检查注册中心里有没有对应服务这在开发环境特别有用。因为很多团队喜欢把订单服务先启动但用户服务还没拉起来如果check是默认的true消费方启动就直接报No provider available。Service public class OrderService { DubboReference(timeout 3000, retries 0) private UserService userService; public String getOrderWithUser(Long userId) { UserDTO user userService.getUserById(userId); return order for user: user.getNickname(); } }timeout和retries这两个参数建议显式声明。Dubbo的默认超时是1000毫秒默认重试次数是2也就是说一次调用最坏情况下会等1秒再重试两次总共耗时可能接近3秒。如果你的服务有慢SQL或者批量查询场景这个默认值会引发雪崩式的超时重试。我在内部服务之间调用时幂等接口会保留一次重试非幂等写接口一律设为0。3.4 启动验证与调用链路确认启动Nacos Server后分别启动user-service和order-service。去Nacos控制台服务列表页应该能看到user-service和order-service两个应用名服务。如果是Dubbo 3.x还需要在订阅者栏确认order-service确实订阅了user-service。然后调用消费方的HTTP接口验证链路。curl http://127.0.0.1:8080/api/order/100返回结果{ orderId: ORDER-100, user: { id: 100, nickname: 用户-100 } }到这一步一个最基本的Spring Cloud整合Dubbo的工程就通了。链路是浏览器或客户端发起HTTP请求到order-service的Tomcat端口order-service内部通过Dubbo协议调user-service的Dubbo端口响应原路返回。整个过程里服务发现依赖Nacos实际数据传输走Dubbo协议这就是整合后的架构形态。4. 关键配置参数与调优细节4.1 参数速查表看完了基础工程的搭建下面把这些年摸出来的参数配置心得整理成速查表方便直接在项目里对照使用。配置项默认值建议原因dubbo.protocol.port20880-1或指定段-1自动分配避免本机多实例冲突dubbo.consumer.checktrue开发false生产true开发时保证消费方可先启动dubbo.consumer.timeout1000ms按接口实际情况调一刀切会导致超时重试放大故障dubbo.consumer.retries2写接口0读接口1重试非幂等写操作会产生重复数据dubbo.provider.threads200结合压测定线程数不是越大越好上下文切换开销大dubbo.provider.connections100与消费方规模匹配限制单服务最大长连接数dubbo.registry.timeout10000ms3000即可注册中心慢时影响启动速度dubbo.metadata-report.address无Nacos地址远端元数据服务便于运维排查别把这些参数当成固定模板。每个服务的数据量、QPS、分库分表策略不一样合理的配置肯定不同。我的习惯是先按上表设置压测有问题再针对单个服务微调不要让Dubbo配置变成团队里的玄学。4.2 超时、重试与线程模型怎么配这里单独说超时和重试因为这是生产事故的高发区。Dubbo的超时是分层的消费方设timeout服务提供方也可以设dubbo.provider.timeout。如果两边都设置了消费方的时间短就以消费方的为准。建议在消费方统一治理接口级超时提供方不上调全局超时。默认的线程模型是fixed线程池200个线程。当服务调用量达到峰值时如果线程被慢调用耗尽后续请求会排队等待随着队列堆积响应时间越拉越长最终整个应用假死。出现这种问题第一反应不是加线程数而是先看是不是下游服务变慢了再考虑线程池参数。线程数加大会增加内存占用和上下文切换最好基于压测结果计算线程数 QPS峰值 x 平均耗时(秒)这个公式能给出一个粗略的合理起点。4.3 元数据与配置中心Dubbo 3.x默认开启了元数据中心把接口描述、配置快照等数据上报到远端。整合Spring Cloud后推荐把元数据地址配置为同一个Nacos。dubbo: metadata-report: address: nacos://127.0.0.1:8848这样做的价值是Nacos控制台可以直接看到某个服务暴露了哪些接口、参数类型是什么、配置快照长什么样排查问题时不用挨个问服务提供方要接口文档。由于元数据上报是异步的不影响主调用链路所以不用太担心它成为瓶颈。但要注意如果Nacos版本是2.x务必要在防火墙里放行9848端口。这个端口是Nacos 2.x新增的gRPC通信端口只放行8848会出现一种很迷惑的现象服务注册偶尔成功消费方却拿不到实例列表。5. 实战中高频踩坑与排查实录5.1 服务总是找不到先分清注册还是订阅的问题碰到No provider available不要急着改代码。先按两步排查第一步去Nacos控制台看user-service在不在服务列表里。如果不在问题在服务提供方检查scan.base-packages、DubboService注解以及提供方进程日志里有没有Export service successfully。第二步如果提供方在但消费方日志依旧报找不到问题在订阅侧看消费方注册的分组和版本号是否一致。Dubbo默认分组是DUBBO如果提供方被设为dev分组消费方用的默认分组就永远发现不了。5.2 Nacos 2.x连接异常Nacos 2.x把客户端通信升级成了gRPC服务发现和配置变更推送都是gRPC。如果你部署在云上或使用容器网络只开放了8848端口就会遇到服务能注册但心跳超时的怪问题。日志里会出现Client(8548815) connection is interrupted, try to reconnect...排查办法很简单netstat -an | grep 9848看TCP连接状态。如果9848不通在安全组或防火墙里放行。另外如果Nacos服务端是多集群部署客户端配置的server-addr建议用域名或负载均衡地址不要写某一个节点的IP。5.3 版本冲突与NoSuchMethodError这类报错几乎都是版本矩阵没对齐造成的。一个典型的报错是启动时出现ClassNotFoundException: org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor原因是Spring Boot版本与Spring Cloud Alibaba版本不匹配spring.factories里引用了不存在的类。遇到这种问题第一件事先查版本矩阵而不是去网上搜报错原文。我把版本矩阵贴在了前面按那个表对一遍自己的pom.xml多数问题当场解决。5.4 序列化导致的奇怪报错Dubbo默认使用Hessian2序列化。它要求传参对象实现java.io.Serializable接口并且最好有一个无参构造函数。不满足这两个条件时调用不会在启动时报错而是等到运行时抛SerializationException表现非常迷惑。更隐蔽的是当你把一个DTO类升级新增了字段但服务提供方还是旧版本老的实例反序列化时可能读到null。为了减少这类问题推荐给DTO的字段都写清楚private static final long serialVersionUID 1L同时要求接口变更做到向后兼容老字段只增不改。提示跨团队使用API包时一定要约定序列化兼容规则。新字段必须允许为null不要轻易改变已有的字段类型。否则会出现本地测试好好的联调时莫名报错的乌龙。5.5 现场排查的完整命令清单最后分享一套我自己在线上排查Dubbo问题的命令组合拳。服务起不来先看日志tail -200f logs/user-service.log | grep -i error\|exception\|warn启动成功但注册不上用Dubbo的telnet命令连上本机Dubbo端口telnet 127.0.0.1 20880 ls ls com.example.api.UserService如果Dubbo端口没暴露检查qos-enable是不是被关了或者QoS端口跟其他服务冲突。这套操作看起来简单但能劝退70%的假连接问题。6. 整合后的性能与监控实践6.1 监控指标怎么接整合以后监控不能只看Spring Cloud那套HTTP指标了Dubbo的RPC指标同样重要。Dubbo 3.x自带metrics模块可以通过dubbo.metrics.enabletrue开启然后暴露Prometheus格式的指标。在spring-cloud-alibaba体系下如果你的基础设施已经有Prometheus和Grafana直接让Dubbo暴露一个新的监控端点就能把dubbo_provider_service_duration_seconds、dubbo_consumer_service_duration_seconds这些指标全部拉进来。我常用的几个关键指标dubbo_provider_service_total调用次数看服务的实际流量。dubbo_provider_service_duration_seconds响应耗时分布配合P99看性能瓶颈。dubbo_provider_thread_pool_active线程池活跃线程数超过80%的时候就得警惕了。6.2 线程池和连接数调整线程池调整要基于监控数据做。比如接口平均耗时50毫秒目标QPS是1000那么按前面的公式1000 x 0.05 50初始线程池设为128左右就足够预留50%的缓冲冗余。如果压测后发现线程池还是被打满优先优化接口的慢查询而不是无限调大线程池。连接数方面由于Dubbo是长连接一个消费方进程对同一个提供方实例默认建一个连接就够了。提供方的connections参数要大于等于消费方数量。假如你有30个订单服务实例都调用用户服务用户服务的connections就要设大于30。否则后启动的消费方可能会拿到多余的连接或者排队等待表现就是调用偶发超时。6.3 灰度流量怎么切整合之后做灰度可以利用Nacos权重和Dubbo的路由规则配合。Nacos控制台里可以直接修改某个实例的权重从0改到100实现流量的按比例切换。Dubbo 3.x还支持tag路由在服务方设置dubbo.provider.tagv1消费方用dubbo.consumer.tagv1指定只访问灰度实例。# 灰度消费方配置 dubbo: consumer: tag: v1这种方式在内部服务做灰度发布时很实用旧版本保留在Nacos里权重调低新版本带tag灰度上线验证后移除tag并调高权重。我实践下来比网关级灰度更细力度能精确到某个RPC接口的流量走向。6.4 运维层面的额外提醒整合成功后团队容易沉浸在终于能互相调了的喜悦里忽略运维侧的事情。这里提醒几件必须做的事第一Nacos、Dubbo服务端和应用的时区、网络时间要一致时间偏移会影响心跳判断第二生产环境的日志格式建议单独加traceId字段方便把HTTP入口和Dubbo内部调用串起来排查第三定期做一次注册中心的容量审视Nacos实例数超过一定规模后要考虑给Dubbo服务单独拆分Namespace避免注册数据互相干扰。提示整合后的应用本质上还是原Spring Boot工程所以原来Spring Cloud的监控、日志、链路追踪、配置中心方案都可以继续用。Dubbo只是把服务间通信换成了更高效的通道不要因此推倒重做运维体系。写在最后Spring Cloud整合Dubbo技术上并不复杂真正复杂的是理解两套框架各自的边界然后在组织协作、版本管理、监控治理上把它理顺。我踩过版本冲突的坑也见过生产环境因为超时重试导致雪崩的场面最后悟出来一个道理框架整合只是开始稳定运行靠的是团队对每个参数的敬畏和对可观测性的重视。最后分享一个小技巧在所有服务上线前强制把消费方的check改为true跑一遍冒烟测试这能倒逼团队把所有服务在研发环境都拉起来而不是每个人只验证自己那一亩三分地。整合Dubbo之后跨团队联调的频率会明显增加提前把规范定好后面省下的时间是巨大的。
返回列表