关于后端:我坚定的认为这个源码肯定是有-BUG-的

34次阅读

共计 4893 个字符,预计需要花费 13 分钟才能阅读完成。

你好呀,我是歪歪。

上周我不是发了《我试图给你分享一种自适应的负载平衡。》这篇文章嘛,外面一种叫做“自适应负载平衡”的负载平衡策略,外围思路就是从多个服务提供者中随机抉择两个进去,而后持续抉择两者中“负载”最小的那个节点。

前几天有读者看了文章后找到我,提出了两个问题。

有点意思,我给你盘一下。

第一个问题

第一个问题是这样的:

他的图片,指的是文章中的这个局部:

过后我也没有细看,所以我的回复是 timeout 是个配置项,我这里取出来都是 30000 的起因是因为我没有进行配置。

而后,他对于问题进行了进一步的形容:

.png)

我点到源码外面一看,好家伙,它是这样写的:

int timeout1 = getTimeout(invoker2, invocation);
int timeout2 = getTimeout(invoker2, invocation);

两次调用 getTimeout 办法的入参截然不同。这个中央你用脚指头想也应该能晓得它的参数传递谬误了嘛。

我甚至能猜到作者写这一行代码的时候,按了一个 ctrl+d 快捷键,复制了一行进去,后果只是把后面的 timeout1 改成了 timeout2,遗记改前面了。

这种低级谬误 …

我也犯过好几次。

而后我叫这个读者能够去提一个 pr,当前进来吹牛的时候就能够说:我已经给 apache 顶级开源我的项目奉献过源码。

然而这个读者可能比拟低调,把这个机会让给我了。

于是 …

我就厚着脸皮去提 pr 了,而后被 merge 了:

https://github.com/apache/dubbo/pull/12636

把 invoker2 批改为 invoker1,搞定。

难受了,在我到处混 pr 的光彩事迹中,又增加了浓墨重彩的一笔。

第二个问题

第二个问题,其实我在之前的文中也提到了。

文章外面对于“随机抉择两个”进去这个动作的代码实现,我感觉是有 BUG 的,所以提出了一个大胆的质疑:

然而秉着“又不是不能用”的外围思路,过后也没有细想。

当我后面的那个 pr 被 merge 的时候,我决定:要不好人做到底,把这个 BUG 也帮它们修复一下吧。

首先,我来具体解释一下,我为什么会认为这个中央有 BUG。

首先,把它的整个源码拿过去:

int pos1 = ThreadLocalRandom.current().nextInt(length);
int pos2 = ThreadLocalRandom.current().nextInt(length - 1);
if (pos2 == pos1) {pos2 = pos2 + 1;}

我就以最简略的状况,只有三个服务提供者,即 length=3,而后随机选两个进去,这种状况来进行阐明。

我也不进行数学论证了,间接就是给你表演一个穷举大法。

首先,咱们的 invokers 汇合外面有三个服务提供方,invoker1,invoker2,invoker3:

当执行这一行代码的时候:

int pos1 = ThreadLocalRandom.current().nextInt(length);

length=3,即

int pos1 = ThreadLocalRandom.current().nextInt(3);

所以 pos1 的取值范畴是 [0,3)。

后面说了,我要用穷举大法,所以咱们要剖析 pos1 别离为 0,1,2 的时候。

pos1=0

首先,咱们剖析 pos1=0 的状况。

当 pos1=0 时,咱们要算 pos2 的值,当执行这行代码的时候:

int pos2 = ThreadLocalRandom.current().nextInt(length – 1);

它的取值范畴是 [0,2)。

所以,通过两行随机的代码之后,咱们能失去这样的组合:

针对组合一,又因为有这个判断在这里:

if (pos2 == pos1) {pos2 = pos2 + 1;}

当 pos2 = pos1,即随机到同一个下标的时候,pos2 须要加一,免得应用同一个 invoker。

所以,当 pos1=0 时,随机的最终只会是这两种状况:

pos1=1

同理,咱们能够得出 pos1=1 时,状况是这样的:

pos1=2

当 pos1=2 时,状况是这样的:

汇总

此时,咱们所有状况都剖析实现了,穷举大法曾经应用结束。

这个时候咱们把所有的状况组合起来看一下:

  • invoker1 被选中了 4 次
  • invoker2 被选中了 5 次
  • invoker3 被选中了 3 次

来,请你大声点的通知我,这个算法是不是偏心的?

都不是 1:1:1 了,还偏心个啥啊。

所以,我在之前的文章外面是这样说的:

事实也证实了,的确是对于最初一个元素是不偏心的。

于是,我开始筹备着手敲代码,打算再混一个 pr。

我想换成的源码也很简略。因为它外围指标是从 list 汇合中随机返回两个对象嘛。

那我间接就是这样:

Object invoker1 = invokerList.remove(ThreadLocalRandom.current().nextInt(invokerList.size()));
Object invoker2 = invokerList.remove(ThreadLocalRandom.current().nextInt(invokerList.size()));

你认真的嗦一嗦这个代码,是不是很偏心?

当一个元素被选中之后,我就把它给踢出去。这样第二次随机的时候,invokerList.size() 的值就实现了减一的逻辑。

既能够保障第二次随机的时候,不会随机到一样的元素。

也能够保障剩下的每个元素都有机会再次参加到随机过程中。

为此,我还专门写了一个 Demo 来验证这个写法:

public class MainTest {private final static HashMap<Integer, Integer> COUNT_MAP = new HashMap<>();
    
