分布式系统中的相关概念

大型互联网项目架构目标

传统项目
比如 OA 系统,也就是办公系统,提倡无纸化办公,在线流程审批;HR 系统,人力资源系统;CRM 系统,客户关联关系系统,主要面向企业员工

互联网项目特点
比如天猫、百度、微信,面向网民

  • 用户多
  • 流量大,并发高
  • 海量数据
  • 易受攻击
  • 功能繁琐
  • 变更快

目标

  • 高性能:提供快速的访问体验
    衡量网站的性能指标

    • 响应时间:指执行一个请求从开始到最后收到响应数据所花费的总体时间。

    • 并发数:指系统同时能处理的请求数量。

      • 并发连接数:指的是客户端向服务器发起请求,并建立了 TCP 连接。每秒钟服务器连接的总 TCP 数量

      • 请求数:也称为 QPS(Query Per Second)指每秒多少请求。

        我们访问百度时,浏览器会先建立 TCP 连接,然后发送第一个 HTTP 请求获取页面。页面解析过程中,CSS、JS、PNG 图片、接口等资源也会继续发起 HTTP 请求。这些请求可能复用已有连接,也可能使用新的连接;每个 HTTP 请求通常都可以计入 QPS。
        所以有 QPS >= 并发连接数

      • 并发用户数:单位时间内有多少用户(按 IP 算用户数量)

    • 吞吐量:指单位时间内系统能处理的请求数量。

      • QPS:Query Per Second 每秒查询数。
      • TPS:Transactions Per Second 每秒事务数。
        一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。

      一个页面的一次访问,只会形成一个 TPS;但一次页面请求,可能产生多次对服务器的请求,就会有多个 QPS。

    QPS >= 并发连接数 >= TPS

  • 高可用:网站服务一直可以正常访问

  • 可伸缩:通过硬件增加/减少,提高/降低处理能力

  • 高可扩展:系统间耦合低,方便通过新增/移除方式,增加/减少新的功能/模块

  • 安全性:提供网站安全访问和数据加密,安全存储等策略

  • 敏捷性:随需应变,快速响应

集群和分布式

  • 集群:很多人一起,干一样的事
    • 一个业务模块,部署在多台服务器上。
  • 分布式:很多“人”一起,干不一样的事。这些不一样的事,合起来是一件大事。
    • 一个大的业务系统,拆分为小的业务模块,分别部署在不同的机器上。

架构演进

Dubbo 是 SOA 时代的产物,SpringCloud 是微服务时代的产物

单体架构

优点

  • 简单:开发部署都很方便,小型项目首选

缺点

  • 项目启动慢;可靠性差(有一个模块出问题,其他模块也都用不了了);可伸缩性差;扩展性和可维护性差;性能低

垂直架构

垂直架构是指将单体架构中的多个模块拆分为多个独立的项目。形成多个独立的单体架构。
垂直架构就相当于一个大项目拆成两个不互通的子项目,比如把大奶茶店拆成蜜雪冰城和幸运咖

单体架构存在的问题:项目启动慢;可靠性差;可伸缩性差;扩展性和可维护性差;性能低
垂直架构存在的问题:重复功能太多

分布式架构

  • 分布式架构是指在垂直架构的基础上,将公共业务模块抽取出来,作为独立的服务,供其他调用者消费,以实现服务的共享和重用。
  • RPC:Remote Procedure Call 远程过程调用。有非常多的协议和技术来都实现了 RPC 的过程。比如:HTTP REST 风格、Java RMI 规范、WebService SOAP 协议、Hession 等等。
  • 对外 http/https,对内 rpc

垂直架构存在的问题:重复功能太多
分布式架构存在的问题:服务提供方一旦产生变更,所有消费方都需要变更。(比如服务提供方的 IP 变更了)

SOA架构

SOA:(Service-Oriented Architecture,面向服务的架构)是一个组件模型,它将应用程序的不同功能单元(称为服务)进行拆分,并通过这些服务之间定义良好的接口和契约联系起来。
ESB:(Enterprise Service Bus)企业服务总线,服务中介。主要是提供了一个服务于服务之间的交互。ESB 包含的功能如:负载均衡,流量控制,加密处理,服务的监控,异常处理,监控告急等等。
分布式架构存在的问题:服务提供方一旦产生变更,所有消费方都需要变更。

  • ESB 类似于注册中心

