Linux性能优化-NAT

目录

NAT 原理

iptables 与 NAT


 

NAT 原理

NAT 技术可以重写 IP 数据包的源 IP 或者目的 IP,被普遍地用来解决公网 IP 地址短缺的问题。它的主要原理
就是,网络中的多台主机,通过共享同一个公网 IP 地址,来访问外网资源。同时,由于 NAT 屏蔽了内网网络,
自然也就为局域网中的机器提供了安全隔离。

既可以在支持网络地址转换的路由器(称为 NAT 网关)中配置 NAT,也可以在 Linux 服务器中配置 NAT。如
果采用第二种方式,Linux 服务器实际上充当的是“软”路由器的角色。

NAT 的主要目的,是实现地址转换。根据实现方式的不同,NAT 可以分为三类:

  • 静态 NAT,即内网 IP 与公网 IP 是一对一的永久映射关系;
  • 动态 NAT,即内网 IP 从公网 IP 池中,动态选择一个进行映射;
  • 网络地址端口转换 NAPT(Network Address and Port Translation),即把内网 IP 映射到公网 IP 的不同端口
  • 上,让多个内网 IP 可以共享同一个公网 IP 地址。

NAPT 是目前最流行的 NAT 类型,我们在 Linux 中配置的 NAT 也是这种类型。

而根据转换方式的不同,我们可以把 NAPT 分为三类

  1. 第一类是源地址转换 SNAT,即目的地址不变,只替换源 IP 或源端口。SNAT 主要用于,多个内网 IP 共享同一个公网 IP ,来访问外网资源的场景。
  2. 第二类是目的地址转换 DNAT,即源 IP 保持不变,只替换目的 IP 或者目的端口。DNAT 主要通过公网 IP 的不同端口号,来访问内网的多种服务,同时会隐藏后端服务器的真实 IP 地址。
  3. 第三类是双向地址转换,即同时使用 SNAT 和 DNAT。当接收到网络包时,执行 DNAT,把目的 IP 转换为内网 IP,而在发送网络包时,执行 SNAT,把源 IP 替换为外部 IP。

双向地址转换,其实就是外网 IP 与内网 IP 的一对一映射关系,所以常用在虚拟化环境中,为虚拟机分配浮动
的公网 IP 地址。

下面是一个例子:

本地服务器的内网 IP 地址为 192.168.0.2;
NAT 网关中的公网 IP 地址为 100.100.100.100;
要访问的目的服务器 baidu.com 的地址为 123.125.115.110。
那么 SNAT 和 DNAT 的过程,就如下图所示:

 

从图中,可以发现:

  • 当服务器访问 baidu.com 时,NAT 网关会把源地址,从服务器的内网 IP 192.168.0.2 替换成公网 IP 地址100.100.100.100,然后才发送给 baidu.com;
  • 当 baidu.com 发回响应包时,NAT 网关又会把目的地址,从公网 IP 地址 100.100.100.100 替换成服务器内网IP 192.168.0.2,然后再发送给内网中的服务器。

 

 

iptables 与 NAT

Linux 内核提供的 Netfilter 框架,允许对网络数据包进行修改(比如 NAT)和过滤(比如防火墙)。在这个基
础上,iptables、ip6tables、ebtables 等工具,又提供了更易用的命令行接口,以便系统管理员配置和管理
NAT、防火墙的规则。

其中,iptables 就是最常用的一种配置工具。要掌握 iptables 的原理和使用方法,最核心的就是弄清楚,网络
数据包通过 Netfilter 时的工作流向,下面这张图就展示了这一过程

在这张图中,绿色背景的方框,表示表(table),用来管理链。Linux 支持 4 种表,包括 filter(用于过滤)
、nat(用于 NAT)、mangle(用于修改分组数据) 和 raw(用于原始数据包)等。

跟 table 一起的白色背景方框,则表示链(chain),用来管理具体的 iptables 规则。每个表中可以包含多条
链,比如:

  • filter 表中,内置 INPUT、OUTPUT 和 FORWARD 链
  • nat 表中,内置 PREROUTING、POSTROUTING、OUTPUT 等

当然,你也可以根据需要,创建你自己的链。
灰色的 conntrack,表示连接跟踪模块。它通过内核中的连接跟踪表(也就是哈希表),记录网络连接的状态,
是 iptables 状态过滤(-m state)和 NAT 的实现基础。

