程序员肖邦的博客 享受编程和技术所带来的快乐

高性能负载均衡 DPVS 的 SNAT 功能介绍

2020-02-11
肖邦
 

我们已经知道 DPVS 是一个基于 DPDK 的高性能负载均衡器,它能够支持 DR、FNAT、NAT 等工作模式。另外支持还能够支持 SNAT 功能,实现原理和 FNAT 很相似,因此也具备高性能的特点。

在实际生产环境中,有些服务器需要通过 SNAT 功能来访问外网服务,可能的需求原因如下:

  • 只提供内网服务,比如负载均衡反向代理后端的服务器,只配置内网地址
  • 机房环境的原因,相关服务器临近的交换机不能配置公网 IP 资源
  • 有些是出于公司商务成本考虑,为了节省开支,没有提供出公网的能力
  • 有些是系统架构的设计或系统安全性的考虑

出于以上的某种原因(可能还有其他别的原因),服务器没有配置公网 IP 地址,默认是不能够出公网访问的。但实际有时会有访问外网的需求,比如,通过 yum 仓库安装软件包,或是调用外部第三方的某个接口来获取数据等等的需求场景,这时 SNAT 系统能够很方便的解决这些问题。据了解,很多公司都会提供 SNAT 服务,可能没有负载均衡服务那么重要的地位,但也属于公司内部底层基础架构的组成部分,大多时候也是必不可少的。

最常见的 SNAT 实现方式

SNAT 最常见也是最经典的实现方式就是通过 Linux 提供的 iptables 功能,操作简单,大概操作方式如下:

# 开启 IPv4 的转发功能
$ echo 1 > /proc/sys/net/ipv4/ip_forward
$ iptables -t nat -A POSTROUTING -d 192.168.1.160/32 -p tcp --dport 9200 -j SNAT --to-source 192.168.1.7;
# 客户端设置默认路由,网关指向 SNAT 设备 IP 地址即可
$ ip route add default via 192.168.1.1 dev eth0
# 注:只描述大概操作步骤和方式。

在实际生产环境中,为了节省大部分服务器出公网时只设置了少量的公网 IP 地址。生产环境中的 SNAT 服务器一般会做成主备模式或 BGP 集群。

  • 主备模式。一般是 2 台服务器,都配置相同的 iptables 规则,另外需要运行 keepalived 服务来负责 网关IPWAN IP 管理,本质上也是 VIP 的形式存在,如果 A 服务器宕机,B 服务器 keepalived 服务感知到后就会在本机上生成相应的 WAN IP网关IP 地址,继续提供 SNAT 服务。
  • BGP 集群方式。主备方式比较简单,但是设备利用效率较低,同一时间只有一台服务器提供服务器。BGP 集群方式,VIP 不是由 keepalived 来管理,而是由交换机负责将访问的 VIP 以等价路由的方式,均衡轮询的分配到多台 SNAT 服务器上,这样内网服务器通过网关访问外网时,流量就会分摊到多台的 SNAT 设备上了。

这两种方式用来实现 SNAT 功能也比较成熟和稳定,不过由于 iptables 自身的一些问题,如 nf_conntrack 表的限制,以及大量的 iptables 规则导致的查找性能下降,整体来看,iptables 实现方式存在明显的性能瓶颈。在生产环境中,经常会遇到某个业务流量跑高导致的 cpu 告警等问题。

DPVS 关于 SNAT 功能

DPVS 本身是一个基于 DPDK 的高性能负载均衡器,比较常用的工作模式是 FNAT,这种方式从运维和管理上比较友好一些,而且它能够充分的利用多核 CPU 的处理能力,同时也做了很多无锁的优化,它的整体性能也是非常强劲。SNAT 功能在实现原理和逻辑流程上,与 FNAT 非常相似,因此也具备高性能的特点。