微服务架构
微服务架构是在 SOA 上做的升华,微服务架构强调的一个重点是“业务需要彻底的组件化和服务化”,原有的单个业务系统会拆分为多个可以独立开发、设计、运行的小应用。这些小应用之间通过服务完成交互和集成。
微服务架构 = 80% 的 SOA 服务架构思想 + 100% 的组件化架构思想 + 80% 的领域建模思想

特点

  • 服务实现组件化:开发者可以自由选择开发技术。也不需要协调其他团队
  • 服务之间交互一般使用 REST API
  • 去中心化:每个微服务有自己私有的数据库持久化业务数据
  • 自动化部署:把应用拆分成为一个一个独立的单个服务,方便自动化部署、测试、运维

Duubo 概述

Dubbo 是阿里巴巴公司开源的一个高性能、轻量级的 Java RPC 框架。(中文文档,很友好)

https://cn.dubbo.apache.org/zh-cn/

Dubbo 架构

{% note info %}
这是 Dubbo 2.x 的架构图
{% endnote %}

节点角色说明

  • Provider 暴露服务的服务提供方
  • Consumer 调用远程服务的服务消费方
  • Registry 服务注册与发现的注册中心
  • Monitor 统计服务的调用次数和调用时间的监控中心
  • Container 服务运行容器

Dubbo 快速入门

环境准备

启动一个 nacos 配置中心,或者启动 zookeeper 配置中心。

这里我们用 zookeeper 做一下,官网上下载一下 zookeeper,这里我用的是 3.8.6 版本 下载,选择 -bin.tar.gz,然后解压到本地,这里我放到了 /Users/ice/Desktop/cola/environment/zookeeper 目录下,同目录下创建一个空文件夹 zkdata,用来存放数据

然后进入 conf 目录下,把 zoo_sample.cfg 文件复制一份,重命名为 zoo.cfg。然后改动 zoo.cfg 中的 dataDir 属性

dataDir=/Users/ice/Desktop/cola/environment/zookeeper/zkdata

改动完就可以了,之后启动

# 进入 bin 目录
cd /Users/ice/Desktop/cola/environment/zookeeper/apache-zookeeper-3.8.6-bin/bin

# 启动
./zkServer.sh start

# 查看状态
./zkServer.sh status

# 关闭
./zkServer.sh stop

或者更简单的方式,使用 Docker

docker run -d \
  --name zk386 \
  -p 2181:2181 \
  -v /Users/ice/Desktop/cola/environment/zookeeper/docker/zkData:/data \
  -v /Users/ice/Desktop/cola/environment/zookeeper/docker/zkDatalog:/datalog \
  zookeeper:3.8.6

Zookeeper 和 Nacos 作为独立的服务端软件,通常不需要专门为了 Dubbo 去修改它们自己的配置文件。

快速入门

快速入门 | 代码链接

下载源码,JDK17

{% note info %}
这个代码是 Dubbo 官网快速入门案例,其实官网还有个例子更符合我们下面的讲解,API、Provider、Consumer 都是单独的模块 dubbo-samples-spring-boot
{% endnote %}

项目结构

我把项目结构也重构了一下,有 api,provider,consumer 三个模块

项目结构如下:

quickstart
├── quickstart-api        // 公共接口模块(提供者和消费者共用)
	—— DemoService
├── quickstart-provider   // 服务提供者(核心:@DubboService配置)
	—— dubbo
		—— DemoServiceImpl
    —— QuickStartApplication
    —— application.yml
└── quickstart-consumer   // 服务消费者
	—— dubbo
		—— consumer       
    —— QuickStartConsumerApplication
    —— application.yml

Maven 依赖(服务提供者、消费者模块)

首先是父工程的 pom.xml,它负责版本管理,这里需要我们引入 dubbo-bom 来管理 dubbo 版本,并且有些依赖是从这个里面引入的,比如 dubbo-zookeeper-curator5-spring-boot-starter。另外还需要导入 api 模块

{% note info %}
为什么需要 api 模块?因为 providerconsumer 都需要这个接口,前者是实现这个接口,后者是要使用这个接口,所以在父工程把这个引入,那么子工程就不用自己定义了,直接使用就可以。

  • 这个公共接口打成 jar 包之后,别人导入 jar 包引用就可以了
    {% endnote %}
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>3.2.3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-bom</artifactId>
            <version>3.3.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>quickstart-api</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</dependencyManagement>

