vailable\n");? ? ? ? ? return NULL;? ? ? }? ? ? ? return dest;? }? ? static struct ip_vs_scheduler ip_vs_offh_scheduler =? {? ? ? .name =? ? ? ? "offh",? ? ? .refcnt =? ? ? ATOMIC_INIT(0),? ? ? .module =? ? ? THIS_MODULE,? ? ? .n_list? =? ? ? LIST_HEAD_INIT(ip_vs_offh_scheduler.n_list),? ? ? .init_service =? ? ip_vs_offh_init_svc,? ? ? .done_service =? ? ip_vs_offh_done_svc,? ? ? .update_service =? ip_vs_offh_update_svc,? ? ? .schedule =? ? ip_vs_offh_schedule,? };? ? static ssize_t ipvs_sch_offset_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)? {? ? ? int ret = 0;? ? ? ret = sprintf(buf, "offset:%u;offlen:%u\n", offset, offlen);? ? ? return ret;? }? ? /*? ?* 设置offset/offset length? ?* echo offset:$value1? offlen:$value2 >/proc/net/ipvs_sch_offset? ?*/? static int ipvs_sch_offset_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)? {? ? ? int ret = count;? ? ? char *p = buf, *pstart;? ? ? if ((p = strstr(p, "offset:")) == NULL) {? ? ? ? ? ret = -EINVAL;? ? ? ? ? goto out;? ? ? }? ? ? p += strlen("offset:");? ? ? pstart = p;? ? ? if ((p = strstr(p, " ")) == NULL) {? ? ? ? ? ret = -EINVAL;? ? ? ? ? goto out;? ? ? }? ? ? p[0] = 0;? ? ? offset = skip_atoi(&pstart);? ? ? if (offset == 0 && strcmp(pstart, "0")) {? ? ? ? ? ret = -EINVAL;? ? ? ? ? goto out;? ? ? }? ? ? p += strlen(";");? ? ? if ((p = strstr(p, "offlen:")) == NULL) {? ? ? ? ? ret = -EINVAL;? ? ? ? ? goto out;? ? ? }? ? ? p? += strlen("offlen:");? ? ? pstart = p;? ? ? offlen = skip_atoi(&pstart);? ? ? if (offlen == 0 && strcmp(pstart, "0")) {? ? ? ? ? ret = -EINVAL;? ? ? ? ? goto out;? ? ? }? out:? ? ? return ret;? }? ? /*? ?* 由于不想修改用户态的配置接口,还是觉得procfs这种方式比较靠普? ?**/? static const struct file_operations ipvs_sch_offset_file_ops = {? ? ? .owner? ? ? ? ? = THIS_MODULE,? ? ? .read? ? ? ? ? = ipvs_sch_offset_read,? ? ? .write? ? ? ? ? = ipvs_sch_offset_write,? };? ? struct net *net = &init_net;? static int __init ip_vs_offh_init(void)? {? ? ? int ret = -1;? ? ? if (!proc_create("ipvs_sch_offset", 0644, net->proc_net, &ipvs_sch_offset_file_ops)) {? ? ? ? ? printk("OFFH: create proc entry failed\n");? ? ? ? ? goto out;? ? ? }? ? ? return register_ip_vs_scheduler(&ip_vs_offh_scheduler);? out:? ? ? return ret;? }? ? static void __exit ip_vs_offh_cleanup(void)? {? ? ? remove_proc_entry("ipvs_sch_offset", net->proc_net);? ? ? unregister_ip_vs_scheduler(&ip_vs_offh_scheduler);? }? ? ? module_init(ip_vs_offh_init);? module_exit(ip_vs_offh_cleanup);? MODULE_LICENSE("GPL");?
实际上,很多高大上的负载均衡实现都不是基于内核协议栈的,它们要么是直接用硬卡来做,要么是用户态协议栈,所以本文的原则也是可以用到那些方面的,只不过,我所能为力的并且简单的只有Linux IPVS,毕竟先把代码跑起来要比长篇大论好的多,起码我是这么认为的。
4.问题在哪里-连接缓存
我认为IPVS机制该改了,同时我觉得nf_conntrack也该改了。
我们知道,在IPVS中,可能只有一个流的第一个数据包才会去调用“特定协议”的conn_schedule回调,选出一个destination,即real server之后,这些信息就会被保存在“特定协议‘的conn缓存中。如果你看一下这个所谓的“特定协议”,就会发现它事实上是“第四层协议”,即传输层协议,TCP或者UDP,而在这一层,很显然,一个连接就是一个5元组。那么,即便我针对第一个数据包,即一个流的首包选择了一个real server,并将其存入了conn缓存,那么该流的客户端在IP地址变化了之后,显然conn缓存中找不到了,那么就会自动进入conn_schedule,由于使用固定偏移的paylaod进行schedule,那么肯定还是原来的那个real server被选择,此时会在conn缓存中增加一条新的条目用于以后的匹配,老的那条conn缓存没有用了,等待过期,只要客户端不改变IP地址且新的这个conn缓存项不过期,这个缓存将会一直命中,一旦客户端改变了IP地址,一切重新开始。可见,这是一个自动且正确的过程。但是,最好有一个针对旧五元组的删除通知机制,而不是等待它自己过期。
如果等待它自己过期,那么试想一种超时时间很久的情况。客户端A五元组为tuple1使用sessionID1匹配到了一个real server1,设置了conn缓存conn1,过了一些时间,客户端A更换了IP地址,此时理所当然地,它不会再匹配到conn1,缓存不命中,依靠不变的sessionID1它在conn_schedule中选择了同样的real server1,设置了新的conn2缓存项,然后conn1就变成僵尸了,等待超时删除。过了很久,客户端2携带接管了客户端1的老的IP地址和UDP端口,访问了同样的UDP服务, |