Dubbo

Dubbo 应用场景


JavaEE 应用三层架构,在很长一段时间都没有出问题,直到大数据时代的到来,某些数据量巨大的公司开始遇到了新的挑战,某些业务方法频繁调用,需要配置大量资源,某些业务很少调用。只需要配置少量资源。比如:商城应用中

订单服务、积分服务、商品服务使用频率远远大于其他服务,如果仅仅通过布置多台服务器的方式,会造成服务器成本大量提升,资源得不到充分利用,如果遇到某个关系型数据库不好处理的业务场景,技术选型时,也要考虑系统稳定性和冲突问题。所以再次拆分服务,基于服务的分布式应用架构,这种架构就是微服务应用架构,目前比较成熟,社区活跃的微服务架构有 Dubbo,SpringCloud。

  • 生产者:提供业务逻辑实现的角色,对外提供服务
  • 消费者:调用生产者提供的服务。

微服务的优势

  • 降低系统耦合度
  • 服务之间都是相互独立的,互相不干扰,每个服务都能更好的选择符合业务场景的技术
  • 充分利用服务器的硬件资源
  • 降低维护成本和难度
  • 提高应用系统的稳定性

分布式服务和微服务的差别

分布式服务不一定是微服务应用,分布式服务是部署在不同的机器上的。一个服务可能负责几个功能,是一种面向 SOA 架构,服务之间也是通过 RPC 来交互或者 webService 来交互,系统应用部署在超过一台服务器或虚拟机上。且各自分开部署的部分彼此通过各种通讯协议交互信息,就可以算作分布式部署。

生产环境下的微服务肯定是分布式部署的,分布式部署的应用不一定是微服务架构,比如集群部署,它是把相同应用赋值到不同服务器上,逻辑功能还是单体应用。简单说微服务就是很小的服务,小到一个服务只对一个单一的功能,只做一件事。这个服务可以单独部署运行,服务之间通过 RPC 来交互。每个微服务都是由独立的小团队开发,测试,部署,上线,负责它的整个生命周期。

RPC 调用原理


序列化和反序列化

序列化:把 Java 对象转变成二进制数据
反序列化:把二进制数据转变成 Java 对象

注意:在反序列的过程中,需要知道该二进制数据还原成哪几个 Java 对象,因此必须要有序列化 ID 号,在 Java 规范中,必须实现 Serializable 接口开启序列化功能

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
@Setter
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private Long id;
private String name;
}

public class App {
@Test
public void test1() throws Exception {
User u = new User(10L, "逍遥");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("f:/user.txt"));
// 序列化
out.writeObject(u);
out.close();
}

@Test
public void test2() throws Exception {
User u = null;
ObjectInputStream in = new ObjectInputStream(new FileInputStream("f:/user.txt"));
// 反序列化
u = (User) in.readObject();
in.close();
System.out.println(u);
}
}

流程图

RPC 远程调用底层的核心技术就是序列化和反序列化

  1. 生产者发布服务
  2. 消费者按照 RPC 协议要求调用生产者发布的服务
  3. 生产者执行方法得到返回的对象
  4. 生产者把对象序列化后返回给消费者
  5. 消费者接收到数据后对其反序列化,得到 Java 对象

Dubbo


Dubbo 是阿里巴巴公司开源的一个高性能优秀的服务框架,公国使用高性能的 RPC 实现服务的输出和输入功能,可以和 Spring 框架无缝集成。Dubbo 是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力

  1. 面向接口的远程方法调用
  2. 智能容错和负载均衡
  3. 服务自动注册和发现

核心组件:

  • Remoting: 网络通信框架,实现了 sync-over-async 和 reuqest-response 消息机制
  • RPC: 一个远程过程调用的接口,支持负载均衡、容灾和集群功能
  • Registry: 服务目录框架用于服务的注册和服务事件的发布和订阅