此外,provider 和 consumer 导入依赖如下

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!-- 前面只是版本管理,这里才负责真正引入 -->
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>quickstart-api</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo-spring-boot-starter</artifactId>
    </dependency>
    <!-- 用于 zookeeper 3.4.x 版本以上 -->
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo-zookeeper-curator5-spring-boot-starter</artifactId>
    </dependency>
</dependencies>

其实版本上确实有点乱,更多内容可以查看 Dubbo 支持的 Spring Boot Starter 清单

服务定义(api)

以下是基于 Java Interface 的标准 Dubbo 服务定义。

public interface DemoService {
    String sayHello(String name);
}

DemoService 中,定义了 sayHello 这个方法。后续服务端发布的服务,消费端订阅的服务都是围绕着 DemoService 接口展开的。

服务实现(provider)

代码实现

定义了服务接口之后,可以在服务端这一侧定义对应的业务逻辑实现。

@DubboService
public class DemoServiceImpl implements DemoService {
    @Override
    public String sayHello(String name) {
        return "Hello " + name;
    }
}

DemoServiceImpl 类上添加 @DubboService 注解,通过这个配置可以基于 Spring Boot 去发布 Dubbo 服务。

配置文件

由于我们创建的是一个 Spring Boot 应用,Dubbo 相关配置信息都存放在 application.yml 配置文件中。基于以下配置,Dubbo 进程将在 50051 端口监听 triple 协议请求,同时,实例的 ip:port 信息将会被注册到 zookeeper。

# application.yml
dubbo:
  # 注册中心配置(服务注册/发现)
  registry:
    address: zookeeper://127.0.0.1:2181
    # 如果你用 nacos,如下
    # address: nacos://${nacos.address:127.0.0.1}:8848?username=nacos&password=nacos
    register-mode: instance
    
  # 3. 协议配置(服务暴露的IP/端口)
  protocol:
    name: tri
    port: 50051
    
  # 应用基本信息(必填)
  application:
  	 # 应用名称(必填,消费者通过此定位服务)
    name: QuickStartProvider
    logger: slf4j

{% note warning %}
这个时候你可以尝试启动 Provider 了,如果启动失败,控制台有输出 198.18.0.1,那就是梯子的问题了,解决办法是要么关闭梯子,要么改一下 IP,先找到本机 IP 地址,然后修改协议配置

dubbo:
  protocol:
    name: tri
    port: 50051
	host: 172.25.91.196 # 本机 IP

{% endnote %}

以下是整个应用的启动入口,@EnableDubbo 注解用来加载和启动 Dubbo 相关组件。如果在启动类上添加了 @EnableDubbo 注解,且没有指定参数,它默认会扫描 启动类所在的包及其子包

@SpringBootApplication
@EnableDubbo
public class QuickStartApplication {
    public static void main(String[] args) {
        SpringApplication.run(QuickStartApplication.class, args);
    }
}

服务调用(consumer)

配置文件
dubbo:
  application:
    name: QuickStartConsumer
    logger: slf4j

  registry:
    address: zookeeper://localhost:2181
调用远程服务

创建一个 Controller,使用 @DubboReference 注入远程服务代理。

@RestController
public class HelloController {
    // @DubboReference 会自动从 Zookeeper 发现服务并生成代理对象
    /**
	1. 从 zookeeper 注册中心获取 userService 的访问 URL
	2. 进行远程调用 RPC
	3. 将结果封装为一个代理对象,给变量赋值
    */
    @DubboReference // 类似依赖注入
    private DemoService demoService;

    @GetMapping("/hello")
    public String sayHello(@RequestParam String name) {
        return demoService.sayHello(name);
    }
}
@RestController
public class HelloController {

    // @DubboReference 会自动从 Zookeeper 发现服务并生成代理对象
    /**
    	1. 从 zookeeper 注册中心获取 userService 的访问 URL
    	2. 进行远程调用 RPC
    	3. 将结果封装为一个代理对象,给变量赋值
    */
    @DubboReference    // 类似依赖注入
    private HelloService helloService;

    @GetMapping("/hello")
    public String sayHello(@RequestParam String name) {
        // 像调用本地方法一样调用远程接口
        return helloService.sayHello(name);
    }
}
启动类

同样需要 @EnableDubbo

@SpringBootApplication
@EnableDubbo
public class QuickstartConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(QuickstartConsumerApplication.class, args);
    }

}

测试

