IPVS(也叫LVS)的源码分析之persistent参数

最近在用 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 访问

时间: 2024-08-25 15:34:16

IPVS(也叫LVS)的源码分析之persistent参数的相关文章

memcached源码分析-----memcached启动参数详解以及关键配置的默认值

转载请注明出处: http://blog.csdn.net/luotuo44/article/details/42672913 本文开启本系列博文的代码分析.本系列博文研究是memcached版本是1.4.21. 本文将给出memcached启动时各个参数的详细解释以及一些关键配置的默认值.以便在分析memcached源码的时候好随时查看.当然也方便使用memcached时可以随时查看各个参数的含义.<如何阅读memcached源码>说到memcached有很多全局变量(也就是关键配置),这些

springmvc源码分析之请求参数、类型转换、数据绑定

前言 通过前面的分析,我们知道了请求过来,怎么找到相应的handlerMethod.本篇对请求参数的转换进行讲解. 概述 在进行分析之前,我们回到DispatcherServlet的doDispatch方法去看一下,请求过来后真正执行业务控制器的入口是从HandlerAdapter的handle方法.我们熟悉一下几个类,HandlerAdapter.RequestMappingHandlerAdapter. HandlerAdapter接口我们关注supports方法.handle方法.supp

faac源码分析之解码参数配置

FAAC定义了一个结构体用来定义解码器的工作解码参数,该结构体的定义如下所示: typedef struct faacEncConfiguration { /* config version */ int version; /* library version */ char *name; /* copyright string */ char *copyright; /* MPEG version, 2 or 4 */ unsigned int mpegVersion; /* AAC obje

YII框架源码分析(百度PHP大牛创作-原版-无广告无水印)

                        YII 框架源码分析             百度联盟事业部--黄银锋   目 录 1. 引言 3 1.1.Yii 简介 3 1.2.本文内容与结构 3 2.组件化与模块化 4 2.1.框架加载和运行流程 4 2.2.YiiBase 静态类 5 2.3.组件 6 2.4.模块 9 2.5 .App 应用   10 2.6 .WebApp 应用   11 3.系统组件 13 3.1.日志路由组件  13 3.2.Url 管理组件  15 3.3.异常

Android -- 消息处理机制源码分析(Looper,Handler,Message)

android的消息处理有三个核心类:Looper,Handler和Message.其实还有一个Message Queue(消息队列),但是MQ被封装到Looper里面了,我们不会直接与MQ打交道,因此我没将其作为核心类.下面一一介绍: Looper Looper的字面意思是“循环者”,它被设计用来使一个普通线程变成Looper线程.所谓Looper线程就是循环工作的线程.在程序开发中(尤其是GUI开发中),我们经常会需要一个线程不断循环,一旦有新任务则执行,执行完继续等待下一个任务,这就是Lo

Android消息处理机制(源码分析)

前言 虽然一直在做应用层开发,但是我们组是核心系统BSP,了解底层了解Android的运行机制还是很有必要的.就应用程序而言,Android系统中的Java应用程序和其他系统上相同,都是靠消息驱动来工作的,它们大致的工作原理如下: 1. 有一个消息队列,可以往这个消息队列中投递消息. 2. 有一个消息循环,不断从消息队列中取出消息,然后处理 . 为了更深入的理解Android的消息处理机制,这几天空闲时间,我结合<深入理解Android系统>看了Handler.Looper.Message这几

OpenStack_Swift源码分析——Ring基本原理及一致性Hash算法

1.Ring的基本概念 Ring是swfit中最重要的组件,用于记录存储对象与物理位置之间的映射关系,当用户需要对Account.Container.Object操作时,就需要查询对应的Ring文件(Account.Container.Object都有自己对应的Ring),Ring 使用Region(最近几个版本中新加入的).Zone.Device.Partition和Replica来维护这些信息,对于每一个对象,根据你在部署swift设置的Replica数量,集群中会存有Replica个对象.

Memcached源码分析之内存管理

先再说明一下,我本次分析的memcached版本是1.4.20,有些旧的版本关于内存管理的机制和数据结构与1.4.20有一定的差异(本文中会提到). 一)模型分析在开始解剖memcached关于内存管理的源代码之前,先宏观上分析一下memcached内存管理的模型是怎样子的: 提个建议,我觉得memcached内存管理的模型与我们平时做作业的作业本“画格子给我们往格子里面写字”的逻辑很像,一本本作业本就是我们的内存空间,而我们往里写的字就是我们要存下来的数据,所以分析的时候可以想像一下用方格作业

linux内存源码分析 - 内存回收(整体流程)

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 概述 当linux系统内存压力就大时,就会对系统的每个压力大的zone进程内存回收,内存回收主要是针对匿名页和文件页进行的.对于匿名页,内存回收过程中会筛选出一些不经常使用的匿名页,将它们写入到swap分区中,然后作为空闲页框释放到伙伴系统.而对于文件页,内存回收过程中也会筛选出一些不经常使用的文件页,如果此文件页中保存的内容与磁盘中文件对应内容一致,说明此文件页是一个干净的文件页,就不需要进行回写,直接将此