一、什么是ApacheDubbo
Apache Dubbo (incubating) |ˈdʌbəʊ| 是一款高性能、轻量级的开源 Java RPC 分布式服务框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。她最大的特点是按照分层的方式来架构,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦合)。从服务模型的角度来看,Dubbo 采用的是一种非常简单的模型,要么是提供方提供服务,要么是消费方消费服务,所以基于这一点可以抽象出服务提供方(Provider)和服务消费方(Consumer)两个角色。
二、架构
节点角色说明
节点 | 角色说明 |
---|---|
Provider | 暴露服务的服务提供方 |
Consumer | 调用远程服务的服务消费方 |
Registry | 服务注册与发现的注册中心 |
Monitor | 统计服务的调用次数和调用时间的监控中心 |
Container | 服务运行容器 |
调用关系说明
- 服务容器负责启动,加载,运行服务提供者。
- 服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
三、Dubbo 功能特点
- 面向接口代理的高性能 RPC 调用: 提供高性能的基于代理的远程调用能力,服务以接口为粒度,为开发者屏蔽远程调用底层细节。
- 智能负载均衡: 内置多种负载均衡策略,智能感知下游节点健康状况,显著减少调用延迟,提高系统吞吐量。
- 服务自动注册与发现: 支持多种注册中心服务,服务实例上下线实时感知。
- 高度可扩展能力: 遵循微内核 + 插件的设计原则,所有核心能力如 Protocol、Transport、Serialization 被设计为扩展点,平等对待内置实现和第三方实现。
- 运行期流量调度: 内置条件、脚本等路由策略,通过配置不同的路由规则,轻松实现灰度发布,同机房优先等功能。
- 可视化的服务治理与运维: 提供丰富服务治理、运维工具:随时查询服务元数据、服务健康状态及调用统计,实时下发路由策略、调整配置参数。
四、创建工程
4.1 父工程
父工程ApacheDubbo的pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.qiang</groupId>
<artifactId>ApacheDubbo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>apache-dubbo-dependencies</module>
<module>apache-dubbo-provider</module>
<module>apache-dubbo-consumer</module>
</modules>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<licenses>
<license>
<name>Apache 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.qiang</groupId>
<artifactId>apache-dubbo-dependencies</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<profiles>
<profile>
<id>default</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<spring-javaformat.version>0.0.12</spring-javaformat.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>io.spring.javaformat</groupId>
<artifactId>spring-javaformat-maven-plugin</artifactId>
<version>${spring-javaformat.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*Tests.java</include>
</includes>
<excludes>
<exclude>**/Abstract*.java</exclude>
</excludes>
<systemPropertyVariables>
<java.security.egd>file:/dev/./urandom</java.security.egd>
<java.awt.headless>true</java.awt.headless>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>enforce-rules</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<bannedDependencies>
<excludes>
<exclude>commons-logging:*:*</exclude>
</excludes>
<searchTransitive>true</searchTransitive>
</bannedDependencies>
</rules>
<fail>true</fail>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
<inherited>true</inherited>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<repositories>
<repository>
<id>spring-milestone</id>
<name>Spring Milestone</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshot</id>
<name>Spring Snapshot</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestone</id>
<name>Spring Milestone</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-snapshot</id>
<name>Spring Snapshot</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
4.2 统一依赖管理
统一依赖管理apache-dubbo-dependencies
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.qiang</groupId>
<artifactId>apache-dubbo-dependencies</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<dubbo.version>2.7.3</dubbo.version>
<dubbo-actuator.version>2.7.1</dubbo-actuator.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
<spring-cloud-alibaba.verion>2.1.0.RELEASE</spring-cloud-alibaba.verion>
<alibaba-spring-context-support.version>1.0.2</alibaba-spring-context-support.version>
<guava.version>15.0</guava.version>
</properties>
<licenses>
<license>
<name>Apache 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.verion}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>${dubbo.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
</exclusion>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-actuator</artifactId>
<version>${dubbo-actuator.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.spring</groupId>
<artifactId>spring-context-support</artifactId>
<version>${alibaba-spring-context-support.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>spring-milestone</id>
<name>Spring Milestone</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshot</id>
<name>Spring Snapshot</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestone</id>
<name>Spring Milestone</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-snapshot</id>
<name>Spring Snapshot</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
4.3 服务提供者
由于我们已经有了 Nacos 注册中心,Sentinel 熔断限流控制中心,所以我们不再使用 Zookeeper 和 Dubbo Admin 来管理我们的 Dubbo 应用程序,Dubbo 仅当作我们微服务中的 RPC 通信框架,真正实现对内 RPC,对外 REST。
服务提供者apache-dubbo-provider
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.qiang</groupId>
<artifactId>ApacheDubbo</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>apache-dubbo-provider</artifactId>
<packaging>pom</packaging>
<licenses>
<license>
<name>Apache 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<modules>
<!-- 专门暴露接口的模块 -->
<module>apache-dubbo-provider-api</module>
<!-- 专门实现接口的模块 -->
<module>apache-dubbo-provider-service</module>
</modules>
</project>
创建服务提供者接口模块apache-dubbo-provider-api
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.qiang</groupId>
<artifactId>apache-dubbo-provider</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>apache-dubbo-provider-api</artifactId>
<packaging>jar</packaging>
<licenses>
<license>
<name>Apache 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
</project>
定义接口
package com.qiang.apache.dubbo.provider.api;
/**
* @author: 小强崽
* @create: 2020-08-29 07:07
* @description: 定义接口
*/
public interface EchoService {
/**
* 输出字符串
* @param string
* @return
*/
String echo(String string);
}
创建服务提供者接口实现apache-dubbo-provider-service
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.qiang</groupId>
<artifactId>apache-dubbo-provider</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>apache-dubbo-provider-service</artifactId>
<packaging>jar</packaging>
<licenses>
<license>
<name>Apache 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-nacos</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.spring</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>com.qiang</groupId>
<artifactId>apache-dubbo-provider-api</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-apache-dubbo-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
</dependency>
<!-- sentinel数据持久化到nacos -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<scope>compile</scope>
</dependency>
<!-- 这里只是引入springcloud包作为nacos和sentinel连接的桥梁 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.qiang.apache.dubbo.provider.ProviderApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yml
主要增加了 Dubbo 包扫描路径和 Nacos Server 配置。
spring:
application:
name: dubbo-provider
main:
allow-bean-definition-overriding: true
dubbo:
scan:
base-packages: com.qiang.apache.dubbo.provider.service
protocol:
name: dubbo
port: 20880
registry:
address: nacos://172.25.0.12:8848
provider:
loadbalance: roundrobin
Application
package com.qiang.apache.dubbo.provider;
import com.qiang.apache.dubbo.provider.configuration.DubboSentinelConfiguration;
import com.qiang.apache.dubbo.provider.service.fallback.EchoServiceFallback;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author: 小强崽
* @create: 2020-08-29 07:16
* @description:
*/
@SpringBootApplication(scanBasePackageClasses = {EchoServiceFallback.class, DubboSentinelConfiguration.class})
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}
Service
package com.qiang.apache.dubbo.provider.service;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.qiang.apache.dubbo.provider.api.EchoService;
import com.qiang.apache.dubbo.provider.service.fallback.EchoServiceFallback;
import org.apache.commons.io.FileExistsException;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Value;
/**
* @author: 小强崽
* @create: 2020-08-29 07:18
* @description:
*/
@Service(version = "1.0.0")
public class EchoServiceImpl implements EchoService {
@Value("${dubbo.protocol.port}")
private String port;
@Override
@SentinelResource(value = "getByUsername", fallback = "getEchoServiceFallback", fallbackClass = EchoServiceFallback.class)
public String echo(String string) {
if ("hello".equals(string)){
throw new IllegalArgumentException("invalid arg");
}
return "Echo Hello Dubbo " + string + " i am from port: " + port;
}
}
验证是否成功
访问http://172.25.0.12:8848/nacos
4.4 服务消费者
apache-dubbo-consumer
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.qiang</groupId>
<artifactId>ApacheDubbo</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>apache-dubbo-consumer</artifactId>
<packaging>jar</packaging>
<licenses>
<license>
<name>Apache 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo-registry-nacos</artifactId>
<version>2.7.7</version>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.spring</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>com.qiang</groupId>
<artifactId>apache-dubbo-provider-api</artifactId>
<version>${project.parent.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.qiang.apache.dubbo.consumer.ConsumerApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yml
主要增加了 Dubbo 包扫描路径、健康检查以及 Nacos Server 配置。
spring:
application:
name: dubbo-consumer
main:
allow-bean-definition-overriding: true
dubbo:
scan:
base-packages: com.qiang.apache.dubbo.consumer.controller
protocol:
name: dubbo
port: -1
registry:
address: nacos://172.25.0.12:8848
server:
port: 8080
endpoints:
dubbo:
enabled: true
management:
health:
dubbo:
status:
defaults: memory
extras: threadpool
endpoints:
web:
exposure:
include: "*"
Application
package com.qiang.apache.dubbo.consumer;
/**
* @author: 小强崽
* @create: 2020-08-30 01:21
* @description:
*/
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
Controller
通过 org.apache.dubbo
包下的 @Reference
注解像调用本地服务一样调用远程服务,轻松实现透明的远程过程调用。
package com.qiang.apache.dubbo.consumer.controller;
import com.qiang.apache.dubbo.provider.api.EchoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* @author: 小强崽
* @create: 2020-08-30 01:23
* @description:
*/
@RestController
public class EchoController {
@Reference(version = "1.0.0")
private EchoService echoService;
@GetMapping(value = "/echo/{string}")
public String echo(@PathVariable String string) {
return echoService.echo(string);
}
}
验证是否成功
访问http://172.25.0.12:8848/nacos
Echo Hello Dubbo hi i am from port: 20880
服务端点检查
http://localhost:8080/actuator/health
{
"status": "UP"
}
五、负载均衡
修改 dubbo-provider
项目的负载均衡策略,默认的负载均衡策略是 随机,我们修改为 轮循,可配置的值分别是:random
,roundrobin
,leastactive
,consistenthash
dubbo:
provider:
loadbalance: roundrobin
修改 dubbo-provider
的协议端口为 20880 和 20881,并启动多个实例,IDEA 中依次点击 Run -> Edit Configurations 并勾选 Allow parallel run 以允许 IDEA 多实例运行项目。
Nacos Server 控制台可以看到 dubbo-provider
有 2 个实例。
修改 dubbo-provider
项目的 EchoServiceImpl
中的测试方法。
package com.qiang.apache.dubbo.provider.service;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.qiang.apache.dubbo.provider.api.EchoService;
import com.qiang.apache.dubbo.provider.service.fallback.EchoServiceFallback;
import org.apache.commons.io.FileExistsException;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Value;
/**
* @author: 小强崽
* @create: 2020-08-29 07:18
* @description:
*/
@Service(version = "1.0.0")
public class EchoServiceImpl implements EchoService {
@Value("${dubbo.protocol.port}")
private String port;
@Override
@SentinelResource(value = "getByUsername", fallback = "getEchoServiceFallback", fallbackClass = EchoServiceFallback.class)
public String echo(String string) {
if ("hello".equals(string)){
throw new IllegalArgumentException("invalid arg");
}
return "Echo Hello Dubbo " + string + " i am from port: " + port;
}
}
重启服务,通过浏览器访问 http://localhost:8080/echo/hi ,反复刷新浏览器,浏览器交替显示。
六、外部化配置
我们以 dubbo-consumer 项目为例,修改 pom.xml ,引入 Nacos Config Starter。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
Controller
完成上述两步后,应用会从 Nacos Config 中获取相应的配置,并添加在 Spring Environment 的 PropertySources 中。这里我们使用 @Value
注解来将对应的配置注入到 EchoController
的 username
字段,并添加 @RefreshScope
打开动态刷新功能。
package com.qiang.apache.dubbo.consumer.controller;
import com.qiang.apache.dubbo.provider.api.EchoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* @author: 小强崽
* @create: 2020-08-30 01:23
* @description:
*/
@RefreshScope
@RestController
public class EchoController {
@Reference(version = "1.0.0")
private EchoService echoService;
@GetMapping(value = "/echo/{string}")
public String echo(@PathVariable String string) {
return echoService.echo(string);
}
}
使用控制台发布配置。
spring:
application:
name: dubbo-consumer
main:
allow-bean-definition-overriding: true
dubbo:
scan:
base-packages: com.qiang.apache.dubbo.consumer.controller
protocol:
name: dubbo
port: -1
# 高速序列化
serialization: kryo
registry:
address: nacos://172.25.0.12:8848
server:
port: 8080
endpoints:
dubbo:
enabled: true
management:
health:
dubbo:
status:
defaults: memory
extras: threadpool
endpoints:
web:
exposure:
include: "*"
user:
name: "傻狗"
修改客户端配置
创建名为 bootstrap.properties
的配置文件并删除之前创建的 application.yml
配置文件。
spring.application.name=dubbo-consumer-config
spring.cloud.nacos.config.server-addr=172.25.0.12:8848
spring.cloud.nacos.config.file-extension=yaml
通过浏览器访问 http://localhost:8080/echo/hi ,浏览器输出如下。
Echo Hello Dubbo hi i am from port: -1 傻狗
动态刷新配置
在 Nacos Server 控制台修改配置文件,将 user.name
属性修改为 大傻狗
,此时观察控制台日志,你会发现我们已经成功刷新了配置。
验证是否成功
通过浏览器访问 http://localhost:8080/echo/hi ,浏览器输出如下。
Echo Hello Dubbo hi i am from port: -1 大傻狗
可以使用 spring.cloud.nacos.config.refresh.enabled=false
来关闭动态刷新。
七、服务限流Sentinel
7.1 部署
docker-compose.yaml
version: '3.0'
services:
sentinel:
image: 172.25.0.15/xiaoqiangzai/bladex/sentinel-dashboard:latest
container_name: sentinel
network_mode: "host"
restart: always
privileged: true
ports:
- '8858:8858'
environment:
- auth.username=sentinel
- auth.password=sentinel
jar包
https://github.com/alibaba/Sentinel/releases
启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
其中 -Dserver.port=8080
用于指定 Sentinel 控制台端口为 8080
,从 Sentinel 1.6.0 起,Sentinel 控制台引入基本的 登录 功能,默认用户名和密码都是 sentinel。
-Dsentinel.dashboard.auth.username=sentinel
用于指定控制台的登录用户名为 sentinel。-Dsentinel.dashboard.auth.password=123456
用于指定控制台的登录密码为 123456;如果省略这两个参数,默认用户和密码均为 sentinel。-Dserver.servlet.session.timeout=7200
用于指定 Spring Boot 服务端 session 的过期时间,如 7200 表示 7200 秒;60m 表示 60 分钟,默认为 30 分钟。
7.2 Dubbo 集成 Sentinel
使用时需要引入 3 个依赖。
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-apache-dubbo-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
</dependency>
依赖说明
sentinel-apache-dubbo-adapter
:Sentinel 提供的 Apache Dubbo 适配模块 (注意:sentinel-dubbo-adapter
是未毕业版本的 Dubbo 适配模块)。sentinel-transport-simple-http
: 用于暴露一个特定的端口,Sentinel Dashboard 通过 HTTP 的形式进行数据推送,客户端接收后将规则保存在本地内存中。sentinel-annotation-aspectj
:Sentinel 提供了@SentinelResource
注解用于定义资源,并提供了 AspectJ 的扩展用于自动定义资源、处理BlockException
等。
7.3 配置启动参数
在启动时,需要在 JVM 中添加以下启动参数。
-Djava.net.preferIPv4Stack=true
-Dcsp.sentinel.api.port=8720
-Dproject.name=dubbo-provider
-Dcsp.sentinel.dashboard.server=172.25.0.12:8858
参数说明
-Dcsp.sentinel.api.port=客户端端口,用于上报信息,默认 8720 即可,Sentinel 发现端口冲突会自动递增
-Dproject.name=显示在控制台上的应用名称
-Dcsp.sentinel.dashboard.server=控制台地址
7.4 Dubbo 配置 Sentinel
我们需要使用 Spring AOP 的方式显示的注册 SentinelResourceAspect
为一个 Bean。
package com.qiang.apache.dubbo.provider.configuration;
import com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author: 小强崽
* @create: 2020-08-30 02:29
* @description: 仅 Dubbo 服务需要该配置,Spring Cloud Alibaba 无需加载该配置
*/
@Configuration
public class DubboSentinelConfiguration {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
加载配置
配置单独封装至 configuration-sentinel
项目中,方便其它 Dubbo 服务依赖,所有 Dubbo 服务提供者都需要调用该模块并指定加载配置类,修改 Application
代码如下。
package com.qiang.apache.dubbo.provider;
import com.qiang.apache.dubbo.provider.configuration.DubboSentinelConfiguration;
import com.qiang.apache.dubbo.provider.service.fallback.EchoServiceFallback;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author: 小强崽
* @create: 2020-08-29 07:16
* @description:
*/
@SpringBootApplication(scanBasePackageClasses = {EchoServiceFallback.class, DubboSentinelConfiguration.class})
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}
7.5 配置熔断类
可以为需要熔断降级和限流的方法创建一个专门的熔断类,在需要使用熔断和限流的方法上使用 @SentinelResource
注解指定该类中的方法即可实现熔断降级功能。
注意: 熔断方法必须是 static
函数。
package com.qiang.apache.dubbo.provider.service.fallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author: 小强崽
* @create: 2020-08-30 02:32
* @description: 服务熔断后处理的方法
*/
public class EchoServiceFallback {
private static final Logger logger = LoggerFactory.getLogger(EchoServiceFallback.class);
public static String getEchoServiceFallback(String username, Throwable ex) {
logger.warn("Invoke getEchoServiceFallback: " + ex.getClass().getTypeName());
ex.printStackTrace();
return "服务熔断";
}
}
7.6 配置熔断方法
package com.qiang.apache.dubbo.provider.service;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.qiang.apache.dubbo.provider.api.EchoService;
import com.qiang.apache.dubbo.provider.service.fallback.EchoServiceFallback;
import org.apache.commons.io.FileExistsException;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Value;
/**
* @author: 小强崽
* @create: 2020-08-29 07:18
* @description: 熔断器的使用
* {@link SentinelResource#value()} 对应的是 Sentinel 控制台中的资源,可用作控制台设置【流控】和【降级】操作。
* {@link SentinelResource#fallback()} 对应的是 {@link EchoServiceFallback#getEchoServiceFallback(String, Throwable)},并且必须为 `static`。
* 如果不设置 {@link SentinelResource#fallbackClass()},则需要在当前类中创建一个 `Fallback` 函数,函数签名与原函数一致或加一个 {@link Throwable} 类型的参数。
*/
@Service(version = "1.0.0")
public class EchoServiceImpl implements EchoService {
@Value("${dubbo.protocol.port}")
private String port;
@Override
@SentinelResource(value = "getByUsername", fallback = "getEchoServiceFallback", fallbackClass = EchoServiceFallback.class)
public String echo(String string) {
if ("hello".equals(string)){
throw new IllegalArgumentException("invalid arg");
}
return "Echo Hello Dubbo " + string + " i am from port: " + port;
}
}
7.7 测试熔断限流
7.7.1 配置流控规则
在 Sentinel 控制台配置流控规则,需要至少调用一次服务,才能在控制台看到监控效果。
流控规则说明
- 资源名: 对应
@SentinelResource
注解中的value
属性。 - QPS:平均每秒的请求响应数。
- 单机阈值: 为方便测试这里设置为 1,就是每秒钟中只允许一个响应请求,如果设置为 5,则代表请求每 200 ms 才能通过一个,多出的请求将排队等待通过。超时时间代表最大排队时间,超出最大排队时间的请求将会直接被拒绝。
排队等待模式下 QPS 不要超过 1000(请求间隔 1 ms)。
7.8 测试熔断效果
在出现异常时会自动启动熔断,增加测试代码如下。
package com.qiang.apache.dubbo.provider.service;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.qiang.apache.dubbo.provider.api.EchoService;
import com.qiang.apache.dubbo.provider.service.fallback.EchoServiceFallback;
import org.apache.commons.io.FileExistsException;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Value;
/**
* @author: 小强崽
* @create: 2020-08-29 07:18
* @description:
*/
@Service(version = "1.0.0")
public class EchoServiceImpl implements EchoService {
@Value("${dubbo.protocol.port}")
private String port;
@Override
@SentinelResource(value = "getByUsername", fallback = "getEchoServiceFallback", fallbackClass = EchoServiceFallback.class)
public String echo(String string) {
if ("hello".equals(string)){
throw new IllegalArgumentException("invalid arg");
}
return "Echo Hello Dubbo " + string + " i am from port: " + port;
}
}
触发异常时,控制台 (dubbo-provider
) 打印日志如下。
java.lang.IllegalArgumentException: invalid arg
at com.qiang.apache.dubbo.provider.service.EchoServiceImpl.echo(EchoServiceImpl.java:25)
at com.qiang.apache.dubbo.provider.service.EchoServiceImpl$$FastClassBySpringCGLIB$$9193b731.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:749)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
at com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect.invokeResourceWithSentinel(SentinelResourceAspect.java:57)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
at com.qiang.apache.dubbo.provider.service.EchoServiceImpl$$EnhancerBySpringCGLIB$$555ba878.echo(<generated>)
at org.apache.dubbo.common.bytecode.Wrapper0.invokeMethod(Wrapper0.java)
at org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:47)
at org.apache.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:84)
at org.apache.dubbo.config.invoker.DelegateProviderMetaDataInvoker.invoke(DelegateProviderMetaDataInvoker.java:56)
at org.apache.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:56)
at org.apache.dubbo.rpc.filter.ExceptionFilter.invoke(ExceptionFilter.java:55)
at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:82)
at com.alibaba.csp.sentinel.adapter.dubbo.SentinelDubboProviderFilter.invoke(SentinelDubboProviderFilter.java:71)
at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:82)
at org.apache.dubbo.monitor.support.MonitorFilter.invoke(MonitorFilter.java:92)
at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:82)
at org.apache.dubbo.rpc.filter.TimeoutFilter.invoke(TimeoutFilter.java:48)
at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:82)
at org.apache.dubbo.rpc.protocol.dubbo.filter.TraceFilter.invoke(TraceFilter.java:81)
at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:82)
at org.apache.dubbo.rpc.filter.ContextFilter.invoke(ContextFilter.java:96)
at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:82)
at org.apache.dubbo.rpc.filter.GenericFilter.invoke(GenericFilter.java:148)
at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:82)
at org.apache.dubbo.rpc.filter.ClassLoaderFilter.invoke(ClassLoaderFilter.java:38)
at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:82)
at org.apache.dubbo.rpc.filter.EchoFilter.invoke(EchoFilter.java:41)
at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:82)
at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$CallbackRegistrationInvoker.invoke(ProtocolFilterWrapper.java:157)
at org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol$1.reply(DubboProtocol.java:152)
at org.apache.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.handleRequest(HeaderExchangeHandler.java:102)
at org.apache.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.received(HeaderExchangeHandler.java:193)
at org.apache.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java:51)
at org.apache.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:57)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
限流效果
com.alibaba.csp.sentinel.slots.block.flow.FlowException
2020-08-31 04:30:01.853 WARN 10932 --- [:20881-thread-7] c.q.a.d.p.s.f.EchoServiceFallback : Invoke getEchoServiceFallback: com.alibaba.csp.sentinel.slots.block.flow.FlowException
2020-08-31 04:30:46.078 WARN 10932 --- [:20881-thread-8] c.q.a.d.p.s.f.EchoServiceFallback : Invoke getEchoServiceFallback: java.lang.IllegalArgumentException
八、扩展
8.1 什么是 RPC
分布式是促使 RPC 诞生的领域,RPC 是一种编程模型,并没有规定你具体要怎样实现,无论使用 HTTP 或是 RMI 都是可以的。
假设你有一个计算器接口,Calculator,以及它的实现类 CalculatorImpl,那么在系统还是 单体应用 时,你要调用 Calculator 的 add 方法来执行一个加运算,直接 new 一个 CalculatorImpl,然后调用 add 方法就行了,这其实就是非常普通的 本地函数调用,因为在 同一个地址空间,或者说在同一块内存,所以通过方法栈和参数栈就可以实现。
现在,基于高性能和高可靠等因素的考虑,你决定将系统改造为分布式应用,将很多可以共享的功能都单独拎出来,比如上面说到的计算器,你单独把它放到一个服务里头,让别的服务去调用它。
这下问题来了,服务 A 里头并没有 CalculatorImpl 这个类,那它要怎样调用服务 B 的 CalculatorImpl 的 add 方法呢?
RPC 要解决的两个问题
- 解决分布式系统中,服务之间的调用问题。
- 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑。
8.2 如何实现一个 RPC
实际情况下,RPC 很少用到 HTTP 协议来进行数据传输,毕竟我只是想传输一下数据而已,何必动用到一个文本传输的应用层协议呢,我为什么不直接使用二进制传输?比如直接用 Java 的 Socket 协议进行传输?
不管你用何种协议进行数据传输,一个完整的 RPC 过程,都可以用下面这张图来描述。
以左边的 Client 端为例,Application 就是 RPC 的调用方,Client Stub 就是我们上面说到的代理对象,也就是那个看起来像是 Calculator 的实现类,其实内部是通过 RPC 方式来进行远程调用的代理对象,至于 Client Run-time Library,则是实现远程调用的工具包,比如 JDK 的 Socket,最后通过底层网络实现实现数据的传输。
这个过程中最重要的就是 序列化 和 反序列化 了,因为数据传输的数据包必须是二进制的,你直接丢一个 Java 对象过去,人家可不认识,你必须把 Java 对象序列化为二进制格式,传给 Server 端,Server 端接收到之后,再反序列化为 Java 对象。
8.3 RPC vs Restful
RPC 是面向过程,Restful 是面向资源,并且使用了 HTTP 动词。从这个维度上看,Restful 风格的 URL 在表述的精简性、可读性上都要更好。
8.4 阿里为何放弃 Zookeeper
CAP
有个思考,从 CAP 角度考虑,服务注册中心是 CP 系统还是 AP 系统呢?
- 服务注册中心是为了服务间调用服务的,那么绝对不允许因为服务注册中心出现了问题而导致服务间的调用出问题。
- 假如有 node1,node2,node3 集群节点。保存着可用服务列表 ip1,ip2,ip3,试想如果此时不一致,比如 node1 只保存了ip1,ip2,此时服务读取 node1 的节点,那么会造成什么影响?
调用 node1 的服务,顶多就是负载均衡时不会有流量打到 ip3,然后等 node1 同步回 ip3 后,又一致了,这对服务其实没什么太大影响。所以,推测出服务注册中心应该是个 AP 系统。
Zookeeper 是个 CP 系统,强一致性
- 场景1,当 master 挂了,此时 Zookeeper 集群需要重新选举,而此时服务需要来读取可用服务,是不可用的。影响到了服务的可用性当然你可以说服务本地有缓存可用列表。然而下面这种方式就更无法处理了。
- 场景2,分区可用。试想,有 3 个机房,如果其中机房 3 和机房 1,2 网络断了,那么机房 3 的注册中心就不能注册新的机器了,这显然也不合理从健康检查角度来看。
Zookeeper 是通过 TCP 的心跳判断服务是否可用,但 TCP 的活性并不代表服务是可用的,如:连接池已满,DB 挂了等。
理想的注册中心
- 服务自动注册发现。最好有新的服务注册上去时还能推送到调用端。
- 能对注册上来的机器方便的进行管理,能手动删除(发送信号让服务优雅下线)、恢复机器。
- 服务的健康检查,能真正的检测到服务是否可用。
- 可以看到是否有其他调用服务正在订阅注册上来的服务。
- 能够带上些除了 IP 外的其它信息。
九、常见错误
问题:
java.lang.NoClassDefFoundError: com/alibaba/nacos/client/naming/utils/StringUtils
at org.apache.dubbo.registry.nacos.NacosRegistryFactory.putPropertyIfAbsent(NacosRegistryFactory.java:104) ~[dubbo-2.7.2.jar:2.7.2]
at org.apache.dubbo.registry.nacos.NacosRegistryFactory.setProperties(NacosRegistryFactory.java:94) ~[dubbo-2.7.2.jar:2.7.2]
at org.apache.dubbo.registry.nacos.NacosRegistryFactory.buildNacosProperties(NacosRegistryFactory.java:73) ~[dubbo-2.7.2.jar:2.7.2]
解决:
这里版本号nacos跟dubbo冲突,修改dubbo的版本即可
<dubbo.version>2.7.3</dubbo.version>