Dubbo 快速理解构建模型
公司里同时跑着 com.alibaba.dubbo 的上古服务和 Dubbo 3,老代码为什么长那样,答案全在历史里。这篇理清 Dubbo 的定位、二十年演进、协议与序列化这两个正交维度、ZK 与 Nacos 的取舍,最后给一条 2.x 到 3.x 的升级路径。
我最准备开始学习了,就从微服务的dubbo开始吧。我以前也写过一篇,不过之后就没有继续学习了。那篇《Dubbo》里,照着官方文档整理的速查,用的还是 2.7的版本。公司里面我看了dubbo有2.6版本com.alibaba.dubbo,也有2.7版本以后的org.apache.dubbo,包名都修改了,还有dubbo3的版本。这让我想到了Java web也改包名,Javax到Jakarta,因为 Java EE 从 Oracle 转移到了 Eclipse 基金会,并改名为 Jakarta EE,所以相应的包名都改动了,就像dubbo从Alibaba到Apache一样,所以里面还有不同的版本问题。
一、没有两个 Dubbo
我之前错误地以为这是两个项目,一个是阿里巴巴的,一个是Apache的,并不知道他们的关联。「该学阿里云的还是 Apache 的 dubbo」,这个问题不成立。
阿里云 MSE / EDAS 上跑的就是 Apache Dubbo。云厂商卖的是托管的 Nacos、治理控制台和可观测面板,不是另一个 Dubbo 发行版。阿里内部那套自研的 HSF 也已经在 3.0 里和 Dubbo 融合了。
所以学 Apache Dubbo 3.x 就够,不存在两份要学的东西。
1.1 Dubbo解决什么问题
让跨进程调用写起来像本地方法调用,并且把失败异常处理这一整套治理一起包了。
单体应用拆成多个服务之后,凭空多出四类问题,Dubbo 给的答案分别是:
| 拆开之后多出来的问题 | Dubbo 的答案 |
|---|---|
| 对方进程在哪台机器、哪个端口 | 注册中心 + 客户端进程内负载均衡,不经过中心节点转发 |
| 调不通、调得慢怎么办 | 超时、重试、熔断、降级、限流,配置即生效 |
| 想灰度、想按标签分流 | 内置条件路由 / 标签路由,Dubbo Admin 动态下发规则 |
| 接口契约谁来保证 | 契约就是 Java interface 本身,签名改了两边编译不过 |
最后一条是它和「自己写 HTTP 调用」体感差别最大的地方,后面第 4.2 节展开。
反过来说,Dubbo 不管的事情也要心里有数:它不管数据库、不管对外网关、不管容器编排。它只负责服务之间那一段。
注意
上手第一天就该改掉的默认值:Dubbo 默认容错策略是 failover,调用超时会自动重试两次。对查询没问题,对下单、扣款、发短信这类非幂等方法就是重复执行。非幂等的接口一律显式配 cluster="failfast",别指望下游都做好了幂等。
二、Dubbo的历史
| 时间 | 事件 | 今天还看得见的痕迹 |
|---|---|---|
| 2008 | 阿里内部诞生 | |
| 2011 | 开源,包名 com.alibaba.dubbo | 上古项目的 import |
| 2014 | 阿里停止维护,内部转投 HSF | 社区断档三年 |
| 2014~2017 | 当当网 fork 出 Dubbox | 有些老项目依赖 com.dangdang;rest 协议和 Kryo 序列化最早来自这里 |
| 2017 | 阿里重启维护 | |
| 2018 | 捐给 Apache 孵化 | 2.7.0 起包名改成 org.apache.dubbo |
| 2019 | 毕业为 Apache 顶级项目 | |
| 2021 | Dubbo 3.0,HSF 与 Dubbo 融合 | 应用级服务发现、Triple 协议 |
| 2023 | 3.2,跟上 Spring Boot 3 / JDK 17 | |
| 2024 起 | 3.3,Triple Rest、HTTP/3 |
这条线里最有用的是那个断档:2014 到 2017 年阿里不维护 Dubbo。当当网自己 fork 了一份继续加功能,也就是 Dubbox。今天在老项目里看到 rest 协议或者 Kryo 序列化,八成能追溯到那三年。
com.alibaba.dubbo 是 2.6 及以前,对应 2011 到 2018 年写的代码;org.apache.dubbo 是 2.7 以后。
这两个包名在协议报文层面是兼容的,2.6 的消费方和 2.7 的提供方能互相调通。但代码层面完全不兼容:import 要全改,注解也换了名字(@Reference → @DubboReference,@Service → @DubboService)。所以升级是一次纯体力活,风险不高但改动面大。
三、HTTP/1、2、3 的区别
要理解 Dubbo 3 为什么把主推协议换成了 HTTP/2,得先把这三个版本分清。
备注
HTTP/2 不是 HTTPS。HTTPS = HTTP + TLS,讨论的是「加不加密」;HTTP/1.1、2、3 是协议版本。HTTP/1.1 一样可以是 HTTPS。之所以容易混,是因为浏览器厂商规定只有加密连接才启用 HTTP/2,于是现实中你见到的 h2 全是 https。
| HTTP/1.1(1997) | HTTP/2(2015) | HTTP/3(2022) | |
|---|---|---|---|
| 报文形式 | 纯文本 | 二进制分帧 | 二进制分帧 |
| 底层 | TCP | TCP | QUIC over UDP |
| 并发 | 一条连接同时只跑一个请求 | 一条连接并发几十个流 | 同上,且流之间真正独立 |
| 队头阻塞 | 应用层和 TCP 层都有 | 只解决了应用层,TCP 层仍在 | 基本消除 |
| 头部 | 每次重复发送 | HPACK 压缩 | QPACK 压缩 |
1、HTTP/1.1 的一条 TCP 连接同一时刻只能跑一个请求,前一个不回来后面就得排队。浏览器的应对是对同一域名开 6 条连接硬扛——前端那些「雪碧图」「域名分片」的优化,全是为了绕这个限制。
2、HTTP/2 改成二进制分帧,一条连接上可以并发跑几十个请求互不干扰。但队头阻塞只解决了一半:应用层不排队了,TCP 层还排。丢一个包,TCP 要等重传,这条连接上所有请求一起卡住。弱网下它反而可能比 HTTP/1.1 更惨。
3、HTTP/3 把底层换成 QUIC。关键不是「跑在 UDP 上所以快」——UDP 只是个载体,真正的内容是 QUIC 在用户态重新实现了一套可靠传输:丢包只影响那一个流、握手和 TLS 合并成 1-RTT(重连甚至 0-RTT)、还有连接迁移(手机从 WiFi 切 4G,IP 变了连接不断)。
所以 HTTP/3 的优势主要在弱网和移动端。机房内网千兆环境下,它和 HTTP/2 差别不大。
回到 Dubbo:Triple 协议默认跑在 HTTP/2 上,这也是它能和 gRPC 互通的原因;3.3 版本加了 HTTP/3 支持,官方宣传的「弱网效率提升 6 倍」就是 QUIC 那套东西带来的。
四、传输协议与序列化
4.1 协议全表
Dubbo 支持的协议有十几种,但真正需要知道的只有四个,其余是博物馆藏品。
dubbo(2.x 默认,老服务基本都是它)
自定义 TCP 协议,16 字节固定头加变长 body。头里有个魔数 0xdabb,抓包看到这四个字节就知道是 Dubbo 流量。设计取向写在文档第一行:单一长连接 + NIO 异步 + 小数据量高并发。消费方和提供方之间默认只建 1 条 TCP 连接,所有请求靠 request id 复用它。
短板也在这:私有二进制协议,网关看不懂、跨语言要专门写 SDK、Istio / Envoy 这类 Sidecar 无法解析包内容;默认单请求 payload 上限 8MB,传大文件直接报错。
备注
Sidecar、Envoy、Istio 是什么
Envoy 是一个网络代理。它的部署方式是在每个业务容器旁边再挂一个代理容器,进出这个服务的所有流量都先经过它——这个「贴着主容器一起跑」的部署形态就叫 Sidecar(边车,摩托车旁边那个挎斗)。业务代码毫不知情,以为自己在直接收发请求。
Istio 是管这一群 Sidecar 的控制面:你在 Istio 里配「10% 流量走新版本」「这个调用超时 2 秒」「这两个服务之间要 mTLS」,它把规则下发给所有 Envoy 去执行。两者合起来就是 Service Mesh(服务网格)。
它和 Dubbo 其实在抢同一件事——服务治理,但路线相反:Dubbo 把治理做进框架(进程内),Mesh 把治理挪到进程外。Mesh 的好处是治理不进业务代码,不用引框架、不用改一行 Java,换成 Go 或 Python 写的服务照样受同一套规则管。
代价是 Sidecar 必须看得懂流量。它靠解析 HTTP 的方法、路径、header 来做路由、重试和统计;而一个私有二进制协议在它眼里只是一团不透明的 TCP 字节,只能原样转发,七层治理一样都做不了。这正是下面 4.2 节里 Dubbo 3 换到 HTTP/2 的动机之一。
tri(3.x 主推)
HTTP/2 承载,兼容 gRPC 的线格式。下一节专门讲。
injvm
同一个 JVM 内直接方法调用,不走网络也不序列化。单元测试、以及提供方消费方在同一个应用里时用它。如果发现「改了 provider 代码 consumer 立刻生效」,就是因为走了 injvm。
rest
2.6.x 从 Dubbox 合并进来,底层是 RESTEasy,用 JAX-RS 注解(@Path、@GET)。3.3 之后被 Triple Rest 取代。
其余的看到认得出即可:
| 协议 | 是什么 |
|---|---|
rmi | JDK RMI,Java 原生序列化,早已废弃 |
hessian | HTTP + Hessian,跑在 servlet 容器里 |
http | Spring HTTP Invoker |
webservice | SOAP / CXF |
thrift / grpc | 集成外部框架 |
memcached / redis | 把缓存包装成 Dubbo 服务,几乎没人用 |
4.2 Triple 能被 curl 调通,那它和一个 Controller 差在哪
Triple 服务可以直接用 curl 调:
curl --http2-prior-knowledge \
-H "Content-Type: application/json" \
-d '[1]' \
http://127.0.0.1:50051/com.example.UserService/getUser能 curl 通说明传输层上确实没有本质区别,Triple 就是个普通 HTTP 服务。注意那个路径:接口全限定名/方法名,不是你自己起的 /api/user/1。这已经暗示了核心区别——
Controller 是面向 URL 的,Dubbo 是面向接口的。
写 Controller,你要设计 path、决定 GET 还是 POST、写 @PathVariable 做参数绑定;调用方要用 RestTemplate / Feign 自己拼 URL、自己反序列化、自己保证字段名对得上。契约是「文档加口头约定」,写错了编译期发现不了。
用 Dubbo,调用方拿到 api jar 里的 interface,userService.getUser(1L) 直接调,IDE 有补全,改了签名两边编译不过。契约是 Java 类型本身。
其他几个实际差别:
| Controller | Dubbo Triple | |
|---|---|---|
| 怎么找到对方 | Nginx / K8s Service / 网关 | 客户端从注册中心拉地址,进程内负载均衡 |
| 重试、超时、熔断 | 自己写拦截器,或靠网关 | 配置一行 |
| 灰度、标签路由 | 网关层配 | 框架内置,Dubbo Admin 动态下发 |
| 序列化 | 基本只有 JSON | 可选 protobuf 二进制,性能高一截 |
| 流式 | 要另外上 SSE / WebSocket | 原生双向流 |
所以选型的直觉是:对外给浏览器和第三方用 Controller,服务之间互相调用 Dubbo。同一个应用里两者可以并存,Controller 收外部请求,内部再用 @DubboReference 调下游。
顺带说清 Dubbo 3 为什么要费劲换到 HTTP/2:老的私有协议,网关想转发就得做泛化调用、上面说过的 Envoy / Istio 那套 Sidecar 看不懂包内容、其他语言想接得专门写 SDK。换成 HTTP/2 之后,curl 能调、网关能直接转、Sidecar 也能按路径和 header 做治理,还顺带和 gRPC 生态互通。
4.3 那为什么还留着 rest / http 协议
Triple 既然已经是 HTTP 了,为什么协议列表里还有 rest 和 http?这里要分清三件事。
1、rest 和 http 是历史遗留。它们诞生在 Dubbo 2.x,那时只有私有的 dubbo 协议,想暴露 HTTP 接口就得靠 rest(RESTEasy 加 JAX-RS)或 http(Spring HTTP Invoker)。它们填的是「Triple 出现之前的空白」。
2、「HTTP 可达」不等于「RESTful」。你能 curl 通 Triple,但路径是 /接口全限定名/方法名,方法固定 POST,body 是参数数组。真正的 REST 要的是 GET /users/1、DELETE /users/1、query 参数、自定义 header、状态码语义。这些早期 Triple 给不了。
3、3.3 的 Triple Rest 才是答案。它直接在 Triple 协议上支持 Spring MVC 注解:
@DubboService
@RequestMapping("/users")
public class UserServiceImpl implements UserService {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) { ... }
}同一个服务、同一个端口,内部调用走 RPC 语义,外部 GET /users/1 就能访问。到这一步 rest 协议确实没有存在的必要了。
所以现在的选择逻辑:新项目只用 tri,需要对外就加 Triple Rest 注解;老项目里看到 protocol: rest 加 JAX-RS 注解,知道那是 2.x 的写法就行,能不动就别动。
(Triple Rest 只影响「别人怎么调你」,不影响「你怎么调别人」,@DubboReference 那套照常运作。)
4.4 序列化是另一个正交维度
这是最容易和协议搞混的地方:协议管「字节怎么在网络上排布」,序列化管「一个 Java 对象怎么变成字节」,两者可以自由组合。dubbo 协议默认配 hessian2,但也能换成别的。
| 序列化 | 说明 |
|---|---|
hessian2 | 默认,跨语言,体积中等 |
fastjson2 | 3.x 推荐,性能好,报文可读,排查方便 |
kryo / fst | Dubbox 时代引入,性能高但需要注册类,坑多 |
protobuf | Triple 加 IDL 场景下的最优解 |
java | JDK 原生,慢,且有一长串反序列化漏洞史,别用 |
Dubbo 3.1 起加了序列化白名单校验,于是新手常撞上这个报错:
Serialized class com.example.XxxDTO is not in allow list它出在序列化这一层,和你用什么协议无关。解法是把包名加进白名单,或者回头确认这个类是不是真该被传输。
五、注册中心:ZooKeeper 还是 Nacos
一句话概括:ZK 是通用协调服务,注册中心只是它的一种用法;Nacos 是专门为服务发现和配置管理造的。
| ZooKeeper | Nacos | |
|---|---|---|
| 一致性 | CP(ZAB 协议) | 默认 AP(临时实例,Distro 协议),也可切 CP |
| 注册原理 | 临时节点,session 断了自动删 | 心跳 / 长连接上报,超时标记不健康后剔除 |
| 变更推送 | Watcher,一次性,触发后要重新注册 | 2.x 起 gRPC 长连接推送 |
| 配置中心 | 没有,得自己往节点里塞数据 | 内置,带灰度、历史版本、回滚 |
| 控制台 | 没有,靠 zkCli / ZooInspector | 自带 Web UI |
| 运维 | 奇数节点、JVM 调优、内存敏感 | 起一个 Docker 就能用 |
5.1 关键差异:CP 还是 AP
这是最本质的一条。ZK 保证强一致,代价是 Leader 挂掉重新选举期间整个集群不可写,几十秒内没法注册新服务。
而服务发现这个场景其实不需要强一致:注册表里多一个已经死掉的节点、或者少一个刚上线的节点,都能靠客户端重试兜住;但注册中心整体不可用,影响面就大了。阿里当年那篇《阿里巴巴为什么不用 ZooKeeper 做服务发现》讲的就是这件事,核心论点是服务发现要的是可用性,不是一致性。
还有个实际问题是羊群效应:网络抖动导致一批 ZK session 超时,临时节点被批量删除,大量服务瞬间「消失」,Watcher 又把变更广播给所有订阅方,进一步加剧网络压力。Nacos 靠心跳容忍度和保护阈值缓解这一点——健康实例比例低于阈值时,把不健康的实例也一并返回给客户端,宁可让调用方去重试,也不返回一个空列表。
5.2 在 Dubbo 里的实际区别
配置上就一行的差别:
dubbo.registry.address: zookeeper://127.0.0.1:2181
dubbo.registry.address: nacos://127.0.0.1:8848但 Nacos 多了 namespace 加 group 两层隔离,dev / test / prod 可以放在同一个集群里互不干扰;ZK 只能靠路径前缀(dubbo.registry.group)手工区分。反过来,「服务明明起来了却 No provider available」在 Nacos 环境下第一个要查的就是 namespace 两边是否一致。
警告
用 Docker 起 Nacos 时,9848 端口必须一起映射,只开 8848 会连不上。原因就在上面那张表里:Nacos 2.x 把推送从 HTTP 长轮询换成了 gRPC 长连接,8848 是 HTTP 端口,9848 才是客户端 gRPC 端口。
5.3 两者可以并存
老项目大概率在 ZK(Dubbo 2.x 时代它是事实标准),新项目应该都在 Nacos。Dubbo 支持多注册中心,迁移期可以双注册:
dubbo:
registries:
zk:
address: zookeeper://127.0.0.1:2181
nacos:
address: nacos://127.0.0.1:8848提供方同时注册到两边,消费方按各自版本订阅其中一个,等老消费方全部迁完再把 ZK 摘掉。比一刀切安全得多。
六、gRPC:同一层的竞品,也是 Triple 的兼容对象
gRPC 是 Google 2015 年开源的 RPC 框架,和 Dubbo 是同一层的东西,都解决「跨进程调远程方法」。它的三个特征:
1、传输走 HTTP/2。这就是 Dubbo 3 的 Triple 能和它互通的原因——Triple 本质上是在 gRPC 的线格式(wire format)上做兼容和扩展。你可以用 gRPC 客户端直接调 Dubbo Triple 服务,反过来也行。
2、接口定义用 Protobuf IDL,这是它和 Dubbo 最大的体感差异。Dubbo 的契约是 Java interface,gRPC 的契约是一个 .proto 文件:
service UserService {
rpc GetUser (UserRequest) returns (UserReply);
}
message UserRequest {
int64 id = 1;
}然后用 protoc 生成各语言的 stub,Java、Go、Python、C++、Rust 都能生成。语言中立是它的核心卖点,而 Dubbo 的 api jar 方案天然只能给 JVM 用。
3、Protobuf 二进制序列化,比 JSON 小一半左右、快好几倍,代价是不可读,curl 看不了内容,得用 grpcurl。
它和 Dubbo 的分工大致是:
| gRPC | Dubbo | |
|---|---|---|
| 强项 | 跨语言、协议规范、云原生生态 | 服务治理:注册发现、路由、灰度、限流 |
| 服务发现 | 不管,自己接 etcd / K8s / Istio | 内置,Nacos / ZK 开箱即用 |
| 流量治理 | 靠 Istio 这类 Service Mesh 在外面做 | 框架内置,Dubbo Admin 下发规则 |
| 契约 | .proto 文件 | Java interface,也支持 .proto |
一句话:gRPC 是「通信协议加代码生成」,Dubbo 是「通信加全套治理」。所以在国内以 Java 为主的公司里 Dubbo 更常见(治理开箱即用),在多语言、云原生、已经上了 Istio 的环境里 gRPC 更常见(治理交给 Mesh)。
实际意义是:哪天公司有 Go 或 Python 服务要和 Java 服务互调,Dubbo 3 的 Triple 就是那座桥——Java 侧照常写 Dubbo,Go / Python 侧直接用 gRPC 客户端,不需要为它们找 Dubbo SDK。这正是 Dubbo 3 放弃私有协议的最大动机。
七、混合环境:2.x 和 3.x 怎么共存、怎么升
能互通吗? 能。Dubbo 3 明确保证兼容 2.x,2.x 的消费方可以调 3.x 的提供方。
关键是 3.x 提供方要保持 dubbo.application.register-mode=all(这也是默认值),同时按接口级和应用级双注册,老消费方才找得到——2.x 只认接口级注册。
注意
有人为了「优化注册表体积」把 register-mode 改成 instance,老服务会立刻集体 No provider available。等所有消费方都升到 3.x 之后再改这个值。
升级顺序,三步,别跳:
1、所有应用先升到 3.x,协议保持 dubbo 不变。这一步纯换依赖和改 import,行为一致,风险最低。
2、提供方开单端口双协议:ext-protocol: tri 加 preferred-protocol: tri,同一个 20880 端口同时说两种协议。
3、消费方逐个升级。3.3+ 的消费方会自动识别 preferred-protocol 切到 Triple,升不动的老消费方继续走 dubbo 协议,互不影响。
优先处理 2.6 及以前的服务。那些还在 com.alibaba.dubbo 包名下的,早就没有安全更新了,Hessian 反序列化的历史漏洞都在那条线上;2.7 也已经 EOL。
八、一页速查
排查时先按这张表对症状,大部分问题不用翻源码:
| 症状 | 大概率原因 |
|---|---|
No provider available | Nacos namespace / group 两边不一致;或 register-mode 被改成了 instance |
| Nacos 连不上,日志一直重连 | Docker 只映射了 8848,没映射 9848 |
Serialized class xxx is not in allow list | 序列化白名单,和协议无关 |
| 下单、扣款重复执行 | 默认 failover 超时自动重试,非幂等方法要改 failfast |
| 传大报文直接报错 | dubbo 协议单请求 payload 默认上限 8MB |
| 改了 provider 代码 consumer 立刻生效 | 走的是 injvm,根本没过网络 |
抓包看到 0xdabb 开头 | 那是 dubbo 私有协议的流量,不是 Triple |
这篇没展开的是 Dubbo 项目里的对象分层——DTO 该放 api jar、别把 Entity 直接暴露给消费方这类约定,单独写在了《Java 里的 DAO、DTO、POJO 都是什么》里。
评论
评论加载中……