Dubbo

yin_bo_ Lv2

1. 分布式系统相关概念

  • 项目架构目标

  • 衡量网站的性能指标

    • 响应时间:指执行一个请求从开始到最后收到响应数据所花费的总体时间
    • 并发数:指系统同时能处理的请求数量
      • 并发连接数:指的是客户端向服务器发起请求,并建立了TCP连接,每秒钟服务器连接的总TCP数量
      • 并发用户数单位时间内有多少用户
    • 吞吐量:指系统单位时间内能处理的请求数量。
      • QPS每秒查询数
      • TPS:每秒事务数。
        • 一个事务是指一个客户机向服务器发送请求,然后服务器做出反应的进程
  • 分布式架构是指在垂直架构的基础上,将公共业务模块抽取出来,作为独立的服务,供其他调用者消费,以实现服务的共享和重用。

  • RPC(Romote Procedure Call):既远程过程调用,是一种通信规范思想

    • 核心思想:像调用本地方法一样,去调用远程服务器上的方法
    • RPC框架基于通讯协议去实现RPC远程调用,如gRPC是基于HTTP/2,而HTTP/2基于TCP。
  • 通讯协议,通讯规范,编程接口的区别:

    1. 像TCP/UDP,HTTP都是通讯协议,是通话规则
    2. Socket是编程接口,类比于电话机,程序都是通过Socket使用通讯协议来进行通信的
    3. RPC是一种通讯规范思想,Dubbo,gRPC都是遵循这个规范的框架。

2. Dubbo概述

  • Dubbo是开源的一个高性能,轻量级Java RPC框架,除此之外他的定位也是一个微服务开发框架,类似于SpringCloud,也有服务发现,负载均衡,动态配置等功能。

    • 相对的gRPC是一个跨语言RPC框架,核心目标是定义RPC通讯规范和标准实现
  • SpringCloudDubbo的区别

    • 他们都有微服务的功能,而Dubbo则更侧重于RPC通信
    • SpringCloud通常的远程调用组件使用OpenFeign等,都是基于HTTP通讯协议,使用的一般是JSON序列化。
    • 而Dubbo默认基于TCP,使用二进制序列化,支持长连接连接复用等,性能通常比SpringCloud更高。
  • 在RPC方面,不同于gRPC只能基于HTTP/2协议,Dubbo服务间可通过多种RPC通讯协议并支持灵活切换。

    • 因此,Dubbo开发中也支持gRPC通信,在Dubbo3中完全兼容gRPC。

3. 简述Zookeeper

  • 之后会系统性的学习Zookeeper,这里只是用于辅助学习Dubbo

  • Zookeeper可以用来注册/配置中心以及服务发现,就像SpringCloud中的Nacos组件,还可以像Redisson一样有分布式锁的功能。

  • 这里引用Zookeeper主要是用来当作注册/配置中心。

  • 这里我使用docker来安装zookeeper

1
docker run -d --name zk -p 2181:2181 zookeeper:3.8
  • 另外可以安装一下Zookeeper的可视化界面ZooNavigator
    • 这里直接用docker-compose来部署
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
services:

zookeeper:
image: bitnami/zookeeper:3.9
restart: unless-stopped
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
ports:
- "2181:2181"

dubbo-admin:
image: apache/dubbo-admin
restart: unless-stopped
depends_on:
- zookeeper
environment:
- ADMIN_REGISTRY_ADDRESS=zookeeper://zookeeper:2181
ports:
- "38080:38080"

provider:
image: apache/dubbo-demo-provider
restart: unless-stopped
depends_on:
- zookeeper
environment:
- DUBBO_REGISTRY_ADDRESS=zookeeper://zookeeper:2181

consumer:
image: apache/dubbo-demo-consumer
restart: unless-stopped
depends_on:
- zookeeper
- provider
environment:
- DUBBO_REGISTRY_ADDRESS=zookeeper://zookeeper:2181

4. Dubbo入门

  • 先使用一个demo来快速了解Dubbo
    快速创建Dubbo demo网站:https://start.dubbo.apache.org/bootstrap.html

  • 这里我们选择需要的组件

    • Dubbo组件:
      1. Java Interface
      2. 注册中心:Zookeeper
      3. 网络协议: TCP
    • 常用微服务组件:
      1. SpringWeb
      2. Mybatis

alt DubboDemo
alt DubboDemo

  • 基于以上选项,生成的项目将以Zookeeper为注册中心,以高性能Dubbo2 TCP协议为RPC通讯协议,并增加了WebMybatis等组件依赖和示例。

5. Dubbo功能

  • Dubbo有以下的功能
    • 远程调用
    • 服务发现
    • 负载均衡
    • 流量管控等功能。