启动项目,先启动 Provider,再启动 Consumer,测试访问

入门案例详解

这里对入门案例中出现的一些内容进行详细解释

注册与协议

在配置文件里面出现了 Zookeeperprotocol 两个配置,分别是干什么的?

Zookeeper 只负责告诉调用者服务在哪里,protocol 才负责真正怎么调用服务

Zookeeper

Zookeeper 只负责注册,不执行业务,也不转发请求。

  • Provider 启动后,会告诉 Zookeeper,我是 QuickStartProvider,我提供 DemoService 服务,我的调用地址是 tri://ip:50051
  • Consumer 启动后,会问 Zookeeper,我要调用 DemoService,谁能提供这个服务,Zookeeper 返回 QuickStartProvider 可以提供,地址是 xxx

这个时候还没发生调用。

dubbo.registry.address 这个含义是 Dubbo 去这个地址去注册和发现服务,所以 Provider 和 Consumer 都要配置

协议
dubbo:
  protocol:
    name: tri
    port: 50051

含义是 Provider 用 tri 协议,在 50051 端口暴露 Dubbo 服务,所以 Provider 启动后,会开放一个 RPC 端口,tri://ip:55051,这个端口才是真正给 Consumer 调用的,当 Consumer 拿到地址后,根据地址就可以调用了。

图解流程

注册模式

这个 register-mode=instance 含义是应用级注册,是 Dubbo 3 的新特性,这也是为什么要 dubbo.application.name 的原因。也就是 DemoService -> QuickStartProvider -> IP:50051
官网现在推荐这种,不推荐原来的接口级注册,原来的只关心接口不关心应用,也就是直接 DemoService -> IP:50051

如果你的服务想做集群,那就开启多个实例,注意这个时候这几个实例的 dubbo.application.name 保持一致才可以,这样注册的时候 Zookeeper 就知道你们几个是属于某个应用的

{% note info %}
加入应用 A 和应用 B 名字都叫 OrderProvider,但是 A 提供 OrderServicePayService,应用 B 只提供 OrderService。会如何?
这个时候 Zookeeper 会记录这个信息的,OrderProvider 有两个实例,每个实例下面有哪些服务。在消费者调用 OrderService 的时候,会去注册中心看这个属于谁,发现属于 OrderProvider,它下面有两个实例,然后检查实例谁有 OrderService,发现都有,就都返回,然后!!!Dubbo 去做负载均衡

但是这样不好,通常建议同一个 dubbo.application.name 表示同一组能力一致的实例。
{% endnote %}

注解

@DubboService 作用不仅会放入 Spring 容器,还会暴露成远程 RPC 服务。方式之后,程序启动,这个 DemoServiceImpl 会被用 tri 协议暴露到 50051 端口,并且服务地址和元数据注册到 Zookeeper

@DubboReference 作用是自动注入 Dubbo 服务代理实例,注意不是注入的 DemoServiceImpl 对象,而是一个代理对象,之后当它进行调用时,实际在调用的代理对象,代理对象把方法名、参数、接口名封装成 RPC 请求,然后根据 Zookeeper 拿到 Provider 地址,通过 tri 协议发给 Provider 的 50051 端口,Provider 找到 DemoServiceImpl.sayHello() 并执行,然后把结果返回

Dubbo 高级特性

Dubbo-admin

  • dubbo-admin 管理平台,是图形化的服务管理页面
  • 从注册中心中获取到所有的提供者 / 消费者进行配置管理
  • 路由规则、动态配置、服务降级、访问控制、权重调整、负载均衡等管理功能
  • dubbo-admin 是一个前后端分离的项目。前端使用 vue,后端使用 springboot
  • 安装 dubbo-admin 其实就是部署该项目

安装

这里我们使用 docker 安装,当然你也可以去 github 搜索 dubbo-admin 在本地配置

{% note warning %}
我配置的时候问题有很多,因为项目比较老,node.js 必须用老版的,jdk8 编译项目也有问题,需要 jdk11,并且还因为梯子的原因端口配置也有问题,所以用 docker 最省心
{% endnote %}

zookeeper 已经用 docker 启动好了,创建一个网络

docker network create dubbo-net

