构建机房运维基础架构(六): 构建基于LVS+OSPF+FULLNAT的前端负载均衡架构

提到负载均衡,基本任何一个访问大点的网站都会用到,负载均衡的意义就不说了。现在的负载均衡无非是两种实现,一种是TCP层的负载均衡,一种是HTTP层的负载均衡,TCP四层负载均衡比HTTP 七层负载均衡性能强N倍,但是它运行在TCP层自然没办法对HTTP请求进行匹配、转发等操作,还有一个缺点是,TCP负载均衡的后端可能没法直接拿到HTTP请求的源IP。

 

市面上四层负载均衡有LVS,七层负载均衡有Haproxy、Nginx,目前用的最多的就是这三种了。另外,aws也提供四层和七层负载均衡,七层负载均衡会设置 X-Forwarded-For,后端就能拿到源IP,对于四层负载均衡,也有办法拿到源IP,有兴趣的参考文章:

http://nosa.me/?p=91

 

一开始我们也用Haproxy,后来发现,qps 到几万的时候会把Haproxy 机器的单核CPU打满,造成访问延迟很大,这是因为Haproxy 是单核驱动的,所以我们放弃Haproxy了。

 

剩下LVS 和 Nginx了, LVS具有极高的转发性能,而Nginx也是性能极高的Web服务器,如果我们只用LVS,就没法对HTTP请求做转发处理,如果只用Nginx,频繁修改Nginx配置 即使是reload在大流量下也可能会影响请求的正常处理。所以我们一起用了,即:

请求 —> 网络 —> LVS集群 —> Nginx 集群 —> APP 

 

来看一下用什么LVS的什么模式比较好,我了解的人一般用DR 和 NAT两种模式:

1. DR模式性能是最好的,LVS改写了目的MAC地址,直接从二层转发到后端,三层的目的IP地址还是VIP,所以为了能够正常工作,要在lo上配置VIP。而且,因为LVS机器和后端机器是通过二层转发的,没法跨三层网络(路由器),所以LVS和后端机器要在一个二层网络里,扩展性不好。

2. NAT模式的原理是LVS会修改IP包目的地址,后端机器的网关要设置成LVS的IP,否则数据没法返回。网关这个事有点坑爹,还是不要用NAT模式。

DR和NAT都不够好,我觉得最好的是FULLNAT模式,配合OSPF,就组成了LVS+OSPF+FULLNAT集群,棒死了。

 

先简单说下OSPF,抄下小米博客(http://noops.me/?p=974,很好的文章,可以参考,LVS用的是DR模式)的架构图:

51

好处有:

1). LVS调度机自由伸缩,横向线性扩展(最多机器数受限于三层设备允许的等价路由数目 maximum load-balancing );
2). LVS机器同时工作,不存在备机,提高利用率;
3). 做到了真正的高可用,某台LVS机器宕机后,不会影响服务(但因为华3设备ospfd调度算法的问题,一台宕机会使所有的长连接的断开重连,目前还无法解决;思科的设备已经支持一至性哈希算法,不会出现这个问题)。

这里不说三层设备的OSPF配置和LVS机器的OSPF配置了,参考小米博客吧,哈哈哈。

 

小米博客中用到的是DR模式,我们用到的是FULLNAT模式,FULLNAT模式要重编LVS机器内核,这里说说如何重编。

1). 准备环境

yum -y install m4 gcc redhat-rpm-config xmlto asciidoc elfutils-libelf-devel binutils-devel newt-devel perl-ExtUtils-Embed hmaccalc rng-tools 

2).  拿到源码

cd /home/op/
wget ftp://ftp.redhat.com/pub/redhat/linux/enterprise/6Server/en/os/SRPMS/kernel-2.6.32-220.23.1.el6.src.rpm
wget http://kb.linuxvirtualserver.org/images/a/a5/Lvs-fullnat-synproxy.tar.gz
wget http://kb.linuxvirtualserver.org/images/b/b7/Linux-2.6.32-220.23.1.el6.x86_64.lvs.src.tar.gz
tar zxf Lvs-fullnat-synproxy.tar.gz
tar zxf Linux-2.6.32-220.23.1.el6.x86_64.lvs.src.tar.gz

