概述
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项目。希望我的分析能够为大家起到抛砖引玉的作用 ;)