docker network connect dubbo-net zk386
docker run -d \
  --name dubbo-admin \
  --platform linux/amd64 \
  --network dubbo-net \
  -p 8083:38080 \
  -e "admin.registry.address=zookeeper://zk386:2181" \
  -e "admin.config-center=zookeeper://zk386:2181" \
  -e "admin.metadata-report.address=zookeeper://zk386:2181" \
  -e "admin.root.user.name=root" \
  -e "admin.root.user.password=root" \
  apache/dubbo-admin:0.6.0

然后访问 http://localhost:8083 即可,密码和账号是 root。

{% hideToggle 本机配置 %}

本机配置大致流程如下,因为调整版本,一直没成功,想试就自己试试

我们这里下载 0.6.0 版本,Java语言实现的

解压后进入 \dubbo-admin-server\src\main\resource 目录,修改 application.properties 文件配置,修改 Zookeeper 地址

admin.registry.address=zookeeper://127.0.0.1:2181
admin.config-center=zookeeper://127.0.0.1:2181
admin.metadata-report.address=zookeeper://127.0.0.1:2181

分别是注册中心、配置中心、元数据中心

然后在 dubbo-admin-0.6.0 目录(根目录下)执行打包命令

{% note danger %}
用 JDK11 打包,不要用 JDK8 或者 JDK 17
{% endnote %}

mvn clean package -Dmaven.test.skip=true

{% note info %}
项目的 Spring Boot 版本是 2.3.12.RELEASE,这里推荐使用 JDK11,因为试了一下 JDK8,也不行,它不支持 Maven 编译的一个 --release 参数。
{% endnote %}

然后切到目录 dubbo-admin-distribution/target 下,执行命令启动 dubbo-admin

java -jar dubbo-admin-0.6.0.jar

然后就到了启动前端(自己配置好 node.js),进入 dubbo-admin-ui 目录,执行命令

npm run dev

启动后浏览器输入 http://localhost:8081/ 用户名和密码都是 root

{% endhideToggle %}

简单说一下服务查询

搜索类型,主要分为【服务名】【IP地址】【应用】三种类型查询,在左侧输入,* 就是查询所有

详情页能看到提供者、消费者信息,元数据也能看到


序列化

两个机器传输数据,如何传输 Java 对象?

  • dubbo 内部已经将序列化和反序列化的过程内部封装了,所以我们对序列化是无感知的,不需要我们手动序列化和反序列化
  • 我们只需要在定义 pojo 类时实现 Serializable 接口即可
  • 把公共的对象抽出来,形成一个 pojo 模块,确保这些对象实现了 serilizable 接口,让生产者和消费者都依赖该模块。

那我们动手试试吧,先创建一个模块,名为 quickstart-pojo,里面我们只引入了 lombok 依赖
pojo 包下创建 User 类

@Data
@AllArgsConstructor
public class User {

    private Integer id;
    private String username;
    private String password;
}

之后在 quickstart-api 中的 DemoService 创建接口方法

public interface DemoService {

    String sayHello(String name);
    // 新建一个方法
    public User findUserById(Integer id);

}

然后在 quickstart-service 实现它

@Override
public User findUserById(Integer id) {
    User user = new User(1, "zhangsan", "123");
    return user;
}

之后在 quickstart-consumer 中调用它

@GetMapping("/find")
public User find(@RequestParam Integer id) {
    return demoService.findUserById(id);
}

以上用到 User 的地方都导入

<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>quickstart-pojo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

浏览器访问 http://localhost:8082/find?id=1 ,报错!

嘿嘿,故意留的错,注意要给 User 加上 implements Serializable,再重新启动访问

地址缓存

注册中心挂了,服务是否可以正常访问?

这个面试也经常会问

  • 可以,因为 dubbo 服务消费者在第一次调用时,会将服务提供方地址缓存到本地,以后再调用则不会访问注册中心。
  • 当服务提供者地址发生变化时,注册中心会通知服务消费者。

我们可以测试一下,刚刚已经 consumer 访问过并且成功了,说明信息已经注册到 Zookeeper,现在我们停掉 Zookeeper,再访问 http://localhost:8082/find?id=1 ,看是否能成功。

可以的哈!!!也可以看到控制台是一直在打印东西,它在尝试重连 Zookeeper,这时候我们再启动 Zookeeper,程序就又连接上了。

{% note info %}
但是新的服务加入不进去了,原来的服务如果信息变更消费者也不知道了,所以要及时修复哦
{% endnote %}

超时重试