5.1 远程调用

  • 这里我们Dubbo默认使用基于 TCP 的 Dubbo 协议(长连接二进制协议),走RPC,配置的注册中心是Zookeeper

    • 下面是Dubbo远程调用OpenFeign的差别
      alt DubboDemo
      alt DubboDemo
  • 微服务架构中,采用结构与实现分离模式。

1
2
3
4
project
├── demo-api (只存接口)
├── demo-provider (实现层)
└── demo-consumer (调用方)
  • 完整调用流程如下
    1. Provider 启动
    2. Provider 注册服务到 ZooKeeper
    3. Consumer 启动
    4. Consumer 订阅服务
    5. Dubbo 创建远程代理对象
    6. 调用方法
    7. 通过 TCP 长连接发送请求
    8. Provider 执行方法并返回结果

5.1.1 定义服务

  • Provider定义服务,并实现业务逻辑

    • 实现业务逻辑包括服务层实现层
    • 将服务接口存入api模块,将实现层存入service模块
  • @DubboService注解

    • 注意,如果要将服务暴露为Dubbo服务 需要加@DubboService注解
    • 启动时将该服务注册到注册中心Zookeeper
1
2
3
4
5
 public interface DemoService {

String sayHello(String name);

}
1
2
3
4
5
6
7
8
@DubboService  //这里加入DubboService注解 将服务推入Dubbo
public class DemoServiceImpl implements DemoService {

@Override
public String sayHello(String name) {
return "Hello " + name;
}
}

5.1.2 发现服务

  • 为了使Comsumer调用服务,一般我们要将Provider打jar包并引入。

    • 这里因为我们这里是Demo,所以直接import我们的服务定义模块即可
  • @DubboReference注解

    • Consumer会通过该注解从注册中心查找服务
    • 通过RPC调用Provider
1
2
3
4
public class Consumer {
@DubboReference
private DemoService demoService;
}

5.2 服务发现

  • Dubbo提供的是一种Client-Based的服务发现机制,依赖第三方注册中心组件来协调服务发现,比如Zookeeper,Nacos等等。

  • Client-Based服务发现机制

    • 由客户端自己负责从注册中心获取服务地址,并自己做负载均衡。(都是由客户端Client完成的)。
    • 过程中没有中间代理服务器,符合RPC高性能
  • 相对的就是Server-Based服务发现机制,比如nginxk8s,中间由负载均衡服务器进行调用

  • 如下图,服务发现包括Provider,Consumer,注册中心三个角色参与。

    • Provider会将服务URL地址注册到注册中心

    • 注册中心将数据进行聚合

    • Consumer会从注册中心读取地址列表并订阅变更

      • 每当地址发生改变,注册中心将最新的列表通知到订阅的Consumer。
    • 也就是说Dubbo是半拉半推模型

      • Consumer启动时,会从注册中心拉取完整服务列表,并注册监听器监听订阅服务是否变更
      • 如果服务变更,则注册中心会主动通知Consumer变更地址列表

alt 服务发现机制图
alt 服务发现机制图

5.3 负载均衡

  • 在集群负载均衡时,Dubbo提供了多种负载均衡策略,默认为weight random也就是加权随机负载均衡

  • Dubbo提供的是客户端负载均衡,由Consumer通过负载均衡将请求提交给Provider实例

  • Dubbo常用的负载均衡策略:

    1. 加权随机Weighted Random LoadBalance 默认算法,默认权重相同。
    2. 加权轮询RoundRobin LoadBalance 与nginx一样的轮询算法,默认权重相同。
    3. 最少活跃优先+加权随机LeastActive LoadBalance 能者多劳的思想
    4. 一致性哈希ConsitentHash 确定的入参,确定的提供者,适用于有状态请求

