咱们知道,对于服务治理框架来讲,服务通讯(RPC)和服务管理两部分必不可少,而服务管理又分为服务注册、服务发现和服务人工介入,咱们来看看Dubbo框架的结构图(来源网络):java
图中能够看出,服务提供者Provider往服务注册中心Registry注册服务,而的消费者Consumer从服务注册中心订阅它须要的服务,而不是所有服务,当有新的Provider出现,或者现有Provider宕机,注册中心Registry都应该能尽早发现,并将新的Provider列表推送给对应的Consumer,有了这样的机制,Dubbo才能作到Failover,而Failover的时效性,由注册中心Registry的实现决定。缓存
Dubbo线上支持三种注册中心:自带的SimpleRegistry、Redis和Zookeeper,固然,最经常使用的仍是Zookeeper做为注册中心,由于太多分布式的中间件须要依赖Zookeeper做为协做者。那么怎么才能让Dubbo知道咱们使用哪一个实现做为注册中心呢?咱们只须要在dubbo的xml配置文件中配置dubbo:registry节点便可:网络
<dubbo:registry id="dubboRegistry"protocol="zookeeper"address="${dubbo.registry.address}"/>负载均衡
没错,protocol就指明了注册中心的实现。框架
要想作到服务的可靠,避免分布式系统的单点问题,除了Provider能够集群部署外,注册中心的弱依赖也是必须的,注册中心的宕机,不会影响现有服务的运行,只是不能注册新的服务和进行服务发现,Failover仍是能够作的,好比Consumer能够经过服务调用来简单判断当前的Provier是否可用。若是某个Consumer宕机了,当它重启后,发现注册中心也挂了,那咋办?为了防止这种问题出现,Dubbo的Consumer会将本身须要的Provider列表在本地保存一份,固然,里面也包括本身暴露的服务信息(即本身也做为Provider),咱们能够看看AbstractRegistry中的实现:异步
public AbstractRegistry(URL url) {分布式
setUrl(url);ide
// 启动文件保存定时器this
syncSaveFile= url.getParameter(Constants.REGISTRY_FILESAVE_SYNC_KEY,false);url
String filename =url.getParameter(Constants.FILE_KEY, System.getProperty("user.home") +"/.dubbo/dubbo-registry-"+ url.getHost() +".cache");
File file = null;
if (ConfigUtils.isNotEmpty(filename)) {
file = newFile(filename);
if(! file.exists() &&file.getParentFile() !=null&&! file.getParentFile().exists()){
if(! file.getParentFile().mkdirs()){
throw new IllegalArgumentException("Invalid registry store file "+ file +", cause: Failed to create directory" + file.getParentFile()+"!");
}
}
}
this.file= file;
loadProperties();
notify(url.getBackupUrls());
}
注意看黄底代码部分,若是没有在属性文件中配置file(Constants.FILE_KEY),就将在用户的当前用户目录/.dubbo/目录下新建一个dubbo-registry开头的保存全部URL信息的Cache文件,一般来讲一个应用能够在多个注册中心暴露本身的服务,也能够从多个注册中心订阅不一样的服务,因此这里的Cache文件名加入了注册中心的主机名。还有一个lock文件,用来防止不一样的JVM进程同时修改Cache文件,注意,这里只是防止,因此意味着同一目录的Cache文件能够由多个JVM进程共享,当多个JVM进程恰巧同时修改Cache文件时,将会有一个进程获取lock文件的锁失败,见保存Cache的过程的AbstractRegistry#doSaveProperties方法的片断:
FileChannel channel = raf.getChannel();
try {
FileLocklock = channel.tryLock();
if (lock == null) {
thrownew IOException("Can not lock theregistry cache file "+file.getAbsolutePath() + ", ignore and retrylater, maybe multi java process use the file, please config:dubbo.registry.file=xxx.properties");
}
这将致使某个URL更新到Cache文件失败,但Dubbo提供了重试机制,以保证Cache文件中信息能和内存中的信息最终一致。但不要认为Cache文件中的Provider和Consumer列表是和当前运行的服务一致,由于当一个服务部署多个应用时,Cache文件被多个JVM同时写的几率仍是很大的,因此这时总有JVM进程度lock文件获取锁失败(即FileChannel#tryLock()失败),这时它只能乖乖稍后重试了。写Cache的方式也很简单粗暴,即先读取整个Cache文件,而后再往其写入当前处理的URL,而后再全量写入,可见,若是某个服务(URL)已经再也不使用,它有可能一直存在于Cache文件中。
保存Cache还分为同步保存和异步保存,咱们知道内存中服务列表的更新相对于服务调用来讲确定是异步的,但为啥保存Cache文件还要分同步和异步呢?由于在Dubbo中,服务(或者叫URL)是一个个来更新的,也就是说,当服务比较多时,使用异步保存Cache文件能使应用启动和服务更新速度更快,而整个更新过程是由AbstractRegistry#notify来触发的。
咱们再来看看若是选择使用Zookeeper用来作Dubbo的注册中心,那么Provider和Consumer的数据在上是怎么存储的。Dubbo在ZK的全部数据都在/dubbo节点下,以下图:
/dubbo
/com.manzhizhen.user.Service1
/consumers
/routers
/providers
/configurators
/com.manzhizhen.user.Service2
/consumers
/routers
/providers
/configurators
/com.manzhizhen.user.Service3
/consumers
/routers
/providers
/configurators
咱们能够看到,每一个服务(URL)在dubbo节点下都会有一个对应的ZK持久化节点,而每一个服务节点下面都会有四个持久化子节点,表明消费者(consumer)、路由(routers)、提供者(providers)和配置(configurators),consumer和providers节点好理解,放的就是该URL下消费者和提供者的URL所有信息,而routers和configurators主要用于控制路由规则,这在正常状况下是用的比较少的,因此这两个节点数据一般为空。
如今咱们说说和服务注册相关的两个异常信息, 先给出Dubbo的集群容错图:
一个常见的异常信息是"Forbid consumer XXXXXaccess service XXXXX from registry XXXXX use dubbo version 2.5.3, Please checkregistry access list (whitelist/blacklist).",当咱们须要调用服务时,会先从本地的注册目录也就是RegistryDirectory来拿取调用(Invoker)列表,见上图Directory节点,RegistryDirectory#doList代码片断以下:
public List<Invoker<T>> doList(Invocation invocation) {
if (forbidden) {
thrownew RpcException(RpcException.FORBIDDEN_EXCEPTION,"Forbid consumer "+ NetUtils.getLocalHost() + " access service" +getInterface().getName() + " from registry "+ getUrl().getAddress() +" use dubbo version " + Version.getVersion() + ", Please check registry access list(whitelist/blacklist).");
}
List<Invoker<T>> invokers = null;
Map<String, List<Invoker<T>>>localMethodInvokerMap =this.methodInvokerMap;// local reference
可见,当forbidden为false时,会抛出该异常信息,当注册中心给它推送最新的Provider列表时,上面的forbidden的值已经变成了false,见RegistryDirectory#refreshInvoker代码片断:
private void refreshInvoker(List<URL>invokerUrls){
if(invokerUrls !=null&&invokerUrls.size() ==1&& invokerUrls.get(0) !=null
&& Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
this.forbidden=true;//禁止访问
this.methodInvokerMap=null; // 置空列表
destroyAllInvokers(); // 关闭全部Invoker
} else{
this.forbidden=false;//容许访问
Map<String, Invoker<T>>oldUrlInvokerMap =this.urlInvokerMap;// local reference
if (invokerUrls.size() ==0&&this.cachedInvokerUrls!=null){
invokerUrls.addAll(this.cachedInvokerUrls);
} else{
this.cachedInvokerUrls=new HashSet<URL>();
this.cachedInvokerUrls.addAll(invokerUrls);//缓存invokerUrls列表,便于交叉对比
}
从上面代码能够看出,当该URL协议为empty时,说明该URL已经被禁止(forbidden)了,那何时URL的协议会被设置成empty呢?咱们看看ZookeeperRegistry#toUrlsWithEmpty方法:
private List<URL> toUrlsWithEmpty(URLconsumer, String path, List<String> providers) {
List<URL> urls = toUrlsWithoutEmpty(consumer, providers);
if (urls == null || urls.isEmpty()) {
int i = path.lastIndexOf('/');
String category = i < 0? path : path.substring(i +1);
URL empty = consumer.setProtocol(Constants.EMPTY_PROTOCOL).addParameter(Constants.CATEGORY_KEY, category);
urls.add(empty);
}
return urls;
}
可见,当providers列表为空时,也就是某个URL下没有活着的Provider时,Consumer会将本地的invokerUrl的协议设置成empty,而toUrlsWithEmpty是在ZookeeperRegistry订阅方法doSubscribe中被调用的,这里再也不给出代码。
另外一个是"Failed to invoke the method XXXXXin the service XXXXX. No provider available for the service XXXXX from registryXXXXX on the consumer XXXXX using the dubbo version 2.5.3. Please check if theproviders have been started and registered.",由于每次调用时都会去检查调用列表,若是列表有多个可用服务(即多个Provider),将会使用配置的负载均衡方式来选择一个服务来调用,但若是服务列表为空,则会抛异常,也就是在上图的Invoker节点抛出异常,这种状况通常是说明当前没有可用的Provider,见AbstractClusterInvoker#checkInvokers代码:
protected void checkInvokers(List<Invoker<T>> invokers, Invocation invocation) {
if (invokers == null|| invokers.size() ==0) {
thrownew RpcException("Failed to invokethe method "
+ invocation.getMethodName() +" in the service "+ getInterface().getName()
+ ". No provideravailable for the service "+directory.getUrl().getServiceKey()
+ " from registry" + directory.getUrl().getAddress()
+ " on the consumer" + NetUtils.getLocalHost()
+ " using the dubboversion " + Version.getVersion()
+ ". Please check ifthe providers have been started and registered.");
}
}
对于这两个异常的直接结论是,若是某个URL去注册中心注册过,但后来该URL下没有Provider了,那么此时Consumer调用Provider将报第一种异常;若是Consumer调用了一个从未去注册中心注册过的URL,则会报第二种异常。
须要明确一点的是,注册中心的两个重要目的是服务发现和服务人工介入,线上的Provider和Consumer都不能强依赖注册中心,哪怕注册中心是双机部署,但要作到对注册中心的弱依赖,Consumer端须要有简单的负载均衡和Failover机制。
本文转自:http://blog.csdn.net/manzhizhen/article/details/53025666