Dubbo 基础概念

博文目录


Dubbo 2.7 用法示例

URL

URL也就是Uniform Resource Locator,中文叫统一资源定位符。Dubbo中无论是服务消费方,或者服务提供方,或者注册中心。都是通过URL进行定位资源的。

标准的URL格式如下:protocol://username:password@host:port/path?key=value&key=value

  • protocol:一般是 dubbo 中的各种协议 如:dubbo、thrift、http、zk
  • username/password:用户名/密码
  • host/port:主机IP地址/端口号
  • path:接口名称
  • parameter:参数键值对

举例

dubbo://192.168.1.6:20880/moe.cnkirito.sample.HelloService?timeout=3000
描述一个 dubbo 协议的服务

zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=demo-consumer&dubbo=2.0.2&interface=org.apache.dubbo.registry.RegistryService&pid=1214&qos.port=33333&timestamp=1545721981946
描述一个 zookeeper 注册中心

consumer://30.5.120.217/org.apache.dubbo.demo.DemoService?application=demo-consumer&category=consumers&check=false&dubbo=2.0.2&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=1209&qos.port=33333&side=consumer&timestamp=1545721827784
描述一个消费者

dubbo中用6个要素来区分不同的服务提供者, 协议/地址/端口/服务/分组/版本, 稍微有一点不同, 就是两个不同的服务

不同粒度配置的覆盖关系

不同粒度配置的覆盖关系

以 timeout 为例,下面是配置的查找顺序,其它 retries, loadbalance, actives 等类似:

  • 方法级优先,接口级次之,全局配置再次之。
  • 如果级别一样,则消费方优先,提供方次之。

其中,服务提供方配置,通过 URL 经由注册中心传递给消费方。

(建议由服务提供方设置超时,因为一个方法需要执行多长时间,服务提供方更清楚,如果一个消费方同时引用多个服务,就不需要关心每个服务的超时设置)。

理论上 ReferenceConfig 中除了interface这一项,其他所有配置项都可以缺省不配置,框架会自动使用ConsumerConfig,ServiceConfig, ProviderConfig等提供的缺省配置。

直连提供者

Dubbo 中点对点的直连方式。在开发自测时,经常需要绕过注册中心,只测试指定服务提供者,这时候可能需要点对点直连,点对点直连方式,将以服务接口为单位,忽略注册中心的提供者列表,A 接口配置点对点,不影响 B 接口从注册中心获取列表。

线上禁止使用这种方式

<dubbo:reference id="xxxService" interface="com.alibaba.xxx.XxxService" url="dubbo://localhost:20890" />
# 这样不知道可不可以?
dubbo.reference.com.foo.BarService.url=dubbo://localhost:20890

只订阅不注册

为方便开发测试,经常会在线下共用一个所有服务可用的注册中心,这时,如果一个正在开发中的服务提供者注册,可能会影响消费者不能正常运行。

可以让服务提供者开发方,只订阅服务(开发的服务可能依赖其它服务),而不注册正在开发的服务,通过直连测试正在开发的服务。

<dubbo:registry address="10.20.153.10:9090" register="false" />

多协议

不同服务不同协议

不同服务在性能上适用不同协议进行传输,比如大数据用短连接协议,小数据大并发用长连接协议

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd"> 
    <dubbo:application name="world"  />
    <dubbo:registry id="registry" address="10.20.141.150:9090" username="admin" password="hello1234" />
    <!-- 多协议配置 -->
    <dubbo:protocol name="dubbo" port="20880" />
    <dubbo:protocol name="rmi" port="1099" />
    <!-- 使用dubbo协议暴露服务 -->
    <dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService" protocol="dubbo" />
    <!-- 使用rmi协议暴露服务 -->
    <dubbo:service interface="com.alibaba.hello.api.DemoService" version="1.0.0" ref="demoService" protocol="rmi" /> 
</beans>

多协议暴露服务

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
    <dubbo:application name="world"  />
    <dubbo:registry id="registry" address="10.20.141.150:9090" username="admin" password="hello1234" />
    <!-- 多协议配置 -->
    <dubbo:protocol name="dubbo" port="20880" />
    <dubbo:protocol name="hessian" port="8080" />
    <!-- 使用多个协议暴露服务 -->
    <dubbo:service id="helloService" interface="com.alibaba.hello.api.HelloService" version="1.0.0" protocol="dubbo,hessian" />
