dubbo服务注册与发现
dubbo默认使用zk作为注册中心,也可以用redis等作为注册中心,下面分析一下怎么利用zk的?
zookeeper
先了解一下zk,zk其实类似于linux的文件目录结构,它的数据模型就是由多个ZNode组成的树节点,每个ZNode可存有data也可以有子节点。最重要的几个特性:
- 节点分为临时(Ephemeral)和永久(Persistent)的,临时的生命周期就是Session,当连接断开时,该Session内创建的所有ZNode都将自动删除。而永久的除非手动删除,否则一直存在;
- 节点可以顺序自动编号,分布式锁就是依靠这个特性实现的;
- 监听器模式Watcher,可以监听节点内容的变化和子节点数量的变化;
服务注册
dubbo在服务导出时,会将服务注册到zk上,其实就是在zk上建立一个ZNode,并且是临时的,这样做的目的是为了服务提供者下线后,该服务建立的的ZNode都将自动删除,从而实现失效自动踢出。dubbo用的zk客户端是CuratorZookeeperClient,下面展示一下dubbo在zk注册后,zk的结构图(假设有一个服务是DemoService,那服务提供者的节点路径就是DemoService/providers,data就是URL)
服务发现
dubbo服务发现的逻辑是在服务字典模块中的,白话讲就是服务字典存储了provider列表。
负载均衡前需要调用directory.list(invocation),而directory就是服务字典的对象,实现类有StaticDirectory 和 RegistryDirectory,就是RegistryDirectory实现了服务发现的功能。该类实现了NotifyListener接口,当注册中心的provider有变化时,将会回调这个接口通知consumer。
这个接口的具体实现逻辑:
1.RegistryDirectory缓存了一个MAP<method,invoker>;
2.notify方法就是写这个map(provider的上线下线)
3.list方法就是读这个map(list方法里还有服务路由的逻辑)
Invoker
Invoker 是由 ProxyFactory 创建而来,Dubbo 默认的 ProxyFactory 实现类是 JavassistProxyFactory.
调用链
- 当我们引用dubbo服务时(如使用@Reference注入),注入的是dubbo生成的代理类;具体过程如下:
- 服务引用的入口方法为 ReferenceBean 的 getObject 方法,该方法定义在 Spring 的 FactoryBean 接口中,ReferenceBean 实现了这个方法
- 该方法最重要的逻辑是createProxy,先构建Invoker实例,并生成代理类;代理类的方法调用其实是委托给Invoker。另外,filter链也是在此处初始化的;
- 当我们调用dubbo服务的某个方法时,过程如下:
(1)调用代理类方法
(2)代理类委托给InvokerInvocationHandler#invoke
(3)经过AbstractClusterInvoker#invoke,也就是负载均衡,选择其中一个服务提供者
(4)经过filter链处理
(5)DubboInvoker#doInvoke
(6)委托给Exchange层创建request并发起远程请求
(7)Exchange层包装request,然后委托给Transport层发送请求
(8)Transport层默认采用netty发起网络socket请求
proxy0#sayHello(String)
—> InvokerInvocationHandler#invoke(Object, Method, Object[])
—> MockClusterInvoker#invoke(Invocation)
—> AbstractClusterInvoker#invoke(Invocation)
—> FailoverClusterInvoker#doInvoke(Invocation, List<Invoker<T>>, LoadBalance)
—> ListenerInvokerWrapper#invoke(Invocation)
—> Filter#invoke(Invoker, Invocation) // 包含多个 Filter 调用
—> AbstractInvoker#invoke(Invocation)
—> DubboInvoker#doInvoke(Invocation)
—> ReferenceCountExchangeClient#request(Object, int)
—> HeaderExchangeClient#request(Object, int)
—> HeaderExchangeChannel#request(Object, int)
—> AbstractPeer#send(Object)
—> AbstractClient#send(Object, boolean)
—> NettyChannel#send(Object, boolean)
—> NioClientSocketChannel#write(Object)
面试问题
1. dubbo的rpc支持哪几种协议实现?
dubbo、http、rmi、hession、webservice、redis、thrift等,默认使用dubbo协议。
- Dubbo 协议
采用单一长连接和NIO异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。当需要传输大数据,请使用rmi或者http。
单一长连接:一个consumer和一个provider只建立一个连接,防止单个consumer把producer击垮,长连接是为了减少连接释放耗时,提高传输效率。
NIO异步通讯:复用线程池,单线程监听多个socket(多路复用),防止 C10K 问题 - hessian协议
多连接,短连接,同步传输,底层使用http协议,适用范围:传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件。 - http协议 多连接,短连接,同步传输,使用表单序列化(json或xml),适用范围:传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入参数,暂不支持传文件。
- rmi协议 多连接,短连接,同步传输,jdk标准二进制序列化,适用范围:传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件。
2. dubbo负载均衡有哪几种?原理是什么?
Dubbo 提供了4种负载均衡实现,分别是基于权重随机算法的 RandomLoadBalance(缺省)、基于最少活跃调用数算法的 LeastActiveLoadBalance、基于 hash 一致性的 ConsistentHashLoadBalance,以及基于加权轮询算法的 RoundRobinLoadBalance。这四种算法都是实现了LoadBalance接口,用户也可以自定义负载均衡实现。
3. dubbo集群容错有哪几种方式?
- Failover Cluster - 失败自动切换
FailoverClusterInvoker 在调用失败时,会自动切换 Invoker 进行重试。默认配置下,Dubbo 会使用这个类作为缺省 Cluster Invoker - Failfast Cluster - 快速失败
FailfastClusterInvoker 只会进行一次调用,失败后立即抛出异常。适用于幂等操作,比如新增记录。 - Failsafe Cluster - 失败安全
FailsafeClusterInvoker 是一种失败安全的 Cluster Invoker。所谓的失败安全是指,当调用过程中出现异常时,FailsafeClusterInvoker 仅会打印异常,而不会抛出异常。适用于写入审计日志等操作。 - Failback Cluster - 失败自动恢复
FailbackClusterInvoker 会在调用失败后,返回一个空结果给服务消费者。并通过定时任务对失败的调用进行重传,适合执行消息通知等操作 - Forking Cluster - 并行调用多个服务提供者
ForkingClusterInvoker 会在运行时通过线程池创建多个线程,并发调用多个服务提供者。只要有一个服务提供者成功返回了结果,doInvoke 方法就会立即结束运行。ForkingClusterInvoker 的应用场景是在一些对实时性要求比较高读操作(注意是读操作,并行写操作可能不安全)下使用,但这将会耗费更多的资源。
4. dubbo序列化默认用的是什么?为什么?
默认用的是hession2,支持的序列化框架还有kyro,fst,dubbo,jdk,protobuf以及jason序列化框架fastjson,gson;hession2相对于jdk序列化效率高很多,但是,即使不注册任何类,Kryo和FST的性能依然普遍优于hessian和dubbo序列化。 可以配置为Kryo和FST。dubbo 2.0之所以还默认是hession2,可能是为了兼容dubbo1.0,而且一般都是小数据量序列化影响不是特别大。 hession序列化的bug:1.会将Byte反序列化成Integer 2.当子类和父类拥有相同属性时,会导致反序列化时取不到改属性的值(实际取得是父类的值)
5. Dubbo的RpcContext是怎么传递的?
(1)隐式传参过程 RpcContext保存了当前调用的上下文信息,实际上是一个ThreadLocal变量
private static final InternalThreadLocal<RpcContext> LOCAL = new InternalThreadLocal<RpcContext>() {
@Override
protected RpcContext initialValue() {
return new RpcContext();
}
};
参考上面的调用链,在其中的ConsumerContextFilter中会初始化RpcContext
RpcContext.getContext()
.setInvoker(invoker)
.setInvocation(invocation)
.setLocalAddress(NetUtils.getLocalHost(), 0)
.setRemoteAddress(invoker.getUrl().getHost(),
invoker.getUrl().getPort());
然后在调用远程服务前,dubboInvoker会将RpcContext.getAttachments传给参数invocation,那么在服务提供端可以拿到invocation,拿到之后会在ContextFilter中将所有的参数重新赋值给服务端的RpcContext
Map<String, String> attachments = invocation.getAttachments();
if (attachments != null) {
attachments = new HashMap<String, String>(attachments);
attachments.remove(Constants.PATH_KEY);
attachments.remove(Constants.GROUP_KEY);
attachments.remove(Constants.VERSION_KEY);
attachments.remove(Constants.DUBBO_VERSION_KEY);
attachments.remove(Constants.TOKEN_KEY);
attachments.remove(Constants.TIMEOUT_KEY);
attachments.remove(Constants.ASYNC_KEY);// Remove async property to avoid being passed to the following invoke chain.
}
RpcContext.getContext()
.setInvoker(invoker)
.setInvocation(invocation)
// .setAttachments(attachments) // merged from dubbox
.setLocalAddress(invoker.getUrl().getHost(),
invoker.getUrl().getPort());
// mreged from dubbox
// we may already added some attachments into RpcContext before this filter (e.g. in rest protocol)
if (attachments != null) {
if (RpcContext.getContext().getAttachments() != null) {
RpcContext.getContext().getAttachments().putAll(attachments);
} else {
RpcContext.getContext().setAttachments(attachments);
}
}
(2)RpcContext是怎么在主线程和线程池之间传递的?
在服务提供端接受到请求后,准确的讲,nettyServer监听到channel写入数据时,首先会通过解码器对数据进行解码,解码后生成一个Request对象,该对象的数据主要为RpcInvocation(客户端传过来的),很多RpcContext的数据都存在RpcInvocation中,所以客户端的RpcContext是通过RpcInvocation传给服务端的,然后在服务端重新创建一个RpcContext。
解码后的对象传递给下一个入站处理器的指定方法。编解码过程我们先忽略,处理链的入口NettyHandler处理,接下去的处理链如下:
NettyHandler#messageReceived(ChannelHandlerContext, MessageEvent)
—> AbstractPeer#received(Channel, Object)
—> MultiMessageHandler#received(Channel, Object)
—> HeartbeatHandler#received(Channel, Object)
—> AllChannelHandler#received(Channel, Object)
—> ExecutorService#execute(Runnable) // 由线程池执行后续的调用逻辑
AllChannelHandler其实就是一个disPatcher委托的线程派发类,持有线程池ExecuterService
public class AllDispatcher implements Dispatcher {
public static final String NAME = "all";
@Override
public ChannelHandler dispatch(ChannelHandler handler, URL url) {
return new AllChannelHandler(handler, url);
}
}
根据不同的派发策略有不同的ChannelHandler,包括AllChannelHandler、MessageOnlyChannelHandler、 ExecutionChannelHandler和ConnectionOrderedChannelHandler,Direct策略没有对应的ChannelHandler,因为是直接在IO线程上执行。
到这里主线程的工作基本完成了,这里并没有看到RpcContext的任何信息;只是将Message解码组装成Request对象并传递给ChannelHandler。接下来我们看默认派发器AllChannelHandler的逻辑:
ExecutorService cexecutor = getExecutorService();
try {
cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));
}
封装了ChannelEventRunnable对象,该对象也是委托给DecodeHandler处理,该处理器只做解码逻辑,解码操作默认是在线程池完成,这个与选择的线程派发器有关系。
接下来交给HeaderExchangeHandler处理,在这里做请求校验、创建响应对象并将处理交给下层
Object result = handler.reply(channel, msg);
到目前为止transport、exchange层的逻辑已经处理完毕,接下去就是Protocol层,我们看下DubboProtocol的处理逻辑:
private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {
@Override
public Object reply(ExchangeChannel channel, Object message) throws RemotingException {
if (message instanceof Invocation) {
Invocation inv = (Invocation) message;
Invoker<?> invoker = getInvoker(channel, inv);
// need to consider backward-compatibility if it's a callback
//回调逻辑,先忽略
RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
return invoker.invoke(inv);
}
}
Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException {
boolean isCallBackServiceInvoke = false;
boolean isStubServiceInvoke = false;
int port = channel.getLocalAddress().getPort();
String path = inv.getAttachments().get(Constants.PATH_KEY);
// if it's callback service on client side
//可忽略
String serviceKey = serviceKey(port, path, inv.getAttachments().get(Constants.VERSION_KEY), inv.getAttachments().get(Constants.GROUP_KEY));
DubboExporter<?> exporter = (DubboExporter<?>) exporterMap.get(serviceKey);
if (exporter == null)
throw new RemotingException(channel, "Not found exported service: " + serviceKey + " in " + exporterMap.keySet() + ", may be version or group mismatch " + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + inv);
return exporter.getInvoker();
}
注意在这里已经初始化RpcContext了,后续在filter链中会继续设置属性到RpcContext中(上文中已经提到的ContextFilter),Invoker是通过serviceKey到内存中获取对应的exporter,exporter是在服务导出的时候放到内存中的,服务导出是监听spring的刷新事件发生时中启动的。
后面的调用过程与消费过程类似,调用链如下:
ChannelEventRunnable#run()
—> DecodeHandler#received(Channel, Object)
—> HeaderExchangeHandler#received(Channel, Object)
—> HeaderExchangeHandler#handleRequest(ExchangeChannel, Request)
—> DubboProtocol.requestHandler#reply(ExchangeChannel, Object)
—> Filter#invoke(Invoker, Invocation)
—> AbstractProxyInvoker#invoke(Invocation)
—> Wrapper0#invokeMethod(Object, String, Class[], Object[])
—> DemoServiceImpl#sayHello(String)
完整调用图
综上所述:
消费者将RpcContext的数据通过Invocation传给提供者;
提供者将Invocation解码并封装到Request中;
提供者主线程将Request传给线程池;
线程池中将Request数据拿出来,新建一个RpcContext放进去;
6. dubbo的消费过程?
参考上文
7. dubbo的服务注册和服务发现是怎么实现的?
参考上文
8. dubbo的Spi怎么实现的?自适应扩展点怎么实现的?
参考上一篇文章 juejin.im/post/684490…
9. 了解spring cloud吗?与dubbo有什么区别?
Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智能路由,微代理,控制总线)。
简单来讲,spring cloud致力于让开发者可以快速的搭建起一个复杂的分布式系统,分布式系统里面所需要的一切功能或组件都可以由spring cloud提供,就是一站式解决方案,目前主要有以下重要特性:
- 分布式/版本化配置(spring cloud config)
- 服务注册和发现(eureka)
- 路由(Zuul)
- service - to - service调用(feign)
- 负载均衡(ribbon)
- 断路器(hystrix)
- 分布式消息传递(spring cloud stream)
这里重点比较一下spring cloud的rpc组件feign和dubbo的区别:
| 核心要素| dubbo | spring cloud |
|--|-------|-----|-----|
|服务注册中心|zookeeper、redis|eukare|
|远程调用协议|dubbo(默认),http,rmi|http|
|服务网关|无|zuul|
|断路器|不完善(简单的服务降级)|hystrix|
|分布式追踪| 无|sleuth|
|负载均衡|内部实现|ribbon|
1、dubbo由于是二进制的传输,占用带宽会更少
2、springCloud是http协议传输,带宽会比较多,同时使用http协议一般会使用JSON报文,消耗会更大
3、dubbo的开发难度较大,原因是dubbo的jar包依赖问题很多大型工程无法解决
4、springcloud的接口协议约定比较自由且松散,需要有强有力的行政措施来限制接口无序升级
5、dubbo的注册中心可以选择zk,redis等多种,springcloud的注册中心只能用eureka或者自研
原文链接:blog.csdn.net/xuri24/arti…
10.dubbo并发控制怎么实现的?
provider通过ExecuteLimitFilter实现该功能,具体是通过Semaphore,根据配置参数创建一个Semaphore(max),每当一个请求进来就tryacquire,请求返回之后就release。 dubbo默认的线程池是固定线程池,大小是200,超过直接抛异常。
dubbo消费者发送请求后线程处于什么状态?
waiting。
- 业务线程发出请求,
DefaultFuture实例,继承于CompletableFuture。 - 在调用 future.get() 之前,先调用 ThreadlessExecutor.wait(),wait 会使业务线程在一个阻塞队列上等待take(),直到队列中被加入元素。
- 当业务数据返回后,生成一个 Runnable Task 并放入 ThreadlessExecutor 队列
- 业务线程将 Task 取出并在本线程中执行:反序列化业务数据并 set 到 Future。
- 业务线程拿到结果直接返回
那是怎么将request和response绑定上的呢? 每个request都是有一个唯一id,在DefaultFuture类中有一个Map维护id->DefaultFuture的映射,其实就是id对应的request的映射,这样收到了一个response消息就能对应上request。
那怎么知道收到的是request还是response?
协议头里面有一bit专门表示
怎么实现超时逻辑的?
DefaultFuture有一个TimeOutTask,为每个请求定时
blog.csdn.net/leisurelen/…
配置优先级
1.精确优先(方法级>接口级>全局配置(消费者))
2.消费者设置优先(在生产者和消费者中同时设置超时时间(防止由于网络等原因造成的堵塞),消费者设置的超时时间生效)