0 start ,生产者启动 Dubbo 容器,生产服务对象
1 register, 生产者把生成的服务发布到注册中心
2 消费者想注册中心订阅服务,把发布的服务下载到本地缓存
3 订阅服务后,注册中心会通知本地缓存会自动更新生产者发布的服务
4 当消费者需要调用服务时,按照 RPC 协议要求,向生产者发起服务的调用
5 生产者把返回的对象交给 Dubbo 容器进行序列化处理后返回给消费者,消费者接受到返回的数据后对其反序列化,得到 Java 对象,监视器对服务性能做监控统计。

注意:注册中心和监视器都不是必须的,可以缺少。如:缺少注册中心后,消费者就不能自动更新生产者发布的服务信息,当生产者信息发生改变时,消费者很可能调用服务失败。比较出名的注册中心有:zookeeper,eruka

微服务项目结构


上图中可以看出,至少需要创建5个项目

  • product-api: 定义商品相关的业务方法
  • member-api: 定义会员相关的业务方法
  • product-sever: 商品服务的提供者,同时也是会员服务的消费者,底层会去调用会员服务的相关功能
  • member-sever: 会员服务的提供者,提供会员业务方法的实现
  • website: 商品服务的消费者,使用商品服务提供的业务功能

Zookeeper


zookeeper 的功能非常多,大数据技术领域的核心组件之一,我们仅仅只是作为注册中心使用,没有注册中心的情况下,dubbo 也可以正常的运行。默认端口:2181

安装

1
brew install zookeeper

安装好后

1
2
cd /usr/local/etc/zookeeper
cat zoo.cfg
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
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/usr/local/var/run/zookeeper/data
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the
# administrator guide before turning on autopurge.
#
# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1

启动 zookeeper,同 brew 下载的其他服务

1
2
3
brew services list
brew services start zookeeper
==> Successfully started `zookeeper` (label: homebrew.mxcl.zookeeper)

查看zookeeper状态

1
2
3
4
5
zkServer status

ZooKeeper JMX enabled by default
Using config: /usr/local/etc/zookeeper/zoo.cfg
Mode: standalone

standalone 表示单机模式。

Dubbo 微服务开发


案例:

商品服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) throws IOException {
// 1. 创建应用对象,设置应用的名称
ApplicationConfig applicationConfig = new ApplicationConfig("product-server");
// 2. 创建协议对象,配置协议信息
ProtocolConfig dubbo = new ProtocolConfig("dubbo", 20880);
// 3. 创建配置中心的注册对象,配置注册中心的地址
RegistryConfig registryConfig = new RegistryConfig("zookeeper://127.0.0.1:2181");
// 4. 创建服务发布对象,设置相关参数
ServiceConfig<IProductService> config = new ServiceConfig<>();
config.setApplication(applicationConfig);
config.setProtocol(dubbo);
config.setRegistry(registryConfig);
config.setInterface(IProductService.class);
config.setRef(new ProductServiceImpl());
// 5. 发布服务
config.export();

// 模拟服务器一直运行
System.in.read();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
// 1. 创建应用对象,设置应用名称
ApplicationConfig applicationConfig = new ApplicationConfig("website");
// 2. 创建配置中心注册对象,配置注册中心地址
RegistryConfig registryConfig = new RegistryConfig("zookeeper://127.0.0.1:2181");
// 3. 创建引用对象配置对象,设置需要引用的服务
ReferenceConfig<IProductService> ref = new ReferenceConfig<>();
ref.setApplication(applicationConfig);
ref.setRegistry(registryConfig);
ref.setInterface(IProductService.class);
// 如果没有注册中心,则需要配置 URL 地址
// ref.setUrl("服务地址");

// 4. 获取服务引用的动态代理对象
IProductService productService = ref.get();
Product product = productService.get(1l, null);
System.out.println(product);
}

SpringBoot 集成 Dubbo


website.pom.xml

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
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
<version>2.7.0</version>
</dependency>

<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.0</version>
</dependency>

