kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

TCP_CORK以及TCP_NODELAY

默认情况下 tp->nodelay = 0; 也就是delay=1


https://blog.csdn.net/dog250/article/details/5941637

所谓的cork就是塞子的意思,形象地理解就是用cork将连接塞住,使得数据先不发出去,等到拔去塞子后再发出去,而nodelay事实上是为了禁用Nagle算法,Nagle算法为了增加了网络的吞吐量而牺牲了响应时间体验,这在有些应用中是不合适的,比如交互式应用(终端登录或者远程X应用 etc.),因此有必要提供一个选项将它禁用掉,Nagle算法在RFC1122中有提及,它的实现实际上很简单,利用了tcp本身的一些特性,在算法描述中,关键的一点是“什么时候真实的发送数据”,这个问题的解答也是很简单,原则上只要发出的包都被对端ack了就可以发送了,这实际上也是一种权衡,Nagle算法最初的目的在于解决大量小包存在于网络从而造成网络拥塞的问题(一个小包可能只有几个字节,比如ls,cat等等,然而为每个小包封装几个协议头,其大小就不可忽视了,大量此类小包存在于网络势必会使得网络带宽的利用率大大下降),如果包被ack了,说明包已经离开了网络进入了对端主机,这样就可以发送数据了,此时无需再等,有多少数据发送多少(当然要考虑窗口大小和MTU),如果很极端地等待更多的数据,那么响应度会更低,换句话简单的说Nagle算法只允许一个未被ack的包存在于网络,它并不管包的大小。

可以看出,Nagle算法完全由tcp协议的ack机制决定,这会带来一些问题,比如如果对端ack回复很快的话,Nagle事实上不会拼接太多的数据包,虽然避免了网络拥塞,网络总体的利用率依然很低,Nagle真的做到了“只做好一件事”的原则,然而有没有另外一种算法,可以提高整体网络利用率呢?也就是说尽量以不能再多的数据发送,这里之所以说是尽量还是权衡导致的,某时可以发送数据的时候将会发送数据,即使当前数据再小也不再等待后续的可能拼接成更大包的数据的到来。

实际上,这样的需求可以用TCP_CORK来实现,但是实现得可能并不像你想象的那么完美,cork并不会将连接完全塞住。内核其实并不知道应用层到底什么时候会发送第二批数据用于和第一批数据拼接以达到MTU的大小,因此内核会给出一个时间限制,在该时间内没有拼接成一个大包(努力接近MTU)的话,内核就会无条件发送,这里给出的只是一个大致的思想,真实的情况还要受到窗口大小以及拥塞情况的影响,因此tcp“何时发送数据”这个问题非常复杂。

Nagle算法主要避免网络因为太多的小包(协议头的比例非常之大)而拥塞,而CORK算法则是为了提高网络的利用率,使得总体上协议头占用的比例尽可能的小。如此看来这二者在避免发送小包上是一致的,在用户控制的层面上,Nagle算法完全不受用户socket的控制,你只能简单的设置TCP_NODELAY而禁用它,CORK算法同样也是通过设置或者清除TCP_cork使能或者禁用之,然而Nagle算法关心的是网络拥塞问题,只要所有的ack回来则发包,而CORK算法却可以关心内容,在前后数据包发送间隔很短的前提下(很重要,否则内核会帮你将分散的包发出),即使你是分散发送多个小数据包,你也可以通过使能CORK算法将这些内容拼接在一个包内,如果此时用Nagle算法的话,则可能做不到这一点。

接下来看一下内核代码,然后给出一个测试程序来感性感受这些选项。tcp的发送函数是tcp_sendmsg,这个函数中存在一个大循环,用于将用户数据置入skb中,它的形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t size)
{

	while (--iovlen >= 0) {
		0.更新数据结构元数据;
		while (seglen > 0) {
			int copy;
			skb = sk->sk_write_queue.prev;
			1.如果既有skb的长度过大或者根本还没有一个skb则分配一个skb;
			2.将数据拷贝到既有的skb或者新的skb中;
			3.更新skb和用户数据的元数据;
			//如果数据还没有达到mss,则继续,换句话就是如果数据已经达到mss了就接着往下走来权衡是否马上发送。
			if (skb->len != mss_now || (flags & MSG_OOB))
				continue;
			4.权衡发送与否
			continue;
		}
	}
out:
	//如果循环完成,所有数据都进入了skb,调用tcp_push来权衡是否发送
	tcp_push(sk, tp, flags, mss_now, tp->nonagle);
}