可以对比 FNAT 与 SNAT 来进行理解:

  • 对于 FNAT 功能,是外网用户客户端访问流量到达 FNAT 时,DPVS 将数据包的源目的 IP 和端口都进行了转换,分发给后端的某个真实服务器。
  • 而对于 SNAT 功能,是内网服务器作为客户端访问流量到达 FNAT 时,DPVS 同样是将源 IP 和端口转换为公网 IP,转发出去到达外网真实服务器
  • 如果将 FNAT 流程反过来理解的话,就能理解 FNAT 与 SNAT 的流程非常相似。

(一)基本原理说明

先通过下图简单说明 snat 实现的大致原理:

dpvs snat

  1. 内网服务器 s1,没有配置公网 IP,需要访问 baidu.com 的某个接口,s1 要将默认网关设置为 SNAT 的 gw 192.168.20.1,那么访问 baidu 的数据包就会转发给下一跳(SNAT)。
  2. 数据包到达 SNAT 设备后,SNAT 会根据源 IP 和协议类型来判断是否为需要处理的报文,如果查找到对应的 service 配置,则会根据查到的服务信息修改数据包,将源 IP 修改为配置的公网 IP(假设为 wip1),然后通过外网网卡转发出去。
  3. 外网 baidu 服务器收到请求后,正常处理并回复响应数据包,响应数据包到达 SNAT 网关后,这时的目标 IP 是 wip1,SNAT 会根据源 IP 等信息查找连接表,查到连接表后,根据连接表信息将数据包的目标 IP 修改为内网服务器的 IP 地址,然后通过内网网卡转发出去。
  4. 内网服务器 s1 收到响应数据包时,都已经复原为正常的连接信息,到此为止,s1 就正常的接收到了响应的数据包了。

(二)DPVS SNAT 的基础使用操作

这里只罗列在 DPVS 上如何配置 SNAT 服务。

假设设备上有两张网卡,一个外网,一个内网,测试 SNAT 双臂模式:

  • 内网网卡:dpdk0
  • 外网网卡:dpdk1
  • wan IP 配置时必须携带 sapool 选项
  • SNAT 使用 match 类型的 service,而不是 vip:port
  • SNAT 设备上 wan 接口上需要配置默认路由(出公网)

SNAT 设置如下:

WAN_IP=123.1.2.3        # WAN IP can access Internet.
WAN_PREF=24             # WAN side network prefix length.
GATEWAY=123.1.2.1       # WAN side gateway

LAN_IP=192.168.100.1
LAN_PREF=24

# add WAN-side IP with sapool
./dpip addr add $WAN_IP/$WAN_PREF dev dpdk1 sapool # must add sapool for WAN-side IP
# add LAN-side IP as well as LAN route (generated)
./dpip addr add $LAN_IP/$LAN_PREF dev dpdk0

# add default route for WAN interface
./dpip route add default via $GATEWAY dev dpdk1

MATCH0='proto=tcp,src-range=192.168.100.0-192.168.100.254,oif=dpdk1'
MATCH1='proto=icmp,src-range=192.168.100.0-192.168.100.254,oif=dpdk1'

./ipvsadm -A -s rr -H $MATCH0
./ipvsadm -a -H $MATCH0 -r $WAN_IP:0 -w 100 -J

./ipvsadm -A -s rr -H $MATCH1
./ipvsadm -a -H $MATCH1 -r $WAN_IP:0 -w 100 -J

通过上述配置,就配置了两个 service,分别是 MATCH1 和 MATCH2,这时如果通过 ipvsadm -ln 就能看到 svc 已经生效,这里就不贴展示的内容了。

同样上述命令行实现的配置效果,用 keepalived 的配置方式也同样能够实现:

virtual_server match SNAT1 {
    protocol TCP
    lb_algo rr
    lb_kind SNAT
    src-range 192.168.100.0-192.168.100.254
    oif dpdk1

    real_server 123.1.2.1  0 {
        weight 4
    }
}
virtual_server match SNAT2 {
    protocol ICMP
    lb_algo rr
    lb_kind SNAT
    src-range 192.168.100.1-192.168.100.254
    oif dpdk1
    iif dpdk0

    real_server 123.1.2.1  0 {
        weight 4
    }
}