5.3.1 加权随机

  • 实现原理非常简单。比如有两个服务S1,S2.

    • S1的权重为7,S2的权重为3
    • 则总权重为10,我们生成1到10的随机数,落在S1部分选择S1服务处理请求,反之选择S2
  • 下边是RandomLoadBalance的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class RandomLoadBalance extends AbstractLoadBalance {

public static final String NAME = "random";

@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {

int length = invokers.size();
boolean sameWeight = true;
int[] weights = new int[length];
int totalWeight = 0;
// 下面这个for循环的主要作用就是计算所有该服务的提供者的权重之和 totalWeight(),
// 除此之外,还会检测每个服务提供者的权重是否相同
for (int i = 0; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
totalWeight += weight;
weights[i] = totalWeight;
if (sameWeight && totalWeight != weight * (i + 1)) {
sameWeight = false;
}
}
if (totalWeight > 0 && !sameWeight) {
// 随机生成一个 [0, totalWeight) 区间内的数字
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
// 判断会落在哪个服务提供者的区间
for (int i = 0; i < length; i++) {
if (offset < weights[i]) {
return invokers.get(i);
}
}

return invokers.get(ThreadLocalRandom.current().nextInt(length));
}

}

5.3.2 加权轮询

  • 与加权随机类似,如果S1权重为7,S2权重为3,则10次请求会被S1处理7次,S2处理3次。
  • 不是随机,而是轮询

5.3.3 最少活跃优先+加权随机

  • 只看名字不直观,其实就是:

    • 初始状态下所有Provider的活跃数为0(每个Provider中的特定方法都对应一个活跃数)
    • 每收到一个请求,对应Provider活跃数+1请求处理完之后活跃数-1
  • 因此,Dubbo就认为,谁的活跃数越少,谁的处理速度越快,性能越好,这样就优先将请求交给最少活跃Provider处理。

    • 如果多个Provider的活跃数相等就再走一边RandomLoadBalance
  • 下边是该算法的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class LeastActiveLoadBalance extends AbstractLoadBalance {

public static final String NAME = "leastactive";

@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size();
int leastActive = -1;
int leastCount = 0;
int[] leastIndexes = new int[length];
int[] weights = new int[length];
int totalWeight = 0;
int firstWeight = 0;
boolean sameWeight = true;
// 这个 for 循环的主要作用是遍历 invokers 列表,找出活跃数最小的 Invoker
// 如果有多个 Invoker 具有相同的最小活跃数,还会记录下这些 Invoker 在 invokers 集合中的下标,并累加它们的权重,比较它们的权重值是否相等
for (int i = 0; i < length; i++) {
Invoker<T> invoker = invokers.get(i);
// 获取 invoker 对应的活跃(active)数
int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
int afterWarmup = getWeight(invoker, invocation);
weights[i] = afterWarmup;
if (leastActive == -1 || active < leastActive) {
leastActive = active;
leastCount = 1;
leastIndexes[0] = i;
totalWeight = afterWarmup;
firstWeight = afterWarmup;
sameWeight = true;
} else if (active == leastActive) {
leastIndexes[leastCount++] = i;
totalWeight += afterWarmup;
if (sameWeight && afterWarmup != firstWeight) {
sameWeight = false;
}
}
}
// 如果只有一个 Invoker 具有最小的活跃数,此时直接返回该 Invoker 即可
if (leastCount == 1) {
return invokers.get(leastIndexes[0]);
}
// 如果有多个 Invoker 具有相同的最小活跃数,但它们之间的权重不同
// 这里的处理方式就和 RandomLoadBalance 一致了
if (!sameWeight && totalWeight > 0) {
int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexes[i];
offsetWeight -= weights[leastIndex];
if (offsetWeight < 0) {
return invokers.get(leastIndex);
}
}
}
return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);
}
}
  • 活跃数通过RpcStatus中的一个ConcurrentMap保存的,根据URL以及Provider被调用的方法的名字,我们便可以获取对应的活跃数,也就是说每个Provider的方法的活跃数都是独立的
    • 也就是说一个方法的活跃度低,不代表方法的服务活跃度低。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RpcStatus {

private static final ConcurrentMap<String, ConcurrentMap<String, RpcStatus>> METHOD_STATISTICS =
new ConcurrentHashMap<String, ConcurrentMap<String, RpcStatus>>();

public static RpcStatus getStatus(URL url, String methodName) {
String uri = url.toIdentityString();
ConcurrentMap<String, RpcStatus> map = METHOD_STATISTICS.computeIfAbsent(uri, k -> new ConcurrentHashMap<>());
return map.computeIfAbsent(methodName, k -> new RpcStatus());
}
public int getActive() {
return active.get();
}

}

5.3.4 一致性哈希

  • 在我们分库分表的时候也会用到一致性哈希,该算法中没有权重的概念
    • 具体是哪个Provider处理请求是由请求参数决定的,也就是说相同参数的请求总是发到同一个Provider
    • 另外,Dubbo 为了避免数据倾斜问题(节点不够分散,大量请求落到同一节点),还引入了虚拟节点的概念。通过虚拟节点可以让节点更加分散,有效均衡各个节点的请求量。

6. Dubbo序列化协议

  • Dubbo 支持多种序列化方式:JDK 自带的序列化、hessian2、JSON、Kryo、FST、Protostuff,ProtoBuf 等等。

    • Dubbo 默认使用的序列化方式是 hessian2
  • 一般我们不会使用JDK自带的序列化方式,主要原因如下:

    1. 不支持跨语言调用
    2. 性能差,序列化之后的字节数组体积较大
  • JSON序列化由于性能问题,也不被考虑。

7. 结语

  • 寒假一直走亲戚,学习效率太低了… Dubbo拖了很久才学完🥲
  • 标题: Dubbo
  • 作者: yin_bo_
  • 创建于 : 2026-01-18 21:31:50
  • 更新于 : 2026-02-24 04:14:10
  • 链接: https://www.blog.yinbo.xyz/2026/01/18/分布式/Dubbo/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。