MASQUERADE target在负载均衡中引出的问题

linux中如何配置路由负载均衡是一个古老的问题,可是至今仍然没有什么好的解决方案,我指的解决方案是配置意义上的,如果可以触动内核源代码的话,补丁倒是有好几个,不过很多的linux服务器是不允许给内核打补丁的,成本太高了,要停机,编译同版本内核,如果不同版本内核还要编译所有驱动,测试应用兼容性等等。因此如果有人让你配置负载均衡了,那么你能做到的也就是仅仅“看起来像那么回事”罢了,所谓看起来像的意义就是在用户空间实现一个脚本,每隔一段时间刷新内核的路由表,而路由表中配置多条到同一目的地的路由,如此一来在每次刷新完路由表后,再有数据包过来的时候就会重新查找路由了,结果就可能查到不同的路由,从而实现负载均衡(看起来像),但是如果你要是配置了MASQUERADE target的NAT,你可要当心了,会有问题的,看一下内核源码就明白了:
static unsigned int masquerade_target(...)
{
...
    struct rtable *rt;
    ct = ip_conntrack_get(*pskb, &ctinfo);
    mr = targinfo;
    {
        struct flowi fl = { .nl_u = { .ip4_u =
                          { .daddr = (*pskb)->nh.iph->daddr,
                        .tos = (RT_TOS((*pskb)->nh.iph->tos) |
                            RTO_CONN),
                          } } };
        if (ip_route_output_key(&rt, &fl) != 0) { //重新查找
            return NF_DROP;
        }
        if (rt->u.dst.dev != out) {//如果在从路由表或者cache中得到out和上面调用ip_route_output_key之间在用户空间调用了ip route flush cache的话,那么由于重新查找fib得到的dst.dev就可能会变化(为了负载均衡配置多个weight值相等到同一目的地的路由,因此选择的结果可能是随机的)
            if (net_ratelimit())
                printk("MASQUERADE:" //打印出错信息
                       " Route sent us somewhere else./n");
            ip_rt_put(rt);
            return NF_DROP;
        }
    }
...
}
      实际上,即使修正上上述问题,也就是不再使用MASQUERADE target,而是使用SNAT --to-source,负载均衡结果也不会令人满意,真正的负载均衡在linux内核(排除内核patch,仅讨论mainline)中是不做的(不知道别的OS是否做到,起码IOS能做到),所谓的真正就是基于路由cache的负载均衡而不是基于路由表即fib的负载均衡,基于fib的负载均衡实际上很没有意义,毕竟内核路由模块几乎总是首先从cache中获取路况信息,即使你在fib中做了负载均衡,如果cache中已经有了转发信息的话,内核是不会再去查表的,因此负载均衡也就失去了意义,一种做法是每隔一定的时间调用一次ip route flush cache进行路由表cache的刷新,但是这么做的代价太大了,一切能从cache中获得的优点全被flush掉了,因此这种方式并不合理,它仅仅看起来实现了负载均衡,但是没有达到负载均衡的效果。
      负载均衡可以分为基于流的和基于包的,基于流的实现更合理但是有的时候必须需要基于包的,只可惜,linux内核并没有很好的实现基于包的负载均衡,我们看看这是为什么,究其原因就是因为2.6早期的内核实现了一个multipath的机制,可以认为是一个负载均衡器,但是它却是基于fib的,可以认为是基于流的,因为每一个流的第一个包从cache中或者fib中查找到路由后项后就会cache起来,然后后续的包直接就是用cache的路由信息了,以2.6.8内核为例:
ip_route_input_slow(...)
{
...
#ifdef CONFIG_IP_ROUTE_MULTIPATH  //只有在定义了该宏的时候才启用“负载均衡”
    if (res.fi->fib_nhs > 1 && fl.oif == 0)  //下一跳多于一个的情况下进行均衡
        fib_select_multipath(&fl, &res); //该函数实现了一个简陋的随机查找操作,在所有的下一跳中随机选择一个出来作为结果
#endif
...
//后续的make route cache操作,就将该项路由信息插入cache了
...
}
一旦路由信息进入了cache,那么后续的包就会使用这个cache的路由信息被转发,因此起码2.6.8的内核不支持真正的基于包的cache路由负载均衡操作,如果你非要做到看起来像负载均衡,那就调用下面的脚本吧:
#!/bin/bash
while [ 1 ]; do
    sleep $1
    ip route flush cache