<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
<!--依赖商品服务的API-->
<dependency>
<groupId>cn.lizhaoloveit</groupId>
<artifactId>product-api</artifactId>
<version>1.0</version>
</dependency>

product-server.pom.xml

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
<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>
<version>2.7.0</version>
</dependency>

<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.0</version>
</dependency>

<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>cn.lizhaoloveit</groupId>
<artifactId>product-api</artifactId>
<version>1.0</version>
</dependency>

<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.41.Final</version>
</dependency>

用一个类来模拟数据库
product-server.productData.java

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class ProductData {
private static Map<Long, Product> datas = new HashMap<>();

static {
datas.put(1L, new Product(1L, "IPhone XR"));
datas.put(2L, new Product(2L, "华为P30"));
datas.put(3L, new Product(3L, "小米9"));
}
public static Product get(Long id) {
return datas.get(id);
}
}

在上面使用 Dubbo 微服务开发的时候,创建的所有配置项,都在 application.properties 中重新配置。

1
2
3
4
5
6
7
8
9
10
# 配置生产者的发布信息
dubbo.application.name=product-server
# 配置协议和端口
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880
# 配置注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181

# 允许 bean 的定义被覆盖
spring.main.allow-bean-definition-overriding=true

最后一行配置的意思是因为,在 dubbo 内部默认会创建一个初始化的空的 config 的 bean。而我们重新配置发布者信息、协议和端口、地址后,要允许新的配置 bean 会覆盖原有的配置 bean。

@EnableDubbo

在生产者的 springboot 配置类中要加上 @EnableDubbo 注解。

1
2
3
4
5
6
7
@SpringBootApplication
@EnableDubbo
public class ProductServer {
public static void main(String[] args) {
SpringApplication.run(ProductServer.class, args);
}
}

@EnableDubbo 注解是 @EnableDubboConfig@DubboComponenetScan 两者组合的注解。

通过 @EnableDubbo 可以在指定的包名下(scanBasePackages) 或者 (scanBasePackageClasses) 指定的类名下,扫描 Dubbo 服务提供者,以 @Service 注解标注,以及 Dubbo 的服务消费者 以 @Reference 注解标注。

扫描到 Dubbo 的服务提供方和消费者之后,对其做响应的组装并初始化,最终完成服务暴露和引用的工作。

@Service

@Service 来配置 Dubbo 的服务提供方

1
2
3
4
5
6
@Service
public class AnnotatedGreetingService implements GreetingService {
public String sayHello(String name) {
return "hello, " + name;
}
}

通过 @Service 提供的属性可以定制化 Dubbo 服务提供者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.apache.dubbo.config.annotation;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE}) // #1
@Inherited
public @interface Service {
Class<?> interfaceClass() default void.class; // #2
String interfaceName() default ""; // #3
String version() default ""; // #4
String group() default ""; // #5
boolean export() default true; // #6
boolean register() default true; // #7

String application() default ""; // #8
String module() default ""; // #9
String provider() default ""; // #10
String[] protocol() default {}; // #11
String monitor() default ""; // #12
String[] registry() default {}; // #13
}
  1. @Service 贴在服务的实现类上,表示服务的具体实现
  2. interfaceClass 指定服务提供者的 api 类
  3. interfaceName 指定服务提供者的 api 类名
  4. version 服务的版本号
  5. group 指定服务的分组
  6. export 是否暴露服务
  7. registry 是否向注册中心注册服务
  8. application 应用配置
  9. module 模块配置
  10. provider 服务提供者配置
  11. protocol 协议配置
  12. monitor 监控中心配置
  13. registry 注册中心配置

application module provider protocal monitor registry (8->13) 需要提供的是对应的 Spring bean 的名字,这些 bean 的组装要么通过传统的 XML 配置方式完成,要么通过 Java Config 完成。如果用 springboot 集成项目,会在 application.properties 中进行相应配置,并且 springboot 对其已经有了默认的配置项。