cat >  ~/.rpmmacros << EOF
%_topdir /home/op/rpms
%_tmppath /home/op/rpms/tmp
%_sourcedir /home/op/rpms/SOURCES
%_specdir /home/op/rpms/SPECS
%_srcrpmdir /home/op/rpms/SRPMS
%_rpmdir /home/op/rpms/RPMS
%_builddir /home/op/rpms/BUILD
EOF
cd /home/op
mkdir rpms
mkdir rpms/tmp
mkdir rpms/SOURCES
mkdir rpms/SPECS
mkdir rpms/SRPMS
mkdir rpms/RPMS
mkdir rpms/BUILD

rpm -ivh kernel-2.6.32-220.23.1.el6.src.rpm

cd /home/op/rpms/SOURCES
对于LVS,最好增加 HASH SIZE,否则请求量大的时候可能出现丢包情况(这是一个坑,后面的文章再说)。
vim config-generic  把 第一行的 改成 # x86_64,把 CONFIG_IP_VS_TAB_BITS=12 改成 CONFIG_IP_VS_TAB_BITS=20

cd /home/op/rpms/SPECS
vim kernel.spec  ,把 # % define buildid .local 修改为 %define buildid .ipvs_20bit

cd /home/op/rpms/SPECS
rpmbuild -bp kernel.spec

3). 增加patch
cd /home/op/rpms/BUILD/kernel-2.6.32-220.23.1.el6/linux-2.6.32-220.23.1.el6.ipvs_20bit.x86_64/
cp /home/op/lvs-fullnat-synproxy/lvs-2.6.32-220.23.1.el6.patch  .
patch -p1 < ./lvs-2.6.32-220.23.1.el6.patch
cp configs/kernel-2.6.32-x86_64.config .config  (默认的.config是DEBUG模式,开启的話很耗CPU,所以替换掉,这是一个坑。)

改下内核子版本号。
 vim Makefile
  EXTRAVERSION = -220.23.1.e16

make -j24
make modules_install;
make install

修改内核启动参数
default=0,并在kernel一行中,添加“nohz=off ”(如果不关闭nohz,大压力下CPU0可能会消耗过高,压力不均匀)
/etc/grub.conf,重启

 

编译内核重启之后,开始安装LVS TOOLS(编译完内核先重启机器加载新的内核,否则下面模块无法安装)

cd /home/op/lvs-fullnat-synproxy
tar xzf  lvs-tools.tar.gz
cd tools

1). keepalived install
cd keepalived
./configure –with-kernel-dir=”/lib/modules/`uname -r`/build”
make
make install

2). ipvsadm install
cd ipvsadm;
make
make install

3). quaage install
cd quagga;
./configure –disable-ripd –disable-ripngd –disable-bgpd –disable-watchquagga –disable-doc  –enable-user=root –enable-vty-group=root –enable-group=root –enable-zebra –enable-ospfd –localstatedir=/var/run/quagga
make
make install
mkdir /var/run/quagga

4). 卸载有提权漏洞的gcc(在某些centos版本上有提权漏洞,最好卸载)
rpm -e gcc

 

因为我们用FULLNAT模式,所以在每台LVS机器上配置了28个local ip,对于一个请求LVS会选取一个IP和一个端口和后端机器(我们是NGINX)建立连接和转发数据,这样其实后端机器只能看到LVS的一个local ip了,对了让后端机器看到源请求IP,后端机器要装toa模块,和LVS一样要重编内核,来看看编译过程。

重复一下编译LVS内核的1) 和2) 步骤,然后:

1). 增加patch
cd /home/op/rpms/BUILD/
cd kernel-2.6.32-220.23.1.el6/linux-2.6.32-220.23.1.el6.x86_64/
cp /home/op/lvs-fullnat-synproxy/toa-2.6.32-220.23.1.el6.patch ./
patch -p1<toa-2.6.32-220.23.1.el6.patch
cp configs/kernel-2.6.32-x86_64.config .config (默认的.config是DEBUG模式,开启的話很耗CPU,所以替换掉。)

2). 编译和安装
vim Makefile
EXTRAVERSION = -220.23.1.e16

make -j24  (以M,即module方式编译
make modules_install
make install

3). 修改内核启动参数default=0,并在kernel一行中,添加“nohz=off ”(如果不关闭nohz,大压力下CPU0可能会消耗过高,压力不均匀)/etc/grub.conf,重启。

加载toa,把 modprobe toa 写在/etc/rc.d/rc.local

4). 卸载有提权漏洞的gcc(在某些centos版本上有提权漏洞,最好卸载)
rpm -e gcc

 

 

LVS fullnat模式下 syn_proxy机制对 建立连接过程 和 TCP option 的影响