背景

  • 服务消费者在调用服务提供者的时候发生了阻塞、等待的情形,这个时候,服务消费者会一直等待下去。
  • 在某个峰值时刻,大量的请求都在同时请求服务消费者,会造成线程的大量堆积,势必会造成雪崩。

dubbo 的解决方案

  • 超时
  • 重试

超时

超时时间

  • dubbo 利用超时机制来解决这个问题,设置一个超时时间,在这个时间段内,无法完成服务访问,则自动断开连接。
  • 使用 timeout 属性配置超时时间,默认值 1000,单位毫秒。

提供两种方式,一种是全局配置,在 yaml 文件中配置

dubbo:
	provider:
		timeout: 5000

另一种方式是局部配置到特定的服务上面,比如我们给提供端接口实现配置

@DubboService(timeout=5000)
public class DemoServiceImpl implements DemoService {}

当然,其实还可以在消费端配置,指定服务或者特定方法

@DubboReference(timeout=5000)
private DemoService demoService; // 在变量上

指定到特定方法

// 消费端指定
@DubboReference(methods = {@Method(name = "sayHello", timeout = 5000)})
private DemoService demoService;

// 服务端指定
@DubboService(methods = {@Method(name = "sayHello", timeout = 5000)})
public class DemoServiceImpl implements DemoService{}

{% note info %}
提供端指定的超时时间可以作为消费端的默认值,如果消费端有指定则优先级更高。但是一般来说提供端更了解业务,更清楚超时时间设置的合理性。
{% endnote %}

链接里面还有 deadline 机制,是一个调用链路的超时时间问题,用到的时候再了解

重试

超时了之后就重试

  • 设置了超时时间,在这个时间段内,无法完成服务访问,则自动断开连接。
  • 如果出现网络抖动,则这一次请求就会失败。
  • Dubbo 提供重试机制来避免类似问题的发生。
  • 通过 retries 属性来设置重试次数。默认为 2 次。

默认重试两次,所以一共会发三次请求

做个测试吧

给服务提供方配置超时重试

@DubboService(timeout = 2000, retries = 2)
public class DemoServiceImpl implements DemoService {

    @Override
    public String sayHello(String name) {
        return "Hello " + name;
    }
    private static int i = 0;
    @Override
    public User findUserById(Integer id) {
        System.out.println("服务提供者被调用了:" + (++i) + " 次");
        User user = new User(1, "zhangsan", "123");
        try {
            if (i < 3){
                Thread.sleep(3000);
            } else {
                Thread.sleep(1000);
            }

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return user;
    }
}

然后在服务调用方打印一下内容方便我们查看

@RestController
public class HelloController {

    @DubboReference
    private DemoService demoService;

    @GetMapping("/hello")
    public String sayHello(@RequestParam String name) {
        return demoService.sayHello(name);
    }
    @GetMapping("/find")
    public User find(@RequestParam Integer id) {
        return demoService.findUserById(id);
    }
}

重启查看访问 http://localhost:8082/find?id=1 ,结果就是刚开始的两次调用是 3s,但是超时时间是 2s,所以执行第一次 2s 失败,执行第二次 2s 失败,然后进行第三次尝试,1s,没有超时,最后一次成功!!

{% note warning %}

  • 注意不是在 Controller 层重新调用三次 /find,而是 demoService.findUserById(id) 调用了三次(三次调用,两次重试)
  • 消费者超时,不等于服务端方法被杀掉,比如这里服务端睡 3 秒,假设业务就是执行 3s,超时后它依然在执行,只不过执行完,消费者就不要这个结果了,所以重试机制还要考虑幂等性!
    {% endnote %}

多版本

  • 灰度发布:当出现新功能时,会让一部分用户先使用新功能,用户反馈没问题时,再将所有用户迁移到新功能。
  • dubbo 中使用 version 属性来设置和调用同一个接口的不同版本

{% note info %}
建议使用两位数字版本,比如 1.0
{% endnote %}

版本与分组 | Dubbo

比如 DemoService 有两个实现类,一个旧版一个新版

@DubboService(version = "1.0")
public class DemoServiceImpl implements DemoService {

@DubboService(version = "2.0")
public class DemoServiceImpl2 implements DemoService {} // 改动 User 返回的 username 为 new version

consumer 自行选择用老版本还是新版本

@DubboReference(version = "2.0")
private DemoService demoService;

负载均衡

负载均衡

负载均衡策略的配置,这里只介绍四个,官网一共 7 个。