iptables 的所有规则,就会放到这些表和链中,并按照图中顺序和规则的优先级顺序来执行。

针对上面的主题,要实现 NAT 功能,主要是在 nat 表进行操作。而 nat 表内置了三个链:

  • PREROUTING,用于路由判断前所执行的规则,比如,对接收到的数据包进行 DNAT。
  • POSTROUTING,用于路由判断后所执行的规则,比如,对发送或转发的数据包进行 SNAT 或 MASQUERADE。
  • OUTPUT,类似于 PREROUTING,但只处理从本机发送出去的包。

熟悉 iptables 中的表和链后,相应的 NAT 规则就比较简单了。

 

SNAT

根据刚才内容,我们知道,SNAT 需要在 nat 表的 POSTROUTING 链中配置。我们常用两种方式来配置它。
第一种方法,是为一个子网统一配置 SNAT,并由 Linux 选择默认的出口 IP。这实际上就是经常说的
MASQUERADE:

$ iptables -t nat -A POSTROUTING -s 192.168.0.0/16 -j MASQUERADE

第二种方法,是为具体的 IP 地址配置 SNAT,并指定转换后的源地址:

$ iptables -t nat -A POSTROUTING -s 192.168.0.2 -j SNAT --to-source 100.100.100.100


DNAT

再来看 DNAT,显然,DNAT 需要在 nat 表的 PREROUTING 或者 OUTPUT 链中配置,其中, PREROUTING 链更常用
一些(因为它还可以用于转发的包)。

$ iptables -t nat -A PREROUTING -d 100.100.100.100 -j DNAT --to-destination 192.168.0.2

 

双向地址转换

双向地址转换,就是同时添加 SNAT 和 DNAT 规则,为公网 IP 和内网 IP 实现一对一的映射关系,即:

iptables -t nat -A POSTROUTING -s 192.168.0.2 -j SNAT --to-source 100.100.100.100
iptables -t nat -A PREROUTING -d 100.100.100.100 -j DNAT --to-destination 192.168.0.2


在使用 iptables 配置 NAT 规则时,Linux 需要转发来自其他 IP 的网络包,所以你千万不要忘记开启 Linux
的 IP 转发功能。
可以执行下面的命令,查看这一功能是否开启。如果输出的结果是 1,就表示已经开启了 IP 转发:

sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

如果还没开启,可以执行下面的命令,手动开启:

sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1

为了避免重启后配置丢失,可以将配置写入 /etc/sysctl.conf 文件中

cat /etc/sysctl.conf | grep ip_forward
net.ipv4.ip_forward=1

NAT技术能够重写IP数据包的源IP或目的IP,所以普遍用来解决公网IP地址短缺的问题,它可以让网络中的多台主机,通过共享一个公网IP地址,来访问外网资源,同时,由于NAT屏蔽了内网网络,也为局域网中机器起到了安全隔离的作用
Linux中的NAT,基于内核的链接跟踪模块实现,所以它维护每个链接状态的同时,也会带来很高的性能成本
 

案例准备

预先安装 docker、tcpdump、curl、ab、SystemTap 等工具,比如

  # Ubuntu
  $ apt-get install -y docker.io tcpdump curl apache2-utils
 
  # CentOS
  $ curl -fsSL https://get.docker.com | sh
  $ yum install -y tcpdump curl httpd-tools

SystemTap 是 Linux 的一种动态追踪框架,它把用户提供的脚本,转换为内核模块来执行,用来监测和跟踪内核的行为

# Ubuntu
apt-get install -y systemtap-runtime systemtap
# Configure ddebs source
echo "deb http://ddebs.ubuntu.com $(lsb_release -cs) main restricted universe multiverse
deb http://ddebs.ubuntu.com $(lsb_release -cs)-updates main restricted universe multiverse
deb http://ddebs.ubuntu.com $(lsb_release -cs)-proposed main restricted universe multiverse" | \
sudo tee -a /etc/apt/sources.list.d/ddebs.list
# Install dbgsym
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F2EDC64DC5AEE1F6B9C621F0C8CAB6595FDFF622
apt-get update
apt install ubuntu-dbgsym-keyring
stap-prep
apt-get install linux-image-`uname -r`-dbgsym
 
# CentOS
yum install systemtap kernel-devel yum-utils kernel
stab-prep

案例中的结构图如下

为了对比 NAT 带来的性能问题,首先运行一个不用 NAT 的 Nginx 服务,并用 ab 测试它的性能。
在终端一中,执行下面的命令,启动 Nginx,注意选项 --network=host ,表示容器使用 Host 网络模式,即不使用 NAT

