说一说提供者启动流程?
ServiceAnnotationBeanPostProcessor
实现了BeanDefinitionRegistryPostProcessor
接口,在它的registerServiceBean方法中,会为带有@Service注解的类注册两个BeanDefine信息:原始类对应的BeanDefine(类名首字母小写)
ServiceBean对应的BeanDefine(ServiceBean:com.sxy.sdubbo.service.ILearn)
. 所以,当我们使用了Dubbo的@Service注解之后,其实没必要再使用Spring的相关注解了. 还有一点需要注意的是ServiceBean里面持有原始Bean的name
接下来就都是
ServiceBean
的舞台,ServiceBean
实现了InitializingBean
接口,在它的afterPropertiesSet
方法中会完成所有和提供者相关的工作. 流程如下:
2.1 如果有,则设置相关属性: ProviderConfig ApplicationConfig ModuleConfig RegistryConfig MetadataReportConfig ConfigCenterConfig MonitorConfig MetricsConfig ProtocolConfig
2.2 前置校验,在这个步骤中,如果有必要会启动配置中心
2.3 找到所有的注册中心(n)和需要暴露的协议(m),即一个提供者需要暴露的服务数为n*m
2.4 构造参数到Map中: application metrics module provider version methods 等
2.5 根据scope属性选择本地暴露还是远程暴露,默认情况下先暴露本地服务,然后暴露远程服务
2.6 利用Javassist
动态生成一个代理对象Wrapper
,该代理对象持有原始实现类的引用
2.7 生成一个AbstractProxyInvoker
对象,在它的doInvoke
方法中,调用了代理对象Wrapper
的invokeMethod
方法
2.8 服务暴露,默认情况下,网络通信通过Netty4,即开启一个NettyServer. 这部分和DubboProtocol
相关
2.9 注册服务到注册中心,这部分和RegistryProtocol
相关. 以ZK为例,注册服务到providers
节点,同时监听configurators
节点
2.10 最终返回一个Exporter
对象,该对象内部持有Invoker
引用.可以简单的认为Exporter
对象即代表最终暴露的那个服务,在服务有变化的时候,比如动态配置变了,需要重新刷新Exporter
对象如果有,元数据中心处理
发布
ServiceBeanExportedEvent事件
上面的流程记不住是吧?那来个简单版的:
- 启动时执行
ServiceBean#afterPropertiesSet
方法 - 默认情况下先暴露本地服务,然后暴露远程服务
- 利用Javassist动态生成一个代理对象
Wrapper
,然后再创建一个AbstractProxyInvoker
,在它的doInvoke
方法中调用了Wrapper#invokeMethod
方法 - 服务暴露. 默认情况下网络通信使用Netty4,即开启一个NettyServer. 这部分和
DubboProtocol
相关 - 注册服务到注册中心,并监听动态配置节点
- 最终返回一个
Exporter
对象,该对象内部持有Invoker
引用,即关系如下:Exporter => Invoker =>Wrapper(代理对象) => 原始类方法
说一说提消费者启动流程?
-
ReferenceAnnotationBeanPostProcessor
实现了InstantiationAwareBeanPostProcessorAdapter
接口,所以它会在Bean实例化前后执行. 里面涉及到一个doGetInjectedBean
方法,根据名字也可以猜测到这里就是要注入我们依赖的Bean, 主要就是处理@Reference
- 在处理
@Reference
的时候,涉及到这么几个对象:
2.1doGetInjectedBean
方法返回的是一个代理对象(JDK动态代理),假如它是proxy1
2.2ReferenceBean
: 是一个FactoryBean
,但是它的getObject
返回的是一个代理对象,假如它是proxy2
2.3 在创建proxy1
的时候,需要提供一个InvocationHandler,这里是ReferenceBeanInvocationHandler
, 在创建ReferenceBeanInvocationHandler
的时候需要依赖proxy2
2.4 所以,最终注入的是proxy1
,但是在调用的时候是:proxy1 => ReferenceBeanInvocationHandler => proxy2
- 所有和消费者相关的启动操作都在
ReferenceBean
中,流程如下:
3.1 前置校验,有必要就启动注册中心
3.2 构造参数到Map中: application metrics module interfaceName consumer methods 等
3.3 创建Invoker
. 如果是单注册中心直接返回一个Invoker
; 如果是多注册中心则为每个注册中心创建一个Invoker
,然后将将这些Invokers
合并成一个
3.4Invoker
其实是基于监听providers
节点来创建的.先注册到consumers
节点,然后监听providers routers configurators
,监听器中拿到providerUrls
后,根据为每条url生成一个Invoker
对象. 需要注意的是,在生成Invoker
时,涉及到NettyClient与NettyServer建立连接
3.5 基于步骤3.3创建的Invoker
,使用Javassist
创建代理对象并返回(proxy2
)
3.6 所以最后的调用关系是:proxy2 => Invoker => 网络通信
说一说消费者发送请求到接收响应整个流程?
- 先把提供者和消费者的启动流程大体说一下.
为什么呢?
你得先把那两个代理对象引出来,才好继续往下说,要不然无从下手 - 为了方便,起一些简短别名
2.1 消费者代理对象:p0
2.2 提供者代理对象:p1
消费端发送请求
- 通过
p0
执行对应方法,然后调用Invoker
的对应方法 - 经过负载均衡层找到一个合适的
Invoker
- 通过NettyClient发送请求到NettyServer端
- 需要注意的是消费端发送请求的时候会创建一个唯一ID:
requestId
. 返回的是一个DefaultFuture
对象,并且有一个Map缓存所有请求ID和DefaultFuture的关系
服务端响应请求
- NettyServer收到请求,先将消息转发到线程池
- 线程池收到消息,先对消息进行解码
- 经过一系列
xxHandler
处理,请求来到DubboProtocol.requestHandler
,然后执行:Exporter => Invoker => p1 => 原始对象
- 创建一个
Response
对象,它的ID属性值就是Request
对象的requestId
值,这样请求和响应就关联起来了(实际者一块没这么简单,涉及到回调处理,这里就不详细说了) - 将
Response
对象发送给消费端
消费端响应结果
- NettyClient收到请求,先将消息转发到线程池
- 线程池收到消息,先对消息进行解码
- 根据前面介绍我们已经知道: 一个请求和其对应的响应使用的是同一个ID; 请求ID和DefaultFuture的映射关系被缓存在DefaultFuture的静态Map中
- 先通过请求ID获取DefaultFuture,然后执行
CompletableFuture#complete
方法,这样就可以让执行了CompletableFuture#get
的用户线程得到响应,获取结果返回
说一说路由规则在前还是负载均衡在前?
- 我不知为啥问一个这样的问题,如果非要说:
路由在前
- 路由的目的其实是根据用户配置的路由规则动态筛选
Invoker
,消费端监听了routers
节点,所以在路由规则发生变化的时候,就会刷新Invoker列表
- 负载均衡目的是从可用的
Invoker列表
中选出一个Invoker
,这时候的Invoker列表
其实是经过路由筛选过的Invokers
. 为了避免大多数流量都请求到同一台机器或部分机器没有流量,需要根据一种负载算法选择一个Invoker
说一说Dubbo的扩展机制?
- 首先你要区分JDK自带的SPI机制与Dubbo扩展机制有和不同:
1.1 可以根据key获取对应的实现类
1.2 IOC支持
1.3 AOP支持 - IOC支持是基于
ExtensionFactory
实现. 支持两种属性的自动注入:
2.1: SPI类型的属性. 这个是基于SpiExtensionFactory
实现
2.2: Spring里面的Bean. 这个是基于SpringExtensionFactory
实现,里面直接通过ApplicationContext获取Bean - AOP支持是基于
Wrapper机制
实现,即包一层. 这种Wrapper类
有一个特点,构造函数需要传入被包装实例
,如下
public ProtocolFilterWrapper(Protocol protocol) {
if (protocol == null) {
throw new IllegalArgumentException("protocol == null");
}
this.protocol = protocol;
}
说一说Filter机制?
-
Filter
是一个SPI接口 - 最终所有的
Filter
都构建成一条Filter链
.消费端对应一条Filter链
,提供端对应一条Filter链
- 在服务暴露和生成服务引用的时候都会构建
Filter链
,核心类为ProtocolFilterWrapper
,它实现了Protocol
接口. 从名字可以看出这是一个对Protocol
的AOP处理,处理逻辑就是构造Filter链
private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
Invoker<T> last = invoker;
// 获取消费端 或 提供端 的所有 Filter
List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
if (!filters.isEmpty()) {
for (int i = filters.size() - 1; i >= 0; i--) {
final Filter filter = filters.get(i);
final Invoker<T> next = last;
last = new Invoker<T>() {
......
@Override
public Result invoke(Invocation invocation) throws RpcException {
Result asyncResult;
try {
asyncResult = filter.invoke(next, invocation);
} catch (Exception e) {
// onError callback
if (filter instanceof ListenableFilter) {
Filter.Listener listener = ((ListenableFilter) filter).listener();
if (listener != null) {
listener.onError(e, invoker, invocation);
}
}
throw e;
}
return asyncResult;
}
......
};
}
}
return new CallbackRegistrationInvoker<>(last, filters);
}
说一说优雅停机?
为了实现优雅停机需要解决一些问题:
1.1 新的请求不能再发往正在停机的服务提供者
1.2 若关闭服务提供者,已经接收到服务请求需要处理完毕才能下线服务
1.3 若关闭服务消费者,已经发出的服务请求,需要等待响应返回核心是基于JDK的ShutdownHook函数, 对应的核心类为
DubboShutdownHook
主要分两个步骤:
- 注销注册中心
1.1 删除对应的节点,然后取消订阅,然后关闭服务端与ZK的连接
1.2 考虑到网络延迟等原因,在注销注册中心之后,等待一段时间,默认10s
- 注销所有
Protocol
2.1 会向消费端发送READ_ONLY
事件. 消费者接受之后主动排除这个节点,将请求发往其他正常节点
2.2 关闭业务线程池,这个过程将会尽可能将线程池中的任务执行完毕,再关闭线程池
2.3 关闭NettyServer
2.4 关闭NettyClient
说一说回声测试?
- 回声测试用于检测服务是否可用,回声测试按照正常请求流程执行,能够测试整个调用是否通畅,可用于监控. 也就是说我只是想看看整个调用是否通畅,而不是发出实际的调用
- 对于消费者: 服务引用(即为消费端生成的那个代理对象)实现了
EchoService
接口,只所以需要将任意服务引用强制转换为EchoService
即可使用 - 对于提供者: 在
EchoFilter
中进行拦截,判断是否是回声测试方法,如果是则直接返回入参; 如果不是则发起正常调用
// 回声测试可用性
EchoService echoService = (EchoService) memberService; // 强制转型为EchoService
String status = echoService.$echo("OK");
@Activate(group = CommonConstants.PROVIDER, order = -110000)
public class EchoFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
if (inv.getMethodName().equals($ECHO) && inv.getArguments() != null && inv.getArguments().length == 1) {
return AsyncRpcResult.newDefaultAsyncResult(inv.getArguments()[0], inv);
}
return invoker.invoke(inv);
}
}
说一说服务降级?
- 在提供者不可用时,或者提供者抛出异常时,返回配置的默认值,不影响主流程
- 这时候可以使用
Mock
机制,Mock
使用方式有两种:
2.1 提供端直接返回一个固定的字符串
2.2 在接口服务xxService的目录下创建相应的mock业务处理类接口名+Mock后缀
,同时实现业务接口
xxService() - 实现原理: 在消费端发起调用的时候,会判断是否需要mock,即
MockInvoker#invoke
方法
说一说本地存根?
- 可以用来做一些缓存 容错 处理
- 实现原理: 消费端创建的代理服务为
p0
,以p0
为入参,创建一个Stub
对象,然后暴露给用户(最终的p0
)
public class DemoServiceStub implements DemoService {
// 这里的 demoService 代表的是消费端的代理对象p0
private final DemoService demoService;
public DemoServiceStub(DemoService demoService) {
this.demoService = demoService;
}
public String sayHello(String name) {
System.out.println("进入DemoServiceStub");
return demoService.sayHello(name);
}
}
说一说泛化调用?
- 正常情况下,消费者调用提供者.需要依赖提供者接口.那再没有提供者接口的时候怎么办呢?可以通过泛化调用,参数及返回值中的所有 POJO 均用Map表示,核心类为
GenericService
- 消费端处理: 将POJO转成Map,然后再调用服务提供者,基于
GenericImplFilter
实现 - 提供端处理: 接收到Map转换成POJO,再调用Service方法. 若返回值有POJO则转换成Map再返回. 基于
GenericFilter
实现