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也改包名,JavaxJakarta,因为 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.dangdangrest 协议和 Kryo 序列化最早来自这里
2017阿里重启维护
2018捐给 Apache 孵化2.7.0 起包名改成 org.apache.dubbo
2019毕业为 Apache 顶级项目
2021Dubbo 3.0,HSF 与 Dubbo 融合应用级服务发现、Triple 协议
20233.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)
报文形式纯文本二进制分帧二进制分帧
底层TCPTCPQUIC 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 取代。

其余的看到认得出即可:

协议是什么
rmiJDK RMI,Java 原生序列化,早已废弃
hessianHTTP + Hessian,跑在 servlet 容器里
httpSpring HTTP Invoker
webserviceSOAP / CXF
thrift / grpc集成外部框架
memcached / redis把缓存包装成 Dubbo 服务,几乎没人用

4.2 Triple 能被 curl 调通,那它和一个 Controller 差在哪

Triple 服务可以直接用 curl 调:

bash
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 类型本身。

其他几个实际差别:

ControllerDubbo 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 了,为什么协议列表里还有 resthttp?这里要分清三件事。

1、resthttp 是历史遗留。它们诞生在 Dubbo 2.x,那时只有私有的 dubbo 协议,想暴露 HTTP 接口就得靠 rest(RESTEasy 加 JAX-RS)或 http(Spring HTTP Invoker)。它们填的是「Triple 出现之前的空白」。

2、「HTTP 可达」不等于「RESTful」。你能 curl 通 Triple,但路径是 /接口全限定名/方法名,方法固定 POST,body 是参数数组。真正的 REST 要的是 GET /users/1DELETE /users/1、query 参数、自定义 header、状态码语义。这些早期 Triple 给不了。

3、3.3 的 Triple Rest 才是答案。它直接在 Triple 协议上支持 Spring MVC 注解:

java
@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默认,跨语言,体积中等
fastjson23.x 推荐,性能好,报文可读,排查方便
kryo / fstDubbox 时代引入,性能高但需要注册类,坑多
protobufTriple 加 IDL 场景下的最优解
javaJDK 原生,慢,且有一长串反序列化漏洞史,别用

Dubbo 3.1 起加了序列化白名单校验,于是新手常撞上这个报错:

Serialized class com.example.XxxDTO is not in allow list

它出在序列化这一层,和你用什么协议无关。解法是把包名加进白名单,或者回头确认这个类是不是真该被传输。

五、注册中心:ZooKeeper 还是 Nacos

一句话概括:ZK 是通用协调服务,注册中心只是它的一种用法;Nacos 是专门为服务发现和配置管理造的。

ZooKeeperNacos
一致性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 里的实际区别

配置上就一行的差别:

yaml
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 支持多注册中心,迁移期可以双注册:

yaml
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 文件:

protobuf
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 的分工大致是:

gRPCDubbo
强项跨语言、协议规范、云原生生态服务治理:注册发现、路由、灰度、限流
服务发现不管,自己接 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: tripreferred-protocol: tri,同一个 20880 端口同时说两种协议。

3、消费方逐个升级。3.3+ 的消费方会自动识别 preferred-protocol 切到 Triple,升不动的老消费方继续走 dubbo 协议,互不影响。

优先处理 2.6 及以前的服务。那些还在 com.alibaba.dubbo 包名下的,早就没有安全更新了,Hessian 反序列化的历史漏洞都在那条线上;2.7 也已经 EOL。

八、一页速查

排查时先按这张表对症状,大部分问题不用翻源码:

症状大概率原因
No provider availableNacos 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 都是什么》里。

评论

评论加载中……

登录后再评论

注册要用邮箱收个验证码,只为确认邮箱能收信,不会拿去做别的。账号设置