我们的前端负载均衡已经切换到了LVS FULLNAT模式,它比LVS DR 模式 的好处是 LVS和后端机器不用在一个二层网络里面,非常好扩展,不好的地方是后端机器要装一台TOA模块来识别 HTTP请求的源IP。

想了解LVS FULLNAT的话可以参考这个链接:

http://kb.linuxvirtualserver.org/wiki/IPVS_FULLNAT_and_SYNPROXY

用LVS fullnat的时候其实需要注意一些TCP option的设置,比如 timestamps 和 windows scaling,而且 TCP option 和 synproxy 还有关系,在这篇文章我们手动测试,得出 怎么在LVS fullnat机器上 设置TCP option 。

 

环境:

LVS VIP :  60.28.208.10
LVS fullnat localip 段:10.0.19.0/24
后端IP :  10.0.11.12

 

探测的抓包

LVS 检测后端时LVS的抓包记录:

18:45:36.023248 IP 10.0.19.226.63923 > 10.0.11.12.http: Flags [S], seq 1537556744, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0 18:45:36.023270 IP 10.0.19.226.19042 > 10.0.11.12.https: Flags [S], seq 520304659, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0 18:45:36.023544 IP 10.0.11.12.http > 10.0.19.226.63923: Flags [S.], seq 3628316982, ack 1537556745, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0 18:45:36.023560 IP 10.0.19.226.63923 > 10.0.11.12.http: Flags [.], ack 1, win 115, length 0 18:45:36.023565 IP 10.0.11.12.https > 10.0.19.226.19042: Flags [S.], seq 458068257, ack 520304660, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0 18:45:36.023572 IP 10.0.19.226.19042 > 10.0.11.12.https: Flags [.], ack 1, win 115, length 0 18:45:36.023589 IP 10.0.19.226.63923 > 10.0.11.12.http: Flags [R.], seq 1, ack 1, win 115, length 0 18:45:36.023602 IP 10.0.19.226.19042 > 10.0.11.12.https: Flags [R.], seq 1, ack 1, win 115, length 0

LVS 检测后端时后端的抓包记录:

18:46:26.041901 IP 10.0.19.226.61028 > 10.0.11.12.http: Flags [S], seq 3032456781, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0

18:46:26.041937 IP 10.0.11.12.http > 10.0.19.226.61028: Flags [S.], seq 1104080475, ack 3032456782, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
18:46:26.041951 IP 10.0.19.226.40349 > 10.0.11.12.https: Flags [S], seq 849356025, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
18:46:26.041960 IP 10.0.11.12.https > 10.0.19.226.40349: Flags [S.], seq 1050279505, ack 849356026, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
18:46:26.042113 IP 10.0.19.226.61028 > 10.0.11.12.http: Flags [.], ack 1, win 115, length 0
18:46:26.042147 IP 10.0.19.226.40349 > 10.0.11.12.https: Flags [.], ack 1, win 115, length 0
18:46:26.042310 IP 10.0.19.226.61028 > 10.0.11.12.http: Flags [R.], seq 1, ack 1, win 115, length 0
18:46:26.042373 IP 10.0.19.226.40349 > 10.0.11.12.https: Flags [R.], seq 1, ack 1, win 115, length 0

上面可以看到,LVS 从localip 段里面选一个IP,起两个端口分别探测 80 和 443,建立连接之后 里面发送 RST 给后端,这样探测也成功了。

 

请求的抓包

在一台机器上执行下面命令:
curl -H “Host:10.0.11.12” http://60.28.208.10/test

LVS 上面抓VIP 的包如下:
tcpdump -n -i em1 host 60.28.208.10 and port 80
17:08:00.122008 IP 111.206.15.146.15405 > 60.28.208.10.http: Flags [S], seq 1679917190, win 14600, options [mss 1460,sackOK,TS val 1412716450 ecr 0,nop,wscale 7], length 0
17:08:00.122018 IP 60.28.208.10.http > 111.206.15.146.15405: Flags [S.], seq 2689133917, ack 1679917191, win 14600, options [mss 1452,sackOK,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,wscale 1], length 0
17:08:00.132352 IP 111.206.15.146.15405 > 60.28.208.10.http: Flags [.], ack 1, win 115, length 0
17:08:00.132439 IP 111.206.15.146.15405 > 60.28.208.10.http: Flags [P.], seq 1:169, ack 1, win 115, length 168
17:08:00.132877 IP 60.28.208.10.http > 111.206.15.146.15405: Flags [.], ack 169, win 123, length 0
17:08:01.138207 IP 60.28.208.10.http > 111.206.15.146.15405: Flags [P.], seq 1:215, ack 169, win 123, length 214
17:08:01.148585 IP 111.206.15.146.15405 > 60.28.208.10.http: Flags [.], ack 215, win 123, length 0
17:08:01.148741 IP 111.206.15.146.15405 > 60.28.208.10.http: Flags [F.], seq 169, ack 215, win 123, length 0
17:08:01.149060 IP 60.28.208.10.http > 111.206.15.146.15405: Flags [F.], seq 215, ack 170, win 123, length 0
17:08:01.159344 IP 111.206.15.146.15405 > 60.28.208.10.http: Flags [.], ack 216, win 123, length 0