</beans>
# Dubbo Registry
dubbo.registry.address=zookeeper://localhost:2181
# Dubbo Protocol
# 下面是默认一个协议的配置方法, 大多都是这种使用方式
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880

# 下面是多个协议的配置方法, 启动时每个服务和每个协议做笛卡尔积组合, 即代码只有1, 但服务在20881-20883端口上各启动1个共3个
# name可以是不同的协议, 比如1个http, 2个dubbo. 
# 多协议配置下, 可以使用 protocol 指定其中的一个(如p1/dubbo1), 就不会每种协议都提供服务了
dubbo.protocols.p1.id=dubbo1
dubbo.protocols.p1.name=dubbo
dubbo.protocols.p1.port=20881
dubbo.protocols.p1.host=0.0.0.0
dubbo.protocols.p2.id=dubbo2
dubbo.protocols.p2.name=dubbo
dubbo.protocols.p2.port=20882
dubbo.protocols.p2.host=0.0.0.0
dubbo.protocols.p3.id=dubbo3
dubbo.protocols.p3.name=dubbo
dubbo.protocols.p3.port=20883
dubbo.protocols.p3.host=0.0.0.0

服务组

使用服务分组区分服务接口的不同实现。当一个接口有多种实现时,可以用 group 区分。

服务端提供

<dubbo:service group="feedback" interface="com.xxx.IndexService" />
<dubbo:service group="member" interface="com.xxx.IndexService" />

消费端引用

<dubbo:reference id="feedbackIndexService" group="feedback" interface="com.xxx.IndexService" />
<dubbo:reference id="memberIndexService" group="member" interface="com.xxx.IndexService" />

消费端引用任意组

# 总是只调一个可用组的实现, 可能是通过某种机制来调用某一个实现接口的服务
<dubbo:reference id="barService" interface="com.foo.BarService" group="*" />

服务版本

为同一个服务配置多个版本

服务提供者

<dubbo:service interface="com.foo.BarService" version="1.0.0" />
<dubbo:service interface="com.foo.BarService" version="2.0.0" />

服务消费者

<dubbo:reference id="barService" interface="com.foo.BarService" version="1.0.0" />
<dubbo:reference id="barService" interface="com.foo.BarService" version="2.0.0" />

启动时检查

Dubbo 缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止 Spring 初始化完成,以便上线时,能及早发现问题,默认 check=“true”。

可以通过 check=“false” 关闭检查,比如,测试时,有些服务不关心,或者出现了循环依赖,必须有一方先启动。

如果 check=“false”,总是会返回引用,当服务恢复时,能自动连上。

dubbo.reference.com.foo.BarService.check=false
# 强制改变所有 reference 的 check 值,就算配置中有声明,也会被覆盖
dubbo.reference.check=false
# 设置 check 的缺省值,如果配置中有显式的声明,如:<dubbo:reference check="true"/>,不会受影响
dubbo.consumer.check=false
# 前面两个都是指订阅成功,但提供者列表是否为空是否报错,如果注册订阅失败时,也允许启动,需使用此选项,将在后台定时重试
dubbo.registry.check=false

集群容错

集群调用失败时,Dubbo 提供的容错方案。缺省为 failover 重试。

注意必须是同一个服务有多个提供者时,才会有集群容错。使用 cluster 来配置

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

负载均衡

loadbalance=random/roundrobin/leastactive/consistenthash

消费端做负载均衡, 即局部负载均衡, 这样不影响服务调用新能. 服务端做的话太难了, 需要由注册中心来做, 每次调用都要查询和更新, 压力太大

