soul网关学习16-插件实现1-Divide2-负载均衡算法

接着上篇的文章来。

负载均衡算法

  • 代码出处,使用了一个工具类完成了负载均衡后端节点的选取
DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
  • 看下这里的源码
    public static DivideUpstream selector(final List<DivideUpstream> upstreamList, final String algorithm, final String ip) {
        LoadBalance loadBalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getJoin(algorithm);
        return loadBalance.select(upstreamList, ip);
    }
  • 可以看出这里是通过SPI的机制,根据传入算法名称,运行时动态得到具体的负载均衡实现,由扩展加载器加载到的对应的负载均衡实例
  • 先看看这里的SPI扩展点机制,然后再去看具体的负载均衡算法

SPI扩展点机制

  • org.dromara.soul.spi.ExtensionLoader关键代码
    public static <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz) {
        if (clazz == null) {
            throw new NullPointerException("extension clazz is null");
        }
        if (!clazz.isInterface()) {
            throw new IllegalArgumentException("extension clazz (" + clazz + ") is not interface!");
        }
        if (!clazz.isAnnotationPresent(SPI.class)) {
            throw new IllegalArgumentException("extension clazz (" + clazz + ") without @" + SPI.class + " Annotation");
        }
        ExtensionLoader<T> extensionLoader = (ExtensionLoader<T>) LOADERS.get(clazz);
        if (extensionLoader != null) {
            return extensionLoader;
        }
        LOADERS.putIfAbsent(clazz, new ExtensionLoader<>(clazz));
        return (ExtensionLoader<T>) LOADERS.get(clazz);
    }
  • 使用SPI机制是soul提供出来的扩展点,如果使用用户想要实现一套自己的负载均衡算法,则只要根据soul的规范,并做相应的配置就可以完成了。
    // TODO 写一个简单例子演示
  • 我们了解下 扩展点加载器ExtensionLoader,该加载器参考了dubbo中的ExtensionLoader,为其缩减版;对比之下,主要减少了依赖注入功能。跟jdk原生的SPI机制相比,实现了按需加载,因jdk会一次性将所有的扩展实现一次性加载的。
  • 关于SPI扩展点机制
  • soulSPI扩展点机制就先介绍到这里,我们看下soul提供的几种负载均衡算法

负载均衡算法实现

  • 从源码得出,负载均衡算法实现默认🈶️3种randomroundRobin,hash
    负载均衡算法实现

random-加权随机

  1. 看代码实际上是一个加权的随机算法
    public DivideUpstream doSelect(final List<DivideUpstream> upstreamList, final String ip) {
        // 计算所有后端节点的权重值
        int totalWeight = calculateTotalWeight(upstreamList);
        // 判断是否所有的节点为相同权重的情况
        boolean sameWeight = isAllUpStreamSameWeight(upstreamList);
        if (totalWeight > 0 && !sameWeight) {
            // 不是,则要加权随机
            return random(totalWeight, upstreamList);
        }
        // If the weights are the same or the weights are 0 then random
        // 如果各节点权重都相同,则按照总的节点数N,生成一个1-N之间的随机数X,然后选取出该X节点
        return random(upstreamList);
    }
  1. 加权随机的实现稍稍复杂点,看下源码
    private DivideUpstream random(final int totalWeight, final List<DivideUpstream> upstreamList) {
        // If the weights are not the same and the weights are greater than 0, then random by the total number of weights
        // 先拿到随机数为区间,为1-总权重值N之间
        int offset = RANDOM.nextInt(totalWeight);
        // Determine which segment the random value falls on
        // 遍历所有的节点,每次循环都用区间=区间-当前权重值,然后再看区间是否小于0;
        // 若小于0,则证明刚好位于当前节点的区间,返回当前节点;若没有,则继续循环
        for (DivideUpstream divideUpstream : upstreamList) {
            offset -= getWeight(divideUpstream);
            if (offset < 0) {
                return divideUpstream;
            }
        }
        // 最后返回第一个兜底
        return upstreamList.get(0);
    }