LVS 上面抓连接后端机器的包:
tcpdump -n -i em2 net 10.0.19.0/24 and host 10.0.11.12
17:08:00.132468 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [S], seq 3159822860, win 5000, options [Unknown Option 2003c2d6fce0f92,mss 1440,nop,nop,sackOK,nop,wscale 7], length 0
17:08:00.132656 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [S.], seq 2205177579, ack 3159822861, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
17:08:00.132686 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [P.], seq 1:169, ack 1, win 115, options [Unknown Option 2003c2d6fce0f92], length 168
17:08:00.132855 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [.], ack 169, win 123, length 0
17:08:01.138194 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [P.], seq 1:215, ack 169, win 123, length 214
17:08:01.148598 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [.], ack 215, win 123, length 0
17:08:01.148749 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [F.], seq 169, ack 215, win 123, length 0
17:08:01.149046 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [F.], seq 215, ack 170, win 123, length 0
17:08:01.159357 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [.], ack 216, win 123, length 0

上面的包是在开启syn_proxy的时候抓的,可以看出(注意看时间点),Client 请求来的时候,LVS 会先和Client 建好连接,然后在 Client 请求数据的时候 LVS 选择一个localip端口 和 后端 建立连接,建立成功后LVS 直接 向后端请求数据,获取数据后 再通过 VIP 传给 Client。

 

下面我们把 syn_proxy 关掉之后重新抓一下看看。

tcpdump -n -i em1 host 60.28.208.10 and port 80
17:10:54.142439 IP 111.206.15.146.15408 > 60.28.208.10.http: Flags [S], seq 50780707, win 14600, options [mss 1460,sackOK,TS val 1412890473 ecr 0,nop,wscale 7], length 0
17:10:54.142802 IP 60.28.208.10.http > 111.206.15.146.15408: Flags [S.], seq 629114004, ack 50780708, win 14600, options [mss 1452,nop,nop,sackOK,nop,wscale 7], length 0
17:10:54.153112 IP 111.206.15.146.15408 > 60.28.208.10.http: Flags [.], ack 1, win 115, length 0
17:10:54.153180 IP 111.206.15.146.15408 > 60.28.208.10.http: Flags [P.], seq 1:169, ack 1, win 115, length 168
17:10:54.153360 IP 60.28.208.10.http > 111.206.15.146.15408: Flags [.], ack 169, win 123, length 0
17:10:55.158651 IP 60.28.208.10.http > 111.206.15.146.15408: Flags [P.], seq 1:215, ack 169, win 123, length 214
17:10:55.168912 IP 111.206.15.146.15408 > 60.28.208.10.http: Flags [.], ack 215, win 123, length 0
17:10:55.168940 IP 111.206.15.146.15408 > 60.28.208.10.http: Flags [F.], seq 169, ack 215, win 123, length 0
17:10:55.169284 IP 60.28.208.10.http > 111.206.15.146.15408: Flags [F.], seq 215, ack 170, win 123, length 0
17:10:55.179620 IP 111.206.15.146.15408 > 60.28.208.10.http: Flags [.], ack 216, win 123, length 0

tcpdump -n -i em2 net 10.0.19.0/24 and host 10.0.11.12
17:10:54.142475 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [S], seq 1583761902, win 14600, options [Unknown Option 2003c306fce0f92,mss 1460,sackOK,TS val 1412890473 ecr 0,nop,wscale 7], length 0
17:10:54.142766 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [S.], seq 629114004, ack 1583761903, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
17:10:54.153128 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [.], ack 1, win 115, options [Unknown Option 2003c306fce0f92], length 0
17:10:54.153192 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [P.], seq 1:169, ack 1, win 115, options [Unknown Option 2003c306fce0f92], length 168
17:10:54.153348 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [.], ack 169, win 123, length 0
17:10:55.158639 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [P.], seq 1:215, ack 169, win 123, length 214
17:10:55.168925 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [.], ack 215, win 123, length 0
17:10:55.168944 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [F.], seq 169, ack 215, win 123, length 0
17:10:55.169272 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [F.], seq 215, ack 170, win 123, length 0
17:10:55.179630 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [.], ack 216, win 123, length 0

