LVS分析
概述
LVS是章文嵩博士十几年前的开源项目,已经被何如linux kernel 目录十几年了,可以说是国内最成功的kernle 开源项目, 在10多年后的今天,因为互联网的高速发展LVS得到了极大的应用, 是当今国内互联网公司,尤其是大型互联网公司的必备武器之一, 从这一点上来说,LVS名副其实。
搞了这么多年linux 网络开发维护, 由于一直偏通信方向,自己竟然从来没有读过ipvs的代码,不能不说是一个遗憾。这两天花时间研究了一下LVS,阅读LVS的kernel与ipvsadm的代码,终于搞清楚了其工作原理与细节问题,感觉队LVS的认识提高了一个等级,再次感谢章博。
LVS架构以及代码分析
LVS是非常典型的【控制面 + 数据面】的网络体系架构。
控制面
ipvsadm 作为控制面的工具运行在用户空间,其本身是一个非常简单的linux命令。ipvsadm可以使用两种方式和内核进行通信 : netlink 与 raw socket,这两种方式我们在这里不做详细介绍。现在基本上默认都使用netlink的方式,这也是现在绝大多数的用户空间与内核空间通信所选择的方式。ipvsadm的工作原理和代码都非常简单 : 分析命令行,将命令行信息打包进nl_msg,即netlink与内核通信的数据结构,然后发给内核即可。
数据面
LVS的数据面完全在linux kernel中实现, 并且是实现在netfilter的框架中, 对netfilter的介绍并不在本文范围之内。
LVS本身以内核核心模块的方式存在,我们首先来看其初始化函数【static int __init _vs_init(void)】做了些什么 :
---> ip_vs_control_init() : 初始化virtual server HASH表与virtual server firewall HASH表,注册netdevice 的notification 处理相应事件 ip_vs_dst_notifier
---> ip_vs_protocol_init() : 注册协议处理结构,目前支持tcp,udp,sctp,ah,esp
---> ip_vs_conn_init() : 初始化connection HASH表,默认HASH表头4096个
---> register_pernet_subsys() : 注册namespace子系统
---> register_pernet_device() : 注册namespace device 子系统
---> nf_register_hooks() : 注册LVS处理函数到netfilter框架
---> ip_vs_register_nl_ioctl() : 注册netlink处理函数与set/getsockopt处理函数
核心处理流程在netfilter框架中(我们可以暂时不关注namespace相关的操作)。
以IPV4为例,我们看看LVS都在netfilter框架中做了什么 :
1829 static struct nf_hook_ops ip_vs_ops[] __read_mostly = {
1830 /* After packet filtering, change source only for VS/NAT */
1831 {
1832 .hook = ip_vs_reply4,
1833 .owner = THIS_MODULE,
1834 .pf = NFPROTO_IPV4,
1835 .hooknum = NF_INET_LOCAL_IN,
1836 .priority = NF_IP_PRI_NAT_SRC - 2,
1837 },
1838 /* After packet filtering, forward packet through VS/DR, VS/TUN,
1839 * or VS/NAT(change destination), so that filtering rules can be
1840 * applied to IPVS. */
1841 {
1842 .hook = ip_vs_remote_request4,
1843 .owner = THIS_MODULE,
1844 .pf = NFPROTO_IPV4,
1845 .hooknum = NF_INET_LOCAL_IN,
1846 .priority = NF_IP_PRI_NAT_SRC - 1,
1847 },
1848 /* Before ip_vs_in, change source only for VS/NAT */
1849 {
1850 .hook = ip_vs_local_reply4,
1851 .owner = THIS_MODULE,
1852 .pf = NFPROTO_IPV4,
1853 .hooknum = NF_INET_LOCAL_OUT,
1854 .priority = NF_IP_PRI_NAT_DST + 1,
1855 },
1856 /* After mangle, schedule and forward local requests */
1857 {
1858 .hook = ip_vs_local_request4,
1859 .owner = THIS_MODULE,
1860 .pf = NFPROTO_IPV4,
1861 .hooknum = NF_INET_LOCAL_OUT,
1862 .priority = NF_IP_PRI_NAT_DST + 2,
1863 },
1864 /* After packet filtering (but before ip_vs_out_icmp), catch icmp
1865 * destined for 0.0.0.0/0, which is for incoming IPVS connections */
1866 {
1867 .hook = ip_vs_forward_icmp,
1868 .owner = THIS_MODULE,
1869 .pf = NFPROTO_IPV4,
1870 .hooknum = NF_INET_FORWARD,
1871 .priority = 99,
1872 },
1873 /* After packet filtering, change source only for VS/NAT */
1874 {
1875 .hook = ip_vs_reply4,
1876 .owner = THIS_MODULE,
1877 .pf = NFPROTO_IPV4,
1878 .hooknum = NF_INET_FORWARD,
1879 .priority = 100,
1880 },
我们可以看到,LVS使用了netfilter五个HOOK点中的三个,分别是 : LOCAL_IN,LOCAL_OUT,FORWARD,我们分别介绍 :
LOCAL_IN 节点
LOCAL_IN节点LVS一共注册了两个函数,分别是 ip_vs_reply4 和 ip_vs_remote_request4。
ip_vs_reply4 : 只用于 LVS NAT 模式,并且只能处理TCP,UDP,SCTP
---> ip_vs_out()
---> ip_vs_fill_iph_skb() : 得到IP头
---> 判断是否是ICMP,如果是则调用ip_vs_out_icmp()函数处理
---> ip_vs_proto_data_get() : 取四层处理结构
---> 检查处理分片
---> 调用proto->conn_out_get() 得到当前connection
---> 如果得到connection,则调用handle_response()处理response
---> handle_response()
--->
---> 如果没有connecton,则检测是否有VS和这个包匹配,如果有则再次检测这个包是否是TCP或RST的包,如果不是则发送ICMP目的不可达消息
ip_vs_remote_request4 : For DRand TUN模式
---> ip_vs_in()
---> 首先是合法性检测,ignore不合法的包
---> ipvs_fill_iph_skb() : 得到ip头信息
---> 过滤掉RAW SOCKET的包
---> 处理ICMP包
---> ip_vs_proto_data_get() 找到proto结构,这个结构保存在 net->ipvs->proto_data_table[hash] 表中
---> 调用proto结构的 conn_in_get() 取的connection, connection保存在全局的表 ip_vs_conn_tab[hash] 中
---> 查找失败则调用 proto->conn_schedule() 创建connection
---> ip_vs_scheduler() : 找到与此包匹配的调度策略,创建connection
---> sched->schedule() : 调用调度策略函数,按照既定的调度测率找到real server
---> ip_vs_conn_new() : 创建新的connection
---> kmem_cache_alloc() : 为connection分配内存
---> 初始化connection
---> ip_vs_bind_xmit() : 根据LVS类型绑定connection的发送函数
---> 将此connection加入ip_vs_conn_tab[hash] 表
---> ip_vs_conn_stats() : 更新connection统计信息
---> ip_vs_in_stats() : 更新统计信息
---> connection->packet_xmit() : 发包
---> synchronization 工作
LOCAL OUT 节点
LOCAL OUT 节点注册了两个函数 : ip_vs_local_reply4 和 ip_vs_local_request4
ip_vs_local_reply4 :
---> ip_vs_out() :同上 ip_vs_reply4()
ip_vs_local_request4 :
---> ip_vs_in() : 同上 ip_vs_remote_request4()
FORWARD节点
FORWARD节点注册了两个函数 : ip_vs_reply4 和 ip_vs_forward_icmp
ip_vs_reply4 :
---> ip_vs_out() : 同上
ip_vs_forward_icmp :
---> ip_vs_in_icmp() : 处理 outside to inside 方向的ICMP报文
我们可以看到,其实LVS在内核中核心的函数其实就两个 : ip_vs_in() 与 ip_vs_out()。
数据流过程分析
我们通过一个数据包在LVS架构中的处理流程来分析LVS的工作过程。
假设我们添加了这样的规则 :
ipvsadm -A -t 192.168.132.254:80 -s rr -p 120
我们可以看到,此规则为一个VS : 192.168.132.254:80, 两个real server 分别是 192.168.132.64:80 与 192.168.132.68:80, LVS模式为DR,调度算法为Round Robin。
配置过程我们ignore掉。
加入一个client要访问此虚拟服务器,那么一个TCP发起包为 : 192.168.132.111:2345 -》 192.168.132.254:80, 我们看看这个包的处理流程。
---> LVS 收到这个包, 然后路由发现此包是到本地虚拟server地址的数据包,然后将其上送到LOCAL_IN HOOK 点。
---> ip_vs_reply4() 首先被调用,因为其优先级高
---> 尝试找到与此包关联的connection,因为是第一个包,所以找不到
---> return NF_ACCEPT, 进入下一个HOOK点处理
---> ip_vs_remote_request4() 被调用,
---> Call ip_vs_in() 函数
---> 首先是包的预处理工作,找到IP头,找到protocol处理结构
---> 然后尝试按照此包找到一个已经存在的connection,由于是第一个包,所以失败
---> 然后调用proto->conn_schedule() 创建一个新的connection
---> TCP : tcp_conn_schedule()
---> 首先取得TCP 头
---> 调用ip_vs_service_find() 找到虚拟服务器的管理结构
---> 调用 ip_vs_schedule()
---> 找到此VS所使用的调度器的管理结构,执行调度函数,找到目的real server地址
---> 调用 ip_vs_conn_new() 创建新的connection,将次connection加入全局HASH表
---> 调用 ip_vs_bind_xmit() 为此 connection 绑定发送函数
---> 然后调用 connection->packet_xmit()发包
---> 对于DR模式来说,发送函数是 ip_vs_dr_xmit()
---> 调用 __ip_vs_get_out_rt() 来确定新的路由并设置到此skb包中关联
---> 调用 ip_vs_send_or_cont() 来最终将此包发送到dst指定的real server
---> 重要的是设置skb->ipvs_property = 1
---> 发送过程中要经过LOCAL_OUT hook 点
---> 调用ip_vs_out() 函数,直接返回 NF_ACCEPT
---> 再调用ip_vs_in() 函数,直接返回NF_ACCEPT
---> dst_output() 最终发包
---> 最后更新connection状态
我们的例子是DR模式,real server收到包后可以直接向client返回数据,不必经过LVS server。其他的模式NAT, TUNNEL和DR模式大同小异,在生产环境中DR模式用的多一点,毕竟DR模式在性能上还是有优势的。
总结
LVS是非常好的,基于国内的,linux开源软件,我在上面大致分析了其数据面,即kernel中的数据处理流程,总的来说LVS的设计以及实现非常的简单但是高效,稳定,是一款优秀的linux open src项目。希望我的分析能够为大家起到抛砖引玉的作用 ;)
最近在用 LVS做 LB,发现一个问题客户端总是出现session丢失问题,采用常用配置,均衡策略使用wlc, 看了一下wlc的策略相同的客户端都有可能轮训到不同的后台机器,在后台服务器上并没有对session进行复制,那样的却会导致客户端访问不同的服务器而导致在session丢失。
本简单的以为通过调整均衡策略就可以确保对同一客户端映射到相同的服务器,均衡策略参考(点击打开链接),而策略里面只有Source Hashing Scheduling 看起来可以达到这个目的,但是这个策略并不是推荐的策略。
在查看ipvsadmin的参数的时候,发现了参数-p,
-p, --persistent [timeout]:设置持久连接,这个模式可以使来自客户的多个请求被送到同一个真实服务器
感觉-p 参数和Source Hashing Scheduling 的策略有点类似,还是看代码来解决问题
IPVS也叫LVS
LVS 是属于内核模块中的,代码直接就可以在内核代码中找到,在内核中的名字是IPVS,我们下面还是以IPVS来称呼
Ipvs 的代码就挂载在/net/netfilter/ipvs中,在这里我们也可以看出IPVS是基于Netfilter框架实现的内核模块,Linux 中的netfilter的架构就是在整个网络流程的若干位置放置了一些检测点(HOOK),而在每个检测点上登记了一些处理函数进行处理(如包过滤,NAT等,甚至可以是用户自定义的功能)。
Netfilter实现
Netfilter的状态图:
而Ipvs 在Netfilter中的几个状态中注册了钩子函数
- static struct nf_hook_ops ip_vs_ops[] __read_mostly = {
- /* After packet filtering, forward packet through VS/DR, VS/TUN,
- * or VS/NAT(change destination), so that filtering rules can be
- * applied to IPVS. */
- {
- .hook = ip_vs_in,
- .owner = THIS_MODULE,
- .pf = PF_INET,
- .hooknum = NF_INET_LOCAL_IN,
- .priority = 100,
- },
- /* After packet filtering, change source only for VS/NAT */
- {
- .hook = ip_vs_out,
- .owner = THIS_MODULE,
- .pf = PF_INET,
- .hooknum = NF_INET_FORWARD,
- .priority = 100,
- },
- /* After packet filtering (but before ip_vs_out_icmp), catch icmp
- * destined for 0.0.0.0/0, which is for incoming IPVS connections */
- {
- .hook = ip_vs_forward_icmp,
- .owner = THIS_MODULE,
- .pf = PF_INET,
- .hooknum = NF_INET_FORWARD,
- .priority = 99,
- },
- /* Before the netfilter connection tracking, exit from POST_ROUTING */
- {
- .hook = ip_vs_post_routing,
- .owner = THIS_MODULE,
- .pf = PF_INET,
- .hooknum = NF_INET_POST_ROUTING,
- .priority = NF_IP_PRI_NAT_SRC-1,
- },
而均衡策略主要实现在状态NF_INET_LOCAL_IN所对应的钩子函数ip_vs_in
IPVS中的2个结构体
1. ip_vs_conn 里面记录了客户端的地址,IPVS 所建立的虚拟地址,对应到真实的服务器的地址
2. ip_vs_protocol 记录在不同的协议(TCP, UDP)中的不同的钩子函数,比如使用什么类型的调度,什么函数接受数据
- struct ip_vs_protocol ip_vs_protocol_tcp = {
- .name = "TCP",
- .protocol = IPPROTO_TCP,
- .num_states = IP_VS_TCP_S_LAST,
- .dont_defrag = 0,
- .appcnt = ATOMIC_INIT(0),
- .init = ip_vs_tcp_init,
- .exit = ip_vs_tcp_exit,
- .register_app = tcp_register_app,
- .unregister_app = tcp_unregister_app,
- .conn_schedule = tcp_conn_schedule,
- .conn_in_get = tcp_conn_in_get,
- .conn_out_get = tcp_conn_out_get,
- .snat_handler = tcp_snat_handler,
- .dnat_handler = tcp_dnat_handler,
- .csum_check = tcp_csum_check,
- .state_name = tcp_state_name,
- .state_transition = tcp_state_transition,
- .app_conn_bind = tcp_app_conn_bind,
- .debug_packet = ip_vs_tcpudp_debug_packet,
- .timeout_change = tcp_timeout_change,
- .set_state_timeout = tcp_set_state_timeout,
- };
这是一个TCP下的ip_vs_protocol 的结构体,里面重要的是conn_in_get 函数也是在钩子函数ip_vs_in里调用的
- static unsigned int
- ip_vs_in(unsigned int hooknum, struct sk_buff *skb,
- const struct net_device *in, const struct net_device *out,
- int (*okfn)(struct sk_buff *))
- {
- ...
- pp = ip_vs_proto_get(iph.protocol);
- if (unlikely(!pp))
- return NF_ACCEPT;
- /*
- * Check if the packet belongs to an existing connection entry
- */
- cp = pp->conn_in_get(af, skb, pp, &iph, iph.len, 0);
- ...
- }
我们已常见的TCP为例,最终调用了函数 tcp_conn_in_get
ip_vs_conn的全局数组ip_vs_conn_tab和链表c_list
这是一张全局的ip_vs_conn数组,保存这所有的以连接。通过客户端ip,port算出的hash,来计算到保存的 ip_vs_conn数组。
ip_vs_conn本身也保存一个链表c_list,这是个链表结构保存ip_vs_conn
初始化
在IPVS初始化的时候,就初始化了数组的大小,大小是不可变的(1<<12 )4096
当客户端hash算出相同的时候,通过遍历ip_vs_conn里面的c_list链表找到匹配的客户端(地址和端口相同)
如果找不到对应的ip_vs_conn, 这时候才调用
- if (!pp->conn_schedule(af, skb, pp, &v, &cp))
- return v;
而对tcp 中的conn_schedule 的钩子函数就是tcp_conn_schedule,也就是我们前面说到的调度算法的核心函数
- static int
- tcp_conn_schedule(int af, struct sk_buff *skb, struct ip_vs_protocol *pp,
- int *verdict, struct ip_vs_conn **cpp)
- {
- struct ip_vs_service *svc;
- struct tcphdr _tcph, *th;
- struct ip_vs_iphdr iph;
- ip_vs_fill_iphdr(af, skb_network_header(skb), &iph);
- th = skb_header_pointer(skb, iph.len, sizeof(_tcph), &_tcph);
- if (th == NULL) {
- *verdict = NF_DROP;
- return 0;
- }
- if (th->syn &&
- (svc = ip_vs_service_get(af, skb->mark, iph.protocol, &iph.daddr,
- th->dest))) {
- if (ip_vs_todrop()) {
- /*
- * It seems that we are very loaded.
- * We have to drop this packet :(
- */
- ip_vs_service_put(svc);
- *verdict = NF_DROP;
- return 0;
- }
- /*
- * Let the virtual server select a real server for the
- * incoming connection, and create a connection entry.
- */
- *cpp = ip_vs_schedule(svc, skb); //调用了ip_vs_schedule
- if (!*cpp) {
- *verdict = ip_vs_leave(svc, skb, pp);
- return 0;
- }
- ip_vs_service_put(svc);
- }
- return 1;
- }
函数ip_vs_schedule
- struct ip_vs_conn *
- ip_vs_schedule(struct ip_vs_service *svc, const struct sk_buff *skb)
- {
- ...
- /*
- * Persistent service
- */
- if (svc->flags & IP_VS_SVC_F_PERSISTENT)
- return ip_vs_sched_persist(svc, skb, pptr);
- /*
- * Non-persistent service
- */
- if (!svc->fwmark && pptr[1] != svc->port) {
- if (!svc->port)
- pr_err("Schedule: port zero only supported "
- "in persistent services, "
- "check your ipvs configuration\n");
- return NULL;
- }
- <span style="white-space:pre"> </span>....
- return cp;
- }
我们看到了IP_VS_SVC_F_PERSISTENT, 也就是参数persistent
参数persistent 的实现
创建ip_vs_conn 模版
保证在一定的时间内相同的客户端ip还是连接原来的服务器,那就意味着需要保留原来的客户端ip的上次的连接的真实服务器。
在IPVS里并没有生成另一个数组去保留这个状态,而是引入了一个ip_vs_conn 模版,而这个ip_vs_conn 模版仍旧保存在前面提到的全局表中的ip_vs_conn_tab
既然保存在同一个全局表,那么这个模版和普通的ip_vs_conn有什么区别?很简单,这里只要设置客户端的port 为0,而把连到真实的server的IP 保存到ip_vs_conn中去
具体实现参考函数:ip_vs_sched_persist
什么时候清除ip_vs_conn 模版?
-p 参数有指定时间,ip_vs_conn结构体中存在一个timer和timeout的时间,在函数新建连接的时候,设置了timer的执行函数ip_vs_conn_expire
- struct ip_vs_conn *
- ip_vs_conn_new(int af, int proto, const union nf_inet_addr *caddr, __be16 cport,
- const union nf_inet_addr *vaddr, __be16 vport,
- const union nf_inet_addr *daddr, __be16 dport, unsigned flags,
- struct ip_vs_dest *dest)
- {
- <span style="white-space:pre"> </span>......
- setup_timer(&cp->timer, ip_vs_conn_expire, (unsigned long)cp);
- ......
- return cp;
- }
函数ip_vs_conn_expire里从ip_vs_conn_tab移除了ip_vs_conn超时的模版
而在ip_vs_sched_persist 函数里,当每次创建新的连接的时候,同时也更新了ip_vs_conn模版的timer的触发时间(当前时间+-p 参数的timeout时间),实现在函数ip_vs_conn_put中。
完整流程图
IPVS的debug日志
需要从新编译内核,设置config 里的参数
CONFIG_IP_VS_DEBUG=Y
编译后,还要修改参数
/proc/sys/net/ipv4/vs/debug_level
设置为12 ,日志打印到dmesg中
IPVS ip_vs_conn_tab 里的entry
内容可以通过 /proc/net/ip_vs_conn_entries 访问
IPVS 不设置persistent参数
如果不设置persistent参数,也就是意味着不需要保证同一个客户端在一个固定时间段中连接到同一个真实得服务器,那么ipvs会使用调度算法去调度每一个来自客户端得新连接。
函数ip_vs_conn里面,当没有设置persistent参数得时候,会直接调用dest = svc->scheduler->schedule(svc, skb);来调度新得连接。
最新文章
- 开发者最爱的Firebug停止更新和维护
- Sql Server系列:数据表操作
- Numpy Study 2----* dot multiply区别
- asp.net 5 中应用程序根目录及物理文件根目录的获取方式 此文已过期,不再适应rc1以后的版本
- JVM内存监控工具 Jvisualvm
- Using Feedback as a Tool
- 伪类写border, transform: scale3d() 及兼容性
- 用PHP生成随机数的函数
- Code Forces Gym 100971D Laying Cables(单调栈)
- c# 使用递归 循环遍历导航树结构 并解析
- Ninject框架的介绍
- SQL操作语句中的注意点
- 内置函数值 -- chr() ord() -- 字符和ascii的转换
- javascript实现双向数据绑定
- Android图表库MPAndroidChart(五)——自定义MarkerView实现选中高亮
- 在Workload Automation中实现suspend分析
- Luogu P1654 OSU!
- spark MLlib实现的基于朴素贝叶斯(NaiveBayes)的中文文本自动分类
- EF:分页查询 + 条件查询 + 排序
- python高级-异常(13)