从零开始学Dubbo-基础篇-路由使用场景和服务降级

811 阅读4分钟

序言

捞一下之前的文章:

gitee地址gitee.com/davonding/d…

路由使用场景

我们去想象这样的一个场景,一个dubbo的提供者要准备进行上线,一般都提供多台提供者 来同时在线上提供服务。这时候一个请求刚到达一个提供者,提供者却进行了关闭操作。那么此次请求 就应该认定为失败了。所以基于这样的场景,我们可以通过路由的规则,把预发布(灰度)的机器进行从 机器列表中移除。并且等待一定的时间,让其把现有的请求处理完成之后再进行关闭服务。同时,在启 动时,同样需要等待一定的时间,以免因为尚未重启结束,就已经注册上去。等启动到达一定时间之 后,再进行开启流量操作。

实现思路如下:

1.利用zookeeper的路径感知能力,在服务准备进行重启之前将当前机器的IP地址和应用名写入zookeeper。 
2.服务消费者监听该目录,读取其中需要进行关闭的应用名和机器IP列表并且保存到内存中。 
3.当前请求过来时,判断是否是请求该应用,如果是请求重启应用,则将该提供者从服务列表中移除。

首先创建router模块

1.实现zk客户端操作类

引入 Curator 框架,用于方便操作Zookeeper,依赖

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.2.0</version>
</dependency>

编写Zookeeper的操作类,用于方便进行zookeeper处理

public class ZookeeperClients {
    private final CuratorFramework client;
    //单例
    private static ZookeeperClients zookeeperClient;

    static {
        //重试策略
        final ExponentialBackoffRetry exponentialBackoffRetry = new ExponentialBackoffRetry(1000, 3);
        //curator客户端
        final CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("127.0.0.1:2181", exponentialBackoffRetry);
        zookeeperClient=new ZookeeperClients(curatorFramework);
        //启动客户端
        curatorFramework.start();
    }
    
    //私有构造
    private ZookeeperClients(CuratorFramework client) {
        this.client = client;
    }

    public static CuratorFramework client(){
        return zookeeperClient.client;
    }
}

2.实现路径监听器

实现 需要预发布的路径管理器,用于缓存和监听所有待发布服务列表。

public class ReadyRestartInstances implements PathChildrenCacheListener {
    private static final Logger logger=LoggerFactory.getLogger(ReadyRestartInstances.class);
    private static final String PATH ="/study/dubbo/restart/instances";
    //zkclient
    private final CuratorFramework client;
    //节点发生变化 更新该set 保证可见行
    private volatile Set<String> restartInstances=new HashSet<>();

    private ReadyRestartInstances(CuratorFramework client) {
        this.client = client;
    }
    @Override
    public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
        //查询监听路径下所有目录配置信息
        final List<String> list = client.getChildren().forPath(PATH);
        //节点为空 清空set
        if(CollectionUtils.isEmpty(list)){
            restartInstances= Collections.emptySet();
        }
        //节点不为空
        if(!CollectionUtils.isEmpty(list)) {
            restartInstances=new HashSet<>(list);
        }
    }

    public static ReadyRestartInstances create(){
        final CuratorFramework client = ZookeeperClients.client();
        try {
            //检查监听路径是否存在
            final Stat stat = client.checkExists().forPath(PATH);
            //不存在
            if(stat == null){
                client.create().creatingParentsIfNeeded().forPath(PATH);
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("确保路径存在error:{}",e.getMessage());
        }
        final ReadyRestartInstances readyRestartInstances = new ReadyRestartInstances(client);
        //创建nodecache对象
        final PathChildrenCache pathChildrenCache = new PathChildrenCache(client, PATH, false);
        //给节点缓存对象加入监听
        pathChildrenCache.getListenable().addListener(readyRestartInstances);
        try {
            pathChildrenCache.start();
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("启动路径监听失败");
        }
        return readyRestartInstances;
    }

    //增加重启实例方法
    public void addRestartingInstances(String applicationName,String host) throws Exception {
        client.create().creatingParentsIfNeeded().forPath(PATH +"/"+buildApplicationAndInstanceString(applicationName,host));
    }
    //移除重启实例方法
    public void removeRestartingInstances(String applicationName,String host) throws Exception {
        client.delete().forPath(PATH +"/"+buildApplicationAndInstanceString(applicationName,host));
    }
    //判断节点信息是否存在
    public boolean hasRestartingInstances(String applicationName,String host){
        return restartInstances.contains(buildApplicationAndInstanceString(applicationName,host));
    }

    //返回应用名_主机字符串
    private String buildApplicationAndInstanceString(String appliactionName,String host){
        return appliactionName+"_"+host;
    }
}