  • Random:按权重随机,默认值。按权重设置随机概率。
  • RoundRobin:按权重轮询。
  • LeastActive:最少活跃调用数,相同活跃数的随机。
  • ConsistentHash:一致性 Hash,相同参数的请求总是发到同一提供者。

Random

这是默认的负载均衡算法,并且默认权重都相同

@DubboService(loadbalance = LoadbalanceRules.RANDOM, weight = 100)
public class DemoServiceImpl implements DemoService {}

RoundRobin

按权重轮询

比如三个节点权重分别是 3 2 1,调用流程就是,在起始的时候选择一个权重最大的被调用,然后减去合计的权重 6 之后再加上各自的权重作为下一次的起始权重。这是借鉴了 Nginx 的平滑加权轮询算法。

LeastActive

最少活跃调用数,消费者每发出一个请求,就给对应的 Provider 的活跃数 +1,请求返回计数就 -1,每次找这个最小的。相同活跃数就加权随机

ConsistentHash

一致性哈希,相同参数的请求总是发到同一个提供者。

实践建议

首选 Provider 端配置:建议在 @DubboService 上配置好默认策略。因为服务提供者最了解自己的服务特性(是否有状态、是否计算密集)。消费者也可以配置这个选项,并且优先级更高
不确定的选 Random:默认的 Random 在高并发下效果非常好,因为大数定律会保证流量均匀。
慢服务选 LeastActive:如果你的服务处理时间波动很大(有时候 10ms,有时候 1s),一定要用 leastactive,防止慢的机器被压垮。

集群容错

集群容错

Dubbo 的集群容错(Cluster)是指:当消费者调用提供者失败时,Dubbo 应该采取什么策略。

  • Failover Cluster:失败重试,默认值,失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。
  • Failfast Cluster:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
  • Failsafe Cluster:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
  • Failback Cluster:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
  • Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。
  • Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。

依旧是配置在注解上

// 服务提供者端配置
@DubboService(cluster = ClusterRules.FAIL_OVER, retries = 2)

// 服务消费者端配置
@DubboReference(cluster = ClusterRules.FAIL_SAFE)

同样也是消费者配置优先级更高

服务降级

服务降级

服务降级方式

  • mock=force:return null 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。
  • mock=fail:return null 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。

配置方式

  • 简单配置:依旧是在注解当中配置,如 @DubboReference(mock = "force:return null")
  • 复杂配置:需要写一个专门的 Mock 类

第一步:在 Consumer 端开启 Mock

@DubboReference(mock = "true") // 告诉 Dubbo 去找对应的 Mock 类
private DemoService userService;

含义是先发起远程调用,如果远程调用失败,走 Mock 类。

{% note danger %}
只能配置在消费者这里,原因很简单,服务端都不响应了程序都不跑了,配置了有啥用。
{% endnote %}

第二步:创建 Mock 类 Dubbo 有个命名约定:必须在接口包名下创建一个类,类名必须是 接口名 + Mock

假设接口是 org.apache.dubbo.samples.quickstart.dubbo.api.DemoService,你需要创建: org.apache.dubbo.samples.quickstart.dubbo.api.DemoServiceMock

package org.apache.dubbo.samples.quickstart.dubbo.api; // 包名必须和接口一致

public class DemoServiceMock implements DemoService {
    
    @Override
    public String sayHello(String name) {
        // 这里写复杂的降级逻辑
        return "系统繁忙,请稍后再试 (这是来自 Mock 的兜底回复)";
    }

    @Override
    public User findUserById(Integer id) {
        // 比如:远程挂了,查本地缓存
        return new User(0, "本地缓存用户", "");
    }
}

动态配置

在实际工作中,我们很少在代码里硬编码 mock=...,因为一旦写死,想改还得重新发版。
通常是结合 Dubbo Admin (管理控制台) 进行动态降级

  1. 代码里正常写 @DubboReference (不配 mock)。
  2. 服务出问题时,运维人员登录 Dubbo Admin 控制台。
  3. 找到对应的服务,创建一个动态配置规则,设置 mock=force:return null
  4. 该配置会通过注册中心(Zookeeper/Nacos)实时推送到所有消费者,立马生效。

Dubbo 的 mock 主要是**“静态”“手动”**的降级。如果需要根据系统负载(如 CPU 使用率、QPS 阈值)自动触发熔断降级,建议集成 SentinelResilience4j,它们比 Dubbo 自带的 mock 强大得多。

更多推荐