tcp_push很短但是很复杂,
static inline void tcp_push(struct sock *sk, struct tcp_opt *tp, int flags,
				int mss_now, int nonagle)
{
	if (sk->sk_send_head) {
		struct sk_buff *skb = sk->sk_write_queue.prev;
		...
		//如果有MSG_MORE,则当作cork来处理
		__tcp_push_pending_frames(sk, tp, mss_now,
					  (flags & MSG_MORE) ? TCP_NAGLE_CORK : nonagle);
	}
}

static __inline__ void __tcp_push_pending_frames(struct sock *sk,
						 struct tcp_opt *tp,
						 unsigned cur_mss,
						 int nonagle)
{
	struct sk_buff *skb = sk->sk_send_head;
	if (skb) {
		if (!tcp_skb_is_last(sk, skb)) //如果已经有了很多的skb,则尽量马上发送
			nonagle = TCP_NAGLE_PUSH;
		//只有tcp_snd_test返回1才会发送数据,该函数很复杂
		if (!tcp_snd_test(tp, skb, cur_mss, nonagle) ||
			tcp_write_xmit(sk, nonagle))
			tcp_check_probe_timer(sk, tp);
	}
	tcp_cwnd_validate(sk, tp);
}

static __inline__ int tcp_snd_test(struct tcp_opt *tp, struct sk_buff *skb,
				   unsigned cur_mss, int nonagle)
{
	//如果有TCP_NAGLE_PUSH标志(或者tcp_nagle_check同意发送)且未ack的数据够少且...则可以发送
	return (((nonagle&TCP_NAGLE_PUSH) || tp->urg_mode
		 || !tcp_nagle_check(tp, skb, cur_mss, nonagle)) &&
		((tcp_packets_in_flight(tp) < tp->snd_cwnd) ||
		 (TCP_SKB_CB(skb)->flags & TCPCB_FLAG_FIN)) &&
		!after(TCP_SKB_CB(skb)->end_seq, tp->snd_una + tp->snd_wnd));
}

tcp_nagle_check函数是一个很重要的函数,它基本决定了数据是否可以发送的80%,内核源码中对该函数有一条注释:
-3. Or TCP_NODELAY was set.
-4. Or TCP_CORK is not set, and all sent packets are ACKed.

就是说如果TCP_NODELAY值为1就可以直接发送,或者cork被禁用的情况下所有发出的包都被ack了也可以发送数据,这里体现的就是Nagle算法和CORK算法的区别了,Nagle算法只要求所有的出发包都ack就可以发送,而不管当前包是否足够大(虽然它通过tcp_minshall_check保证了包不太小),而如果启用cork的话,可能仅仅数据被ack就不够了,这就是为何在代码注释中说cork要比Nagle更stronger的原因,同时这段代码也说明了为何TCP_CORK和TCP_NODELAY不能一起使用的原因,它们有共同的东西,却在做着不同的事情。看看tcp_nagle_check:

1
2
3
4
5
6
7
8
static __inline__ int
tcp_nagle_check(struct tcp_opt *tp, struct sk_buff *skb, unsigned mss_now, int nonagle)
{
	return (skb->len < mss_now &&
		!(TCP_SKB_CB(skb)->flags & TCPCB_FLAG_FIN) &&
		((nonagle & TCP_NAGLE_CORK) ||
			(!nonagle && tp->packets_out && tcp_minshall_check(tp))));
}

