#1222 GLB

转发表生成规则

GLB 转发表的生成和更新算法采用的是 Rendezvous Hashing,也是一种一致性哈希,基本逻辑如下所示:

https://en.wikipedia.org/wiki/Rendezvous_hashing

for i, entry in enumerated(table_entries):
    # 以转发表条目编号的哈希为前缀,计算 hash(前缀 + 后端 ip) 后的值作为后端 ip 的哈希
    # 以哈希值作为比较的 key,倒序排列所有的后端
    prefix := siphash(i)
    for b in backends:
        b.hash = siphash(prefix + b.ip)
    backends = sorted(backends, key=lambda x: x.hash, reverse=True)

    # 取排序后的列表的前两个为 primary 和 secondary 的后端。
    entry.primary, entry.secondary = backends[0], backends[1]

    # 如果 primary 不是健康状态,交换 primary 和 secondary 的后端
    if not entry.primary.healthy:
        entry.primay, entry.secondary = entry.secondary, entry.primay
}

GLB 只处理 primary 挂的情况,处理的方式是交换 primary 和 secondary。后端的健康状况是每个 GLB director 自己监控,然后自己修改转发表,这样实现的优点是简单,没有中心节点,缺点在于:

  1. 不支持权重(有 Rendezvous Hashing with Weight 算法,没实现主要可能考虑是没必要)。

  2. 只能保证一台机器挂掉情况下的高可用,如果有 >= 2 台机器挂掉,那么转发表中 primary 和 secondary 同时为挂掉机器的条目对应的请求会被转发给 primary,从而导致这部分请求服务不可用,官方对于这个问题的说法是这个对于 github 够用,如果不够用,那么除了 primary、secondary 机器可以再添加更多的跳数。参见:https://github.com/github/glb-director/issues/77

GUE 格式

https://github.com/github/glb-director/blob/master/docs/development/gue-header.md

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\
|          Source port          |        Destination port       | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ UDP
|             Length            |            Checksum           | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+/
| 0 |C|   Hlen  |  Proto/ctype  |             Flags             | GUE
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     Private data type (0)     |  Next hop idx |   Hop count   |\
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
|                             Hop 0                             | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ GLB
|                              ...                              | private
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ data
|                             Hop N                             | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+/

Hop 为 1 的情况下封装的字节数为: IP(20) + UDP(8) + GUE(8+4) = 40。

UDP 目的端口固定为 19523,源端口为原始包包头中一些字段的哈希,这个哈希保证同一条 tcp 连接上所有的包都一样(防止网络包乱序),保证不同连接的包在 ECMP 路径和 NIC RX 队列上尽量打散(均衡)。

为什么要加上 UDP header?

By encapsulating packets in UDP, specialized capabilities in networking hardware for efficient handling of UDP packets can be leveraged.

https://tools.ietf.org/html/draft-ietf-nvo3-gue-05

GUE Tunnel 健康检查

通过 GUE Tunnel 封装并发送一个 ICMP echo 请求包(也就是 ping 包),检测是否有 reply 来判断是否健康。

https://github.com/github/glb-director/blob/v1.0.7/src/glb-healthcheck/TunnelHealthChecker.go#L197

GLB 分别判断 http 健康 和 GUE Tunnel 隧道是否健康来推断 GUE + http 是否健康,直接 GUE + http 实现起来比较麻烦。

glb-redirect 配置

glb-redirect 配置主要包含两个部分:

  • fou 隧道配置,用来 gue 包的解包。

  • iptables 规则,用来将不属于本机的机器转发给下一跳。

# `fou` 模块也支持 gue 编解码
modprobe fou
# 指定 19523 端口的包为 gue 编码的网络包
ip fou add port 19523 gue

# IPv4
ip link set up dev tunl0
ip addr add <ipv4-address>/32 dev tunl0

# IPv6
modprobe sit
ip link set up dev sit0
ip addr add <ipv6-address>/128 dev sit0

内核会根据被封装的 IP 包的目标 IP 以及路由将包转发给最匹配的网卡,如果没有匹配的网卡,默认 IPv4 转发给 tunl0、IPv6 转发给 sit0 这两个网卡。

# 关闭 19523 端口的 conntrack
iptables -t raw -A INPUT -p udp -m udp --dport 19523 -j CT --notrack
# 下面这条规则实现“第二跳”逻辑
iptables -A INPUT -p udp -m udp --dport 19523 -j GLBREDIRECT

https://github.com/github/glb-director/blob/master/docs/setup/backend-proxy-setup.md#configuring-gue

关闭 tunl0 的源地址校验:

# sysctl -ar '\.rp_filter'
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.eth0.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.lo.rp_filter = 1
net.ipv4.conf.tunl0.rp_filter = 1
# sysctl -w net.ipv4.conf.all.rp_filter=0
# sysctl -w net.ipv4.conf.tunl0.rp_filter=0

注意,conf/{all,interface}/rp_filter 都需要配置,因为 The max value from conf/{all,interface}/rp_filter is used when doing source validation on the {interface}.

如果是和 cloudflare 一样对称部署的场景,那么会出现 director 和后端机器是同一台机器的情况,这种情况下 GUE 包的源就是本机地址,这个时候还需要配置 accept_local 参数:

#  sysctl -ar '\.accept_local'
net.ipv4.conf.all.accept_local = 1
net.ipv4.conf.eth0.accept_local = 0
net.ipv4.conf.default.accept_local = 0
net.ipv4.conf.lo.accept_local = 0
net.ipv4.conf.tunl0.accept_local = 0
# sysctl -w net.ipv4.conf.all.accept_local=1

这个参数默认是 FALSE,也就是默认不接受源地址是本机地址的包。

accept_local - BOOLEAN Accept packets with local source addresses. In combination with suitable routing, this can be used to direct packets between two local interfaces over the wire and have them accepted properly. default FALSE

passing argument 4 of ‘proc_create’ from incompatible pointer type

高版本内核编译 glb 会报这个错误,解决方法:

https://stackoverflow.com/questions/64931555/how-to-fix-error-passing-argument-4-of-proc-create-from-incompatible-pointer