    public static void main(String[] args) {for (int i = 0; i < 100000; i++) {List<Integer> list = new ArrayList<Integer>();
            list.add(1);
            list.add(2);
            list.add(3);
            Integer invoker1 = list.remove(ThreadLocalRandom.current().nextInt(list.size()));
            Integer invoker2 = list.remove(ThreadLocalRandom.current().nextInt(list.size()));
            posCount(invoker1);
            posCount(invoker2);
        }
        System.out.println(COUNT_MAP);
    }

    public static void posCount(Integer key) {Integer pos1Integer = COUNT_MAP.get(key);
        if (pos1Integer == null) {COUNT_MAP.put(key, 1);
        } else {
            pos1Integer++;
            COUNT_MAP.put(key, pos1Integer);
        }
    }
}

你粘过来就能跑,运行 10w 次,每个元素被选中的总次数基本上就是 1:1:1。

而把 Dubbo 源码外面的实现拿过去:

public class MainTest {private final static HashMap<Integer, Integer> COUNT_MAP = new HashMap<>();

    public static void main(String[] args) {List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(2);
        list.add(3);
        int length = list.size();
        for (int i = 0; i < 100000; i++) {int pos1 = ThreadLocalRandom.current().nextInt(length);
            int pos2 = ThreadLocalRandom.current().nextInt(length - 1);
            if (pos2 >= pos1) {pos2 = pos2 + 1;}
            posCount(pos1);
            posCount(pos2);
        }
        System.out.println(COUNT_MAP);
    }

    public static void posCount(Integer key) {Integer pos1Integer = COUNT_MAP.get(key);
        if (pos1Integer == null) {COUNT_MAP.put(key, 1);
        } else {
            pos1Integer++;
            COUNT_MAP.put(key, pos1Integer);
        }
    }
}

也跑 10w 次,运行后果是这样的:

卧槽,等等,怎么回事,竟然也是靠近 1:1:1 的?

我过后看到这个运行后果的时候,表情大略是这样的:

这玩意,和我剖析进去的不一样啊。

反转

其实也不能算是反转吧。

因为我后面剖析的时候,给的代码是这样的:

if (pos2 == pos1) {pos2 = pos2 + 1;}

而实在的源码是这样的:

if (pos2 >= pos1) {pos2 = pos2 + 1;}

一个是 ==,一个 >=。

当我把这里换成 == 的时候,运行后果就不再是 1:1:1 了,合乎我后面穷举大法剖析的状况。

而在我的潜意识外面,第一次看代码的时候,我始终认为这个局部的代码就是 ==,所以我始终依照 == 进行的剖析,从而感觉它有问题。

这波,我感觉得让潜意识来背锅。

当是 >= 的时候,咱们只须要从新剖析一下 pos1=0 的状况。

组合一,0>=0,满足条件,最终 pos1=0,pos2 会加一,变成 1,所以还是会变成之前剖析的状况:

过后对于组合二,状况就产生了奥妙的变动。

组合二,1>=0,满足条件,最终 pos1=0,pos2 会加一,变成 2,所以就变成了这样:

invker2 被替换为了 invoker3。

还记得咱们之前,依照 == 的状况,剖析进去的比例吗?

  • invoker1 被选中了 4 次
  • invoker2 被选中了 5 次
  • invoker3 被选中了 3 次

此时,咱们依照 >= 的状况剖析,invoker2 被替换为了 invoker3。

那么比例就变成了:

  • invoker1 被选中了 4 次
  • invoker2 被选中了 4 次
  • invoker3 被选中了 4 次

所以,回到我最最开始说的读者提出的第二个问题:

我在答复读者的时候,也是认为 == 就行了,尽管不偏心,然而也不是不能用。

然而通过后面这一波剖析。

为什么肯定要是 >=,而不能只是 == 呢?

之前,我始终认为不偏心是因为我认为最初一个元素少参加了一次随机。

然而,因为 >= 的存在,并不会存在这种状况。

啊,为什么会产生一种让我想要跪下的感觉?

数学,是因为我在外面加了数学。

神奇的、令人又上头又着迷的数学。

荒腔走板

在这个事件上,我整个心态是从自信满满到一地鸡毛,这个心路历程让我想起了我大学的时候,学过的一门课程叫做《线性代数》。

过后我学的可认真,老师讲的每节课我感觉我都听懂了,期末考试的过程中,包含考完之后我都是信念满满的样子,感觉这题也不难啊。

随随便便考个八十多分问题不大吧。

最初,考试后果进去的时候我没及格,我记得是 56 分还是 58 分的样子,反正差一点点及格,这课竟然挂了?

我过后在宿舍就拍案而起:必定有问题,我要求查卷,我做题的时候很有自信啊。

而后我要到了老师的联系方式,并自报家门,阐明状况,我保持认为应该是某个环节出了问题,看看能不能把卷子找进去再看看。

起初啊 …

老师把卷子拍照发给我了,的确是某个环节出了问题,这个环节就是我本人。

我和答案对了一下,卷面就只有 40 多分的样子。

最终问题有 50 多分是因为老师还算了平时分,由上课出勤率和日常作业实现状况综合算进去的。

那天,我站在宿舍的阳台上,看着手机上的试卷照片,再挑眼看向远方,夕阳西下,残阳如血,六楼的风儿甚至清静,肆意的在我脸上拂过。

楼下熙熙攘攘的学生走过,时不时的暴发出一阵阵银铃般的笑声,我只是感觉吵闹。

随后,我问室友:什么时候补考?有没有人能给我补习一下?

数学,啊,这神奇的、令人又上头又着迷的数学。

正文完
 0