看看__tcp_push_pending_frames的最后,有一个tcp_check_probe_timer调用,就是说在没有数据被发送的时候会调用这个函数。这个函数有两个作用,第一个是防止0窗口导致的死锁,另一个作用就是定时发送由于使能了CORK算法或者Nagle算法一直等待新数据拼接而没有机会发送的数据包。这个timer内置在重传timer之中,其时间间隔和rtt有关,一旦触发则会发送数据包或者窗口探测包。反过来可以理解,如果没有这个timer的话,启用cork的连接将几乎(可能根据实现的不同还会受别的因素影响,太复杂了)每次都发送mtu大小的数据包。该timer调用tcp_probe_timer函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void tcp_probe_timer(struct sock *sk)
{
	struct tcp_opt *tp = tcp_sk(sk);
	int max_probes;
	//1.如果有数据在网络上,则期望马上回来ack,ack中会通告对端窗口
	//2.如果没有数据要发送,则无需关注对端窗口,即使为0也无所谓
	if (tp->packets_out || !sk->sk_send_head) {
		tp->probes_out = 0;
		return;
	}
	//这个sysctl_tcp_retries2是可以调整的
	max_probes = sysctl_tcp_retries2;
	if (tp->probes_out > max_probes) {
		tcp_write_err(sk);
	} else {
		tcp_send_probe0(sk);
	}
}

tcp_send_probe0会调用tcp_write_wakeup函数,该函数会要么发送可以发送的数据,如果由于发送队列越过了发送窗口导致不能发送,则发送一个窗口探测包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int tcp_write_wakeup(struct sock *sk)
{
	if (sk->sk_state != TCP_CLOSE) {
		struct tcp_opt *tp = tcp_sk(sk);
		struct sk_buff *skb;
		if ((skb = sk->sk_send_head) != NULL &&
			before(TCP_SKB_CB(skb)->seq, tp->snd_una+tp->snd_wnd)) {
			...//在sk_send_head队列上取出一个发送出去,其ack会带回对端通告窗口的大小
			err = tcp_transmit_skb(sk, skb_clone(skb, GFP_ATOMIC));
			...
			return err;
		} else {
			...
			return tcp_xmit_probe_skb(sk, 0);
		}
	}
	return -1;
}

这个probe timer虽然一定程度阻碍了cork的满载发送,然而它却是必要的,这是由于tcp并不为纯的ack包(不带数据的ack包)提供确认,因此一旦这种ack包丢失,那么就有可能死锁,发送端的窗口无法更新,接收端由于已经发送了ack而等待接收数据,两端就这样僵持起来,因此需要一个timer,定期发送一个探测包,一个ack丢失,不能所有的ack都丢失吧,在timer到期时,如果本来发送队列上有数据要发送,则直接发送这些数据而不再发送探测包,因为发送了这些数据,所以它“破坏”了cork的承诺,不过也因此增强了响应度。

udp没有连接,没有确认,因此也就不需要什么timer之类的复杂机制,也因此,它是真正承诺的cork,除非你在应用层手工拔掉塞子,否则数据将不会发出。

nf_socket

https://blog.csdn.net/jk110333/article/details/8642261

用户态与内核态交互通信的方法不止一种,sockopt是比较方便的一个,写法也简单. ipvsadm的两种通信方式之一

缺点就是使用 copy_from_user()/copy_to_user()完成内核和用户的通信, 效率其实不高, 多用在传递控制 选项 信息,不适合做大量的数据传输

用户态函数:

发送:int setsockopt( int sockfd, int proto, int cmd, void *data, int datelen);

接收:int getsockopt(int sockfd, int proto, int cmd, void *data, int datalen);

第一个参数是socket描述符;

第二个参数proto是sock协议,IP RAW的就用SOL_SOCKET/SOL_IP等,TCP/UDP socket的可用SOL_SOCKET/SOL_IP/SOL_TCP/SOL_UDP等,即高层的socket是都可以使用低层socket的命令字 的,IPPROTO_IP;

第三个参数cmd是操作命令字,由自己定义;

第四个参数是数据缓冲区起始位置指针,set操作时是将缓冲区数据写入内核,get的时候是将内核中的数 据读入该缓冲区;

第五个参数数据长度

内核态函数

