###############################################################################
#
Name : Mahavairocana
#
Author : Mahavairocana
#
QQ : 10353512
#
WeChat : shenlan-qianlan
#
Blog : http://www.cnblogs.com/Mahavairocana/
#
Description : You are welcome to reprint, or hyperlinks to indicate the
#
source of the article, as well as author
information.
###############################################################################
一、LVS简介
Linux Virtual Server用途:无缝地将网络请求调度到真实服务器上,从而使得服务器集群的结构对客户是透明的,客户访问集群系统提供的网络服务就像访 问一台高性能、高可用的服务器一样。客户程序不受服务器集群的影响不需作任何修改。系统的伸缩性通过在服务机群中透明地加入和删除一个节点来达到,通过检 测节点或服务进程故障和正确地重置系统达到高可用性。
二、术语详解
1、调度算法、工作模型简介
2、三种工作模型数据流处理流程:
3、关于IP的概念
VIP:Virtual IP Director对外提供服务的 Virtual IP
DIP: Director IP Director IP地址
RIP: Real Server IP 实际提供服务的Real Server的IP
CIP:Client IP Client IP 客户端的IP地址
4、健康状态检查
如果状态发生了改变:
下线:ok-fail-fail-fail
上线:fail--> ok-->ok :第一次ok为软状态,第二次OK为硬状态;
三、配置、命令详解
1、ipvsadmin:
ipvsadm是LVS在应用层的管理命令,我们可以通过这个命令去管理LVS的配置。在笔者使用的fedora14系统中,已经集成了LVS相关模块,但是ipvsadm命令仍然需要使用yum单独安装。 基本用法: ipvsadm COMMAND [protocol] service-address [scheduling-method] [persistence options] ipvsadm command [protocol] service-address server-address [packet-forwarding-method] [weight options] 第一条命令用于向LVS系统中添加一个用于负载均衡的virtual server(VS);第二条命令用来修改已经存在的VS的配置,service address用来指定涉及的虚拟服务即虚拟地址,server-address指定涉及的真实地址。 命令: -A, --add-service:为ipvs虚拟服务器添加一个虚拟服务,即添加一个需要被负载均衡的虚拟地址。虚拟地址需要是ip地址,端口号,协议的形式。 -E, --edit-service:修改一个虚拟服务。 -D, --delete-service:删除一个虚拟服务。 -C, --clear:清除所有虚拟服务。 -R, --restore:从标准输入获取ipvsadm命令。一般结合下边的-S使用。 -S, --save:从标准输出输出虚拟服务器的规则。可以将虚拟服务器的规则保存,在以后通过-R直接读入,以实现自动化配置。 -a, --add-server:为虚拟服务添加一个real server(RS) -e, --edit-server:修改RS -d, --delete-server:删除 -L, -l, --list:列出虚拟服务表中的所有虚拟服务。可以指定地址。添加-c显示连接表。 -Z, --zero:将所有数据相关的记录清零。这些记录一般用于调度策略。 --set tcp tcpfin udp:修改协议的超时时间。 --start-daemon state:设置虚拟服务器的备服务器,用来实现主备服务器冗余。(注:该功能只支持ipv4) --stop-daemon:停止备服务器。 -h, --help:帮助。 参数: 以下参数可以接在上边的命令后边。 -t, --tcp-service service-address:指定虚拟服务为tcp服务。service-address要是host[:port]的形式。端口是0表示任意端口。如果需要将端口设置为0,还需要加上-p选项(持久连接)。 -u, --udp-service service-address:使用udp服务,其他同上。 -f, --fwmark-service integer:用firewall mark取代虚拟地址来指定要被负载均衡的数据包,可以通过这个命令实现把不同地址、端口的虚拟地址整合成一个虚拟服务,可以让虚拟服务器同时截获处理去往多个不同地址的数据包。fwmark可以通过iptables命令指定。如果用在ipv6需要加上-6。 -s, --scheduler scheduling-method:指定调度算法。调度算法可以指定以下8种:rr(轮询),wrr(权重),lc(最后连接),wlc(权重),lblc(本地最后连接),lblcr(带复制的本地最后连接),dh(目的地址哈希),sh(源地址哈希),sed(最小期望延迟),nq(永不排队) -p, --persistent [timeout]:设置持久连接,这个模式可以使来自客户的多个请求被送到同一个真实服务器,通常用于ftp或者ssl中。 -M, --netmask netmask:指定客户地址的子网掩码。用于将同属一个子网的客户的请求转发到相同服务器。 -r, --real-server server-address:为虚拟服务指定数据可以转发到的真实服务器的地址。可以添加端口号。如果没有指定端口号,则等效于使用虚拟地址的端口号。 [packet-forwarding-method]:此选项指定某个真实服务器所使用的数据转发模式。需要对每个真实服务器分别指定模式。 -g, --gatewaying:使用网关(即直接路由),此模式是默认模式。 -i, --ipip:使用ipip隧道模式。 -m, --masquerading:使用NAT模式。 -w, --weight weight:设置权重。权重是0~65535的整数。如果将某个真实服务器的权重设置为0,那么它不会收到新的连接,但是已有连接还会继续维持(这点和直接把某个真实服务器删除时不同的)。 -x, --u-threshold uthreshold:设置一个服务器可以维持的连接上限。0~65535。设置为0表示没有上限。 -y, --l-threshold lthreshold:设置一个服务器的连接下限。当服务器的连接数低于此值的时候服务器才可以重新接收连接。如果此值未设置,则当服务器的连接数连续三次低于uthreshold时服务器才可以接收到新的连接。(PS:笔者以为此设定可能是为了防止服务器在能否接收连接这两个状态上频繁变换) --mcast-interface interface:指定使用备服务器时候的广播接口。 --syncid syncid:指定syncid,同样用于主备服务器的同步。 以下选项用于list命令: -c, --connection:列出当前的IPVS连接。 --timeout:列出超时 --daemon: --stats:状态信息 --rate:传输速率 --thresholds:列出阈值 --persistent-conn:坚持连接 --sor:把列表排序。 --nosort:不排序 -n, --numeric:不对ip地址进行dns查询 --exact:单位 -6:如果fwmark用的是ipv6地址需要指定此选项。
四、会话保持
1. LVS的sh算法和持久连接: sh算法全称为source hash(源地址hash),它和持久连接的作用都是"将来自同一个IP的请求都转发到同一个Server",从而保证了session会话定位的问题。两者的不同是: (1)sh算法:使用SH算法,SH算法在内核中会自动维护一个哈希表,此哈希表中用每一个请求的源IP地址经过哈希计算得出的值作为键,把请求所到达的RS的地址作为值。在后面的请求中,每一个请求会先经过此哈希表,如果请求在此哈希表中有键值,那么直接定向至特定RS,如没有,则会新生成一个键值,以便后续请求的定向。但是此种方法在时间的记录上比较模糊(依据TCP的连接时长计算),而且其是算法本身,所以无法与算法分离,并不是特别理想的方法。 (2)持久连接:此种方法实现了无论使用哪一种调度方法,持久连接功能都能保证在指定时间范围之内,来自于同一个IP的请求将始终被定向至同一个RS,还可以把多种服务绑定后统一进行调度。 详细一点说:当用户请求到达director时。无论使用什么调度方法,都可以实现对同一个服务的请求在指定时间范围内始终定向为同一个RS。在director内有一个LVS持久连接模板,模板中记录了每一个请求的来源、调度至的RS、维护时长等等,所以,在新的请求进入时,首先在此模板中检查是否有记录(有内置的时间限制,比如限制是300秒,当在到达300秒时依然有用户访问,那么持久连接模板就会将时间增加两分钟,再计数,依次类推,每次只延长2分钟),如果该记录未超时,则使用该记录所指向的RS,如果是超时记录或者是新请求,则会根据调度算法先调度至特定RS,再将调度的记录添加至此表中。这并不与SH算法冲突,lvs持久连接会在新请求达到时,检查后端RS的负载状况,这就是比较精细的调度和会话保持方法。 2. LVS的三种持久连接方式: (1)PCC:每客户端持久;将来自于同一个客户端的所有请求统统定向至此前选定的RS;也就是只要IP相同,分配的服务器始终相同。 (2)PPC:每端口持久;将来自于同一个客户端对同一个服务(端口)的请求,始终定向至此前选定的RS。例如:来自同一个IP的用户第一次访问集群的80端口分配到A服务器,25号端口分配到B服务器。当之后这个用户继续访问80端口仍然分配到A服务器,25号端口仍然分配到B服务器。 (3)PFMC:持久防火墙标记连接;将来自于同一客户端对指定服务(端口)的请求,始终定向至此选定的RS;不过它可以将两个毫不相干的端口定义为一个集群服务,例如:合并http的80端口和https的443端口定义为同一个集群服务,当用户第一次访问80端口分配到A服务器,第二次访问443端口时仍然分配到A服务器。
3. Session持久机制:
(1)session绑定:始终将来自同一个源IP的请求定向至同一个RS;没有容错能力;有损均衡效果;
(2)session复制:在RS之间同步session,每个RS拥有集群中的所有的session;对规模集群不适用;
(3)session服务器:利用单独部署的服务器来统一管理集群中的session;
五、常用优化
1、调整内核参数 CONFIG_IP_VS_TAB_BITS 1.1 CONFIG_IP_VS_TAB_BITS说明 IPVS connection hash table size,取值范围:[12,20]。该表用于记录每个进来的连接及路由去向的信息。连接的Hash表要容纳几百万个并发连接,任何一个报文到达都需要查找连接Hash表。Hash表的查找复杂度为O(n/m),其中n为Hash表中对象的个数,m为Hash表的桶个数。当对象在Hash表中均匀分布和Hash表的桶个数与对象个数一样多时,Hash表的查找复杂度可以接近O(1)。 连接跟踪表中,每行称为一个hash bucket(hash桶),桶的个数是一个固定的值CONFIG_IP_VS_TAB_BITS,默认为12(2的12次方,4096)。这个值可以调整,该值的大小应该在 8 到 20 之间 LVS的调优建议将hash table的值设置为不低于并发连接数。例如,并发连接数为200,Persistent时间为200S,那么hash桶的个数应设置为尽可能接近200x200=40000,2的15次方为32768就可以了。当ip_vs_conn_tab_bits=20 时,哈希表的的大小(条目)为 pow(2,20),即 1048576,对于64位系统,IPVS占用大概16M内存,可以通过demsg看到:IPVS: Connection hash table configured (size=1048576, memory=16384Kbytes)。对于现在的服务器来说,这样的内存占用不是问题。所以直接设置为20即可。 关于最大“连接数限制”:这里的hash桶的个数,并不是LVS最大连接数限制。LVS使用哈希链表解决“哈希冲突”,当连接数大于这个值时,必然会出现哈稀冲突,会(稍微)降低性能,但是并不对在功能上对LVS造成影响。 1.2 调整 ip_vs_conn_tab_bits的方法: 新的IPVS代码,允许调整 ip_vs_conn_bits 的值。而老的IPVS代码则需要通过重新编译来调整。 在发行版里,IPVS通常是以模块的形式编译的。 确认能否调整使用命令 modinfo -p ip_vs(查看 ip_vs 模块的参数),看有没有 conn_tab_bits 参数可用。假如可以用,那么说时可以调整,调整方法是加载时通过设置 conn_tab_bits参数: 在/etc/modprobe.d/目录下添加文件ip_vs.conf,内容为: options ip_vs conn_tab_bits=20 查看 ipvsadm -l 如果显示IP Virtual Server version 1.2.1 (size=4096),则前面加的参数没有生效 modprobe -r ip_vs modprobe ip_vs 重新查看 IP Virtual Server version 1.2.1 (size=1048576) 假如没有 conn_tab_bits 参数可用,则需要重新调整编译选项,重新编译。 Centos6.2,内核版本2.6.32-220.13.1.el6.x86_64,仍然不支持这个参数,只能自定义编译了。 另外,假如IPVS支持调整 ip_vs_conn_tab_bits,而又将IPVS集成进了内核,那么只能通过重启,向内核传递参数来调整了。在引导程序的 kernel 相关的配置行上,添加:ip_vs.conn_tab_bits=20 ,然后,重启。 或者重新编译内核。 2、系统参数优化 2.1 关闭网卡LRO和GRO 现在大多数网卡都具有LRO/GRO功能,即 网卡收包时将同一流的小包合并成大包 (tcpdump抓包可以看到>MTU 1500bytes的数据包)交给 内核协议栈;LVS内核模块在处理>MTU的数据包时,会丢弃; 因此,如果我们用LVS来传输大文件,很容易出现丢包,传输速度慢; 解决方法,关闭LRO/GRO功能,命令: ethtool -k eth0 查看LRO/GRO当前是否打开 ethtool -K eth0 lro off 关闭GRO ethtool -K eth0 gro off 关闭GRO 2.2 禁用ARP,增大backlog并发数 net.ipv4.conf.all.arp_ignore = 1 net.ipv4.conf.all.arp_announce = 2 net.core.netdev_max_backlog = 500000 3、lvs自身配置调优 3.1 尽量避免sh算法 一些业务为了支持会话保持,选择SH调度算法,以实现 同一源ip的请求调度到同一台RS上;但 SH算法本省没有实现一致性hash,一旦一台RS down,当前所有连接都会断掉;如果配置了inhibit_on_failure,那就更悲剧了,调度到该RS上的流量会一直损失; 实际线上使用时,如需 会话保持,建议配置 persistence_timeout参数,保证一段时间同一源ip的请求到同一RS上。 3.2 增大hash桶锁个数 对于一个16核的服务器来说,可以将桶锁个数调整为8. ipvs源码目录下,修改文件vim ip_vs_conn.c - #define CT_LOCKARRAY_BITS 8; 4、使用TOA获取客户端真实IP地址 toa模块具体实现: 模块的初始化函数为toa_init: /* module init */ static int __init toa_init(void) { ... /* hook funcs for parse and get toa */ hook_toa_functions(); TOA_INFO("toa loaded "); return 0; err: ... return 1; } 函数调用hook_toa_functions函数HOOK两个函数: inet_getname tcp_v4_syn_recv_sock /* replace the functions with our functions */ static inline int hook_toa_functions(void) { /* hook inet_getname for ipv4 */ struct proto_ops *inet_stream_ops_p = (struct proto_ops *)&inet_stream_ops; /* hook tcp_v4_syn_recv_sock for ipv4 */ struct inet_connection_sock_af_ops *ipv4_specific_p = (struct inet_connection_sock_af_ops *)&ipv4_specific; ... inet_stream_ops_p->getname = inet_getname_toa; ... ipv4_specific_p->syn_recv_sock = tcp_v4_syn_recv_sock_toa; ... return 0; } Linux内核在监听套接字收到三次握手的ACK包之后,会从SYN_REVC状态进入到TCP_ESTABLISHED状态。这时内核会调用tcp_v4_syn_recv_sock函数。Hook函数tcp_v4_syn_recv_sock_toa首先调用原有的tcp_v4_syn_recv_sock函数,然后调用get_toa_data函数从TCP OPTION中提取出TOA OPTION,并存储在sk_user_data字段中。 static struct sock * tcp_v4_syn_recv_sock_toa(struct sock *sk, struct sk_buff *skb, struct request_sock *req, struct dst_entry *dst) { struct sock *newsock = NULL; /* call orginal one */ newsock = tcp_v4_syn_recv_sock(sk, skb, req, dst); /* set our value if need */ if (NULL != newsock && NULL == newsock->sk_user_data) { newsock->sk_user_data = get_toa_data(skb); if(NULL != newsock->sk_user_data){ TOA_INC_STATS(ext_stats, SYN_RECV_SOCK_TOA_CNT); } else { TOA_INC_STATS(ext_stats, SYN_RECV_SOCK_NO_TOA_CNT); } } return newsock; } get_toa_data函数的返回值处理比较特殊,并没有给返回结果分配内存空间,而是直接将TOA OPTION做为指针值返回并保存在sk_user_data这一指针变量中。这在64位服务器上没有问题,因为指针变量的大小为8字节,返回的TOA结构大小也为8字节。 static void * get_toa_data(struct sk_buff *skb) { struct tcphdr *th; int length; unsigned char *ptr; struct toa_data tdata; void *ret_ptr = NULL; if (NULL != skb) { th = tcp_hdr(skb); length = (th->doff * 4) - sizeof (struct tcphdr); ptr = (unsigned char *) (th + 1); while (length > 0) { int opcode = *ptr++; int opsize; switch (opcode) { case TCPOPT_EOL: return NULL; case TCPOPT_NOP: /* Ref: RFC 793 section 3.1 */ length--; continue; default: opsize = *ptr++; if (opsize < 2) /* "silly options" */ return NULL; if (opsize > length) return NULL; /* don't parse partial options */ if (TCPOPT_TOA == opcode && TCPOLEN_TOA == opsize) { memcpy(&tdata, ptr - 2, sizeof (tdata)); memcpy(&ret_ptr, &tdata, sizeof (ret_ptr)); return ret_ptr; } ptr += opsize - 2; length -= opsize; } } } return NULL; } 用户在使用套接字中的accept函数时, 会调用inet_getname将sock结构体中存储的源IP地址和端口返回。Hook函数inet_getname_toa首先调用原有函数inet_getname, 然后用tcp_v4_syn_recv_sock_toa函数保存在sk_user_data中数据提取真实IP和Port,对返回结果进行替换。 static int inet_getname_toa(struct socket *sock, struct sockaddr *uaddr, int *uaddr_len, int peer) { int retval = 0; struct sock *sk = sock->sk; struct sockaddr_in *sin = (struct sockaddr_in *) uaddr; struct toa_data tdata; ... /* call orginal one */ retval = inet_getname(sock, uaddr, uaddr_len, peer); /* set our value if need */ if (retval == 0 && NULL != sk->sk_user_data && peer) { if (sk_data_ready_addr == (unsigned long) sk->sk_data_ready) { memcpy(&tdata, &sk->sk_user_data, sizeof(tdata)); if (TCPOPT_TOA == tdata.opcode && TCPOLEN_TOA == tdata.opsize) { ... sin->sin_port = tdata.port; sin->sin_addr.s_addr = tdata.ip; } else { /* sk_user_data doesn't belong to us */ ... } } else { TOA_INC_STATS(ext_stats, GETNAME_TOA_BYPASS_CNT); } } else { /* no need to get client ip */ TOA_INC_STATS(ext_stats, GETNAME_TOA_EMPTY_CNT); } return retval; } 后续应用层程序调用getpeername()时就可以获取到真实的客户端地址了。
六、待补充实验场景与应用验证。