简易RPC框架实现——8、引入SPI机制实现动态可拔插,引入一致性哈希进行负载均衡

本章主要分为两个部分,第一部分是在负载均衡策略上引入了一致性哈希的算法,commit为798bcbb;第二个部分仿照Dubbo源码自己利用SPI机制实现了组件的可拔插,commit为9ac844a

一致性哈希

在原本的负载均衡策略中,我们使用的是普通轮询负载均衡策略,在服务器的物理机硬件差距较大的情况下,会导致大量的请求堆积到硬件差的物理机上,严重的会造成宕机,因此,本章引入了一致性哈希负载均衡算法。

一致性哈希利用一个哈希环来防止某一个集群的服务宕机导致,所有散列表上的缓存失效的致命问题。如下图:
hash环
可以看到,每一个服务器在hash环上的定位需要经过两次hash计算,首先找到他位于哪个集群,之后找到它位于集群中环上的位置。当某一集群中某个节点消失后,会顺时针移动到下一集群,如下图:
删除节点
可以看到,使用这种hash环基本满足了要求,但是仍然存在些许问题。如果节点数量少,而hash环初始化空间很大,导致分布位置不均匀,集群的每个实例上缓存的数据量不一致,造成严重的数据倾斜。如果每一个集群只有一个节点,当这个集群消失时,他负责的所有缓存将交给下一个集群,下一个集群的压力会瞬间增大,有可能引发雪崩。

解决上述的两个问题我们可以引入虚拟节点,一个真实节点可以映射非常多的虚拟节点,这样的话可以使得各个节点在hash环上的分布非常均匀。

同时由于hash算法的随机性,当一个节点失效以后,他所持有的缓存也会均匀地分布给其他节点,如下图:
引入虚拟节点

代码示例

实现hash环,我们可以利用数组或者链表进行模拟,但这样的话查找hash值时可能需要排序,对于大量数据效率不高,因此我们采用TreeMap来模拟实现hash环。出于计算地考虑,本文采用hash算法为FNV1_32_HASH算法。新建一个ConsistentHash类,传入用户自定义地虚拟节点数(默认160个),之后映射到TreeMap中,获取实例,则根据客户端地址地hash值进行获取,如下:

public class ConsistentHash {
    private static TreeMap<Integer,String> Nodes = new TreeMap<>();
    private static int VIRTUAL_NODES = 160;
    private static List<Instance> instances = new ArrayList<>();
    public static final HashMap<String,Instance> map = new HashMap<>();
    public ConsistentHash(List<Instance> instances,int VIRTUAL_NODES){
        ConsistentHash.instances = instances;
        ConsistentHash.VIRTUAL_NODES = VIRTUAL_NODES;
        creatHashRound();
    }

    private void creatHashRound(){
        for(Instance instance : instances){
            String ip = instance.getIp();
            Nodes.put(getHash(ip),ip);
            map.put(ip,instance);
            for(int i = 0;i < VIRTUAL_NODES;i++){
                String virtualNodesName = getVirtualNodeName(ip, i);
                int hash = getHash(virtualNodesName);
                Nodes.put(hash,virtualNodesName);
            }
        }
    }
    private String getVirtualNodeName(String ip,Integer i){
        return ip + "&&VN" + i;
    }

    private String getRealNodeName(String virtualNodeName){
        return virtualNodeName.split("&&")[0];
    }


    public String getServer(String clientInfo){
        int hash = getHash(clientInfo);
        SortedMap<Integer,String> subMap = Nodes.tailMap(hash);
        String virtualNodeName;
        if(subMap == null || subMap.isEmpty()){
            virtualNodeName = Nodes.get(Nodes.firstKey());
        }else{
            virtualNodeName = subMap.get(subMap.firstKey());
        }
        return getRealNodeName(virtualNodeName);
    }