注册:nf_register_sockopt(struct nf_sockopt_ops *sockops);

注销:nf_unregister_sockopt(struct nf_sockopt_ops *sockops);

结构体 nf_sockopt_ops test_sockops

1
2
3
4
5
6
7
8
9
static struct nf_sockopt_ops nso = {
	.pf  = PF_INET,       // 协议族
	.set_optmin = 常数, // 定义最小set命令字
	.set_optmax = 常数+N,   // 定义最大set命令字
	.set  = recv_msg, // 定义set处理函数
	.get_optmin = 常数, // 定义最小get命令字
	.get_optmax = 常数+N,   // 定义最大get命令字
	.get  = send_msg, // 定义set处理函数
};

其中命令字不能和内核已有的重复,宜大不宜小。命令字很重要,是用来做标识符的。而且用户态和内核态要定义的相同,

1
2
3
4
#define SOCKET_OPS_BASE      128
#define SOCKET_OPS_SET        (SOCKET_OPS_BASE)
#define SOCKET_OPS_GET        (SOCKET_OPS_BASE)
#define SOCKET_OPS_MAX        (SOCKET_OPS_BASE + 1)

set/get处理函数是直接由用户空间的 set/getsockopt函数调用的。 setsockopt函数向内核写数据,用getsockopt向内核读数据。 另外set和get的处理函数的参数应该是这样的

1
2
int recv_msg(struct sock *sk, int cmd, void __user *user, unsigned int len);
int send_msg(struct sock *sk, int cmd, void __user *user, unsigned int *len);

用户态 setsockopt/getsockopt 调内核态 ipv4_specific.get/setsockopt -> ip_setsockopt -> nf_setsockopt -> nf_sockopt

内核态的module.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/string.h>
#include <linux/netfilter_ipv4.h>
#include <linux/init.h>
#include <asm/uaccess.h>

#define SOCKET_OPS_BASE       (128+10000)
#define SOCKET_OPS_SET        (SOCKET_OPS_BASE)
#define SOCKET_OPS_GET        (SOCKET_OPS_BASE)
#define SOCKET_OPS_MAX        (SOCKET_OPS_BASE + 1)

#define KMSG          "--------kernel---------"
#define KMSG_LEN      sizeof("--------kernel---------")


static int recv_msg(struct sock *sk, int cmd, void __user *user, unsigned int len)
{
	int ret = 0;
	printk(KERN_INFO "sockopt: recv_msg()\n");

	if (cmd == SOCKET_OPS_SET) {
		char umsg[64];
		int len = sizeof(char)*64;
		memset(umsg, 0, len);
		ret = copy_from_user(umsg, user, len);
		printk("recv_msg: umsg = %s. ret = %d\n", umsg, ret);
	}
	return 0;
}

static int send_msg(struct sock *sk, int cmd, void __user *user, int *len)
{
	int ret = 0;
	printk(KERN_INFO "sockopt: send_msg()\n");
	if (cmd == SOCKET_OPS_GET) {
		ret = copy_to_user(user, KMSG, KMSG_LEN);
		printk("send_msg: umsg = %s. ret = %d. success\n", KMSG, ret);
	}
	return 0;

}

static struct nf_sockopt_ops test_sockops =
{
	.pf = PF_INET,
	.set_optmin = SOCKET_OPS_SET,
	.set_optmax = SOCKET_OPS_MAX,
	.set = recv_msg,
	.get_optmin = SOCKET_OPS_GET,
	.get_optmax = SOCKET_OPS_MAX,
	.get = send_msg,
	.owner = THIS_MODULE,
};

static int __init init_sockopt(void)
{
	printk(KERN_INFO "sockopt: init_sockopt()\n");
	return nf_register_sockopt(&test_sockops);
}

static void __exit exit_sockopt(void)
{
	printk(KERN_INFO "sockopt: fini_sockopt()\n");
	nf_unregister_sockopt(&test_sockops);
}

module_init(init_sockopt);
module_exit(exit_sockopt);
MODULE_LICENSE("GPL");

