负载均衡

负载均衡的定义

In computing, load balancing is the process of distributing a set of tasks over a set of resources (computing units), with the aim of making their overall processing more efficient. Load balancing can optimize response time and avoid unevenly overloading some compute nodes while other compute nodes are left idle.

Load balancing is the subject of research in the field of parallel computers. Two main approaches exist: static algorithms, which do not take into account the state of the different machines, and dynamic algorithms, which are usually more general and more efficient but require exchanges of information between the different computing units, at the risk of a loss of efficiency.

 

负载并不仅是网络,操作系统跨cpu调度、容器的编排也用到负载均衡, 这里我们先看下网路的负载均衡。

一个数据中心会部署多个多种应用,每个应用提供公开的地址(域名或IP),满足外部用户通过该地址发送请求接收响应。负载均衡器的任务就是向主机分发请求,并根相关算法在平衡负载。

负载基于请求中的目的IP, 端口(VIP:虚拟服务IP)做决策,将其转发到该应用的某一台主机上。

由于安全的考虑,会将这些负载放置到DMZ区域中,负载将请求转发到应用系统部署在DMZ区的通讯服务器上, 由通讯服务器转发内部网络的应用服务, 应用服务器处理请求,也可能会调用其他应用完成一次业务处理,并通过负载返回给客户。当然,内部服务间也可以通过负载来分发服务器间的请求。

做应用集成工作时用的负载场景比较单一,应用模式按照比较固定,业务前端多台通讯服务器, 提供多组IP和端口,通过负载完成供统一入口,如果使用SSL服务,还需要为应用配置证书后,并把它加载到负载的加密模块(SSL加速器)上。我们当时用的都是F5设备。

负载均衡的分类

网上介绍负载的文章

原文

https://blog.envoyproxy.io/introduction-to-modern-network-load-balancing-and-proxying-a57f6ff80236

翻译

https://www.infoq.cn/article/modern-network-load-balancing-and-proxying-1?utm_campaign=geek_search&utm_content=geek_search&utm_medium=geek_search&utm_source=geek_search&utm_term=geek_search

https://www.infoq.cn/article/modern-network-load-balancing-and-proxying-2?utm_campaign=geektime_search&utm_content=geektime_search&utm_medium=geektime_search&utm_source=geektime_search&utm_term=geektime_search

根据客户的需求,依据不同要素进行负载,出现了各种类型的负载, 下面是一些分类方式:

 

L4&L7层负载

普遍的一个分类方式可以分为L4层负载,L7层负载。 L4,L7 指OSI模型的第4层和第7层。

L4更关注session或TCP/UDP连接处理,在收到请求后,负载一般是基于session或连接的,将请求转发到真实服务器。

L7可以识别应用层协议,实现更细力度的负载,负载基于具体的协议报文, 如果在一个链接上发送多个协议报文, L7层可以依据配置负载策略,将这些报文分发到真实服务器。

 

本地负载&全局负载

F5与A10产品中主要分了两大类(还有一些安全类别产品)

LTM:包含了L4与L7类型的负载, 配合相关模板如http模板,会话保持模板, ssl模板等完成配置。

GTM:全局负载均衡可以管理和优化多个数据中心之间的应用流量, 是一个智能名称解析器,能够智能地将名称解析为IP地址, 流量实际上并不通过GTM流向服务器, GTM提供了更为复杂和强大的流量管理功能,如基于地理位置的路由。

 

软件负载&硬件负载

硬件如F5,A10, 功能强大,性能强大,还会配置一些专用硬件比如SSL加速器, 但是价格昂贵,不易扩展, 软件如nginx, openresty , LVS, envoy。性能不如硬件,但是容易扩展。业务量较小时软负载非常适合。

 

负载提供的功能

 

服务发现:

后端有哪些服务,开始一般方式通过配置文件进行配置,也就是静态配置。后面逐渐出现动态配置方式。

  • k8s通过环境变量或者DNS方式提供。
  • nginx可以使用配置文件或resolver指令通过DNS方式实现动态更新服务地址。
  • envoy配置文件,xDS APIs实现动态方式配置。
健康检查:
  • 主动健康检查: 主动发起到后端服务器的健康检查, 有基于链接的如tcp,udp,icmp,也有基于内容如http,https等。适合需要精确实时了解服务状态的场景
  • 被动健康检查:监控流量,判断服务状态, 适合实时性要求不高的场景。
负载均衡:

负载均衡算法是分布式系统和网络架构中的关键部分,决定了如何将传入的请求分配到多个服务器或服务实例上。常见的负载均衡算法包括以下几种:

  • 轮询 (Round Robin) 
  • 加权轮询 (Weighted Round Robin)
  • 最少连接 (Least Connections)
  • 加权最少连接 (Weighted Least Connections)
  • 源地址哈希 (Source IP Hash)
  • URL 哈希 (URL Hash)
  • 随机 (Random)
  • 加权随机 (Weighted Random)
  • 最少时间 (Least Time)

 