@Reference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.apache.dubbo.config.annotation;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) // #1
public @interface Reference {
Class<?> interfaceClass() default void.class; // #2
String interfaceName() default ""; // #3
String version() default ""; // #4
String group() default ""; // #5
String url() default ""; // #6

String application() default ""; // #7
String module() default ""; // #8
String consumer() default ""; // #9
String protocol() default ""; // #10
String monitor() default ""; // #11
String[] registry() default {}; // #12
}

其中比较重要的有

  1. @Reference 可以定义在类中的一个字段上,也可以定义在一个方法上,甚至可以用来修饰另一个 annotation 表示一个服务的引用。通常 @Reference 定义在一个字段上。
  2. interfaceClass:指定服务的 interface 的类
  3. interfaceName:指定服务的 interface 的类名
  4. version:指定服务的版本号
  5. group:指定服务的分组
  6. url:通过指定服务提供方的 URL 地址直接绕过注册中心发起调用
  7. application:应用配置
  8. module:模块配置
  9. consumer:服务消费方配置
  10. protocol:协议配置
  11. monitor:监控中心配置
  12. registry:注册中心配置

另外,需要注意的是,application、module、consumer、protocol、monitor、registry(从 7 到 12)需要提供的是对应的 spring bean 的名字,而这些 bean 的组装要么通过传统的 XML 配置方式完成,要么通过现代的 Java Config 来完成。在本文中,将会展示 Java Config 的使用方式。springboot 中会有默认的配置项,在 application.properties 中完成配置。

ProductServiceImpl.java

1
2
3
4
5
6
7
@Service
public class ProductServiceImpl implements IProductService {
@Override
public Product get(Long productId, Long userId) {
return ProductData.get(productId);
}
}

ProductController.java

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("product")
public class ProductController {

@Reference
private IProductService productService;

@RequestMapping("get")
public Object get(Long productId, Long userId) {
return productService.get(productId, userId);
}
}

一个服务提供者,调用其他服务提供者的服务
加入会员服务
member-api、member-server

1
2
3
4
5
6
7
8
9
10
11
// 模拟数据库
public abstract class UserData {
private static Map<Long, User> datas = new HashMap<>();
static {
datas.put(1L, new User(1L, "逍遥"));
datas.put(2L, new User(2L, "bunny"));
}
public static User get(Long id) {
return datas.get(id);
}
}

application.properties

1
2
3
4
5
6
7
8
9
10
# 配置生产者的发布信息
dubbo.application.name=member-server
# 配置协议和端口
dubbo.protocol.name=dubbo
dubbo.protocol.port=20881
# 配置注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181

# 允许 bean 的定义被覆盖
spring.main.allow-bean-definition-overriding=true

UserServiceImpl.java

1
2
3
4
5
6
7
@Service
public class UserServiceImpl implements IUserService {
@Override
public User get(Long id) {
return UserData.get(id);
}
}

在 ProductServiceImpl.java 中远程获取 member-server 服务中的 实现类 bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class ProductServiceImpl implements IProductService {

@Reference
private IUserService userService;

@Override
public Product get(Long productId, Long userId) {
Product product = ProductData.get(productId);
User user = userService.get(userId);
product.setUser(user);
return product;
}
}

最后效果

事务问题


A服务开启事务,对B服务发起远程调用,B服务调用也开启了一个事务,然后进行业务操作,业务正常执行,最终提交事务,A服务拿到远程调用的结果继续执行,但是后面出现异常了
这时 A 的事务回滚能影响 B 服务中的事务吗?

事务 A 和 事务 B 是分别在不同机器上开启的事务,相互独立,是不同的两个事务,在传统的事务管理方式是不能应用在分布系统高德,分布式系统有专门的分布式事务处理方式,强一致性、最终一致性。

Dubbo Admin 控制台


dubbo Admin 是个 springboot 项目
github源码地址

java -jar dubbo-admin.jar

默认端口号是 7001,用户名和密码都是 root,访问 localhost:7001 访问

服务交叉引用问题