用户态的user.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <linux/in.h>
#include <string.h>
#include <errno.h>

#define SOCKET_OPS_BASE       (128+10000)
#define SOCKET_OPS_SET        (SOCKET_OPS_BASE)
#define SOCKET_OPS_GET        (SOCKET_OPS_BASE)
#define SOCKET_OPS_MAX        (SOCKET_OPS_BASE + 1)

#define UMSG          "----------user------------"
#define UMSG_LEN      sizeof("----------user------------")

char kmsg[64];

int main(void)
{
	int sockfd;
	int len;
	int ret;

	sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
	if (sockfd < 0) {
		printf("can not create a socket\n");
		return -1;
	}

	/*call function recv_msg()*/
	ret = setsockopt(sockfd, IPPROTO_IP, SOCKET_OPS_SET, UMSG, UMSG_LEN);
	printf("setsockopt: ret = %d, msg = %s\n", ret, UMSG);
	len = sizeof(char)*64;

	/*call function send_msg()*/
	ret = getsockopt(sockfd, IPPROTO_IP, SOCKET_OPS_GET, kmsg, &len);
	printf("getsockopt: ret = %d, msg = %s\n", ret, kmsg);
	if (ret != 0) {
		printf("getsockopt error: errno = %d, errstr = %s\n", errno, strerror(errno));
	}

	close(sockfd);
	return 0;
}

Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
TARGET = socketopt
OBJS = module.o

KDIR = /lib/modules/`uname -r`/build
PWD = $(shell pwd)

obj-m := $(TARGET).o

$(TARGET)-objs := $(OBJS)

default:
	make -C $(KDIR) SUBDIRS=$(PWD) modules
clean:
	-rm -rf *.o *.ko .$(TARGET).ko.cmd .*.flags *.mod.c modules.order  Module.symvers .tmp_versions

editcap: pcap文件的合并和分隔

centos7 editcap 半静态编译

https://www.wireshark.org/download/src/wireshark-2.6.1.tar.xz

ftp://ftp.icm.edu.pl/vol/rzm6/linux-oracle-repo/OracleLinux/OL7/latest/SRPMS/libpcap-1.5.3-11.el7.src.rpm

editcap_el7

mergecap_el7

libcap.a

1
2
3
4
5
6
7
./configure CFLAGS=-static
make CFLAGS=-static


# CFLAGS=-static 不能完全起作用,
# 通过在 ./libtool 中增加 set -x 后得知 editcap 的链接命令,修改后如下
gcc -std=gnu99 -Wall -Wextra -Wendif-labels -Wpointer-arith -Wformat-security -fwrapv -fno-strict-overflow -fno-delete-null-pointer-checks -Wvla -Waddress -Wattributes -Wdiv-by-zero -Wignored-qualifiers -Wpragmas -Wno-overlength-strings -Wno-long-long -Wc++-compat -Wshadow -Wno-pointer-sign -Wold-style-definition -Wstrict-prototypes -Wlogical-op -Wjump-misses-init -Werror=implicit -fexcess-precision=fast -fvisibility=hidden -Wl,-Bstatic -o editcap editcap-editcap.o editcap-version_info.o -pthread -Wl,-Bstatic ui/libui.a wiretap/.libs/libwiretap.a /usr/local/wireshark/wireshark-2.6.1/wsutil/.libs/libwsutil.a wsutil/.libs/libwsutil.a -lgnutls -lgthread-2.0 -lgmodule-2.0 -lglib-2.0 -lgcrypt -lgpg-error -lz -lm -pthread -Wl,-Bdynamic -lgcc_s -ldl

glib2-2.42.2-5.el7.src.rpm
libgcrypt-1.5.3-12.el7_1.1.src.rpm
libgpg-error-1.12-3.el7.src.rpm
gnutls-3.3.8-14.el7_2.src.rpm

以上部分需要 make CFLAGS=-static 才能生成*.a

wireshark-1.10.14-7.el7.src.rpm

wireshark-1.10.14-7.el7.src.rpm 的./configure CFLAGS=-staticmake CFLAGS=-static过不了,无法使用。。。