通过上述配置,我们可以简单总结到如下的内容:

  • 每个 match service 是有协议类型的,也就是说如果我们需要服务器同时支持访问 TCP、UDP、ICMP 的话,对同一个配置需要设置至少 3 个 match 服务。
  • 配置中的 RS 指的是 wan ip 列表,如果指定多个 RS,那么就会轮询从 RS 列表中选择 wan ip。不过需要注意,RS 中的 Port 必须设置为 0
  • 另外一个问题,DPVS SNAT 设置 WAN IP 地址时必须使用 dpip 命令行携带 sapool 选项才行,keepalived 配置 IP 地址时,默认没有不支持这个选项。这就意味着现有的方式必须通过 dpip 命令行来操作。
  • 如果单机需要支持一对一 SNAT 功能,只需要指定 src-range 为固定 IP,另外 real_server 只指定一个 RS 就能实现一对一的功能。

(三)SNAT 使用时需要考虑的一些问题

SNAT 是爱奇艺团队开发的功能,如果我们要使用的话,需要仔细分析是否与自己的需求一致,能否满足我们的使用方式。

  • 实现基本的 SNAT 功能,这点应该没问题
  • 确定 SNAT 各方面:配置方式、配置管理、使用方式
  • 业务特殊需求:绑定固定的公网 IP 地址,如需要固定白名单场景,EIP 等需求
  • 管理需求:运维查看每个业务的流量、限制每个业务的流量速率
  • 服务管理:内网流量不支持使用 SNAT,是否需要支持 IPv6(貌似是支持 v6)
  • 考虑特殊的情况:公网 IP 被封后如何快速方便的摘除掉

DPVS 的 SNAT 实现原理详解

通过上述内容,可以大概了解 SNAT 的基本实现原理,下边通过代码层面来分析其实现细节。主要根据数据包的流向为思路,来分析 inbond 和 outbond 方向的数据包流程。

(一)outbond 方向数据包处理流程

outbond 方向是指数据包从内网服务器 -> DPVS -> 外网服务器(如 baidu.com )

假设内网服务器需要访问外网某个功能接口,请求数据包到达了 DPVS 服务器。数据包从网卡某个队列 queuex 进入后,被 cpux 接收并开始相关的逻辑处理。收包路径大概包括如下:

  • netif_loop -> lcore_job_recv_fwd -> lcore_process_packets -> netif_deliver_mbuf
  • netif_deliver_mbuf 函数中根据 packet_type 分别调用相应的 func 函数,func 类型主要包括:ipv4、ipv6、arp 三种类型。这里主要以 IPv4 分析 SNAT 整个过程。
  • IPv4 类型的包处理函数是 ipv4_rcv,经过 IP 层相关检查和校验处理后,调用 INET_HOOK 来执行相关的钩子函数,这里调用的是 INET_HOOK_PRE_ROUTING 位置钩子,包括:dp_vs_pre_routing & dp_vs_in 函数,核心代码入口在 dp_vs_in 里边。