假如有两个服务提供者a和b, 应用c调用ab时启用了负载均衡, 应用d直连a调用a不调用b, 从局部来看, c调用ab是负载均衡的, 从全局来看, a的压力要比b大, cd互不影响

  • Random: 随机,按权重设置随机概率, 默认策略
    权重大的, 随机到的概率就大一点, 假如 a权重9, b权重1, 则随机到a的概率是a的权重除以总权重即0.9, b的概率是0.1,

  • RoundRobin: 轮询,按公约后的权重设置轮询比率
    轮询就是多个服务提供者, 一个一个来执行请求. 假如有abc3个节点提供同一个服务, 其性能比例a🅱️c=2:1:1

    • 普通轮询算法: 执行方案可能是 {a,b,c}
    • 加权轮询算法: 配置a的权重是200, b和c的权重是100, 执行方案可能是 {a,a,b,c}
    • 平滑加权轮询算法: 配置a的权重是200, b和c的权重是100, 执行方案可能是 {a,b,a,c}
  • LeastActive: 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差, 使慢的提供者收到更少请求
    消费者缓存每个服务提供者的信息, 每个服务提供者有一个active字段来代表当前消费者正在调用的线程数, 调用前加1, 调用后减1, 调用时找一个active字段最小的, 说明该服务提供者正被调用中的线程少
    我认为这个算法比较好, 一定程度上能够感知到服务端的压力, 动态调整选择的服务提供者

  • ConsistentHash: 一致性 Hash,相同参数的请求总是发到同一提供者. 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动
    简单理解就是将参数的某部分通过某种hash算法算出一个数字, 然后用服务提供者的个数取余, 余数就是服务提供者的索引, 然后就按照余数去访问对应的服务提供者即可

服务降级

可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略

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

服务消费者在调用某个服务提供者时,如果该服务提供者报错了,所采取的措施。就是 Mock

集群容错和服务降级的区别在于:

  1. 集群容错是整个集群范围内的容错
  2. 服务降级是单个服务提供者的自身容错

超时时间

消费端做服务超时限制, 服务端也可以配置, 但是并不会中断服务端线程的执行, 至多执行结束后如果超时则打印日志

超时时间配置优先级顺序:方法>接口>全局, 同级消费端配置优先

举例, 消费端配置方法3秒超时, 服务端配置方法5秒超时, 方法实际执行7秒, 则消费端3秒报错, 服务端7秒执行完毕并打印超时日志(warn)

本地存根和本地伪装

Dubbo 本地存根和本地伪装

为了进一步的方便用户做 Dubbo 开发,框架提出了本地存根 Stub 和本地伪装 Mock 的概念。通过约定大于配置的理念,进一步的简化了配置,使用起来更加方便,并且不依赖额外的 AOP 框架就达到了 AOP的效果。

本地存根

本地存根

消费端实现一个装饰器类, 实现服务接口并持有服务接口的引用, 代理远程接口, 强化接口功能, 相当于Aop的around, 可以在真正调用远程接口前做参数校验, 后做结果缓存等额外的操作

一般我们系统中集成二方三方远程接口时, 都会对接口做一层封装, 以满足我们当前系统的规范, 通常这里都会做参数校验, 异常处理等操作, 和本地存根一致

本地伪装

本地伪装

消费端实现服务接口, 当消费端调用服务最终失败时, 在没有对服务调用使用 try-catch 包裹的情况下, 也能够返回一个预设默认值

异步执行和异步调用

从 2.7.0 开始,Dubbo 的所有异步编程接口开始以 CompletableFuture 为基础

java.util.concurrent.CompletableFuture
Dubbo 同步调用太慢,也许你可以试试异步处理

异步执行

异步执行

说的是服务提供方异步执行

通过 return CompletableFuture.supplyAsync() ,业务执行已从 Dubbo 线程切换到业务线程,避免了对 Dubbo 线程池的阻塞。

// 服务接口定义
public interface AsyncService {
    CompletableFuture<String> sayHello(String name);
}
// 服务实现
public class AsyncServiceImpl implements AsyncService {
    @Override
    public CompletableFuture<String> sayHello(String name) {
        RpcContext savedContext = RpcContext.getContext();
        // 建议为supplyAsync提供自定义线程池,避免使用JDK公用线程池
        return CompletableFuture.supplyAsync(() -> {
            System.out.println(savedContext.getAttachment("consumer-key1"));
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "async response from provider.";
        });
    }
}

异步调用

异步调用

说的是服务消费方异步调用

基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小。
在这里插入图片描述

