Linux Route/NAT 负载均衡

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对同一源的那种苛刻要求了,隧道只要求进入的是什么,出来的也是什么,就此就够了,隧道上的基于包的负载均衡的应用之所以可以不必考虑单一源问题是因为虽然从负载均衡的地方分开成多条路径(隧道),但是这些路由又无一缺漏地聚合于每条路径(隧道)的终点,我们只是对若干隧道的流量做了负载均衡并且可能由于出口不同而改变了源地址(也就是将数据分担到不同的隧道中),并没有触及隧道中传输的数据。一条隧道不能建太宽,但是却可以修多条。



这是什么意思呢?简单来说,如果你有动态IP的连接,一个使用私有IP地址的局域网,通 过NAT (Network Address Translate)上网,现在觉得速度不够,想再加一条使到某一Internet主机的速度加倍(现在中国大陆的典型情况),对不起,你就死了这条心 吧,这是不可能的。 

  这里所说的连接包括 Ethernet,PPPoE,PPPoA,PPP,以及SLIP等等。并且,这是TCP/UDP 协议的内在机制造成的,和软/硬件无关。任何架构在 TCP/UDP 之上的应用,不论硬件还是软件,皆不能绕过此限制。 
注意,如果你的局域网使用公用IP(public IP)而不需要NAT,则不在此列。 

  如果你想知道原因或者质疑我的结论:),请继续往下读。 

  本文假设 

   ①、你已经阅读过 Linux Advanced Routing & Traffic Control HOWTO,特别是 4.2 小节的 Routing for multiple uplinks/providers,4.2.1小节Split access 和4.2.2小节 Load balancing。此文(以下按惯例简称lartc)非常清楚的说明了连接级别的TCP/UDP 负载均衡。 

  ②、为便 于理解,以下以常见的Linux 下 ADSL PPPoE 拨号上网为例,但如前所述,讨论适用于其它各种情况。用数对(IP address, port)来表示一个连结的端点。假定 

     Host     IP     Port 
   ------------------------------------- 
  local host 1 192.168.0.1  5000 
  Linux route if1 100.0.0.1 6000 
  Linux route if2 150.0.0.1 7000 
  remote host 1 200.0.0.1  80 
   ------------------------------------- 

  那么连接级别和包级别的负载均衡究竟有何不同? 

  
1、连结级别的负载均衡 

  Linux 2.4内核支持连结级别的负载均衡。为理解这个问题,这里首先简述 Linux 的静态路由机制。当一部 Linux 主机收到一个需要转发的IP包时,它首先检查Cache中是否有相关的路由信息:如果有的话,会直接使用;否则再查找路由表。Cache中一段时间不使用 的路由信息会被丢弃。如果设置了 MultiPath 路由,则选择哪一个路由是随机的,但随后的IP包都会走这条路由,直到Cache中的路由信息被丢弃。 

  比如,当LAN上的主机 local host 1 向 remote host 1 发起一次连结的时候,Linux router 在Cache中找不到有关remote host 1 的路由信息,因此转去查找路由表,当它发现有一个 MultiPath 路由的时候,会随机选择一个路由,比方说 if1,并把此路由加入Cache;下一个从 local hsot 1到 remote host 1 的 IP 包到达时,Linux router 直接在Cache 中找到路由信息然后直接转发,也就是说,所有目的地为 remote host 1 的 IP 包都会经由 if1 转发直到 Cache 中相关路由信息失效并被丢弃为止而不会经过if2,即使if2完全空闲也是如此。 

  因此,和 remote host 1 之间的通信并不会从附加的第二条连接中获益。其实际效果就是你用 Realplay 看网络电影时效果没有改善,当然心理作用除外icon_smile.gif 

  如果与此同时 local host 1 有发起一条到 remote host 2 的连接,则有可能这一次会使用 if2。其实际效果就是你用 Realplay 看网络电影时用 flashget开8个并发线程到一个FTP站下载一个 600M Redhat ISO 文件,两者不会互相影响。 

  那么,下载 ISO 文件的同时 localhost 2 发起一条到 remote host 2 的连接,会出现怎样的情况?是简单地走if1?又或别有蹊径?请读者思考。 

  2、包级别的 负载均衡 

  现实中这种一边忙一边无所事事的情况在所多有,毋庸赘述icon_smile.gif 。那么如何改善呢?很自然的想到,我们可以把同一条TCP/IP连接中的IP包同时从两条上行连接中发出,左右开弓,不就可以了 吗?实际上,我们在 PPP Multilink 就是这么干的,不过现在没有服务器的支持,要靠自己。这是Internet,因此已经有人想到这一点并且编写了内核补丁供下载,请搜索 equalize_2.4.18.patch。 

  需要特别提醒你的是,请确保该补丁的日期是 Fri Mar 22 2002,而不是有问题的 Thu Mar 21 2002 版本。另外,根据我的经验,这个 patch 也可以用于 2.4.19 内核。 

   重新编译了内核以后,你可以使用 equalize 关键字了。如果你的 LAN 使用公有 IP 地址,那么恭喜你,你现在可以在双倍速度下用 Realplay 看网络电影了。但是正所谓人生不如意事常,如果你使用的是私有 IP 地址配合以NAT上网,让我们来看看当 local host 1 对 remote host 1 发起一次连结时会发生什么。 

  local host 1 第一次从(192.168.0.1,5000)向 remote host 1 (200.0.0.1, 80) 发起一条连接,现在第一个 IP 包 (SYN 包)已经到达 Linux router。既然是第一次,Cache 中当然没有有关路由信息,Linux router 查找路由表,发现带 equalize 标记的 MultiPath 路由,因此随机决定如何转发此IP 包 -- 好,这次茫茫中命里注定从 if2 走icon_smile.gif 并且雁过留声,在 Cache 中添加一条到 remote host 1 的路由信息-- 既然 local host 1 的 IP 是私有的,NAT 起作用,转换源地址为 (150.0.0.1,7000)后发出,没问题,remote host 1 收到并向 (150.0.0.1,7000) 发回SYN+ACK 包表示接受连结。Linux router if2 收到返回的 SYN+ACK 包,早有准备,NAT 再次转换目的地址为 (192.168.0.1, 5000)。 

  第一回合。到目前为止一切 OK。 

   紧接着 local host 1 (192.168.0.1, 5000) 按规矩再向 (200.0.0.1, 80) 发送 ACK 包表示确认。此 ACK 包又来到Linux router 这个岔路口,它将走向何方呢?既然设定了 equalize,Linux 不会根据先前生成的路由表 Cache 中的记录(就是由 SYN 包生成的)来决定走法,相反,它从 Cache 中删除该记录,然后重新查找路由表。这导致又一次重新选择路由,可能是 if1,也可能是 if2。 

  如果又是 if2,很好,remote host 1 收到,握手完成,连接建立。 

  若是 if1,又当如何呢?很不幸,if1 将该包源地址经 NAT 转换为 (100.0.0.1,6000),然后丢上 Internet,一路平安到达 remote host 1。remote host 1 正在苦等从 if2 (也就是 150.0.0.1,7000) 来的 ACK 包,没想却收到一个从(100.0.0.1,6000) 来的 ACK 包,它作何打算呢?难道说,反正是 ACK 包,马马虎虎将就着用用吗?很明显,这不是个好主意。其次,即便意欲如此,remote host 1 如何判断这个 ACK 包和哪一个 SYN 包关联呢?因此,唯一合理的措施就是:忽略它,当没看见好了。 

  丢弃此来路不明的 ACK 包后,remote host 1 继续等待中......结果可想而知:不能同步,连结建立失败。这里 TCP/IP 的重传机制并不能解决问题,remote host 1 并不认为这是一个正常的 TCP 包,虽然它是一个正常的 IP 包。 