通过上面的抓包可以看到(同样看时间点),关闭syn_proxy 的时候,LVS 接受到请求,此时开启一个localip的一个端口 给后端发送 SYN,收到 后端的SYN+ACK之后,LVS通过VIP 给Client 返回 SYN+ACK,然后 Client 会再发送 ACK,VIP 会接收到这个ACK,然后再通过localip 传给 后端,后端收到后 建立连接。看这个流程,LVS 纯粹扮演 Proxy 的角色。

syn_proxy 机制会影响 Client 到 Server 的TCP 连接的Option 选项,下面细看。

 

关于timestamps 的测试

Client —> LVS —> Server

LVS 上面有三个和 timestamps 相关的参数 :
net.ipv4.tcp_timestamps
net.ipv4.vs.fullnat_timestamp_remove_entry
net.ipv4.vs.synproxy_timestamp

(LVS 默认 net.ipv4.vs.fullnat_timestamp_remove_entry = 1 ,net.ipv4.vs.synproxy_timestamp = 0)

在开启 syn_proxy 情况下的测试:

1. 即使 Client 和 Server 的 timestamps 都设置为1 ,只要 LVS net.ipv4.vs.synproxy_timestamp 不设置为1 ,timestamps 就不会启用。此时  LVS 的 net.ipv4.tcp_timestamps 和 net.ipv4.vs.fullnat_timestamp_remove_entry 完全没用。

2. 如果Client 关闭了 net.ipv4.tcp_timestamps ,timestamps 选项不会启用。

3. 比较诧异的是,无论 Server 的 net.ipv4.tcp_timestamps 是否设置为1,只要LVS 开启 net.ipv4.vs.synproxy_timestamp 和 Client 开启 net.ipv4.tcp_timestamps,建立的TCP连接 就会启用 timestamps (此时在Server 上抓包 会看到 TS val 和 ecr )

在关闭 syn_proxy 情况下的测试:

1. 此时 net.ipv4.vs.fullnat_timestamp_remove_entry  参数起作用,即使 Client 和 Server 的 timestamps 都设置为1 ,只要 LVS net.ipv4.vs.fullnat_timestamp_remove_entry 不设置为0 ,timestamps 就不会启用。

2. 如果Client 关闭了 net.ipv4.tcp_timestamps ,timestamps 选项不会启用。

3. 如果Server关闭了 net.ipv4.tcp_timestamps ,timestamps 选项不会启用。

总结:

1. syn_proxy 开启,net.ipv4.vs.synproxy_timestamp 决定LVS 是否启用timestamps。

2. syn_proxy 关闭,net.ipv4.vs.fullnat_timestamp_remove_entry 决定LVS 是否启用timestamps。

3. LVS FULLNAT 模式 的 net.ipv4.tcp_timestamps 参数 完全没用,设置与不设置都不会影响timestamps

4. 简单点,如果想开启LVS的 timestamps,把 net.ipv4.vs.synproxy_timestamp 设置成1,同时把  net.ipv4.vs.fullnat_timestamp_remove_entry 设置成 0

 

关于windows  scaling 的测试

Client —> LVS —> Server

在开启 syn_proxy 情况下的测试:

1. net.ipv4.vs.synproxy_wscale 决定连接 是否 wscale,即使 Client 和 Server 都开启了wscale,只要LVS 不开启 net.ipv4.vs.synproxy_wscale,连接就不会开启wscale。

2. 如果Client 不开启 net.ipv4.tcp_window_scaling ,连接就不会开启 wscale。

3. Server 的 net.ipv4.tcp_window_scaling 变的没用,即使没打开,只要 Client 打开了net.ipv4.tcp_window_scaling 而且LVS 打开了 net.ipv4.vs.synproxy_wscale ,连接就会打开 wscale

在关闭 syn_proxy 情况下的测试:

1. 只要 Client 和 Server 同时开启 wscale ,连接就会开启 wscale,不管 LVS 如何设置。

总结:

1. LVS 的 net.ipv4.tcp_window_scaling 完全没用,可忽略这个参数。

2. 想开启LVS 的wscale, 把 net.ipv4.vs.synproxy_wscale  设置成1