dp_vs_in 中其实调用的是 __dp_vs_in 函数,主逻辑入口在这个函数中体现:

  • 首先调用协议查找函数 dp_vs_proto_lookup,确认此数据包是哪种协议类型,如 TCP 或 UDP 等,每种协议的相关处理逻辑不同,后续的相关操作会根据对应协议注册的回调函数来执行。
  • 假设本次是请求是首包,需要调用 proto 的 conn_sched 函数进行连接的初始化和调度,TCP 的连接调度函数是 tcp_conn_sched
  • 调度时,只有 TCP syn 包才能触发正常调度流程。首先,根据数据包的相关数据确认该包是否属于我们配置的服务,调用 dp_vs_service_lookup 函数进行查找,不过这里查找到的 service 类型是 match 方式。如果找不到响应的 svc 就跳出 SNAT 主流程。
  • 找到 svc 后,调用 dp_vs_schedule 进行主调度流程,并创建初始化新的连接 Entry。
    • 调用 svc->scheduler->schedule 函数,从 RS 列表中挑选出一个 RS 作为 dest
    • 对于 SNAT 模式,调用 dp_vs_snat_schedule() 函数
    • 此时已确认 dest 作为源 IP 地址,调用 sa_fetch 根据源地址选择相对应的源 port
    • 调用 dp_vs_conn_fill_param 将相关数据赋值到 param 变量中去。
  • 确定相关参数后,调用 dp_vs_conn_new 建立初始化新的连接条目。
    • 申请 new conn 连接内存资源,调用 dp_vs_conn_alloc
    • 初始化 inbond 和 outbond 连接表关联的 ntuple 哈希
    • 初始化 new conn 的相关字段
    • 为 conn 绑定对应的 dest,并设置对应的 xmit 函数
    • 添加 new conn 到连接表中 dp_vs_conn_hash
    • 初始化相关的计数器和定时器
    • 到此为止,一个新的 SNAT 连接已经完成,貌似和 FNAT 没有太大的区别。
  • 连接调度成功后,根据模式,SNAT 初始化方向为 DPVS_CONN_DIR_OUTBOUND 不同于其他模式。
  • 调用 proto 的 state_trans 函数,进行相关协议状态机的转换。
  • 调用 xmit_outbound 将数据包发送出去。
    • 调用相关接口,确认 output 路由,并修改三层和四层信息
    • 确认下一跳等相关信息,调用 neigh_output 发出数据包

到此从 SNAT 数据包的接收、处理、转发等过程,完成 SNAT outbond 方向的整个流程。

(二)inbond 方向数据包处理流程

inbound 方向是指数据包从外网服务器(如 baidu.com) -> DPVS -> 内网服务器

内网服务器发送请求数据包后,外网服务器执行相关操作,发出响应数据包,此数据包到达了 DPVS 服务器。此时 DPDK 驱动程序会根据关键字段进行 FDIR 规则匹配,数据包从同样的网卡度列 queuex 进入后,被 cpux 接收并开始相关的逻辑处理,这样整个连接都是被同一个 cpux 来处理的。收包路径大概包括如下:

dp_vs_in 前的收包处理流程和 outbond 基本一致,这里就不再罗列。

  • 根据数据包字段,确认协议类型,调用 dp_vs_proto_lookup 确认本次是 TCP 协议
  • 根据响应数据包的 4 元组来查找连接表(根据元组字段和 tuple hash 字段),正常情况能够找到对应的连接表。
  • 进行协议的状态机转换
  • 调用 xmit_inbound 将数据包发送出去,具体细节与 outbond 很类似。

不断的重复 outbond 和 inbond 流程,就能够通过多次数据包的交互完成连接过程中业务的传输,从而实现 SNAT 功能。

(三)SNAT 其他问题

  • ICMP 处理问题。内网服务器 ping 外网是一个很常见的需求,从实验测试和代码分析来看,响应 ICMP 包可能命中到其它网卡队列,导致连接表 miss 的问题,这个问题可以通过 FDIR 功能将 ICMP 包都定位到固定的网卡 queue 上,这样可以简单的方式来解决这个问题。
  • 配置方式。同一个业务配置信息,需要至少配置 3 个 service,其实也不算是大问题,如果用程序自动管理配置的话也就没什么了。
  • sapool配置只能用 dpip。wan ip 配置时需要携带 sapool 选项,目前发现貌似只能用 dpip 命令行工具来进行配置,keepalived 配置不支持 sapool 选项。

总结先到此为止,在使用过程中遇到的问题还会再来补充更新。


Similar Posts

Comments