done
然后测试一下性能就知道结果了,很无奈!
到了2.6.18内核,内核增加了一个预编译宏--CONFIG_IP_ROUTE_MULTIPATH_CACHED,看起来支持了cache路由的负载均衡,然后查一下代码:
static inline int multipath_select_route(const struct flowi *flp,
                     struct rtable *rth,
                     struct rtable **rp)
{
#ifdef CONFIG_IP_ROUTE_MULTIPATH_CACHED
    struct ip_mp_alg_ops *ops = ip_mp_alg_table[rth->rt_multipath_alg];
    if (ops && (rth->u.dst.flags & DST_BALANCED)) {
        ops->mp_alg_select_route(flp, rth, rp);  //以一个回调函数的方式实现,这样就可以策略化地实现选择多个路由中的一个了,可以是基于当前负载的,也可以是基于weight的,还可以是随机的
        return 1;
    }
#endif
    return 0;
}
看起来不错,但是可以看到,只有在__ip_route_output_key的时候才会调用这个函数,如果linux作为一个路由器,那么它大多数是做forward的,此时就不能使用负载均衡了,可能是因为forward对速度要求很高,而查找多路径操作相对耗时,并且这种需求也不是必须的,故而在2.6.25内核中它就不见了踪影,因此,linux内核最终还是没有将cache路由的负载均衡并入内核的mainline而只能通过补丁来实现,当前有一个2.4内核的补丁,实现得很差劲,其思想几乎和用户空间每间隔一段时间flush掉路由cache一样,只不过它只是flush掉相关的cache而不是将cache全部flush掉,代码如下:
#ifdef CONFIG_IP_ROUTE_MULTIPATH
-    if (res.fi->fib_nhs > 1 && key.oif == 0)
+    if (res.fi->fib_nhs > 1 && key.oif == 0) {
         fib_select_multipath(&key, &res);
+        if (res.fi->fib_flags&RTM_F_EQUALIZE)
+            flags |= RTCF_EQUALIZE;  //打上标志,待日后判断时候清除cache时使用
+    }
ip_route_input:
+    if (rth->rt_flags&RTCF_EQUALIZE) {
+        *rthp = rth->u.rt_next;
+        rth->u.rt_next = NULL;
+        rt_free(rth);
+        break;  //这里就是强制执行路径进入后面的slow函数
+    }
ouput的时候同样进行上面类似input的patch,如此看来,这个补丁为了负载均衡几乎放弃了内核对路由cache带来的好处,确实不怎么样,起码也要启动一个定时器,然后每间隔一段时间再清除掉该路由cache,而不是每次都清除掉,基于hash的cache查找要比查询fib快得多。由此,linux几乎没有拿出一个像样的基于包的负载均衡方案,这难道说明linux不好吗?不是的,因为基于包的负载均衡会带来很多的问题,我们看一下会产生什么问题。基于包的负载均衡在配置nat情况下会对基于流的连接造成影响,甚至致命的影响,负载均衡最直观的感觉就是同一个流的数据分散到不同的出口,而不同的出口会有不同的ip地址,在这些出口做SNAT就会产生问题,因为对于同一个流来讲最终的目的地只能用一个源地址而不是两个或者多个,因此连接可能会莫名的断开。也不是都会出问题,像udp这类连接可能就没有影响,只要最终目的地不在乎源地址就可以,但是谁能保证目的地如何配置呢?另外一个问题就是公网中很多网络机制都是基于流的,如果在负载均衡器的N多个出口做了nat,那么这些路径的汇聚处就会把同一个流当成不同的流,不但策略配置复杂化了,而且还会出现重复误判等问题。因此多路径路由真的会带来很多问题,它能解决的问题敌不过为了实现它而付出的成本。虽然基于包的负载均衡不值得实现,那么基于流的呢?基于流的负载均衡实际上linux已经实现了,这就是multipath,也就是2.6.8内核中就已经存在的那种,一个流的源IP,目的IP都是一样的,因此该流使用一个路由cache项,而另一个流由于和第一个流的源不同,因此只能查找fib,得到对应同一个目的地址的另一个路由表项,但是如果同一台机器发起了到同一个目的地的多个流,linux仍然不会做负载均衡,linux会认为那是一个流,毕竟在IP层是认不到传输层协议和端口的。因此linux实现的基于流的负载均衡仅仅是一种粗粒度的基于主机的负载均衡,要想实现真的基于流的负载均衡,还要使用ip_conntrack模块,但是这样就涉及到了四层信息,比较耗时.最终的结论就是linux的负载均衡几乎没有实现.
      可是即使是基于包的负载均衡,在有一种情况还是很有意义的,不但不会带来问题,而且真的可以提高性能,那就是隧道中的负载均衡,由于隧道的两端并不是数据的终点,隧道仅仅是一条路而已,所以也就没有基于流的连接如tcp对同一源的那种苛刻要求了,隧道只要求进入的是什么,出来的也是什么,就此就够了,隧道上的基于包的负载均衡的应用之所以可以不必考虑单一源问题是因为虽然从负载均衡的地方分开成多条路径(隧道),但是这些路由又无一缺漏地聚合于每条路径(隧道)的终点,我们只是对若干隧道的流量做了负载均衡并且可能由于出口不同而改变了源地址(也就是将数据分担到不同的隧道中),并没有触及隧道中传输的数据。一条隧道不能建太宽,但是却可以修多条。

阅读更多
版权声明:本文为博主原创,无版权,未经博主允许可以随意转载,无需注明出处,随意修改或保持可作为原创! https://blog.csdn.net/dog250/article/details/5640819
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