均衡负载里的算法

在均衡负载重,轮询算法是最常用的一个算法。 通常会使用带权重的轮询(wrr).

轮询

轮询的实现非常简单,假设我们有一组节点 [a,b,c,d] ,在不带有权重的时候,只需要一个 next 变量就可以完成。
使用 next 变量记录当前的位置,下一个就是当前位置加一后与节点的长度求余。

1
2
3
4
5
6
7
next := 0 // 当前选择
.....

func rr(){
next = (next+1)%len(servers)
return next
}

带权重的轮询

通常情况下,我们都会选择带权重的轮询来作为均衡算法,带权重有几个好处:

  • 将某个机器的权重调整为 0 来进行上下线的操作
  • 上线新功能或者新机器,可以逐步调大权重,来灰度部分流量来验证功能。

假设我们有这样一组节点 [ a:10 ,b:20,c:30] , 用来表示 3 个节点和其对应的权重。
我们在做计算的时候,需要对所有的权重求出他们的最小公倍数,让他们的表示成[a:1,b:2,c:3] 这样的值。

这里涉及到一个 gcd 的算法,用来求最小公倍数

gcd

gcd 的算法非常简单,叫做辗转求余,代码如下

1
2
3
4
5
6
func gcd(a int, b int) int {
if b == 0 {
return a
}
return gcd(b, a%b)
}

具体可以参看 gcd

wrr

通过 gcd 计算得到 [a:1,b:2,c:3] 后,我们需要按照权重来选择节点。这个时候,可以将前面的节点变成这样的数组 [a,b,b,c,c,c] ,数组中的每个节点都出现的权重指定的次数,这样我们就可以通过前面 rr 里面的算法 next = (next+1)%len(servers) 来进行计算。
但是这里会出现一个问题,a,b,c 的访问在时间片上面是不均衡的,可能会导致某个时间片对某个区间的直接访问。 这就需要对数组里的内容进行打散。
在 nginx 的 wrr 算法: https://github.com/phusion/nginx/commit/27e94984486058d73157038f7950a0a36ecc6e35 保证节点选择的平滑。

算法如下: 对于节点权重 {5,1,1} ,当前一个节点被选中后,权重减去所有权重和,这里是 7 ,当开始选择时,每个节点加上自身的权重。

这里稍微有一些理论,可以想象一下,这些节点在跑道上排成一队,在选择之前,每个人向前走自己的权重步,如果谁在最前面的话,就能够被选中,被选中的人需要向后退权重和的步数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 a  b  c
0 0 0 (initial state)

5 1 1 (a selected) 第一次选择了 a ,那么 a 的权重 - 7
-2 1 1 第一次选择后的结果

对第一次选择结果加 5 1 1 得到 3 2 2
3 2 2 (a selected) 第二次选择 a ,那么 a 的权重 -7 得到 -4 2 2
-4 2 2 如此反复整过过程。

1 3 3 (b selected)
1 -4 3

6 -3 4 (a selected)
-1 -3 4

4 -2 5 (c selected)
4 -2 -2

9 -1 -1 (a selected)
2 -1 -1

7 0 0 (a selected)
0 0 0

这个算法能够很好的使算法能够均衡。

hash

使用 hash 作为均衡负载算法,会应用到很多地方,通常情况下我们会根据请求的一些值来作为均衡的 key。

特别是一致性的 hash 算法,处在网络请求的很多位置都会用到,可能所用到的 hash 算法不同,但是本质都是为了保证同一个数据包能够发送到一个位置。
通常会根据不同的需求,对 源 ip、源端口 、目标 ip、源端口、目标端口、协议号 这五元组来做 hash 算法。 比如以下的场景

  • 数据包到达网口,网卡多队列
  • cpu 支持 DUMA 的时,需要让相同的数据包进来在一个核上处理
  • 四层均衡负载中保证 tcp/udp 是到同一个后端
  • 七层均衡负载中保证同一个客户端到同一个后端处理

meglev

Maglev算法是Google开发的一种网络负载均衡器。在很多新的四层均衡负载中喜欢使用,比如 facebook 的 katran。
maglev 使用的一个大的槽位,通过一致性 hash 来将计算分布到槽位上。
使用时固定了槽位,查询效率为 O(1).

