深入解析ipvsadm -L --timeout选项在TCP、TCPFIN和UDP协议中的应用
LVS持久化
在实际应用场景中,轮询调度并不都是适用的。有些情况下,需要我们把同一个会话的请求都调度给一个RS节点。这时候就需要LVS提供持久化的能力,能够实现会话保持。
一、LVS的持久化主要包括以下两个方面。
1. 把来自同一个客户端IP的请求转发到同一个RS的持久化时间:persistence_timeout。通过这个持久化时间,我们可以实现会话保持。
2. 一个连接创建后处于空闲状态的超时时间。包括三种:
tcp连接的空闲超时时间
LVS收到客户端FIN消息的超时时间
udp的超时时间
二、持久化时间的机制
[root@node1 ~]# ipvsadm -lnc
IPVS connection entries
pro expire state source virtual destination
TCP 01:28 FIN_WAIT 192.31.56.1:52176 192.31.56.105:80 192.168.100.35:80
TCP 01:45 ESTABLISHED192.31.56.1:52226 192.31.56.105:80 192.168.100.35:80
TCP 05:34 NONE 192.31.56.1:0 192.31.56.105:80 192.168.100.35:80
如果用户配置了持久化时间persistence_timeout,当客户端的请求到达LB后,IPVS会在记录表里添加一条state为NONE的连接记录。该连接记录的源IP为客户端IP,端口为0,超时时间为上面所说的持久化时间persistence_timeout,会逐步减小。当NONE的超时时间减到0时,如果IPVS记录中还存在ESTABLISHED或FIN_WAIT状态的连接,则persistence_timeout值会刷新为初始值。
在该NONE状态的连接记录存在的期间,同一客户端IP的消息都会被调度到同一个RS节点。(NONE状态连接不是表示一条具体的连接,而是代表一个客户端IP过来的连接的模板,源端口用0表示。具体的连接会在IPVS上记录具体的连接状态,会显示具体的源端口)
ESTABLISHED前面的超时时间就是tcp|tcpfin|udp中的tcp的值。该值表示一条TCP连接记录的空闲释放时间。如果客户端和服务端建立了连接,则IPVS中会出现一条ESTABLISHED的记录。每当客户端和服务端的连接中有信息交互时,该超时时间都会刷新为初始值。如果连接处于空闲状态,即一直没有信息交互,则等到该值超时后,ESTABLISHED的记录会直接消失(这种情况下IPVS记录不会进入FIN_WAIT),实际上TCP连接还是存在的,并没有中断,但是由于持久化时间到了,后续同一客户端(IP+Port)过来的请求会重新调度。所以长连接业务场景需要注意根据业务需要设置好这个TCP空闲连接的超时时间。
FIN_WAIT前面的超时时间就是tcp|tcpfin|udp中的tcpfin的值。在IPVS记录的每一条连接中,如果客户端发起了FIN断连,则IPVS中记录的连接状态会从ESTABLISHED变为FIN_WAIT。该值超时后,FIN_WAIT状态的记录直接消失。
还有一个细节要注意,如果用户没有配置持久化时间persistence_timeout,那么在ipvsadm -lnc查询的记录里面是不会生成NONE记录模板的,因为此时不需要持久化。但是!!!ipvsadm -lnc记录中还是会生成ESTABLISHED记录的,后续同一客户端(IP+PORT)的请求都会调度给同一个服务器,直到该连接达到了TCP空闲连接超时时间后,ESTABLISHED记录消失,ipvs才会重新调度该客户端的请求。这个机制是必须的,不能算作持久化(持久化针对的是同一客户端IP,可以是不同端口)。因为TCP在传输的过程中可能出出现报文分片,如果ipvs把来自同一客户端(IP+PORT)的不同分片调度给了不同的服务器,那么服务器收到报文分片后无法重新组合报文。
三、持久化时间的配置
示例:
配置持久化时长为360秒(不需要配置持久化就不带 -p参数即可)
ipvsadm -A -t 192.168.1.100:80 -s rr -p 360
ipvsadm -Ln查看配置
#ipvsadm -Ln
IP Virtual Server version x.x.x (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
FWM 99 rr persistent 360
配置tcp/tcpfin/udp超时时间
ipvsadm --set 300 60 100
表示tcp空闲等待超时时间为300秒;客户端
ipvsadm -Ln --timeout查看配置
Timeout (tcp tcpfin udp): 900 120 300
四、 LVS虚拟服务分类
常见的LVS虚拟服务有以下三种:
1.指定IP端口的虚拟服务:将客户端对于某个虚拟服务IP端口的请求调度给真实服务器。
配置方法:
ipvsadm -A -t 192.168.1.100:80 -s rr -p 3600(添加一个VS,轮询,持久化时长1小时)
ipvsadm -a -t 192.168.1.100:80 -r 192.168.23 -g -w 1(DR模式,添加一个RS)
ipvsadm -a -t 192.168.1.100:80 -r 192.168.24 -g -w 1(DR模式,添加一个RS)
2.指定IP不指定端口的虚拟服务:将客户端IP对于虚拟服务IP的请求调度给真实服务器。
配置方法:
ipvsadm -A -t 192.168.1.100:0 -s rr -p 3600
ipvsadm -a -t 192.168.1.100:0 -r 192.168.1.23 -g -w 1
ipvsadm -a -t 192.168.1.100:0 -r 192.168.1.24 -g -w 1
3.指定防火墙标记的虚拟服务:将带有指定防火墙标记的数据包调度给真实服务器(相当于按防火墙标记匹配进行调度,可用于实现跨虚拟服务的关联持久化)
比如LVS同时提供一个http端口80和https端口443的虚拟服务,需要将这两个虚拟服务实现关联持久化。(比如:一个用户在访问购物网站时同时使用http(80)和https(443)两种协议,http服务用于浏览页面,https服务用于加密传输密码等重要信息,这时我们需要将用户的请求调度到同一台Real Server上)
配置方法:(如要实现80端口和443端口的会话保持)
iptables -t mangle -A PREROUTING -d 192.168.1.100 -i eth0 -p tcp --dport 80 -j MARK --set-mark -10(给访问虚拟服务192.168.1.100:80的数据包都加上标记10)
iptables -t mangle -A PREROUTING -d 192.168.1.100 -i eth0 -p tcp --dport 443 -j MARK --set-mark -10(给访问虚拟服务192.168.1.100:443的数据包都加上标记10)
ipvsadm -A -f 10 -s rr -p 3600(增加一个标记为10的虚拟服务,轮询,时长1小时)
ivpsadm -a -f 10 -r 192.168.1.23 -g -w 1(增加标记为10的真实服务器,实际上真实服务器并不会识别标记,这里只是把真实服务器和虚拟服务关联起来)
ivpsadm -a -f 10 -r 192.168.1.24 -g -w 1(增加标记为10的真实服务器,这里的真实服务器不需要指定端口,四层NAT和DR真实服务器的端口都和虚拟服务端口一致,因为LVS转发不改变端口)
注意:这里的标记都是指LB节点上iptables在mangle表上添加的标记规则。PREROUTING指路由前的操作。所以当客户端的请求到达iptables后,匹配到mangle表规则里的目的IP和端口时,iptables会给数据包打上标记10,然后IPVS模块根据配置的虚拟服务规则中指定的防火墙标记进行匹配,匹配到了指定标记的数据包,就将消息调度给真实服务器。这种场景下,防火墙标记实际上就是起到了一个虚拟服务标识的作用,IPVS根据指定的防火墙标记进行转发,不区分IP端口。(标记是内核模块上的记录,不会跟随数据包出去,RS上收到的数据包和普通数据包没有差别,并不会有标记)。
LVS实现关联持久化的原理:
首先客户端的请求(目的端口80)到达iptables,mangle表匹配到80端口后,会给数据包打上标记10,然后IPVS模块匹配到标记10,就把消息调度到一组标记为10的真实服务器;然后客户端的请求(目的端口443)到达iptables,mangle表匹配到443端口后,也会给数据包打上标记10,然后IPVS模块匹配到标记10,就把消息调度到同一组真实服务器。如果虚拟服务有-p指定持久化时长的话,那么同一个客户端的两个请求(目的端口分别为80和443)就会被调度到同一个RS节点上。
这时候访问80端口和443端口的请求都调度给了同一个RS节点,从而实现了从http服务到https服务的会话保持。
推荐阅读
-
深入解析ipvsadm -L --timeout选项在TCP、TCPFIN和UDP协议中的应用
-
go语言Socket编程-Socket编程 什么是Socket Socket,英文含义是插座、插孔,一般称之为套接字,用于描述IP地址和端口。可以实现不同程序间的数据通信。 Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:Socket,该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。 套接字的内核实现较为复杂,不宜在学习初期深入学习,了解到如下结构足矣。 套接字通讯原理示意 在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。 常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。 网络应用程序设计模式 C/S模式 传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。 B/S模式 浏览器(Browser)/服务器(Server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。 优缺点 对于C/S模式来说,其优点明显。客户端位于目标主机上可以保证性能,将数据缓存至客户端本地,从而提高数据传输效率。且,一般来说客户端和服务器程序由一个开发团队创作,所以他们之间所采用的协议相对灵活。可以在标准协议的基础上根据需求裁剪及定制。例如,腾讯所采用的通信协议,即为ftp协议的修改剪裁版。 因此,传统的网络应用程序及较大型的网络应用程序都首选C/S模式进行开发。如,知名的网络游戏魔兽世界。3D画面,数据量庞大,使用C/S模式可以提前在本地进行大量数据的缓存处理,从而提高观感。 C/S模式的缺点也较突出。由于客户端和服务器都需要有一个开发团队来完成开发。工作量将成倍提升,开发周期较长。另外,从用户角度出发,需要将客户端安插至用户主机上,对用户主机的安全性构成威胁。这也是很多用户不愿使用C/S模式应用程序的重要原因。 B/S模式相比C/S模式而言,由于它没有独立的客户端,使用标准浏览器作为客户端,其工作开发量较小。只需开发服务器端即可。另外由于其采用浏览器显示数据,因此移植性非常好,不受平台限制。如早期的偷菜游戏,在各个平台上都可以完美运行。 B/S模式的缺点也较明显。由于使用第三方浏览器,因此网络应用支持受限。另外,没有客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量受到限制。应用的观感大打折扣。第三,必须与浏览器一样,采用标准http协议进行通信,协议选择不灵活。 因此在开发过程中,模式的选择由上述各自的特点决定。根据实际需求选择应用程序设计模式。 简单的C/S模型通信 Server端:Listen函数 func Listen(network, address string) (Listener, error) network:选用的协议:TCP、UDP, 如:“tcp”或 “udp” address:IP地址+端口号, 如:“127.0.0.1:8000”或 “:8000” Listener 接口: type Listener interface { Accept (Conn, error) Close error Addr Addr } Conn 接口: type Conn interface { Read(b byte) (n int, err error) Write(b byte) (n int, err error) Close error LocalAddr Addr RemoteAddr Addr SetDeadline(t time.Time) error SetReadDeadline(t time.Time) error SetWriteDeadline(t time.Time) error } 参看 [<u>https://studygolang.com/pkgdoc</u>](https://studygolang.com/pkgdoc) 中文帮助文档中的demo: 示例代码:TCP服务器.go package main import ( "net" "fmt" ) func main { // 创建监听 listener, err:= net.Listen("tcp", ":8000") if err != nil { fmt.Println("listen err:", err) return } defer listener.Close // 主协程结束时,关闭listener fmt.Println("服务器等待客户端建立连接...") // 等待客户端连接请求 conn, err := listener.Accept if err != nil { fmt.Println("accept err:", err) return } defer conn.Close // 使用结束,断开与客户端链接 fmt.Println("客户端与服务器连接建立成功...") // 接收客户端数据 buf := make(byte, 1024) // 创建1024大小的缓冲区,用于read n, err := conn.Read(buf) if err != nil { fmt.Println("read err:", err) return } fmt.Println("服务器读到:", string(buf[:n])) // 读多少,打印多少。 }