负载实验

LVS提供模型:

  • DR模型 – (Director Routing-直接路由)
  • NAT模型 – (NetWork Address Translation-网络地址转换)
  • fullNAT – (full NAT)
  • ENAT – (enhence NAT 或者叫三角模式/DNAT,阿里云提供)
  • IP TUN模型 – (IP Tunneling – IP隧道)

 

使用LVS搭建一个DR模型负载。 由于是单机使用网络名字空间模拟真实服务器。

DR模型中负载与真实服务器VIP相同, 负载收到网络包后将MAC地址更改为选中的RS的。

通过GPT完成一下步骤整理, 创建四个名字空间, lb, rs1, rs2, client

创建ns,配置网络环境

  • 创建名字空间
ip netns add lb # 创建负载均衡器命名空间
ip netns add rs1 # 创建真实服务器1命名空间 
ip netns add rs2 # 创建真实服务器2命名空间
ip netns add client #创建客户端命名空间

  • 创建veth对和网桥,设置网络接口
# 创建veth对
ip link add veth-lb type veth peer name veth-lb-br
ip link add veth-rs1 type veth peer name veth-rs1-br
ip link add veth-rs2 type veth peer name veth-rs2-br
ip link add veth-client type veth peer name veth-client-br

# 将veth接口一端放入相应的命名空间
ip link set veth-lb netns lb
ip link set veth-rs1 netns rs1
ip link set veth-rs2 netns rs2
ip link set veth-client netns client


# 创建并配置网桥
ip link add name br0 type bridge
ip addr add 10.1.1.1/24 dev br0
ip link set br0 up

# 将veth接口另一端连接到网桥
ip link set veth-lb-br master br0
ip link set veth-rs1-br master br0
ip link set veth-rs2-br master br0
ip link set veth-client-br master br0
ip link set veth-lb-br up
ip link set veth-rs1-br up
ip link set veth-rs2-br up
ip link set veth-client-br up
  • 配置命名空间中接口ip
ip -n lb addr add 10.1.1.10/24 dev veth-lb
ip -n rs1 addr add 10.1.1.11/24 dev veth-rs1
ip -n rs2 addr add 10.1.1.12/24 dev veth-rs2
ip -n client addr add 10.1.1.20/24 dev veth-client

ip -n lb link set veth-lb up
ip -n rs1 link set veth-rs1 up
ip -n rs2 link set veth-rs2 up
ip -n client link set veth-client up
  • 配置路由
ip -n lb route add default via 10.1.1.1
ip -n rs1 route add default via 10.1.1.1
ip -n rs2 route add default via 10.1.1.1
ip -n client route add default via 10.1.1.1
  • 为rs1和rs2的环回接口添加VIP地址
ip netns exec rs1 ip addr add 10.1.1.10/32 dev lo label lo:0
ip netns exec rs2 ip addr add 10.1.1.10/32 dev lo label lo:0

配置LVS

  • 在负载均衡器命名空间中启用IP转发
ip netns exec lb sysctl -w net.ipv4.ip_forward=1
  • 使用ipvsadm设置负载均衡规则
ip netns exec lb ipvsadm -A -t 10.1.1.10:80 -s rr  # 添加服务和调度算法
ip netns exec lb ipvsadm -a -t 10.1.1.10:80 -r 10.1.1.11:80 -g  # 添加真实服务器1
ip netns exec lb ipvsadm -a -t 10.1.1.10:80 -r 10.1.1.12:80 -g  # 添加真实服务器2
  • 配置服务器VIP响应ARP请求
ip netns exec rs1 sysctl -w net.ipv4.conf.lo.arp_ignore=1
ip netns exec rs1 sysctl -w net.ipv4.conf.lo.arp_announce=2
ip netns exec rs2 sysctl -w net.ipv4.conf.lo.arp_ignore=1
ip netns exec rs2 sysctl -w net.ipv4.conf.lo.arp_announce=2
rs1,rs2,lo配置了了与lb相同的地址通过以上两个参数,把他们两个
验证
  • 名字空间启动http服务
ip netns exec rs1 python3 -m http.server 80
ip netns exec rs2 python3 -m http.server 80
  • 通过客户名字空间访问lb
ip netns exec lb curl http://10.1.1.10 -v

可以看到相关输出

*   Trying 10.1.1.10:80...
* Connected to 10.1.1.10 (10.1.1.10) port 80
> GET / HTTP/1.1
> Host: 10.1.1.10
> User-Agent: curl/8.6.0
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.6.8
< Date: Sun, 26 May 2024 23:45:33 GMT
< Content-type: text/html; charset=utf-8
< Content-Length: 3653
  • 查看状态发现rs1 没有流量