    public static int getHash(String str){
        final int p = 16777619;
        int hash = (int) 2166136261L;
        for(int i = 0; i < str.length();i++){
            hash = (hash^str.charAt(i)) * p;
            hash += hash << 13;
            hash ^= hash >> 7;
            hash += hash << 3;
            hash ^= hash >> 17;
            hash += hash << 5;
            if(hash < 0){
                hash = Math.abs(hash);
            }
        }
        return hash;
    }
}

实现LoadBalance接口,由于在一致性hash算法中,每个客户端每次获得的服务器实例应该相同,因此我们根据客户端地址进行获取实例,如下:

public class IpHashLoadBalance implements LoadBalance {

    private String address;


    @Override
    public Instance getInstance(List<Instance> list) {
        ConsistentHash ch = new ConsistentHash(list,100);
        HashMap<String, Instance> map = ch.map;
        return map.get(ch.getServer(address));
    }

    public void setAddress(String address){
        this.address = address;
    }
    
}

完成了一致性哈希负载均衡算法的引入。

实现SPI机制

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。机制如下:

SPI机制
SPI实际上是“接口+策略模式+配置文件”实现的动态加载机制。在系统设计中,模块之间通常基于接口编程,不直接显示指定实现类。一旦代码里指定了实现类,就无法在不修改代码的情况下替换为另一种实现。为了达到动态可插拔的效果,java提供了SPI以实现服务发现,但是java原生的SPI模式在加载外部配置时,会将所有的类进行加载,大量增加了服务器负载。因此我们这里实现SPI机制仿照的dubbo源码中采用键值对配置的方式。为了能够加载指定接口,我们需要自定义注解,以此来标注:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SPI {
}

之后需要自定义加载器,其中创建加载方法,具体代码如下,详细内容见代码注释:

public final class ExtensionLoader<T> {