roundRobin-加权轮询

  1. 加权轮询的算法会有些难懂,我们先看下关键方法doSelect
    public DivideUpstream doSelect(final List<DivideUpstream> upstreamList, final String ip) {
        //TODO question 如果这批upstream第一个节点经常变更,可能会导致前面节点生成的WeightedRoundRobin数据无法释放
        String key = upstreamList.get(0).getUpstreamUrl();
        // 取出此批后端服务的权重对象 key -> 后端server,为ip:port   value -> 后端server对应的权重对象
        ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.get(key);
        if (map == null) {
            methodWeightMap.putIfAbsent(key, new ConcurrentHashMap<>(16));
            map = methodWeightMap.get(key);
        }
        // 总权重值
        int totalWeight = 0;
        // 当前请求权重的最大值,每次请求都会置为MIN_VALUE
        long maxCurrent = Long.MIN_VALUE;
        // 当前系统时间
        long now = System.currentTimeMillis();
        // 选中节点
        DivideUpstream selectedInvoker = null;
        // 选中节点的权重
        WeightedRoundRobin selectedWRR = null;
        //TODO question 轮询这里为什么没有break,应该找到选中节点后就跳出循环,不需要继续循环下去
        for (DivideUpstream upstream : upstreamList) {
            // rkey -> 后端server,ip:port
            String rKey = upstream.getUpstreamUrl();
            // 后端server对应的权重对象
            WeightedRoundRobin weightedRoundRobin = map.get(rKey);
            // 当前后端节点的权重值
            int weight = getWeight(upstream);
            // 如果当前后端server权重对象不存在,则需生成当前后端server的权重对象,并添加到内存
            if (weightedRoundRobin == null) {
                weightedRoundRobin = new WeightedRoundRobin();
                weightedRoundRobin.setWeight(weight);
                map.putIfAbsent(rKey, weightedRoundRobin);
            }
            // 节点权重值发生了变化,则需要重新设置
            if (weight != weightedRoundRobin.getWeight()) {
                //weight changed
                // 权重值发生变化后,将内存中后端server权重更新为最新权重值
                // 同时其内部current值也将更新为0,回到初始状态
                weightedRoundRobin.setWeight(weight);
            }
            // 内部序列从0开始增,每次进来都+自己的权重值
            long cur = weightedRoundRobin.increaseCurrent();
            // 重新设置更新时间
            weightedRoundRobin.setLastUpdate(now);
            // 如果节点当前轮询权重大于此次调用的最大权重值,则满足项,可以作为选中节点
            // 这里选中节点后,并不会停止循环,会遍历所有的节点,将最后一个满足条件的节点作为选中节点
            // 这里是为了找到当前权重最大的那个后端server,将其作为选中节点
            if (cur > maxCurrent) {
                maxCurrent = cur;
                selectedInvoker = upstream;
                selectedWRR = weightedRoundRobin;
            }
            // 总权重
            totalWeight += weight;
        }
        // 更新标志锁位未更新  后端节点数不等于原有节点数  采用compareAndSet取锁,获取到锁之后才更新
        if (!updateLock.get() && upstreamList.size() != map.size() && updateLock.compareAndSet(false, true)) {
            try {
                // copy -> modify -> update reference
                // 新的map
                ConcurrentMap<String, WeightedRoundRobin> newMap = new ConcurrentHashMap<>(map);
                // 如果从轮询开始,到结束,中间间隔了1min钟,则需要移除该后端server的权重对象数据
                // 这里是为了移除掉那些下线的节点,因为只要有被轮询,其LastUpdate=now;而超过1min钟,都没有轮询到的,则就是已下线的节点
                newMap.entrySet().removeIf(item -> now - item.getValue().getLastUpdate() > recyclePeriod);
                // 将新的后端节点的权重数据替换旧有的
                methodWeightMap.put(key, newMap);
            } finally {
                // 最终将锁的标志为设置为false
                updateLock.set(false);
            }
        }
        // 选中节点的处理
        if (selectedInvoker != null) {
            // 选中节点的内部计数的当前权重current要减去总的权重值,降低其当前权重,也就意味着其下次被选中的优先级被降低
            selectedWRR.sel(totalWeight);
            return selectedInvoker;
        }
        // should not happen here
        return upstreamList.get(0);
    }
  1. 因不懂其思想,在debug源码过程中,将关键变量变化的过程用表格记录了下来,方便跟踪逻辑
    RoundRobinComputeProcess
  2. 从上图可以看出:当权重为50:50时,4次调用其最终选取出来的结果为1:1;当权重为10:20时,6次调用其最终选中出来的结果为1:2,前3次调用最终选取出来的结果也为1:2。
  3. //TODO很是神奇,不知晓其中原理
  4. 实现逻辑看起来比较复杂,简单说来就是 对于每个 DivideUpstream 都维护一个权重对象,每次遍历所有权重对象获取到权重最大的那个 DivideUpstream,同时减去被选中的 DivideUpstream 的权重(减去总权重,优先级降为最低)

  5. 参考:【高性能网关soul学习】11. 插件之DevidePlugin

hash-IP哈希

  • 实际上是基于远程客户端IP的一个hash负载均衡算法
  • doSelect的源码
    public DivideUpstream doSelect(final List<DivideUpstream> upstreamList, final String ip) {
        // 并发跳表map key->后端server的hash值 value->后端节点
        final ConcurrentSkipListMap<Long, DivideUpstream> treeMap = new ConcurrentSkipListMap<>();
        for (DivideUpstream address : upstreamList) {
            // 每个实际后端节点会生成5个虚拟节点,用于更大范围映射,类似于一致性hash
            for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
                // 后端server的hash
                long addressHash = hash("SOUL-" + address.getUpstreamUrl() + "-HASH-" + i);
                treeMap.put(addressHash, address);
            }
        }
        // 远程客户端ip的hash
        long hash = hash(String.valueOf(ip));
        // 如果 请求地址的Hash值 右边存在节点,返回右边第一个
        // treeMap.tailMap(hash) 会返回所有(key >= hash)的映射集合,同时是有序的。
        SortedMap<Long, DivideUpstream> lastRing = treeMap.tailMap(hash);
        // 如果找到了,则从找到的entry拿出后端节点
        if (!lastRing.isEmpty()) {
            // 这里也就是取 url hash 值右边的第一个节点
            return lastRing.get(lastRing.firstKey());
        }
        // 没找到,则直接去第一个节点兜底
        return treeMap.firstEntry().getValue();
    }

总结

  1. Divide插件这块的内容还挺多的,后面还需要将负载均衡这块的逻辑重点看一看,搞清楚roundRobinhash算法的实现,同时也去看看其他框架中这两种算法的实现
  2. 已分析Divide插件中重点块:后端节点的探活机制负载均衡算法
  3. 后续还有http服务的请求转发websocket服务的支持
  4. 关于负载均衡分析的好文章

To be continued...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容