本章主要分为两个部分,第一部分是在负载均衡策略上引入了一致性哈希的算法,commit为798bcbb;第二个部分仿照Dubbo源码自己利用SPI机制实现了组件的可拔插,commit为9ac844a。
一致性哈希
在原本的负载均衡策略中,我们使用的是普通轮询负载均衡策略,在服务器的物理机硬件差距较大的情况下,会导致大量的请求堆积到硬件差的物理机上,严重的会造成宕机,因此,本章引入了一致性哈希负载均衡算法。
一致性哈希利用一个哈希环来防止某一个集群的服务宕机导致,所有散列表上的缓存失效的致命问题。如下图:
可以看到,每一个服务器在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实际上是“接口+策略模式+配置文件”实现的动态加载机制。在系统设计中,模块之间通常基于接口编程,不直接显示指定实现类。一旦代码里指定了实现类,就无法在不修改代码的情况下替换为另一种实现。为了达到动态可插拔的效果,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机制的实现。