服务暴露
入口
其实服务暴露的起点在ServiceBean中,这个类继承ServiceConfig,他们实现了ApplicationContextRefreshAware接口实现了onApplicationEvent(),当spring容器刷新的时候,会触发这个事件,然后进入ServiceBean中调用export()方法导出服务
装配配置
在export()方法中首先会装载ApplicationConfig等等各种基础配置,其实就是装载yaml文件里配置的属性,将这些属性设置到对应的config对象中。
暴露服务
接下来会开始暴露服务,因为Dubbo是支持多中心同时注册的,所以将会循环注册到多个注册中心,也可以暴露多种协议比如Dubbo、Rest等,所以将会把服务依次暴露。
其实获取注册中心的过程就是把之前实例化的注册中心的config装配成URL对象,里面包含了各种属性和协议名称地址等信息,然后吧这个注册中心URL的list遍历,将服务暴露到注册中心上。
上面的图可以看到,遍历所有的协议类型,然后将每一个协议类型,注册到所有的注册中心上,所以这里吧注册中心的list传到了暴露过程中。
先来看一下暴露过程的前半部分,其实就是根据通信协议构造服务端的dubbo的URL,最后拼装出来就是这么个玩意儿。
dubbo://172.16.26.98:28886/com.elijah.dubboroutercommon.DubboRouterService?anyhost=true&application=dubbo-v1.0.0&bean.name=ServiceBean:com.elijah.dubboroutercommon.DubboRouterService&bind.ip=172.16.26.98&bind.port=28886&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.elijah.dubboroutercommon.DubboRouterService&methods=selectVersion&pid=7000&qos.enable=false®ister=true&release=2.7.3&side=provider×tamp=1590649717001
看看要啥有啥,本地地址端口接口全限定名协议名乱七八糟的。下半部分就开始真正的暴露并且注册到注册中心上了。
再来看一下protocol.export()方法,这里其实就是根据具体的协议在注册中心(默认是zk)创建节点信息,告诉大家,诶嘿嘿,我上线了~~
这里的protocol其实是一个代理实例,这个实例是通过dubbo的SPI机制自适应拓展生成的一个实例,这个实例会去调用传进来的invoker实例中的url地址,取出里面的协议内容然后再去调用不同的protocol实现逻辑,比如dubboprotocol、registerprotocol等。我们这里断电可以看到,我们之前代理生成的invoker的实例的url其实是register协议的,也就是要进行注册中心注册的。
所以这里代理类会invoke到registerProtocol的export()方法上。这个方法其实干了三件事情:
- 进行本地暴露,也是通过同样的调用中间代理的protocol实例,根据url中的协议类型来选择调用的protocol的export代码,这里是dubboprotocol,dubboprotocol的中会创建一个netty的服务端实例来进行监听。
- 进行远程注册中心注册,以zk为例,就是递归注册节点,先创建持久节点
/provides目录,然后创建临时节点,把地址协议等服务提供者信息注册上去。其实这个过程也是涉及到了自适应拓展的机制,选择了zk进行注册。 - 进行监听接口下configurators节点,用于处理动态配置。
上面就是服务暴露的过程。
服务引用
服务引用的触发的入口是ReferenceBean的getObject(),其实这个触发的原理就是如果你的dubbo服务比如demoService被其他的类引用了,并且DemoService使用了@Reference进行标记,那么当demoService被加载的时候,就会使用spring的FactoryBean加载,这个加载过程中会调用到ReferenceBean这个类的的加载,RefernceBean实现了getObject()方法,spring框架会调用到这个方法,来获取RefernceBean,这个时候远程服务的引用过程就开始了。
public Object getObject() throws Exception {
return get();
}
public synchronized T get() {
if (destroyed) {
throw new IllegalStateException("Already destroyed!");
}
// 检测 ref 是否为空,为空则通过 init 方法创建
if (ref == null) {
// init 方法主要用于处理配置,以及调用 createProxy 生成代理类
init();
}
return ref;
}
其实init()方法主要就是干了两件事,第一个就是解析校验设置消费端的配置,第二个就是创建代理对象的实例,用来封装远程调用这件事情。
主要看创建代理的过程,因为整个调用链路的东西都被封装在这里面了。首先会先检查是不是injvm在同一个jvm的本地引用,如果是的话其实就是直接获取jvm中的实例了。若不是,则读取注解中写的直连配置项,或配置的注册中心 url,并将读取到的 url 存储到 urls 中。然后根据 urls 元素数量进行后续操作。
我们以注册中心的形式来说,具体的直连的方式就不说了,如果url读取的是注册中心的形式,其实也就是这样的格式。
registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-router-consumer&dubbo=2.0.2&pid=2234&qos.enable=false&refer=application%3Ddubbo-router-consumer%26check%3Dfalse%26dubbo%3D2.0.2%26generic%3Dfalse%26interface%3Dcom.elijah.dubboroutercommon.DubboRouterService%26lazy%3Dfalse%26loadbalance%3Drandomgray%26methods%3DselectVersion%26pid%3D2234%26qos.enable%3Dfalse%26register.ip%3D192.168.58.5%26release%3D2.7.3%26side%3Dconsumer%26sticky%3Dfalse%26timestamp%3D1590989384894®istry=zookeeper&release=2.7.3×tamp=1590989412612
这里可以看到协议是registry的,所以上图中标示的REF_PROTOCOL这个属性,其实也是个代理类,就会根据这个url调用到RegistryProtocol的实例中
实例类判断协议类型:
根据协议类型获取拓展类实例:
调用registry的refer方法:
这里的doRefer方法其实干的事情就比较多了,整体逻辑需要说一下,首先它会创建一个RegistryDirectory的类,这个类其实有两件事情需要干,一个是持有所有服务提供者的invoker实例,并且向zk注册监听器,监听服务提供者的路径,这样就能知道服务提供者哪些可用那些不可用,并且在第一次初始化的时候会直接运行监听通知的逻辑,获取所有的提供者列表,并且创建提供者invoker,还会创建一个共享的HeaderExchangeClient也就是netty的客户端缓存起来,并且直接创建连接到providers的channel;第二就是要持有一个router路由过滤器的链,为什么这里叫他路由过滤器,其实他就是来匹配哪些消费者或者哪些服务提供者是符合条件的,比如conditionRouter,他可以根绝消费方和提供方的ip地址的host来判断哪些是不符合条件的,这样在调用的时候就可以过滤掉。这个整个过程结束后,RegistryDirectory会返回,并且被Cluster给包装成一个Invoker,其实这个的目的就是把多个服务提供者的调用逻辑包装在一起,从而实现重试的策略,当然cluster这个东西也是根据具体URL中指定的错误处理策略决定的,如果设置的是failover策略也就是失败了就调用另一个服务提供者,他会将RegistryDirectory设置到FailoverClusterInvoker的实例中,调用的时候如果出错了,就从directory中取出另一个服务提供者来调用。所以这个持有链是分层级的,关注的点不同:
- cluster:持有RegistryDirectory的实例,为了封装重试和失败处理的办法,实集群的高可用的能力。
- RegistryDirectory:持有Router的实例过滤链,可能有多个Router串在一起,并且持有多个服务提供者的Invoker实例,并且实现了监听zk变动通知,这样服务提供方上线下线就能及时刷新本地的缓存。
- Router:具体类型很多,比如ConditionRouter,ScriptRouter,TagRouter等,其实就是为了定义一个哪些服务消费者者应该调用哪些服务提供者,会把不符合规则的服务提供者屏蔽掉不去调用。
其实这里还有一个LoadBalance的逻辑没有说,其实这里就是负载均衡的策略了,这个放到服务调用的地方说。这里还是着重说服务引用中的doRefer的逻辑。
这里最后就是会返回被Cluster封装的invoker的实例,缓存起来,注入到spring调用的类实例中。
服务调用
消费方调用
首先服务消费者通过代理对象 Proxy 发起远程调用,接着通过网络客户端 Client 将编码后的请求发送给服务提供方的网络层上,也就是 Server。Server 在收到请求后,首先要做的事情是对数据包进行解码。然后将解码后的请求发送至分发器 Dispatcher,再由分发器将请求派发到指定的线程池上,最后由线程池调用具体的服务。这就是一个远程调用请求的发送与接收过程。
我们上面说到服务引用完成后,其实真实的是封装成一个FailoverClusterInvoker的实体类中,其实真正的对象还会吧这个invoker生成代理对象,变成proxy0的这样一个对象直接调用,这没关系,直接从failvoerclusterinvoker看好了,因为肯定会调用到这里。
其实这个方法比较简单,其实就是循环干了几件事儿:
- 调用列举invoker的列表,这样做如果失败了在重试的时候可以获取最新的invoker列表,剔除掉已经下线的provider。
- 通过负载均衡选择invoker。
- 使用invoker实际远程调用。
之前没有说复杂均衡Loadbalance,这里说一下,其实他也是通过自适应拓展的机制,代理进行选择的,有这么几种类型:基于权重随机算法的 RandomLoadBalance、基于最少活跃调用数算法的 LeastActiveLoadBalance、基于 hash 一致性的 ConsistentHashLoadBalance,以及基于加权轮询算法的 RoundRobinLoadBalance。
默认是RandomLoadBalance,其实这个原理是比较简单的,RandomLoadBalance 是加权随机算法的具体实现,它的算法思想很简单。假设我们有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3, 2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上。比如数字3会落到服务器 A 对应的区间上,此时返回服务器 A 即可。权重越大的机器,在坐标轴上对应的区间范围就越大,因此随机数生成器生成的数字就会有更大的概率落到此区间内。只要随机数生成器产生的随机数分布性很好,在经过多次选择后,每个服务器被选中的次数比例接近其权重比例。
经过上面的负载均衡,我们已经选择出来了可以调用的invoker,下面就是调用invoker,因为默认是Dubbo协议,所以服务引用的时候根据URL中的协议创建出来的是DubboInvoker,所以直接来看看调用的逻辑。
public class DubboInvoker<T> extends AbstractInvoker<T> {
private final ExchangeClient[] clients;
protected Result doInvoke(final Invocation invocation) throws Throwable {
RpcInvocation inv = (RpcInvocation) invocation;
final String methodName = RpcUtils.getMethodName(invocation);
// 设置 path 和 version 到 attachment 中
inv.setAttachment(Constants.PATH_KEY, getUrl().getPath());
inv.setAttachment(Constants.VERSION_KEY, version);
ExchangeClient currentClient;
if (clients.length == 1) {
// 从 clients 数组中获取 ExchangeClient
currentClient = clients[0];
} else {
currentClient = clients[index.getAndIncrement() % clients.length];
}
try {
// 获取异步配置
boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
// isOneway 为 true,表示“单向”通信
boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
// 异步无返回值
if (isOneway) {
boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
// 发送请求
currentClient.send(inv, isSent);
// 设置上下文中的 future 字段为 null
RpcContext.getContext().setFuture(null);
// 返回一个空的 RpcResult
return new RpcResult();
}
// 异步有返回值
else if (isAsync) {
// 发送请求,并得到一个 ResponseFuture 实例
ResponseFuture future = currentClient.request(inv, timeout);
// 设置 future 到上下文中
RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));
// 暂时返回一个空结果
return new RpcResult();
}
// 同步调用
else {
RpcContext.getContext().setFuture(null);
// 发送请求,得到一个 ResponseFuture 实例,并调用该实例的 get 方法进行等待
return (Result) currentClient.request(inv, timeout).get();
}
} catch (TimeoutException e) {
throw new RpcException(..., "Invoke remote method timeout....");
} catch (RemotingException e) {
throw new RpcException(..., "Failed to invoke remote method: ...");
}
}
// 省略其他方法
}
其实就是根据用户的选项进行了不同的调用,可以异步调用也可以同步调用,默认的同步调用当前的线程就会阻塞当前线程,然后等待调用完成后的唤醒,会通过nettyclient像channel中发送数据,sendAndFlush完成后就会调用一个future对象的await()方法,同时也会讲这个future方法加上一个id缓存下来,这样当channel返回的时候就能够通过id找到对应的future对象,设置返回值返回给客户端。
提供方返回
当客户端写入数据到channle中后,服务提供方就会获取到epoll的就绪通知,筛选出来就绪的channel,进行消息消费。
@Sharable
public class NettyHandler extends SimpleChannelHandler {
private final Map<String, Channel> channels = new ConcurrentHashMap<String, Channel>();
private final URL url;
private final ChannelHandler handler;
public NettyHandler(URL url, ChannelHandler handler) {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
if (handler == null) {
throw new IllegalArgumentException("handler == null");
}
this.url = url;
// 这里的 handler 类型为 NettyServer
this.handler = handler;
}
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
// 获取 NettyChannel
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.getChannel(), url, handler);
try {
// 继续向下调用
handler.received(channel, e.getMessage());
} finally {
NettyChannel.removeChannelIfDisconnected(ctx.getChannel());
}
}
}
接收到请求后,就会调用handler进行线程派发,其实就是将channel封装的任务扔到一个线程池中处理,这个逻辑之前分析netty的时候说过,但是当时只说了netty接受任务后怎么接受数据以及处理器链handler是怎么一回事儿,其实dubbo框架就是把这些过滤和乱七八糟的解析封装成任务扔到线程池里做了,最终调用到dubbo服务端代理的真正的代码的代理类中,进行服务调用。下面把代码贴出来可看可不看,逻辑就是那么回事儿。
public class HeaderExchangeHandler implements ChannelHandlerDelegate {
private final ExchangeHandler handler;
public HeaderExchangeHandler(ExchangeHandler handler) {
if (handler == null) {
throw new IllegalArgumentException("handler == null");
}
this.handler = handler;
}
@Override
public void received(Channel channel, Object message) throws RemotingException {
channel.setAttribute(KEY_READ_TIMESTAMP, System.currentTimeMillis());
ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel);
try {
// 处理请求对象
if (message instanceof Request) {
Request request = (Request) message;
if (request.isEvent()) {
// 处理事件
handlerEvent(channel, request);
}
// 处理普通的请求
else {
// 双向通信
if (request.isTwoWay()) {
// 向后调用服务,并得到调用结果
Response response = handleRequest(exchangeChannel, request);
// 将调用结果返回给服务消费端
channel.send(response);
}
// 如果是单向通信,仅向后调用指定服务即可,无需返回调用结果
else {
handler.received(exchangeChannel, request.getData());
}
}
}
// 处理响应对象,服务消费方会执行此处逻辑,后面分析
else if (message instanceof Response) {
handleResponse(channel, (Response) message);
} else if (message instanceof String) {
// telnet 相关,忽略
} else {
handler.received(exchangeChannel, message);
}
} finally {
HeaderExchangeChannel.removeChannelIfDisconnected(channel);
}
}
Response handleRequest(ExchangeChannel channel, Request req) throws RemotingException {
Response res = new Response(req.getId(), req.getVersion());
// 检测请求是否合法,不合法则返回状态码为 BAD_REQUEST 的响应
if (req.isBroken()) {
Object data = req.getData();
String msg;
if (data == null)
msg = null;
else if
(data instanceof Throwable) msg = StringUtils.toString((Throwable) data);
else
msg = data.toString();
res.setErrorMessage("Fail to decode request due to: " + msg);
// 设置 BAD_REQUEST 状态
res.setStatus(Response.BAD_REQUEST);
return res;
}
// 获取 data 字段值,也就是 RpcInvocation 对象
Object msg = req.getData();
try {
// 继续向下调用
Object result = handler.reply(channel, msg);
// 设置 OK 状态码
res.setStatus(Response.OK);
// 设置调用结果
res.setResult(result);
} catch (Throwable e) {
// 若调用过程出现异常,则设置 SERVICE_ERROR,表示服务端异常
res.setStatus(Response.SERVICE_ERROR);
res.setErrorMessage(StringUtils.toString(e));
}
return res;
}
}
获取exporter并调用:
public class DubboProtocol extends AbstractProtocol {
public static final String NAME = "dubbo";
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<?> invoker = getInvoker(channel, inv);
if (Boolean.TRUE.toString().equals(inv.getAttachments().get(IS_CALLBACK_SERVICE_INVOKE))) {
// 回调相关,忽略
}
RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
// 通过 Invoker 调用具体的服务
return invoker.invoke(inv);
}
throw new RemotingException(channel, "Unsupported request: ...");
}
// 忽略其他方法
}
Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException {
// 忽略回调和本地存根相关逻辑
// ...
int port = channel.getLocalAddress().getPort();
// 计算 service key,格式为 groupName/serviceName:serviceVersion:port。比如:
// dubbo/com.alibaba.dubbo.demo.DemoService:1.0.0:20880
String serviceKey = serviceKey(port, path, inv.getAttachments().get(Constants.VERSION_KEY), inv.getAttachments().get(Constants.GROUP_KEY));
// 从 exporterMap 查找与 serviceKey 相对应的 DubboExporter 对象,
// 服务导出过程中会将 <serviceKey, DubboExporter> 映射关系存储到 exporterMap 集合中
DubboExporter<?> exporter = (DubboExporter<?>) exporterMap.get(serviceKey);
if (exporter == null)
throw new RemotingException(channel, "Not found exported service ...");
// 获取 Invoker 对象,并返回
return exporter.getInvoker();
}
// 忽略其他方法
}
到这里,就说完了从客户端直接到服务端一整条调用链,但是还有一个缺失的点,就是客户端调用服务端其实从代码来看是异步的,只不过客户端使用await()让用户的调用的阻塞等待,导致看起来其实是同步的,所以服务端返回的时候是怎么唤醒用户线程的还是不清楚的。
上面我们可以看到,派发到线程池的时候其实是吧channel也一起扔了进去,所以在调用完实际的业务代码之后,会把返回值直接写到channel,flush过去,客户端其实这里也有一个eventloop用来接受请求,同样的逻辑会派发到一个handler里,不过处理逻辑不不同,经过解码后,会判定这一个Response的数据包,然后就会从数据包中找出透传返回的future的序号,匹配到对应的future实例,然后设置返回值以及唤醒用户线程返回。
public class HeaderExchangeHandler implements ChannelHandlerDelegate {
@Override
public void received(Channel channel, Object message) throws RemotingException {
channel.setAttribute(KEY_READ_TIMESTAMP, System.currentTimeMillis());
ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel);
try {
if (message instanceof Request) {
// 处理请求,前面已分析过,省略
} else if (message instanceof Response) {
// 处理响应
handleResponse(channel, (Response) message);
} else if (message instanceof String) {
// telnet 相关,忽略
} else {
handler.received(exchangeChannel, message);
}
} finally {
HeaderExchangeChannel.removeChannelIfDisconnected(channel);
}
}
static void handleResponse(Channel channel, Response response) throws RemotingException {
if (response != null && !response.isHeartbeat()) {
// 继续向下调用
DefaultFuture.received(channel, response);
}
}
}
public class DefaultFuture implements ResponseFuture {
private final Lock lock = new ReentrantLock();
private final Condition done = lock.newCondition();
private volatile Response response;
public static void received(Channel channel, Response response) {
try {
// 根据调用编号从 FUTURES 集合中查找指定的 DefaultFuture 对象
DefaultFuture future = FUTURES.remove(response.getId());
if (future != null) {
// 继续向下调用
future.doReceived(response);
} else {
logger.warn("The timeout response finally returned at ...");
}
} finally {
CHANNELS.remove(response.getId());
}
}
private void doReceived(Response res) {
lock.lock();
try {
// 保存响应对象
response = res;
if (done != null) {
// 唤醒用户线程
done.signal();
}
} finally {
lock.unlock();
}
if (callback != null) {
invokeCallback(callback);
}
}
}
这篇文章只是梳理整个调用过程写自己的理解,所以直接吧dubbo官方的资料放上来,详细的可以看dubbo官方,不过他那个写的太细了,看着容易迷糊。
这里附上一个自己写的可以实现灰度路由的starter,使用zk作为配置中心,同步路由的配置根据bizzKey进行路由选择,不过不是基于router实现的,是基于loadbalance实现的。dubbo重写loadbalance实现灰度发布demo