3.实现Router接口

实现接口配置路由规则,主要是route方法

public class RestartingInstanceRouter implements Router {
    private final ReadyRestartInstances readyRestartInstances;
    private final URL url;

    public RestartingInstanceRouter(URL url) {
        this.url = url;
        this.readyRestartInstances=ReadyRestartInstances.create();
    }

    @Override
    public URL getUrl() {
        return null;
    }

    @Override
    public <T> List<Invoker<T>> route(List<Invoker<T>> list, URL url, Invocation invocation) throws RpcException {
        //过滤服务器,如果是在待发布列表中的服务则不会返回
        return list.stream().filter(i->!readyRestartInstances.hasRestartingInstances(i.getUrl().getParameter("remote.application"),i.getUrl().getIp())).collect(Collectors.toList());
    }

    @Override
    public boolean isRuntime() {
        return false;
    }

    @Override
    public boolean isForce() {
        return false;
    }

    @Override
    public int getPriority() {
        return 0;
    }
}

4.实现RouterFactory接口,完成spi配置

此处也能配置生效范围@Activate

@Activate
public class RestartingInstanceRouterFactory implements RouterFactory {
    @Override
    public Router getRouter(URL url) {
        return new RestartingInstanceRouter(url);
    }
}

在resources目录下新建META-INF/dubbo目录,以org.apache.dubbo.rpc.cluster.RouterFactory命名。 然后配置: restartInstances=com.study.router.RestartingInstanceRouterFactory

5.consumer引用router依赖

1.实现一个模拟服务增删的类

public class ServerRestartMain {
    public static void main(String[] args) throws Exception {
        final ReadyRestartInstances readyRestartInstances = ReadyRestartInstances.create();
        //模拟新增待发布服务
        readyRestartInstances.addRestartingInstances("dubbo-service-provider","172.16.8.233");
        //模拟删除待发布服务
        readyRestartInstances.removeRestartingInstances("dubbo-service-provider","172.16.8.233");
    }
}

2.测试

首先查看一下zk,目前没有待发布的机器

image.png

测试一下可正常使用

image.png

执行一下新增待发布机器,查看一下zk,有一个待发布的服务,也就是本机。

image.png

再次测试,就会提示没有可用服务了,就不会去调用未发布的服务了。(因为我这里只有一台机器,正常如果是多台机器就会访问别的可用的机器)

image.png

服务降级

什么是服务降级

服务降级,当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务有策略的降低服务级别, 以释放服务器资源,保证核心任务的正常运行。

为什么要服务降级

而为什么要使用服务降级,这是防止分布式服务发生雪崩效应,什么是雪崩?就是蝴蝶效应,当一个请 求发生超时,一直等待着服务响应,那么在高并发情况下,很多请求都是因为这样一直等着响应,直到 服务资源耗尽产生宕机,而宕机之后会导致分布式其他服务调用该宕机的服务也会出现资源耗尽宕机, 这样下去将导致整个分布式服务都瘫痪,这就是雪崩。

1.使用标注@Reference

使用@Reference(mock="return null") 也支持 @Reference(mock="force:return null")

首先将服务调用响应时间设置为1000ms

    String hello=bean.sayHello("test",1000);

,然后响应超时时间(路由设置了)设置为1000ms。查看调用结果请求超时了

image.png

这时候在consumer调用方配置 mock

    @Reference(loadbalance = "random",mock = "return 测试")
    private HelloDubbo helloDubbo;

再次请求,查看结果,会发现请求失败后会返回mock的数据

image.png

如果mock="force:return null"这样配置,则强制返回。

2.使用java动态配置

这里就不做演示了

    RegistryFactory registryFactory = ExtensionLoader
            .getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
    Registry registry = registryFactory
            .getRegistry(URL.valueOf("zookeeper://IP:端 口")); 
    registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService? category=configurators&dynamic=false&application=foo&mock=force:return+null"));