// 调用远程服务, 直接返回 CompletableFuture
CompletableFuture<String> future = asyncService.sayHello("async call request");
// 增加回调
future.whenComplete((v, t) -> {
    if (t != null) {
        t.printStackTrace();
    } else {
        System.out.println("Response: " + v);
    }
});
// 早于结果输出
System.out.println("Executed before response return.");

参数回调

参数回调

通过参数回调从服务器端调用客户端逻辑

参数回调方式与调用本地 callback 或 listener 相同,只需要在 Spring 的配置文件中声明哪个参数是 callback 类型即可。Dubbo 将基于长连接生成反向代理(dubbo协议是长连接),这样就可以从服务器端调用消费端逻辑。

假设有如下服务 FooService

public interface FooService {
	public String bar(String name, String key, CallbackListener callback);
}
// 其中回调函数 CallbackListener 结构如下
public interface CallbackListener {
	public void callback(String data);
}

FooService 的实现

// 指定了第三个参数是回调参数
// 服务端传了一个CallbackListener接口的实现类实例, 服务端这里收到的其实是Dubbo处理过的一个代理对象
// 其功能就是生成一个Invocation对象发送到消费端, invocation对象包含传给消费端的参数和指定要调用的方法, 然后用该参数调用实现类实例的对应方法
// callbacks = 3, 该服务最大同时支持回调数, 超过的会报错?
@Service(
	method = {
		@Method(name = "bar", arguments = {
			@Argument(index = 2, callback = true)
		}),
	callbacks = 3
})
public class FooServiceImpl implements FooService {
	@Override
	public String bar(String name, String key, CallbackListener callback) {
		// 执行业务
		...
		// 执行回调
		callback.callback("server:" + name);
	}
}

消费方调用服务

@Reference
private FooService fooService;

public String bar() {
	String bar1 = fooService.bar("mrathena", "1", new CallbackListenerImpl());
	String bar2 = fooService.bar("mrathena", "2", new CallbackListenerImpl2());
	return bar1 + bar2;
}

泛化调用和泛化服务

泛化调用

使用泛化调用

说的是服务消费方泛化调用

使用 org.apache.dubbo.rpc.service.GenericService 可以替代所有接口引用

假如有一个服务提供者

public interface DemoService {
	String foo(String bar);
	String bar(User user);
}

常规调用方式

@Reference(version = "xxx")
private DemoService demoService;

public Object foo() {
	return demoService.foo("parameter");
}

泛化调用方式

@Reference(id = "demoService", interfaceName = "com.xxx.DemoService", version = "xxx", generic = true)
private GenericService genericService;

public Object foo() {
	return genericService.$invoke("foo", new String[]{"java.lang.String"}, new Object[]{"parameter"});
}
public Object bar() {
	// 用Map表示POJO参数,如果返回值为POJO也将自动转成Map
	Map<String, Object> user = new HashMap<>();
	user.put("username", "foo");
	user.put("password", "bar");
	return genericService.$invoke("bar", new String[]{"com.xxx.User"}, new Object[]{user});
}

使用泛化调用方式时, 无需引入pom, 无需引入DemoService而是引入泛化服务GenericService, @Reference注解需要指定要代理的服务类interfaceName, 并指定接口为泛化调用 generic = true

如果是通过ApplicationContext.getBean()的方式, 如下

GenericService genericService = (GenericService) applicationContext.getBean("demoService");

泛化服务

实现泛化调用

说的是服务提供方泛化服务

一个服务, 通过实现 GenericService 接口处理所有服务请求, 然后把该服务暴露为指定的服务

// 把服务暴露为 DemoService, 消费方可通过泛化调用方式调用该服务
@Service(interfaceName = "com.xxx.DemoService", version = "xxx")
public class GenericDemoService implements GenericService {
	@Override
	public Object $invoke(String s, String[] strings, Object[] objects) throws GenericException {
		if ("foo".equals(s)) {
			return "foo";
		}
		return "bar";
	}
}

REST 支持

REST 快速入门

dubbo 支持使用多种协议暴露一个服务, 如dubbo和rest等

当我们用Dubbo提供了一个服务后,如果消费者没有使用Dubbo也想调用服务,那么这个时候我们就可以让我们的服务支持REST协议,这样消费者就可以通过REST形式调用我们的服务了。