docker run --name nginx-hostnet --privileged --network=host -itd feisky/nginx:80

然后到终端二中,执行 curl 命令,确认 Nginx 正常启动

curl http://【服务端IP】
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>



继续在终端二中,执行 ab 命令,对 Nginx 进行压力测试。如果ulimit比较小的话,需要调整

open files
ulimit -n
1024

ulimit -n 65536

用ab测试这个nginx

# -c 表示并发请求数为 2,-n 表示总的请求数为 1 万
# -r 表示套接字接收错误时仍然继续执行,-s 表示设置每个请求的超时时间为 2s
ab -c 2 -n 10000 -r -s 2 http://47.93.224.133/ 
This is ApacheBench, Version 2.3 <$Revision: 1807734 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 47.93.224.133 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:        nginx/1.15.8
Server Hostname:        47.93.224.133
Server Port:            80

Document Path:          /
Document Length:        612 bytes

Concurrency Level:      2
Time taken for tests:   103.702 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      8450000 bytes
HTML transferred:       6120000 bytes
Requests per second:    96.43 [#/sec] (mean)
Time per request:       20.740 [ms] (mean)
Time per request:       10.370 [ms] (mean, across all concurrent requests)
Transfer rate:          79.57 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        3    5  38.0      4    1034
Processing:     3   16  48.2      4     216
Waiting:        3   16  48.2      4     216
Total:          6   21  61.1      7    1038

Percentage of the requests served within a certain time (ms)
  50%      7
  66%      7
  75%      8
  80%      8
  90%      8
  95%    208
  98%    211
  99%    214
 100%   1038 (longest request)

每秒请求数(Requests per second)为 96;
每个请求的平均延迟(Time per request)为 10ms;
建立连接的平均延迟(Connect)为 5ms。
记住这几个数值,这将是接下来案例的基准指标。

 

回到终端一,停止这个未使用 NAT 的 Nginx 应用:

$ docker rm -f nginx-hostnet

再执行下面的命令,启动今天的案例应用。案例应用监听在 8080 端口,并且使用了 DNAT ,来实现 Host 的
8080 端口,到容器的 8080 端口的映射关系:

$ docker run --name nginx --privileged -p 8080:8080 -itd feisky/nginx:nat

Nginx 启动后,执行 iptables 命令,确认 DNAT 规则已经创建:

iptables -nL -t nat
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination         
DOCKER     all  --  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         
DOCKER     all  --  0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination         
MASQUERADE  all  --  172.18.0.0/16        0.0.0.0/0           

Chain DOCKER (2 references)
target     prot opt source               destination         
RETURN     all  --  0.0.0.0/0            0.0.0.0/0     

再用ab压测一下

ab -c 2 -n 10000 -r -s 2 http://47.93.224.133/ 
This is ApacheBench, Version 2.3 <$Revision: 1807734 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 47.93.224.133 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:        
Server Hostname:        47.93.224.133
Server Port:            80

Document Path:          /
Document Length:        0 bytes

Concurrency Level:      2
Time taken for tests:   17.757 seconds
Complete requests:      10000
Failed requests:        20000
   (Connect: 0, Receive: 10000, Length: 0, Exceptions: 10000)
Total transferred:      0 bytes
HTML transferred:       0 bytes
Requests per second:    563.15 [#/sec] (mean)
Time per request:       3.551 [ms] (mean)
Time per request:       1.776 [ms] (mean, across all concurrent requests)
Transfer rate:          0.00 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:     3    4   0.3      4       8
Waiting:        0    0   0.0      0       0
Total:          3    4   0.3      4       8

Percentage of the requests served within a certain time (ms)
  50%      4
  66%      4
  75%      4
  80%      4
  90%      4
  95%      4
  98%      4
  99%      4
 100%      8 (longest request)


Netfilter 中,网络包的流向以及 NAT 的原理,要保证 NAT 正常工作,就至少需要两个步骤:

  1. 利用 Netfilter 中的钩子函数(Hook),修改源地址或者目的地址。
  2. 利用连接跟踪模块 conntrack ,关联同一个连接的请求和响应。

用前面提到的动态追踪工具 SystemTap 来试试
刚才测试的时候并发数很低,现在把并发数调高了
改成这样
ab -c 5000 -n 10000 -r -s 30 http://[ip]:8080/

cat dropwatch.stp
#! /usr/bin/env stap

############################################################
# Dropwatch.stp
# Author: Neil Horman <nhorman@redhat.com>
# An example script to mimic the behavior of the dropwatch utility
# http://fedorahosted.org/dropwatch
############################################################

# Array to hold the list of drop points we find
global locations

# Note when we turn the monitor on and off
probe begin { printf("Monitoring for dropped packets\n") }
probe end { printf("Stopping dropped packet monitor\n") }

# increment a drop counter for every location we drop at
probe kernel.trace("kfree_skb") { locations[$location] <<< 1 }

# Every 5 seconds report our drop locations
probe timer.sec(5)
{
  printf("\n")
  foreach (l in locations-) {
    printf("%d packets dropped at %s\n",
           @count(locations[l]), symname(l))
  }
  delete locations
}

当看到 probe begin 输出的 “Monitoring for dropped packets” 时,表明 SystemTap 已经将脚本编译为内核模块,并启动运行了。
接着,我们切换到终端二中,再次执行 ab 命令:
$ ab -c 5000 -n 10000 -r -s 30 http://[ip]:8080/
再回到stap的终端观察输出

stap --all-modules dropwatch.stp
9923 packets dropped at nf_hook_slow
358 packets dropped at tcp_v4_rcv
54 packets dropped at sk_stream_kill_queues
12 packets dropped at __udp4_lib_rcv
10 packets dropped at tcp_v4_do_rcv

5140 packets dropped at nf_hook_slow
345 packets dropped at tcp_v4_rcv
58 packets dropped at __udp4_lib_rcv
5 packets dropped at sk_stream_kill_queues
4 packets dropped at tcp_v4_do_rcv

大量丢包都发生在 nf_hook_slow 位置。看到这个名字,应该能想到,这是在 Netfilter Hook 的钩子函数中,出现丢包问题了。但是不是 NAT,还不能确定。接下来,再跟踪 nf_hook_slow 的执行过程,这一步可以通过 perf 来完成
继续用ab压测一次,然后用perf观察结果

# 用perf记录数据
perf record -a -g -- sleep 30

perf report -g graph,0


从这个图我们可以看到,nf_hook_slow 调用最多的有三个地方,分别是 ipv4_conntrack_in、
br_nf_pre_routing 以及 iptable_nat_ipv4_in。换言之,nf_hook_slow 主要在执行三个动作。

第一,接收网络包时,在连接跟踪表中查找连接,并为新的连接分配跟踪对象(Bucket)。
第二,在 Linux 网桥中转发包。这是因为案例 Nginx 是一个 Docker 容器,而容器的网络通过网桥来实现;
第三,接收网络包时,执行 DNAT,即把 8080 端口收到的包转发给容器。
到这里,我们其实就找到了性能下降的三个来源。这三个来源,都是 Linux 的内核机制,所以接下来的优化,自
然也是要从内核入手。


今天的主题是 DNAT,而 DNAT 的基础是 conntrack,所以可以先看看内核提供了哪些 conntrack 的配置
选项,执行下面的命令:

$ sysctl -a | grep conntrack
net.netfilter.nf_conntrack_count = 180
net.netfilter.nf_conntrack_max = 1000
net.netfilter.nf_conntrack_buckets = 65536
net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 60
net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 120
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120
...

这里最重要的三个指标:

  • net.netfilter.nf_conntrack_count,表示当前连接跟踪数;
  • net.netfilter.nf_conntrack_max,表示最大连接跟踪数;
  • net.netfilter.nf_conntrack_buckets,表示连接跟踪表的大小。

所以,这个输出告诉我们,当前连接跟踪数是 180,最大连接跟踪数是 1000,连接跟踪表的大小,则是 65536


前面的 ab 命令,并发请求数是 5000,而请求数是 100000。显然,跟踪表设置成,只记录 1000 个连
接,是远远不够的。

实际上,内核在工作异常时,会把异常信息记录到日志中。比如前面的 ab 测试,内核已经在日志中报出了
“nf_conntrack: table full” 的错误。执行 dmesg 命令,你就可以看到:

$ dmesg | tail
[104235.156774] nf_conntrack: nf_conntrack: table full, dropping packet
[104243.800401] net_ratelimit: 3939 callbacks suppressed
[104243.800401] nf_conntrack: nf_conntrack: table full, dropping packet
[104262.962157] nf_conntrack: nf_conntrack: table full, dropping packet

其中,net_ratelimit 表示有大量的日志被压缩掉了,这是内核预防日志攻击的一种措施。而当你看到 “nf_conntrack: table full” 的错误时,就表明 nf_conntrack_max 太小了。
连接跟踪表,实际上是内存中的一个哈希表。如果连接跟踪数过大,也会耗费大量内存。

其实,我们上面看到的 nf_conntrack_buckets,就是哈希表的大小。哈希表中的每一项,都是一个链表(称为 Bucket),而链表长度,就等于 nf_conntrack_max 除以 nf_conntrack_buckets。

可以估算一下,上述配置的连接跟踪表占用的内存大小:

# 连接跟踪对象大小为 376,链表项大小为 16
nf_conntrack_max* 连接跟踪对象大小 +nf_conntrack_buckets* 链表项大小
= 1000*376+65536*16 B
= 1.4 MB

接下来,我们将 nf_conntrack_max 改大一些,比如改成 131072(即 nf_conntrack_buckets 的 2 倍):

$ sysctl -w net.netfilter.nf_conntrack_max=131072
$ sysctl -w net.netfilter.nf_conntrack_buckets=65536


再用ab测一下,性能已经有提升了

可以用 conntrack 命令行工具,来查看连接跟踪表的内容。比如:

# -L 表示列表,-o 表示以扩展格式显示
conntrack -L -o extended | head
conntrack v1.4.4 (conntrack-tools): 4 flow entries have been shown.
ipv4     2 tcp      6 427593 ESTABLISHED src=114.242.122.147 dst=172.17.6.131 sport=55893 dport=22 src=172.17.6.131 dst=114.242.122.147 sport=22 dport=55893 [ASSURED] mark=0 use=1
ipv4     2 tcp      6 431440 ESTABLISHED src=114.242.122.147 dst=172.17.6.131 sport=55901 dport=22 src=172.17.6.131 dst=114.242.122.147 sport=22 dport=55901 [ASSURED] mark=0 use=1
ipv4     2 tcp      6 431966 ESTABLISHED src=172.17.6.131 dst=100.100.30.25 sport=43054 dport=80 src=100.100.30.25 dst=172.17.6.131 sport=80 dport=43054 [ASSURED] mark=0 use=1
ipv4     2 tcp      6 299 ESTABLISHED src=114.242.122.147 dst=172.17.6.131 sport=56303 dport=22 src=172.17.6.131 dst=114.242.122.147 sport=22 dport=56303 [ASSURED] mark=0 use=1


从这里你可以发现,连接跟踪表里的对象,包括了协议、连接状态、源 IP、源端口、目的 IP、目的端口、跟踪状态等。由于这个格式是固定的,所以我们可以用 awk、sort 等工具,对其进行统计分析。
还是以 ab 为例。在终端二启动 ab 命令后,再回到终端一中,执行下面的命令:

统计总的连接跟踪数

conntrack -L -o extended | wc -l
conntrack v1.4.4 (conntrack-tools): 1014 flow entries have been shown.
1014

统计 TCP 协议各个状态的连接跟踪数

conntrack -L -o extended | awk '/^.*tcp.*$/ {sum[$6]++} END {for(i in sum) print i, sum[i]}'
conntrack v1.4.4 (conntrack-tools): 1010 flow entries have been shown.
ESTABLISHED 4
SYN_SENT 3
TIME_WAIT 1003

统计各个源 IP 的连接跟踪数

conntrack -L -o extended | awk '{print $7}' | cut -d "=" -f 2 | sort | uniq -c | sort -nr | head -n 10
conntrack v1.4.4 (conntrack-tools): 1016 flow entries have been shown.
    528 内网的一个IP
    485 外网IP
      3 114.242.*.*

这里统计了总连接跟踪数,TCP 协议各个状态的连接跟踪数,以及各个源 IP 的连接跟踪数。你可以看到,大部分 TCP 的连接跟踪,都处于 TIME_WAIT 状态,并且它们大都来自于 192.168.0.2 这个 IP 地址(也就是运行 ab 命令的 VM2)。

这些处于 TIME_WAIT 的连接跟踪记录,会在超时后清理,而默认的超时时间是 120s,你可以执行下面的命令来查看:

$ sysctl net.netfilter.nf_conntrack_tcp_timeout_time_wait
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120

所以,如果连接数非常大,确实也应该考虑,适当减小超时时间。
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值