在实际开发中,经常会存在 A 服务引用 B 服务,B 服务也引用 A 服务,那么此时就存在问题,无论哪个服务在启动时都会报错。此时可以设置消费者项目启动时不要去检查服务是否存在,就可以顺利启动项目了,但如果运行时,服务依然不存在,则会报错。

applicaiton.properties

1
2
# 消费者不检测服务是否存在
dubbo.consumer.check=false

服务集群


如果一个服务只有一个服务对象,所有压力都落在这个对象上,逼近极限时,很可能会导致服务挂掉,我们可以通过多发布几个服务对象,通过负载均衡策略来缓解单一服务对象压力过大问题。

  • 生产者多发布几个服务对象,注意修改多个服务发布的端口
1
2
3
# 生产者发布端口
dubbo.protocol.port=20880 # 生产者 A
dubbo.protocol.port=20883 # 生产者 B
  • 消费者修改负载均衡策略
1
2
3
4
RandomLoadBalance: 随机(random),默认策略
RoundRobinLoadBalance: 轮询(roundrobin)
ConsistentHashLoadBalance:hash一致(consistenthash)
LeastActiveLoadBalance: 最少活跃(leastactive)

application.properties

1
2
3
# application.properties
# 修改消费者负载均衡策略
dubbo.consumer.loadbalance=roundrobin

多版本发布


服务升级时,由于不清楚新版本的服务是否存在 bug,往往采取国度的方式进行切换,此时需要两个版本的服务都要存在。

生产者在生产服务的时候指定该服务的版本号

1
2
3
4
5
@Service(version="1.0")
public class UserServiceImpl implements IUserService {...}

@Service(version="2.0")
public class UserServiceImpl implements IUserService {...}

消费者必须明确告知引用哪个版本的服务

1
2
3
4
5
6
7
8
@Reference(version="1.0")
private IUserService userService;

@Reference(version="2.0")
private IUserService userService;

@Reference(version="*") // 随机引用
private IUserService userService;

服务超时,重试,容错


服务调用时,可能由于服务生产者的网络环境差,但消费者不知道,依然请求,长时间没有回应,此时可以设置消费者等待的超时时间,调用超过设置的时间时放弃远程的响应。默认超时1秒。超时时,框架并不会马上放弃服务的调用,还会进行重试,重试次数:2次

1
2
3
4
# 消费者设置超时 1.5s
dubbo.consumer.timeout=1500
# 消费者设置重试次数,重试1次
dubbo.consumer.retries=1

只有幂等性操作才能重试,非幂等性操作不能重试。幂等性的意思是服务执行一次和执行多次结果一样。
此时因超时调用失败,出现的报错页面会直接反馈给消费者,消费者再把报错信息响应出去,用户会直接看到错误页面,这样不友好,应该对错误信息进行统一处理。

服务集群后还能配置集群下的容错机制,

FailoverCluster: 失败自动切换,默认策略,用于幂等性操作,查询
FailfastCluster: 快速失败,只发一次调用,失败立即报错,用于非幂等性操作,插入
FailsafeCluster: 失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作
FailbackCluster: 失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作
ForkingCluster: 并行调用多个服务器,只要一个成功即返回,通常用于实时性要求较高的读操作,但需要浪费更多服务资源,可以通过 fork=”2” 来设置最大并行数。
BroadcastCluster: 广播调用所有提供者,逐个调用,任意一台报错则报错,通常用于通知所有提供者更新缓存或日志等本地资源信息。

1
2
# 消费者配置服务集群容错策略
dubbo.consumer.cluster=failfast

服务降级


为了保证服务 B 的抗压能力,牺牲服务 A,甚至直接把 服务 A 功能关掉,把资源留给服务 B 使用,从 Dubbo Admin 控制台去配置当前服务的降级,消费者访问降级的服务时,不发起远程调用请求,直接返回 null。

文章作者: Ammar
文章链接: http://lizhaoloveit.cn/2019/10/10/Dubbo/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ammar's Blog
打赏
  • 微信
  • 支付宝

评论