刚才提到如果走的是 if2,非常幸运,连接可以建立。同样,以后 remote host 1 会收到所有数据包,不论是从 if1还是 if2 来的,只是从 if1 来的 TCP 包一概按例办理,全部丢弃。好在local host 1 会重传这些被丢弃的包,总有机会走到 if2 而被收到,因此连结还可以勉强维持,只是比较慢而已;当然,在某种极端情况下,也可能中途断掉。 

  所以,equalize 负载均衡和 NAT 在一起工作,可能会导致建立连结失败或者较缓慢的连结。换句话说,1+1 < 1。 

  最后有一个好消息, 不是大好,是小好:ICMP 协议在此种情况下可以工作,也许你可以试着用它干点什么。当然这也意味着一个使用动态IP对你发动 ICMP 攻击的黑客有双倍的可用带宽icon_smile.gif 

  注1:2.4内核中 MultiPath 设定的具体的位置在 Networking options -> TCP/IP networking -> IP: advanced router -> IP: equal cost multipath,.config 文件中的对应行为 CONFIG_IP_ROUTE_MULTIPATH 

  注 2:iproute2 的手册明确指出,仅当内核打过补丁的情况下 equalize 关键字才能工作,原文"equalize only works if the kernel is patched." iproute2-ss010824, p26. 

  注3:本文所说的随机并 不是真正意义的随机,但我们假定考察真正随机的情况。 

  注4:对于如何处理所谓"来路不明的 ACK 包",可能是实现相关的,还请了解的朋友说一说。 

  附录A 

  关于 lartc 中 split access 和 load-balancing 设置的一点说明: 

  在 load-balancing 小节中提到"如果你已经如前所述设置了 split access 的话 load-balancing 真的不是太难"。而在 split access 小节中就给出了一堆命令,T1 T2 P1 P2 IF1 IF2 什么的。很多人不明白这些命令到底有何作用。其实这些命令是说,到 P1_NET 去的走 if1,到 P2_NET 去的走 if2,大家各行其道。那么,我到 P1_NET 去的走if2,到 P2_NET 去的走 if1,可不可以呢?理论上也是可以的,但是这里有个问题。如果 P1_NET 和 P2_NET 分属两家不同的 ISP,则从 if2 到 P1_NET 就要走远路经过交换中心,而这可能会造成瓶颈。 

  于从 P1_NET 和 P2_NET 回来的包,已经超出了我们的控制范围,只能等它到 IF1或 IF2。如果出于某种原因路由不正常,没有收到回音,那我们能做的也只是承认失败而已。 

   因此这段命令想要防止舍近求远,如此而已。 

  外这段命令似乎有点问题。我已经在lartc邮件列表中请求帮助了,希望能尽快有一个 满意的答复。 

  在搞清楚这一点后,如果读者的 P1_NET 和 P2_NET 和我一样其实是同一家 ISP 的网络,这段准备功夫是完全可以省去的。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值