http://qwxingren.blog.sohu.com/304463885.html

拆分

使用wireshark自带的editcap。我们的系统Centos 5.8,执行 yum install wireshark,就已经安装了editcap。

1. 根据时间来拆分,利用-A 起始时间和-B 截止时间来提去某个时间段的数据。

用法:editcap -A <起始时间> -B <截止时间> <源文件名> <目的文件名>

示例:

1
editcap -A "2014-07-12 12:55:00" -B "2014-07-12 12:56:00" eth0-rtp.cap  out_rtp.cap
2.按packge数量拆分为多个文件

用法:editcap -c <每个文件的包数> <源文件名> <目的文件名>

示例:

1
editcap -c 100 dump.pcap test.pcap

合并

在wireshark中通过filter过滤出sip信令,但是在多个文件中,megecap可以将多个pcap文件合并为一个文件。

用法:mergecap -w <输出文件> <源文件1> <源文件2> …

示例:

1
mergecap -w compare.pcap a.pcap b.pcap

nginx 四层转发配置

nginx rpm: http://nginx.org/packages/mainline/centos/7/x86_64/RPMS/

nginx source: http://nginx.org/en/download.html

nginx stream: http://nginx.org/en/docs/stream/ngx_stream_core_module.html

http://www.52devops.com/chuck/1153.html

一、nginx四层代理

Nginx 1.9.0 开发版发布,该版本增加了 stream module 用于一般的 TCP 代理和负载均衡。

三、nginx四层代理实战

3.1编译安装nginx

编译安装nginx需指定–with-stream参数

1
2
[root@linux-node1 nginx-1.9.12]#./configure --prefix=/usr/local/nginx-1.9.12 --user=www --group=www --with-http_ssl_module --with-http_stub_status_module --with-file-aio --with-stream
[root@linux-node1 nginx-1.9.12]#make && make install
3.2 使用nginx实现ssh四层代理

编辑nginx配置文件并启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@linux-node1 conf]# vim nginx.conf
worker_processes  1;
events {
    worker_connections  1024;
}
stream {  #类似于7层的http段
    upstream ssh_proxy {
        hash $remote_addr consistent;
        server 192.168.56.11:22;
        server 192.168.56.12:22;
    }
    server {
        listen 2222;
        proxy_connect_timeout 1s;
        proxy_timeout 300s;
        proxy_pass ssh_proxy;
    }
}
[root@linux-node1 conf]# ../sbin/nginx
[root@linux-node1 conf]# netstat -lntup|grep nginx
tcp        0      0 0.0.0.0:2222            0.0.0.0:*               LISTEN      61127/nginx: master

连接ssh,从结果看把请求抛向了linux-node2的ssh

1
2
3
4
[root@linux-node1 conf]# ssh -p 2222 root@192.168.56.11
root@192.168.56.11's password:
Last login: Sun Mar  6 17:29:42 2016 from linux-node1.example.com
[root@linux-node2 ~]# Connection to 192.168.56.11 closed by remote host.

lvs fwmark 模式

http://blog.51cto.com/angus717/769577

persistent netfilter marked packet persistence 持久防火墙标记(在pre-routing链上打netfilter marked,而且该标记只在防火墙内部有效通常是0-99)

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@slave ~]# ipvsadm -C
[root@slave ~]# iptables -t mangle -A PREROUTING -i eth0 -p tcp -d 172.16.8.120 --dport 80 -j MARK --set-mark 80
[root@slave ~]# iptables -t mangle -A PREROUTING -i eth0 -p tcp -d 172.16.8.120 --dport 443 -j MARK --set-mark 80
[root@slave ~]# ipvsadm -A -f 80 -s rr -p 1000
[root@slave ~]# ipvsadm -a -f 80 -r 172.16.100.7 -g
[root@slave ~]# ipvsadm -a -f 80 -r 172.16.100.6 -g
[root@slave ~]# ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
FWM  80 rr persistent 1000
  -> 172.16.100.6:0               Route   1      0          0        
  -> 172.16.100.7:0               Route   1      0          0