https://github.com/zhangweidev/meglevgo

NAT小记

NAT小记

NAT 的分类

  • 全锥形: 一旦内部主机端口对(iAddr:iPort)被NAT网关映射到(eAddr:ePort),所有后续的(iAddr:iPort)报文都会被转换为(eAddr:ePort);任何一个外部主机发送到(eAddr:ePort)的报文将会被转换后发到(iAddr:iPort)

    外部主机不限制 ip 和端口

  • 限制锥形:一旦内部主机端口对(iAddr:iPort)被映射到(eAddr:ePort),所有后续的(iAddr:iPort)报文都会被转换为(eAddr:ePort);只有 (iAddr:iPort)向特定的外部主机hAddr发送过数据,主机hAddr从任意端口发送到(eAddr:ePort)的报文将会被转发到(iAddr:iPort)。

    外部主机限制ip不限制端口

  • 端口限制锥形: 一旦内部主机端口对(iAddr:iPort)被映射到(eAddr:ePort),所有后续的(iAddr:iPort)报文都会被转换为(eAddr:ePort);只有(iAddr:iPort)向特定的外部主机端口对(hAddr:hPort)发送过数据,由 (hAddr:hPort)发送到(eAddr:ePort)的报文将会被转发到(iAddr:iPort)。

    外部主机限制 ip 和端口

  • 对称型: NAT网关会把内部主机“地址端口对”和外部主机“地址端口对”完全相同的报文看作一个连接,在网关上创建一个公网“地址端口对”映射进行转换,只有收到报文的外部主机从对应的端口对发送回应的报文,才能被转换。即使内部主机使用之前用过的地址端口对去连接不同外部主机(或端口)时,NAT网关也会建立新的映射关系。

    外部主机的响应包才能发送

STUN、TURN、ICE

https://developer.aliyun.com/article/243540

STUN: 为终端提供一种能够获取自己经过NAT映射后的地址.

客户端向公网 STUN 服务器发送 Binding Request ,服务器收到后获取公网 IP:PORT,附加在 Binding Request 返回给客户端. 

TURN: TURN 作为通讯中间人,由服务器负责两方的数据转发.

ICE: 一种框架,可以整合现有的NAT穿透协议,尽可能的找到NAT穿透的数据通道.

打洞过程

  • 两个客户端处于同一 NAT 设备后

Untitled.png

当A向集中服务器发出消息请求与B进行连接,集中服务器S将B的外网地址二元组以及内网地址二元组发给A,同时把A的外网以及内网的地址二元组信息发给B。A和B发往对方公网地址二元组信息的UDP数据包不一定会被对方收到,这取决于当前的NAT设备是否支持不同端口之间的UDP数据包能否到达(即Hairpin转换特性),无论如何A与B发往对方内网的地址二元组信息的UDP数据包是一定可以到达的,内网数据包不需要路由,且速度更快。A与B推荐采用内网的地址二元组信息进行常规的P2P通信。

  • 两个客户端处于不同 NAT 设备后

Untitled.png

同上一个例子一样, A 和 B 得到了对方的外网 ip:port

当 A 往 NAT-B 发送 UDP 消息,经过 NAT-A ,并在 NAT-A 上生成会话表项,根据NAT类型可知除了全锥形NAT,NAT-B 认为设备 A 得消息未授权外网消息,会丢弃该数据包.

这时B设备向A发送一个UDP消息. NAT-B 上也会生成一个到NAT-A 的会话表项.

此时 NAT-A 和NAT-B 都有了对方在外网的二元组,打开了 A 和 B 之间的洞.A 和 B 可以开始数据传输.

  • 两个客户端位于两层 NAT 设备后

Untitled.png

当出现多层级的 NAT 时(这是我们常见的类型),我们通过外网服务器S来打洞,可能存在某个 NAT是最优的选择,但是外网服务器S并不能够观察到,只能够选择离服务器S最近的NAT-C来打洞.

NAT 设备在空闲状态下会对转换表进行清理,比如一些家用的路由设备保存 NAT 时间大概是2-3分钟.某些设备可能短的只有 20s .为了维持可以通过心跳包的方式来维持连接.在连接超时后进行重新打洞.

http://www.52im.net/thread-542-1-1.html