概述
把 Dubbo 拆成 Provider、Consumer、Registry、Monitor 四个角色之后,真正要搞清楚的是一句话:Consumer 发起一次 userService.queryById(1L),中间到底经过了哪些组件。这篇文章先把官方经典架构图复原出来,再把这条调用链拆到"内部结构"这一层------从代理、集群容错一直到 Nacos 上的服务地址推送。
纲要
- 为什么需要这几个角色:远程调用绕不开的三个问题(地址获取、提供者变更、调用监控)
- 五个角色与职责:Container / Provider / Consumer / Registry / Monitor,以及讲义口径的服务暴露、发现、调用、监控编号流程
- 一次调用的完整链路:从 Container 启动 Provider 到 Monitor 统计,逐步拆解
- 调用链的内部分层(本篇核心):Proxy → Cluster → Directory → Router → LoadBalance → Protocol → 网络,以及 Provider 侧的反向链路
- SPI 扩展机制:为什么换注册中心、换协议只是一行配置的事
- 工程结构实读 :
dubbo-demo的真实模块划分与各模块在架构中的位置 - 常见误解澄清:Monitor 是否必需、Consumer 要不要直连 Provider、注册中心挂了会怎样
- 把项目跑起来缺什么:Nacos 与 dubbo-admin
- 配置速览 :Provider 侧与 Consumer 侧的
dubbo.*关键项
为什么 Dubbo 要拆成这几个角色
Dubbo 干的事是远程调用加服务治理。远程调用一旦脱离单机,就绕不开三个问题:
- Consumer 怎么拿到 Provider 的地址。写死在配置里不行,提供者一扩容就要改代码重启。
- Provider 发生变更时怎么快速切换 。提供者从
20881换到20882,甚至下线一台,消费者列表必须跟着动。 - 调用过程怎么监控。调用次数、耗时、有没有慢调用,得有个地方统计,不然线上出问题只能靠猜。
前两个问题,学过 Spring Cloud 的人会立刻想到注册中心------没错,Dubbo 也是这条路。第三个问题才是 Dubbo 长期被忽略的一块:它在框架内部就预留了监控中心,把调用数据收集起来,这也是早期 Dubbo 相对"纯 HTTP 调用"的一个治理优势。
五个角色与职责
先看官方经典架构图。用 Mermaid 复原一遍,节点名保持 Dubbo 原生叫法,方便对照日志和源码里的类名。
#mermaid-svg-AHlUV5dfuts1m3Sp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AHlUV5dfuts1m3Sp .error-icon{fill:#552222;}#mermaid-svg-AHlUV5dfuts1m3Sp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AHlUV5dfuts1m3Sp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AHlUV5dfuts1m3Sp .marker.cross{stroke:#333333;}#mermaid-svg-AHlUV5dfuts1m3Sp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AHlUV5dfuts1m3Sp p{margin:0;}#mermaid-svg-AHlUV5dfuts1m3Sp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AHlUV5dfuts1m3Sp .cluster-label text{fill:#333;}#mermaid-svg-AHlUV5dfuts1m3Sp .cluster-label span{color:#333;}#mermaid-svg-AHlUV5dfuts1m3Sp .cluster-label span p{background-color:transparent;}#mermaid-svg-AHlUV5dfuts1m3Sp .label text,#mermaid-svg-AHlUV5dfuts1m3Sp span{fill:#333;color:#333;}#mermaid-svg-AHlUV5dfuts1m3Sp .node rect,#mermaid-svg-AHlUV5dfuts1m3Sp .node circle,#mermaid-svg-AHlUV5dfuts1m3Sp .node ellipse,#mermaid-svg-AHlUV5dfuts1m3Sp .node polygon,#mermaid-svg-AHlUV5dfuts1m3Sp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AHlUV5dfuts1m3Sp .rough-node .label text,#mermaid-svg-AHlUV5dfuts1m3Sp .node .label text,#mermaid-svg-AHlUV5dfuts1m3Sp .image-shape .label,#mermaid-svg-AHlUV5dfuts1m3Sp .icon-shape .label{text-anchor:middle;}#mermaid-svg-AHlUV5dfuts1m3Sp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AHlUV5dfuts1m3Sp .rough-node .label,#mermaid-svg-AHlUV5dfuts1m3Sp .node .label,#mermaid-svg-AHlUV5dfuts1m3Sp .image-shape .label,#mermaid-svg-AHlUV5dfuts1m3Sp .icon-shape .label{text-align:center;}#mermaid-svg-AHlUV5dfuts1m3Sp .node.clickable{cursor:pointer;}#mermaid-svg-AHlUV5dfuts1m3Sp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AHlUV5dfuts1m3Sp .arrowheadPath{fill:#333333;}#mermaid-svg-AHlUV5dfuts1m3Sp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AHlUV5dfuts1m3Sp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AHlUV5dfuts1m3Sp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AHlUV5dfuts1m3Sp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AHlUV5dfuts1m3Sp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AHlUV5dfuts1m3Sp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AHlUV5dfuts1m3Sp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AHlUV5dfuts1m3Sp .cluster text{fill:#333;}#mermaid-svg-AHlUV5dfuts1m3Sp .cluster span{color:#333;}#mermaid-svg-AHlUV5dfuts1m3Sp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AHlUV5dfuts1m3Sp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AHlUV5dfuts1m3Sp rect.text{fill:none;stroke-width:0;}#mermaid-svg-AHlUV5dfuts1m3Sp .icon-shape,#mermaid-svg-AHlUV5dfuts1m3Sp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AHlUV5dfuts1m3Sp .icon-shape p,#mermaid-svg-AHlUV5dfuts1m3Sp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AHlUV5dfuts1m3Sp .icon-shape .label rect,#mermaid-svg-AHlUV5dfuts1m3Sp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AHlUV5dfuts1m3Sp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AHlUV5dfuts1m3Sp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AHlUV5dfuts1m3Sp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Container 服务运行容器
1 暴露/注册 自身地址
2 订阅 所需服务
3 通知 提供者地址列表
4 发起 RPC 调用
5 上报调用次数与耗时
5 上报调用次数与耗时
Provider 服务提供者
Registry 注册中心
Consumer 服务消费者
Monitor 监控中心
角色职责:
| 节点 | 角色说明 | 对应到业务代码 |
|---|---|---|
| Container | 服务的运行容器,负责启动、加载、运行服务提供者 | Spring Boot 引导类 UserProviderApplication + 内嵌 Tomcat |
| Provider | 暴露服务的服务提供方 | user-provider 模块,@DubboService 修饰的 UserServiceImpl |
| Consumer | 调用远程服务的服务消费方 | user-consumer 模块,@DubboReference 注入的 UserService |
| Registry | 服务注册与发现的注册中心 | Nacos(nacos://127.0.0.1:8848) |
| Monitor | 统计服务调用次数和调用时间的监控中心 | dubbo-admin 控制台 |
关于 Container 有个细节要说明:讲义 2.1 的节点表其实只列了 Provider、Consumer、Registry、Monitor 四个角色,Container 是在调用关系第 1 条里出现的------"服务容器负责启动,加载,运行服务提供者"。官方经典图把它单独画成一个节点,是因为 Provider 本身也是一段要跑起来的 Java 进程,谁来拉起它、谁来管它的生命周期,需要有个明确的角色。在 Spring Boot 里这个角色就是引导类加内嵌容器。
讲义的调用关系说明原文如下,编号我原样保留:
- 服务容器负责启动、加载、运行服务提供者。
- 服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
第 6 条那句"定时每分钟发送一次"是关键信息------监控数据不是每次调用同步上报的,而是在内存里攒着,按分钟批量发。这意味着 Monitor 短暂不可用不会阻塞业务调用,追这一层的时候不用怀疑是不是监控把请求卡住了。
一次调用的完整链路
把上面六条步骤摊到时间轴上看,就得到这篇最该记住的一张图。以"用用户 id 查询用户"为例。
Monitor Consumer Registry(Nacos) Provider Container Monitor Consumer Registry(Nacos) Provider Container #mermaid-svg-fNwLSIjqe7CIyW72{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fNwLSIjqe7CIyW72 .error-icon{fill:#552222;}#mermaid-svg-fNwLSIjqe7CIyW72 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fNwLSIjqe7CIyW72 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fNwLSIjqe7CIyW72 .marker.cross{stroke:#333333;}#mermaid-svg-fNwLSIjqe7CIyW72 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fNwLSIjqe7CIyW72 p{margin:0;}#mermaid-svg-fNwLSIjqe7CIyW72 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fNwLSIjqe7CIyW72 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-fNwLSIjqe7CIyW72 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fNwLSIjqe7CIyW72 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-fNwLSIjqe7CIyW72 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-fNwLSIjqe7CIyW72 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-fNwLSIjqe7CIyW72 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-fNwLSIjqe7CIyW72 .sequenceNumber{fill:white;}#mermaid-svg-fNwLSIjqe7CIyW72 #sequencenumber{fill:#333;}#mermaid-svg-fNwLSIjqe7CIyW72 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-fNwLSIjqe7CIyW72 .messageText{fill:#333;stroke:none;}#mermaid-svg-fNwLSIjqe7CIyW72 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fNwLSIjqe7CIyW72 .labelText,#mermaid-svg-fNwLSIjqe7CIyW72 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-fNwLSIjqe7CIyW72 .loopText,#mermaid-svg-fNwLSIjqe7CIyW72 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-fNwLSIjqe7CIyW72 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fNwLSIjqe7CIyW72 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-fNwLSIjqe7CIyW72 .noteText,#mermaid-svg-fNwLSIjqe7CIyW72 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-fNwLSIjqe7CIyW72 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fNwLSIjqe7CIyW72 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fNwLSIjqe7CIyW72 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fNwLSIjqe7CIyW72 .actorPopupMenu{position:absolute;}#mermaid-svg-fNwLSIjqe7CIyW72 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-fNwLSIjqe7CIyW72 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fNwLSIjqe7CIyW72 .actor-man circle,#mermaid-svg-fNwLSIjqe7CIyW72 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-fNwLSIjqe7CIyW72 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 内存中记录 provider 地址列表 提供者变更时,注册中心基于长连接主动推送 ① 启动、加载、运行 1 ① 注册自身地址 ip:20881 + 服务名 2 ② 启动消费者 3 ② 订阅 cn.mf.user.service.UserService 4 ③ 返回可用 Provider 地址列表 5 ④ 从列表中按负载均衡策略选一台 6 ④ RPC 调用 queryById(1L) 7 ④ 返回结果(序列化后回传) 8 ⑤ 每分钟定时上报调用次数/耗时 9 ⑤ 每分钟定时上报调用次数/耗时 10
逐步看关键动作:
① 启动与注册。 Container 拉起 Provider 进程。Provider 启动完成、端口监听就绪后,把"我提供 UserService,地址是 192.168.x.x:20881"这条信息写到注册中心。注意这里注册的是 Dubbo 协议端口 20881 ,不是 Web 端口 18080------两者各管一摊,一个是 HTTP 接口给浏览器调的,一个是 RPC 给 Consumer 调的。讲义里提到端口从 20881 变成 20882 的场景,说的就是 RPC 端口变化后消费者怎么跟上。
② 订阅。 Consumer 启动时,去注册中心订阅自己需要的服务。订阅的粒度是接口全限定名 + 版本号 ,@DubboReference(version = "2.0.0") 里的 version 就是订阅条件的一部分。
③ 通知。 注册中心把匹配的 Provider 地址列表返回给 Consumer。之后如果有变更------新增实例、下线实例、端口改变------注册中心会基于长连接主动推送,不需要 Consumer 轮询。这条长连接是"注册中心挂掉后已连接双方还能继续调用"的技术根源。
④ 调用。 Consumer 从地址列表里按负载均衡策略挑一台,发起 RPC 调用。讲义给的口径是"基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用"------后半句就是 Dubbo 的集群容错,默认策略 Failover,失败重试到别的节点。
⑤ 监控。 双方各自在内存累计调用次数和耗时,每分钟发一次给监控中心。这一步是可选的:Monitor 配置留空就不上报;生产上更常见的做法是干脆关掉 Dubbo 自带的统计,用 SkyWalking、Prometheus 这类 APM 顶替。所以排查问题发现 Monitor 里没数据,先确认是不是根本没配。
调用链的内部分层
上面的时序图回答的是"哪些角色参与了",但"内部结构"真正指的是下一层:一次 invoke 在框架内部依次穿过了哪些组件。
讲义本节(2.1)只讲了角色级流程,没有展开 Invoker、Cluster 这一层。以下内容按 Dubbo 官方文档的调用链口径补充,用于把"内部结构"这个名字落到实处。
Consumer 侧发起一次调用,方法调用会被一层层包装、过滤、再选路,最终落到网络:
#mermaid-svg-EGzTg95umrHNKfm1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EGzTg95umrHNKfm1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EGzTg95umrHNKfm1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EGzTg95umrHNKfm1 .error-icon{fill:#552222;}#mermaid-svg-EGzTg95umrHNKfm1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EGzTg95umrHNKfm1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EGzTg95umrHNKfm1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EGzTg95umrHNKfm1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EGzTg95umrHNKfm1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EGzTg95umrHNKfm1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EGzTg95umrHNKfm1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EGzTg95umrHNKfm1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EGzTg95umrHNKfm1 .marker.cross{stroke:#333333;}#mermaid-svg-EGzTg95umrHNKfm1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EGzTg95umrHNKfm1 p{margin:0;}#mermaid-svg-EGzTg95umrHNKfm1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EGzTg95umrHNKfm1 .cluster-label text{fill:#333;}#mermaid-svg-EGzTg95umrHNKfm1 .cluster-label span{color:#333;}#mermaid-svg-EGzTg95umrHNKfm1 .cluster-label span p{background-color:transparent;}#mermaid-svg-EGzTg95umrHNKfm1 .label text,#mermaid-svg-EGzTg95umrHNKfm1 span{fill:#333;color:#333;}#mermaid-svg-EGzTg95umrHNKfm1 .node rect,#mermaid-svg-EGzTg95umrHNKfm1 .node circle,#mermaid-svg-EGzTg95umrHNKfm1 .node ellipse,#mermaid-svg-EGzTg95umrHNKfm1 .node polygon,#mermaid-svg-EGzTg95umrHNKfm1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EGzTg95umrHNKfm1 .rough-node .label text,#mermaid-svg-EGzTg95umrHNKfm1 .node .label text,#mermaid-svg-EGzTg95umrHNKfm1 .image-shape .label,#mermaid-svg-EGzTg95umrHNKfm1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-EGzTg95umrHNKfm1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EGzTg95umrHNKfm1 .rough-node .label,#mermaid-svg-EGzTg95umrHNKfm1 .node .label,#mermaid-svg-EGzTg95umrHNKfm1 .image-shape .label,#mermaid-svg-EGzTg95umrHNKfm1 .icon-shape .label{text-align:center;}#mermaid-svg-EGzTg95umrHNKfm1 .node.clickable{cursor:pointer;}#mermaid-svg-EGzTg95umrHNKfm1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EGzTg95umrHNKfm1 .arrowheadPath{fill:#333333;}#mermaid-svg-EGzTg95umrHNKfm1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EGzTg95umrHNKfm1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EGzTg95umrHNKfm1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EGzTg95umrHNKfm1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EGzTg95umrHNKfm1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EGzTg95umrHNKfm1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EGzTg95umrHNKfm1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EGzTg95umrHNKfm1 .cluster text{fill:#333;}#mermaid-svg-EGzTg95umrHNKfm1 .cluster span{color:#333;}#mermaid-svg-EGzTg95umrHNKfm1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EGzTg95umrHNKfm1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EGzTg95umrHNKfm1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-EGzTg95umrHNKfm1 .icon-shape,#mermaid-svg-EGzTg95umrHNKfm1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EGzTg95umrHNKfm1 .icon-shape p,#mermaid-svg-EGzTg95umrHNKfm1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EGzTg95umrHNKfm1 .icon-shape .label rect,#mermaid-svg-EGzTg95umrHNKfm1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EGzTg95umrHNKfm1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EGzTg95umrHNKfm1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EGzTg95umrHNKfm1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务代码调用 userService.queryById(1L)
Proxy 代理层
把接口调用转成 Invocation 对象
Cluster 集群容错
失败重试 / 快速失败 / 并行等
Directory 服务目录
持有可用的 Invoker 列表
Router 路由
标签路由 / 条件路由过滤
LoadBalance 负载均衡
Random / RoundRobin / LeastActive / ConsistentHash
Protocol 协议 + 序列化
dubbo 协议封装请求体
网络传输
Netty 长连接发送到 Provider
Provider 侧协议解码
Filter 过滤器链
限流 / 超时 / 异常处理
Invoker 调用真实实现
UserServiceImpl.queryById()
几处容易卡住的地方:
- Proxy 和真实接口的关系。 你在 Consumer 里注入的
UserService不是user-consumer里写的实现类------这个模块根本没有实现类。它是一个动态代理对象,调用它其实是在往框架里塞一个Invocation。 - Directory 与 Router 的顺序。 Directory 给出的是"能用的全部 Invoker",Router 是在这批里做过滤(比如按标签、按条件表达式筛掉一部分),LoadBalance 再从筛完的结果里挑一个。顺序搞反的话,标签路由会失效。
- Provider 侧并不是只执行方法。 请求进来同样要过 Filter 链,超时、限流、异常兜底都在这一层做,然后才由 Invoker 反射调用到
UserServiceImpl。
SPI:为什么 Dubbo 的组件这么容易换
上面每一层------Protocol、Registry、Cluster、LoadBalance、Filter------在 Dubbo 里都不是写死的类,而是通过 SPI(Service Provider Interface) 加载的可替换实现。
同样,SPI 这段讲义未展开,属于官方机制说明。
带来的实际效果很直接,这也是后面几篇"高级特性""Spring Cloud 整合 Dubbo"能成立的前提:
| 想换的东西 | 只改配置 | 底层发生了啥 |
|---|---|---|
| 注册中心 Zookeeper → Nacos | dubbo.registry.address |
换掉 RegistryFactory 的 SPI 实现 |
| 协议 dubbo → rest / triple | dubbo.protocol.name |
换掉 Protocol 的 SPI 实现 |
| 容错策略 Failover → Failfast | cluster 属性 |
换掉 Cluster 的 SPI 实现 |
| 负载均衡 Random → RoundRobin | loadbalance 属性 |
换掉 LoadBalance 的 SPI 实现 |
Dubbo 的 SPI 比 JDK 原生的 SPI 多了两点:按需加载 和 IOC/AOP 支持 (实现类可以注入别的扩展,也能被 Wrapper 包装成过滤器链)。所以 Filter 链能自动串起来,靠的就是这个机制。
工程结构实读
素材里的 dubbo-demo 就是把这套架构落地的最小工程。真实模块树如下(已忽略 .idea 与 target 目录):
tree
dubbo-demo
├── pom.xml 父 POM,packaging=pom,统一版本管理
├── user-api 契约模块
│ ├── pom.xml
│ └── src/main/java/cn/mf/user/
│ ├── domain/User.java 实体(实现 Serializable)
│ └── service/UserService.java 接口定义
├── user-provider 服务提供者(对应架构图 Provider)
│ ├── pom.xml
│ └── src/main/
│ ├── java/cn/mf/user/
│ │ ├── UserProviderApplication.java 引导类(Container 侧入口)
│ │ ├── mapper/UserMapper.java
│ │ └── service/impl/
│ │ ├── UserServiceImpl.java version = 1.0.0
│ │ └── UserServiceImpl2.java version = 2.0.0
│ └── resources/application.yml protocol / registry / scan
└── user-consumer 服务消费者(对应架构图 Consumer)
├── pom.xml
└── src/main/
├── java/cn/mf/user/
│ ├── UserConsumerApplication.java 引导类
│ └── controller/UserController.java @DubboReference 注入接口
└── resources/application.yml registry / consumer 配置
三个模块在架构里的位置:
user-api= 契约。 只放接口和领域对象,没有任何实现,Provider 和 Consumer 都依赖它。这一步很关键------Dubbo 的远程调用是"面向接口"的,双方必须拿到同一份接口定义才能通信,而接口不能各写一份,否则序列化后的类名、方法签名对不上。user-provider= 提供者。 有UserMapper、数据库连接、真实实现类,对外暴露服务。user-consumer= 消费者。 只有 Controller,没有实现类,UserService通过@DubboReference注入的是代理对象。
两个实读发现,看代码时会碰到:
- 父 POM 的
<modules>里有一个user-server,但目录里并不存在这个模块。 这是课程演进过程留下的残留条目,构建时mvn会报找不到模块。对着素材抄目录树的时候,别把这个模块也一起建出来------真正存在的是user-api、user-provider、user-consumer三个。 - 这个快照处于"多版本"演示状态。 Provider 侧有两个实现,
UserServiceImpl是version = "1.0.0"、UserServiceImpl2是"2.0.0";而 Consumer 的@DubboReference(version = "2.0.0", retries = 0, loadbalance = "Random")指向的是 2.0.0。版本号是订阅条件的一部分,对不上会出现"服务找不到"------这类问题十有八九出在版本或分组上,而不是网络。
契约模块与接口定义长这样,可以直接整段复制:
java
package cn.mf.user.domain;
import lombok.Data;
import java.io.Serializable;
@Data
public class User implements Serializable {
private Long id;
private String username;
private String address;
}
java
package cn.mf.user.service;
import cn.mf.user.domain.User;
public interface UserService {
String queryUsername(Long id);
User queryById(Long id);
}
Provider 侧暴露服务,注解里的 version 就是注册到 Nacos 的版本标签:
java
package cn.mf.user.service.impl;
import cn.mf.user.domain.User;
import cn.mf.user.mapper.UserMapper;
import cn.mf.user.service.UserService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
// 暴露 Dubbo 服务,version 决定消费端按哪个版本订阅
@DubboService(version = "1.0.0")
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public String queryUsername(Long id) {
return userMapper.findById(id).getUsername();
}
@Override
public User queryById(Long id) {
User user = userMapper.findById(id);
user.setUsername(user.getUsername() + "v1.0");
return user;
}
}
Provider 的配置,三个块各管一件事:协议、注册中心、注解扫描包:
yaml
server:
port: 18080
spring:
datasource:
url: jdbc:mysql://localhost:3306/dubbo-demo?useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
application:
name: user-provider
# 配置 Dubbo 提供者
dubbo:
# 使用 dubbo 协议,RPC 端口 20881(不是 Web 端口)
protocol:
name: dubbo
port: 20881
# 注册中心地址
registry:
address: nacos://127.0.0.1:8848
# Dubbo 注解的包扫描(@DubboService 所在包)
scan:
base-packages: cn.mf.user.service
Consumer 侧引用远程服务,这里的 UserService 是代理:
java
package cn.mf.user.controller;
import cn.mf.user.domain.User;
import cn.mf.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
// 引用远程服务,实际注入的是动态代理
@DubboReference(version = "2.0.0", retries = 0, loadbalance = "Random")
private UserService userService;
@GetMapping("/usernames/{id}")
public String queryUsername(@PathVariable("id") Long id) {
return userService.queryUsername(id);
}
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}
}
消费端配置里那两个开关,正好对应前面讲的已知坑:
yaml
server:
port: 18081
spring:
application:
name: user-consumer
dubbo:
registry:
address: nacos://127.0.0.1:8848
consumer:
check: false # 关闭启动检查:本地没有 Provider 时也能启动
retries: 0 # 关闭重试:写操作重复提交会被放大
端口口径说明一句:视频里口述的是 Web 端口 8080 / 8081,讲义 2.4 给的也是 18081 一带,而实读工程里 Provider 的 Web 端口是 18080、Consumer 是 18081、Dubbo 协议端口是 20881。以工程实际配置为准就不容易踩混。
常见误解澄清
排查 Dubbo 问题时,下面几个误解最容易把人带偏:
| 常见说法 | 实际情况 |
|---|---|
| Monitor 是必需组件,没它服务起不来 | Monitor 是可选上报,不配就不上报,不影响调用链路 |
| Consumer 必须知道 Provider 的 IP,属于直连 | Consumer 只认注册中心订阅到的地址列表,Provider 换 IP 由注册中心推送 |
| 注册中心就是个消息队列 | 它是服务地址的登记与推送中心,推送的是地址变更,不做业务消息缓冲 |
| 注册中心一挂,整个系统就废了 | 已建立连接的双方可继续调用,只是无法发现新实例、无法感知下线,属于"降级可用" |
第四条要说准:注册中心不可用时,Consumer 本地内存里已经缓存了 Provider 地址列表,长连接也还在,业务调用照走。真正失效的是服务发现能力------新上线的实例没人告诉 Consumer,下线的实例 Consumer 还可能打过去(要靠失败重试兜底)。这个结论跟 Spring Cloud 里 Eureka / Nacos 挂掉时的表现是一致的,可以放在一起记。
把项目跑起来缺什么
dubbo-demo 里我们能写的只有 Provider 和 Consumer 两部分 Java 代码。图上的另外两个角色不是代码,是要提前装好的外部组件:
- 注册中心 Nacos :
startup.cmd -m standalone单机启动,控制台在http://127.0.0.1:8848/nacos,默认账号密码都是nacos。没有它,Provider 注册不上去,Consumer 也订阅不到,日志里会是连接超时。 - 监控中心 dubbo-admin:Dubbo 官方提供的控制台,能做服务查询、详情展示、服务测试。它是可选组件,本地不装也能完成调用,但排查"服务到底注册上去没有"时会方便很多。
这两样装好之后,再依次启动 UserProviderApplication 和 UserConsumerApplication,访问 http://localhost:18081/user/1,链路才算真正跑通。下一篇就按这个顺序,先把 Nacos 和监控中心装起来。
配置速览
| 配置项 | 位置 | 作用 |
|---|---|---|
dubbo.protocol.name |
Provider | 协议名,dubbo 为默认二进制协议 |
dubbo.protocol.port |
Provider | RPC 服务端口,示例为 20881 |
dubbo.registry.address |
双方 | 注册中心地址,示例 nacos://127.0.0.1:8848 |
dubbo.scan.base-packages |
Provider | 扫描 @DubboService 的包 |
dubbo.consumer.check |
Consumer | 启动检查,false 表示没有提供者也能启动 |
dubbo.consumer.retries |
Consumer | 失败重试次数,写操作建议 0 |
@DubboService(version) |
Provider | 服务版本,注册到注册中心的标签之一 |
@DubboReference(version/loadbalance) |
Consumer | 订阅版本与负载均衡策略 |
官方文档
总结
- Dubbo 的四个角色里,Registry 解决"地址从哪来 + 变更怎么通知",Monitor 解决"调用怎么看",Container 解决"谁来拉起来"。五个角色的分工是绕开远程调用三个基本问题的最小解。
- 讲义的六步流程是角色级视角;真正落到"内部结构",一次调用在 Consumer 侧要依次穿过 Proxy → Cluster → Directory → Router → LoadBalance → Protocol,在 Provider 侧反向穿过 Filter → Protocol → Invoker。Invoker 这一层才是 Dubbo 的统一抽象。
- Monitor 是可选的,生产上更常见的是用 APM 顶替;
retries = 0是写操作的基本自觉。 - 注册中心挂掉不等于系统挂掉------这一段可以跟 Spring Cloud 篇对照看,两个框架的结论一样,只是表现和超时时间不同。
- 要把链路跑通,Provider 和 Consumer 的代码只是一半,另一半是 Nacos 与 dubbo-admin。