    private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);

    /**
     *扩展类存放的地址
     */
    private static final String SERVICE_DIRECTORY = "META-INF/extensions/";
    /**
     *扩展加载器的缓存
     */
    private static final Map<Class<?>,ExtensionLoader<?>> EXTENSION_LOADER = new ConcurrentHashMap<>();
    /**
     *扩展类实例的缓存
     */
    private static final Map<Class<?>,Object> EXTENSION_INSTANCE = new ConcurrentHashMap<>();


    /**
     *扩展类Class文件
     */
    private final Class<?> type;
    /**
     *扩展类实例持有者Holder的缓存
     */
    private final Map<String,Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
    /**
     *扩展类配置列表缓存
     */
    private final Holder<Map<String,Class<?>>> cachedClass = new Holder<>();

    /**
     *构造器指定Class类型
     */
    public ExtensionLoader(Class<?> type){this.type = type;}

    /**
    * @Description: 根据类型得到扩展加载器
    * @Param: Class文件
    * @return: 扩展类型加载器
    * @Author: fzzfrjf
    * @Date: 2022/6/13
    */
    public static <S> ExtensionLoader<S> getExtensionLoader(Class<S> type){
        if(type == null){
            throw new IllegalArgumentException("Extension type should not be null");
        }
        if(!type.isInterface()){
            throw new IllegalArgumentException("Extension type must be an interface");
        }
        if(type.getAnnotation(SPI.class) == null){
            throw new IllegalArgumentException("Extension type must be annotated by @SPI");
        }
        ExtensionLoader<S> extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADER.get(type);
        if(extensionLoader == null){
            EXTENSION_LOADER.putIfAbsent(type,new ExtensionLoader<S>(type));
            extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADER.get(type);
        }
        return extensionLoader;
    }

    /**
    * @Description: 获取扩展实例对象
    * @Param: name 扩展类在配置文件中的名字
    * @return: 扩展类实例对象
    * @Author: fzzfrjf
    * @Date: 2022/6/13
    */
    public T getExtension(String name){
        if(StringUtil.isBlank(name)){
            throw new IllegalArgumentException("Extension name should not be null or empty");
        }
        Holder<Object> holder = cachedInstances.get(name);
        if(holder == null){
            cachedInstances.putIfAbsent(name,new Holder<>());
            holder = cachedInstances.get(name);
        }
        Object instance = holder.getValue();
        if(instance == null){
            synchronized (holder){
                if(instance == null){
                    instance = createExtension(name);
                    holder.setValue(instance);
                }
            }
        }
        return (T) instance;
    }

    /**
    * @Description: 创建对应名字的扩展类对象
    * @Param: name 扩展类在配置文件中的名字
    * @return: 扩展类对象
    * @Author: fzzfrjf
    * @Date: 2022/6/13
    */
    private T createExtension(String name){
        Map<String, Class<?>> extensionClasses = getExtensionClasses();
        Class<?> clazz = extensionClasses.get(name);
        if(clazz == null){
            throw new RuntimeException("No such extension of name:" + name);
        }
        T instance = (T) EXTENSION_INSTANCE.get(clazz);
        if(instance == null){
            try{
                EXTENSION_INSTANCE.putIfAbsent(clazz,clazz.newInstance());
                instance = (T) EXTENSION_INSTANCE.get(clazz);
            }catch (Exception e){
                logger.error(e.getMessage());
                throw new RuntimeException("Fail to create instance of the extension class:" + clazz);
            }
        }
        return instance;
    }

    /**
    * @Description: 获取当前类型的所有扩展类
    * @Param: 
    * @return: 扩展类名字与对应的全类名的Map
    * @Author: fzzfrjf
    * @Date: 2022/6/13
    */
    private Map<String,Class<?>> getExtensionClasses(){
        Map<String, Class<?>> classes = cachedClass.getValue();
        if(classes == null){
            synchronized (cachedClass){
                classes = cachedClass.getValue();
                if(classes == null){
                    classes = new HashMap<>();
                    loadDirectory(classes);
                    cachedClass.setValue(classes);
                }
            }
        }
        return classes;
    }


    /**
    * @Description: 获取指定配置文件中所有的配置
    * @Param: Map<String,Class<?>>
    * @return: void
    * @Author: fzzfrjf
    * @Date: 2022/6/13
    */
    private void loadDirectory(Map<String,Class<?>> extensionClasses){
        String fileName = ExtensionLoader.SERVICE_DIRECTORY +type.getName();
        try{
            Enumeration<URL> urls;
            ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
            urls = classLoader.getResources(fileName);
            if(urls != null){
                while(urls.hasMoreElements()){
                    URL url = urls.nextElement();
                    loadResource(extensionClasses,classLoader,url);
                }
            }
        }catch (IOException e){
            logger.error(e.getMessage());
        }
    }

    /**
    * @Description: 读取指定配置文件中不同的实例化对象配置并放入缓存
    * @Param:
    * @return: void
    * @Author: fzzfrjf
    * @Date: 2022/6/13
    */
    private void loadResource(Map<String,Class<?>> extensionClasses,ClassLoader classLoader,URL url){
        try(BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))){
            String line;
            while((line = reader.readLine()) != null){
                final int ci = line.indexOf("#");
                if(ci >= 0){
                    line = line.substring(0,ci);
                }
                line = line.trim();
                if(line.length() > 0){
                    try{
                        final int ei = line.indexOf("=");
                        String name = line.substring(0,ei).trim();
                        String clazzName = line.substring(ei + 1).trim();
                        if(name.length() > 0 && clazzName.length() > 0){
                            Class<?> clazz = classLoader.loadClass(clazzName);
                            extensionClasses.put(name,clazz);
                        }
                    }catch (ClassNotFoundException e){
                        logger.error(e.getMessage());
                    }
                }
            }
        }catch (IOException e){
            logger.error(e.getMessage());
        }
    }
}

Holder为仿照dubbo中持有对象实例的Holder构造。

之后我们只需在resources文件中创建对应的键值对:

文件结构
最终可以在需要的位置加载我们需要的接口实现组件,例如NettyServer构造方法中选择序列化方法:

this.serializer = ExtensionLoader.getExtensionLoader(CommonSerializer.class).getExtension("kryo");

最终完成了类似dubbo的SPI机制的实现。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值