ip_vs 原理解析 (三)调度器

文章目录

  • 调度器
    • 注册
    • [调度器绑定 svc](#调度器绑定 svc)
  • [ip_vs_schedule 结构体](#ip_vs_schedule 结构体)
  • PE

调度器

ipvs 的 调度器(scheduler) 有很多种,这里不详细介绍各个调度器的算法,主要关注于 ipvs 流程中的调度器注册和使用。

ipvs 的调度器有 rr(轮询),wrr(加权轮询),lc(最小链接),sh(源地址散列),sed(最短预期延时) 等等

注册

每个调度器都是通过 module_init() 进行初始化

复制代码
rr 调度器

static struct ip_vs_scheduler ip_vs_rr_scheduler = {
	.name =			"rr",			/* name */
	.refcnt =		ATOMIC_INIT(0),
	.module =		THIS_MODULE,
	.n_list =		LIST_HEAD_INIT(ip_vs_rr_scheduler.n_list),
	.init_service =		ip_vs_rr_init_svc,
	.add_dest =		NULL,
	.del_dest =		ip_vs_rr_del_dest,
	.schedule =		ip_vs_rr_schedule,
};

static int __init ip_vs_rr_init(void)
{
	return register_ip_vs_scheduler(&ip_vs_rr_scheduler);
}

module_init(ip_vs_rr_init);

register_ip_vs_scheduler 调度函数

将调度器链接在全局链表 ip_vs_schedulers 上

调度器绑定 svc

在第一节 ipvsadm 创建 svc 时 ip_vs_bind_scheduler 会为 svc 绑定调度器

复制代码
ip_vs_bind_scheduler
  | -- init_service       执行调度器的 init_service 函数,调度器
  | -- rcu_assign_pointer(svc->scheduler, scheduler) 将调度器结构赋予虚拟服务结构的 scheduler 成员

看一下 rr 的 init_service

复制代码
ip_vs_rr_init_svc
  | -- svc->sched_data = &svc->destinations;      将 rs 链表赋予 svc 的调度器应用 data,svc->sched_data

看一下 wrr 的 init_service

复制代码
static int ip_vs_wrr_init_svc(struct ip_vs_service *svc)
{
	struct ip_vs_wrr_mark *mark;

	mark = kmalloc(sizeof(struct ip_vs_wrr_mark), GFP_KERNEL);
	if (mark == NULL)
		return -ENOMEM;

	mark->cl = list_entry(&svc->destinations, struct ip_vs_dest, n_list);       // 后端链表
	mark->di = ip_vs_wrr_gcd_weight(svc);                                                  // 计算最大公约数的方法,即权重步长
	mark->mw = ip_vs_wrr_max_weight(svc) - (mark->di - 1);                     // 最大权重计算方法
	mark->cw = mark->mw;                                                                           // 当前权重
	svc->sched_data = mark;

	return 0;
}

ip_vs_schedule 结构体

接着看 ip_vs_rr_scheduler

其中的 .schedule 即调度方法,如 rr 的 ip_vs_rr_schedule

复制代码
static struct ip_vs_dest *
ip_vs_rr_schedule(struct ip_vs_service *svc, const struct sk_buff *skb,
		  struct ip_vs_iphdr *iph)
{
	struct list_head *p;
	struct ip_vs_dest *dest, *last;
	int pass = 0;

	IP_VS_DBG(6, "%s(): Scheduling...\n", __func__);

	spin_lock_bh(&svc->sched_lock);
	p = (struct list_head *) svc->sched_data;
	last = dest = list_entry(p, struct ip_vs_dest, n_list);

	do {
		list_for_each_entry_continue_rcu(dest,
						 &svc->destinations,
						 n_list) {
			if (!(dest->flags & IP_VS_DEST_F_OVERLOAD) &&
			    atomic_read(&dest->weight) > 0)
				/* HIT */
				goto out;
			if (dest == last)
				goto stop;
		}
		pass++;
		/* Previous dest could be unlinked, do not loop forever.
		 * If we stay at head there is no need for 2nd pass.
		 */
	} while (pass < 2 && p != &svc->destinations);

stop:
	spin_unlock_bh(&svc->sched_lock);
	ip_vs_scheduler_err(svc, "no destination available");
	return NULL;

  out:
	svc->sched_data = &dest->n_list;
	spin_unlock_bh(&svc->sched_lock);
	IP_VS_DBG_BUF(6, "RR: server %s:%u "
		      "activeconns %d refcnt %d weight %d\n",
		      IP_VS_DBG_ADDR(dest->af, &dest->addr), ntohs(dest->port),
		      atomic_read(&dest->activeconns),
		      refcount_read(&dest->refcnt), atomic_read(&dest->weight));

	return dest;
}

可以看到轮询的调度算法,svc->sched_data 是当前链表中后端的指针,当调度时,将之前的后端赋值给 last,然后循环链表给 dst,如果循环到的 dst 可用,即 goto out,设置 sched_data 为当前调度到的后端的指针,然后返回当前后端。

其中的 .add_dest 和 .del_dest 即增加和删除 后端的操作,像 rr 算法添加 后端时不需要调整,但删除时需要 ip_vs_rr_del_dest。

复制代码
static int ip_vs_rr_del_dest(struct ip_vs_service *svc, struct ip_vs_dest *dest)
{
	struct list_head *p;

	spin_lock_bh(&svc->sched_lock);
	p = (struct list_head *) svc->sched_data;
	/* dest is already unlinked, so p->prev is not valid but
	 * p->next is valid, use it to reach previous entry.
	 */
	if (p == &dest->n_list)
		svc->sched_data = p->next->prev;
	spin_unlock_bh(&svc->sched_lock);
	return 0;
}

在删除的 后端是当前调度的后端时的情况,这个时候将当前调度的后端改为当前节点在链表的前一个,这样后续调度时也能正常调度到当前节点的下一个。
像 wrr 添加 dst,删除 dst,更新 dst,都需要 ip_vs_wrr_dest_changed,这是由于权重变更后,有可能最大权重,最大公约数都会变化,需要更新整个 svc->sched_data。由此看出,后端的变化会即时更新调度算法。

PE

持久化引擎,当前 ip_vs 只有一种 sip 即源 ip 策略

复制代码
	.ct_match =		ip_vs_sip_ct_match,

static bool ip_vs_sip_ct_match(const struct ip_vs_conn_param *p,
				  struct ip_vs_conn *ct)

{
	bool ret = false;

	if (ct->af == p->af &&
	    ip_vs_addr_equal(p->af, p->caddr, &ct->caddr) &&
	    /* protocol should only be IPPROTO_IP if
	     * d_addr is a fwmark */
	    ip_vs_addr_equal(p->protocol == IPPROTO_IP ? AF_UNSPEC : p->af,
			     p->vaddr, &ct->vaddr) &&
	    ct->vport == p->vport &&
	    ct->flags & IP_VS_CONN_F_TEMPLATE &&
	    ct->protocol == p->protocol &&
	    ct->pe_data && ct->pe_data_len == p->pe_data_len &&
	    !memcmp(ct->pe_data, p->pe_data, p->pe_data_len))
		ret = true;

	IP_VS_DBG_BUF(9, "SIP template match %s %s->%s:%d %s\n",
		      ip_vs_proto_name(p->protocol),
		      IP_VS_DEBUG_CALLID(p->pe_data, p->pe_data_len),
		      IP_VS_DBG_ADDR(p->af, p->vaddr), ntohs(p->vport),
		      ret ? "hit" : "not hit");

	return ret;
}

在 Kubernetes 中,service 的 sessionAffinity: ClientIP 利用了该特性,在 timeout 时间内,同一个 源 ip 的访问会调度到同一个 后端。

相关推荐
疯狂打码的少年5 分钟前
【计算机网络】TCP/IP协议族(四层体系结构,与OSI对比)
网络·笔记·tcp/ip·计算机网络
企鹅的蚂蚁16 分钟前
LubanCat RK3588 实时 Linux 开发(六):启用 gs_usb 驱动并完成 USB-CAN 收发
linux·rk3588·linux驱动·socketcan·gs_usb·usb-can
2601_9622998827 分钟前
Linux执行Python脚本方法
linux·python·脚本·解释器·shebang
snow@li35 分钟前
服务器运维:Linux命令速查手册(全景梳理)
linux·服务器·网络
handler0136 分钟前
【Linux】线程与虚拟内存的底层世界
linux·运维·服务器·c++·线程·虚拟地址空间·共享内存
布莱克60542 分钟前
TCP 三次握手与四次挥手详解
服务器·网络·tcp/ip·计算机网络·网络编程
新时代牛马1 小时前
实时 Linux:PREEMPT_RT、延迟来源与观测
linux·运维·服务器
微露清风1 小时前
# Linux 进程全景:从 PCB 到 O(1) 调度队列
linux·服务器
H_oRIZoN_1 小时前
Linux入门DAY38(手写 HTTP 客户端抓取天气 + 简易 HTTP 服务器实现元器件 Web 系统)
linux·linux应用编程
anxiao_m2 小时前
2026企业AI数字孪生选型攻略,不同场景对应不同解决方案
大数据·网络·数据库