注意:如果某个服务只有REST协议可用,那么该服务必须用@Path注解定义访问路径

控制台

dubbo-admin

可以查看服务信息, 版本信息, 组信息, 服务提供者, 服务治理(条件路由/标签路由/黑白名单/动态配置/权重调整/负载均衡)等功能

README_ZH.md 里有详细的使用说明, 默认账号密码为 root/root

dubbo 注册中心和配置中心可以分开的, 默认配置中心使用注册中心的zookeeper

配置管理

配置管理: 配置的是 application.properties 中 dubbo 相关的内容, 可以创建多个dubbo配置, 使用应用名或global来区分, 当dubbo应用指定了配置中心地址时, 将从配置中心中获取配置. 当应用中有配置且全局配置中也有配置, 则以全局配置为准

动态配置

服务治理-动态配置(服务治理-指定服务-更多-动态配置): 配置的是服务上的参数(timeout等), 可以指定配置到服务端/消费端, 实时生效

服务治理-权重调整/负载均衡: 本质上还是动态配置, 相当于动态配置中权重调整和负载均衡的快捷方式, 因为经常要用, 为了使用更方便, 就单独放出来了

服务路由

路由规则

条件路由

消费端从m个提供者中选中n个, 然后再做负载均衡

标签路由

标签路由通过将某一个或多个服务的提供者划分到同一个分组,约束流量只在指定分组中流转,从而实现流量隔离的目的,可以作为蓝绿发布、灰度发布等场景的能力基础。

判断消费者发送的请求中是否带有标签, 有的话找到该标签对应的服务提供者, 调用对应服务

消费者请求时添加标签: RpcContext.getContext().setAttachment(Constants.REQUEST_TAG_KEY,"tag1");,

举例

  • 搞一个拦截器, 取请求中的号码判断是否为名单用户, 是的话打标签tag1, 不是的话打标签tag2, 访问不通的机器
  • 生产集群中先在一台机器上发布新功能, 通过标签让测试用户访问该机器, 其他用户访问其他机器, 测试好再重新配置
发版方式

什么是蓝绿部署、滚动发布和灰度发布?

  1. 蓝绿发布: 应用集群分为ab两部分, 先把a从负载均衡中移除并发布新版本, 好了后把a加入负载均衡, 然后把b从负载均衡中移除并发布新版本, 好了后把b加入负载均衡, 发布完成
    • 如果出问题,影响范围较大;
    • 发布策略简单;
    • 用户无感知,平滑过渡;
    • 升级/回滚速度快。
  2. 灰度发布: 灰度发布只升级部分服务,即让一部分用户继续用老版本,一部分用户开始用新版本,如果用户对新版本没什么意见,那么逐步扩大范围,把所有用户都迁移到新版本上面来。灰度发布是通过切换线上并存版本之间的路由权重,逐步从一个版本切换为另一个版本的过程。
    • 保证整体系统稳定性,在初始灰度的时候就可以发现、调整问题,影响范围可控;
    • 新功能逐步评估性能,稳定性和健康状况,如果出问题影响范围很小,相对用户体验也少;
    • 用户无感知,平滑过渡。
    • 自动化要求高
  3. 滚动发布: 滚动发布是指每次只升级一个或多个服务(百分比),升级完成后加入生产环境,不断执行这个过程,直到集群中的全部旧版本升级新版本。

综上所述,三种方式均可以做到平滑式升级,在升级过程中服务仍然保持服务的连续性,升级对外界是无感知的。那生产上选择哪种部署方法最合适呢?这取决于哪种方法最适合你的业务和技术需求。如果你们运维自动化能力储备不够,肯定是越简单越好,建议蓝绿发布,如果业务对用户依赖很强,建议灰度发布。如果是K8S平台,滚动更新是现成的方案,建议先直接使用。

  • 蓝绿发布:两套环境交替升级,旧版本保留一定时间便于回滚。
  • 灰度发布:根据比例将老版本升级,例如80%用户访问是老版本,20%用户访问是新版本。
  • 滚动发布:按批次停止老版本实例,启动新版本实例。

Zookeeper 可视化工具

ZooInspector: java -jar zookeeper-dev-ZooInspector.jar

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值