# ip netns exec lb ipvsadm -L -n --stats
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port               Conns   InPkts  OutPkts  InBytes OutBytes
  -> RemoteAddress:Port
TCP  10.1.1.10:80                        1        3        3      180      264
  -> 10.1.1.11:80                        0        0        0        0        0
  -> 10.1.1.12:80                        1        3        3      180      264
# ip netns exec rs1 sysctl net.ipv4.conf.all.arp_announce
net.ipv4.conf.all.arp_announce = 2
# ip netns exec rs1 sysctl net.ipv4.conf.veth-rs1.arp_announce
net.ipv4.conf.veth-rs1.arp_announce = 2

 

抓包发现一直再询问mac地址

[root@iZ8vbd88lmglnbsnad85q3Z ~]# ip netns exec rs1 tcpdump -i veth-rs1 -netvv
dropped privs to tcpdump
tcpdump: listening on veth-rs1, link-type EN10MB (Ethernet), capture size 262144 bytes
5a:ce:20:62:60:78 > Broadcast, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 10.1.1.12 tell 10.1.1.10, length 28
5a:ce:20:62:60:78 > Broadcast, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 10.1.1.12 tell 10.1.1.10, length 28
5a:ce:20:62:60:78 > Broadcast, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 10.1.1.12 tell 10.1.1.10, length 28
5a:ce:20:62:60:78 > Broadcast, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 10.1.1.12 tell 10.1.1.10, length 28
5a:ce:20:62:60:78 > Broadcast, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 10.1.1.12 tell 10.1.1.10, length 28
5a:ce:20:62:60:78 > Broadcast, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 10.1.1.12 tell 10.1.1.10, length 28

 

查看一下lb名字空间arp信息,lb无法获取rs1, rs2的信息

ip netns exec lb arp
Address                  HWtype  HWaddress           Flags Mask            Iface
10.1.1.20                ether   ee:86:01:ae:45:b0   C                     veth-lb
10.1.1.11                        (incomplete)                              veth-lb
10.1.1.12                        (incomplete)                              veth-lb

关于arp连接中知道了关于arp参数 accept_local, 这个参数决定内核是否接收目的地址是本地,源地址也是本地的包, 由于10.1.1.10配置在本地刚好命中了这个规则, 具体的处理函数是 fib_validate_source

https://elixir.bootlin.com/linux/v5.10.218/source/net/ipv4/fib_frontend.c#L421

/* Ignore rp_filter for packets protected by IPsec. */
int fib_validate_source(struct sk_buff *skb, __be32 src, __be32 dst,
            u8 tos, int oif, struct net_device *dev,
            struct in_device *idev, u32 *itag)
{
    int r = secpath_exists(skb) ? 0 : IN_DEV_RPFILTER(idev);
    struct net *net = dev_net(dev);

    if (!r && !fib_num_tclassid_users(net) &&
        (dev->ifindex != oif || !IN_DEV_TX_REDIRECTS(idev))) {
        if (IN_DEV_ACCEPT_LOCAL(idev))
            goto ok;
        /* with custom local routes in place, checking local addresses
         * only will be too optimistic, with custom rules, checking
         * local addresses only can be too strict, e.g. due to vrf
         */
        if (net->ipv4.fib_has_custom_local_routes ||
            fib4_has_custom_rules(net))
            goto full_check;
        if (inet_lookup_ifaddr_rcu(net, src))
            return -EINVAL;

ok:
        *itag = 0;
        return 0;
    }

full_check:
    return __fib_validate_source(skb, src, dst, tos, oif, dev, r, idev, itag);
}

可以通过bpftrace跟踪一下, 代码是gpt提供

// 监控 fib_validate_source 函数的调用
kprobe:fib_validate_source {
    $skb = (struct sk_buff *)arg0;
    $src = arg1;
    $dst = arg2;
    $tos = arg3;
    $oif = arg4;
    $dev = (struct net_device *)arg5;

    printf("fib_validate_source called:\n");
    printf("skb: %p\n", $skb);
    printf("Source IP: %d.%d.%d.%d\n",
           $src & 0xff, ($src >> 8) & 0xff, ($src >> 16) & 0xff, ($src >> 24) & 0xff);
    printf("Destination IP: %d.%d.%d.%d\n",
           $dst & 0xff, ($dst >> 8) & 0xff, ($dst >> 16) & 0xff, ($dst >> 24) & 0xff);
    printf("tos: %d\n", $tos);
    printf("oif: %d\n", $oif);
    printf("dev: %p\n", $dev);

    // 假设 net_device 结构体中 name 字段的偏移量为 0x0,根据实际情况调整
    @net_dev_name[tid] = str($dev + 0x0);
    printf("Network Device Name: %s\n", @net_dev_name[tid]);

    print(kstack);
}

