聊聊selectByP2C中令人懵逼的代码

引子

最近在阅读dubbo源码的时候,发现了一段比较有意思的代码,初读的时候也一下子没太明白,出自AdaptiveLoadBalance类的selectByP2C方法,原始代码是这样的:

我们重点看下上图中红框框出来的这部分,短短5行,是我们今天要聊的重点,为了方便查看,我把源码复制出来

ini 复制代码
int pos1 = ThreadLocalRandom.current().nextInt(length);
int pos2 = ThreadLocalRandom.current().nextInt(length - 1);
if (pos2 >= pos1) {
    pos2 = pos2 + 1;
}

虽然这段代码出自dubbo,但是实际上跟dubbo关系不大,就算你之前完全不了解dubbo,也不影响阅读本文。

P2C算法

从方法名不难看出来,这个方法主要作用是通过负载均衡P2C算法来选择出一个节点。说起负载均衡算法,比较常用有随机、轮询、加权随机、加权轮询等,这些都非常好理解,实现起来也比较直接。

P2C(The Power of Two Random Choices)是另一种负载均衡算法,出自论文《The Power of Two Random Choices: A Survey of Techniques and Results》,主要的策略有2步:

  1. 从备选节点中随机选出2个不同的节点,记作nodeAnodeB
  2. 比较nodeAnodeB的负载情况,选择负载低的

上面说的5行代码,其实就是实现第1步,随机选择2个节点。

疑问

上面的代码从字面意思上不难解读,这几行代码含义分别为:

  1. 从下标[0, n-1]这n个节点中随机选出1个节点,作为pos1
  2. 从下标[0, n-2]这n-1个节点中随机选出1个节点,作为pos2
  3. 如果pos2下标大于等于pos1下标,pos2下标自增1

但是疑问在于,为什么通过这样的策略,选择出来的2个节点是随机的,做到每个节点被选择到的概率都是一样的?

证明

从定性来看,第1步是从n个节点随机选1个,所以每个节点被选择到的概率是一样的;第2步只从[0, n-2]随机取,那么最后1个节点n-1没有机会被选择到,对最后1个节点是不公平的;第3步,如果符合pos2 >= pos1条件的话,pos2要自增1,那下标为0的节点就不会被选择到,对第1个节点是不公平的。

从上面简单的定性分析看,出现偏差的步骤在2和3,分别对最后1个节点和第1个节点是不公平的,所以,关键点在于,这种不公平是否是对等的。

我们来看步骤3,有多大的概率会触发pos2 >= pos1条件:

  • 步骤1,可能选出的结果是0,1,2...n-2,n-1,每个概率是1/n
  • 步骤2,可能选出的结果是0,1,2...n-2,每个概率是1/(n-1)

我们遍历步骤2的每个结果,看步骤1有多少种可能会满足pos2 >= pos1条件:

  • 如果步骤2选择的结果是0,那么步骤1必须选择0,综合概率为 1/(n-1) * 1/n = 1/n*(n-1)
  • 如果步骤2选择的结果是1,那么步骤1可以选择0,1,综合概率为1/(n-1) * 2/n = 1/n*(n-1)

遍历到这里,是不是找到一点感觉了:

  • 如果步骤2选择的结果是n-2,那么步骤1可以选择0,1,2...n-2,综合概率为1/(n-1) * (n-1)/n = (n-1)/n*(n-1)

如果,我们把这些概率都累加起来,会发现分母都是一样的n*(n-1),分子就是1+2+...+n-1,这不就是高斯求和么。

所以,这里分子的和就是n*(n-1)/2,分母是n*(n-1),综合的概率就是1/2,把步骤2和步骤3整合起来看,对每个元素还是公平的。

后话

写到这里,相信你可以大概明白,为什么这几行代码可以做到等概率随机选择2个元素,虽然证明的过程比较复杂,但是这个算法整体来看还是比较简单和高效的。并且这种写法在go zero中也存在,不知道是不是互相借鉴的:

go 复制代码
switch len(p.conns) {
	case 0:// 没有节点,返回错误
		return emptyPickResult, balancer.ErrNoSubConnAvailable
	case 1:// 有一个节点,直接返回这个节点
		chosen = p.choose(p.conns[0], nil)
	case 2:// 有两个节点,计算负载,返回负载低的节点
		chosen = p.choose(p.conns[0], p.conns[1])
	default:// 有多个节点,p2c 挑选两个节点,比较这两个节点的负载,返回负载低的节点
		var node1, node2 *subConn
        // 3次随机选择两个节点
		for i := 0; i < pickTimes; i++ {
			a := p.r.Intn(len(p.conns))
			b := p.r.Intn(len(p.conns) - 1)
			if b >= a {
				b++
			}
			node1 = p.conns[a]
			node2 = p.conns[b]
			// 如果这次选择的节点达到了健康要求, 就中断选择
			if node1.healthy() && node2.healthy() {
				break
			}
		}
		// 比较两个节点的负载情况,选择负载低的
		chosen = p.choose(node1, node2)
	}

当然本文并不是鼓吹这样的写法,代码虽然主要是给机器执行,但是程序员可阅读和易于理解也是另外一个非常重要的维度,对于随机选择2个元素,我们也可以这样直观来写:

less 复制代码
Object node1 = nodeList.remove(ThreadLocalRandom.current().nextInt(nodeList.size()-1));
Object node2 = nodeList.remove(ThreadLocalRandom.current().nextInt(nodeList.size()-1));

这种写法看一眼就能理解,坏处在于破坏了原始的nodeList,所以这里使用的nodeList可能需要额外复制出来。

看到这里了,点个赞再走呗

相关推荐
希忘auto15 分钟前
详解MySQL安装
java·mysql
冰淇淋烤布蕾27 分钟前
EasyExcel使用
java·开发语言·excel
拾荒的小海螺33 分钟前
JAVA:探索 EasyExcel 的技术指南
java·开发语言
Jakarta EE1 小时前
正确使用primefaces的process和update
java·primefaces·jakarta ee
马剑威(威哥爱编程)1 小时前
哇喔!20种单例模式的实现与变异总结
java·开发语言·单例模式
java—大象1 小时前
基于java+springboot+layui的流浪动物交流信息平台设计实现
java·开发语言·spring boot·layui·课程设计
问道飞鱼1 小时前
【微服务知识】开源RPC框架Dubbo入门介绍
微服务·rpc·开源·dubbo
杨哥带你写代码2 小时前
网上商城系统:Spring Boot框架的实现
java·spring boot·后端
camellias_2 小时前
SpringBoot(二十一)SpringBoot自定义CURL请求类
java·spring boot·后端
布川ku子2 小时前
[2024最新] java八股文实用版(附带原理)---Mysql篇
java·mysql·面试