// 监控 inet_lookup_ifaddr_rcu 函数的调用
kprobe:inet_lookup_ifaddr_rcu {
    printf("inet_lookup_ifaddr_rcu called:\n");
    printf("net: %p\n", arg0);
    printf("src: %d.%d.%d.%d\n",
           arg1 & 0xff, (arg1 >> 8) & 0xff, (arg1 >> 16) & 0xff, (arg1 >> 24) & 0xff);
    print(kstack);
}

// 监控 fib_validate_source 函数的返回
kretprobe:fib_validate_source {
    printf("fib_validate_source return value: %d\n", retval);
    print(kstack);
}

 

可以看到在tcp_connect 和arp_process 都用到了fib_validate_source, 通过inet_lookup_ifaddr_rcu检查是否是本地IP

##tcp_connect
fib_validate_source called:
skb: 0xffff88802753d640
Source IP: 10.1.1.10
Destination IP: 10.1.1.11
tos: 0
oif: 0
dev: 0xffff88800da8a000
Network Device Name: veth-rs1

        fib_validate_source+1
        ip_route_input_slow+6103
        ip_route_input_noref+202
        arp_process+3688
        __netif_receive_skb_one_core+314
        process_backlog+573
        napi_poll+516
        net_rx_action+483
        __softirqentry_text_start+498
        asm_call_irq_on_stack+18
        do_softirq_own_stack+133
        do_softirq+150
        __local_bh_enable_ip+220
        ip_finish_output2+1579
        __ip_queue_xmit+1940
        __tcp_transmit_skb+8781
        tcp_connect+2318
        tcp_v4_connect+4197
        __inet_stream_connect+1028
        inet_stream_connect+83
        __sys_connect+257
        __x64_sys_connect+111
        do_syscall_64+51
        entry_SYSCALL_64_after_hwframe+97

inet_lookup_ifaddr_rcu called:
net: 0xffff88800effc480
src: 10.1.1.10

        inet_lookup_ifaddr_rcu+1
        fib_validate_source+901
        kretprobe_trampoline+0

fib_validate_source return value: -22

        kretprobe_trampoline+0

##arp_process
fib_validate_source called:
skb: 0xffff88800d8ea3c0
Source IP: 10.1.1.10
Destination IP: 10.1.1.11
tos: 0
oif: 0
dev: 0xffff88800da8a000
Network Device Name: veth-rs1

        fib_validate_source+1
        ip_route_input_slow+6103
        ip_route_input_noref+202
        arp_process+3688
        __netif_receive_skb_one_core+314
        process_backlog+573
        napi_poll+516
        net_rx_action+483
        __softirqentry_text_start+498
        run_ksoftirqd+42
        smpboot_thread_fn+841
        kthread+872
        ret_from_fork+34

inet_lookup_ifaddr_rcu called:
net: 0xffff88800effc480
src: 10.1.1.10

        inet_lookup_ifaddr_rcu+1
        fib_validate_source+901
        kretprobe_trampoline+0

fib_validate_source return value: -22

        kretprobe_trampoline+0


设置accept_local, 访问成功。

ip netns exec rs1 sysctl -w net.ipv4.conf.all.accept_local=1
ip netns exec rs2 sysctl -w net.ipv4.conf.all.accept_local=1

本来是只是想搭建一下lvs的环境,后面发现全是arp的一些设置导致问题

下面这两个参数是配置文档提供修改参数

  • arp_ignore:针对回复: 0:问谁都回复(热情模式);1:只有你找的那个人回复(冷漠模式)。
  • arp_announce:针对发送:0:使用报文中源ip;1,2:都会寻找在与目标地址在同一子网中的源ip。
  • accept_local: 它决定了是否接受来自本地地址的包。

 

 

参考及引用

LVS介绍

https://plantegg.github.io/2019/06/20/%E5%B0%B1%E6%98%AF%E8%A6%81%E4%BD%A0%E6%87%82%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1–lvs%E5%92%8C%E8%BD%AC%E5%8F%91%E6%A8%A1%E5%BC%8F/

关于arp那两个参数描述

https://ruanhao.cc/blog/2021-01-10-arp.html#:~:text=2%20arp_ignore&text=%E8%BF%99%E4%B8%AA%E5%8F%82%E6%95%B0%E7%9A%84%E4%BD%9C%E7%94%A8%E6%98%AF,%E6%98%AF%E5%90%A6%E8%A6%81%E8%BF%94%E5%9B%9E%E6%AD%A4%E5%93%8D%E5%BA%94%E3%80%82

图片from駿億陳

 

 

Comments are closed.