探讨下RouterOS PCC的工作原理与应用延伸
在讲PCC之前,我先讲讲RouterOS的负载均衡历史,我最早接触的RouterOS版本是2003年看到的v2.7.14,这个版本是有注册机的,2.7.14破解版本在当时很泛滥。那时候RouterOS就能实现WLAN无线传输、防火墙、流控、pppoe认证,web热点认证和策略路由等等,在2003年功能已经非常多了。
当时的多线接入,主要是用在小区,网吧等场景下,如果是不同运营商就配置运营商静态路由表,相同运营商就配置源地址策略路由。最开始相同运营商的网吧还用过ECMP-Equal Cost Multi-Path Routing(等价多路径),但ECMP在nat下是有问题的,因为ECMP会每10分钟重新生成一次路径,到了v2.9版本出现了nth的功能,初步解决多线负载均衡问题,但Nth还是存在bug。
Nth
Nth两个参数“every”和“packet”,Nth通过计数器方式处理。当规则收到数据包,该规则的计数器会增加1,如果计数器匹配值“every”与packet值相等,计数器将重新设置为0,重新计数。 使用Nth将连续的数据包通过计数器分组,比如可以将连续的数据包分配为多个组,重新排列连接序列。
nth匹配特定的第N次收到的数据报的规则,一个计数器最多可以计数16个数据包,包含两个属性:
- Every – 匹配每every数据报,同时指定Counter(计数器值)
- Packet – 匹配给定的包,如:Nth=3,1(every=3,packet=1),每3个数据包,取第1个
下图是一个三线的Nth分流工作原理:
上图,可以看到数据流被Nth分为3个计数器,并根据Packet重新排列数据流的队列。建立一个数组形式的容器,按照数组序列来分发数据包,计数器满3后,将重新设置为0,重新计数,这样按照顺序3个一组走到指定3条外网线路出口。
Nth没有采用哈希算法,仅仅是按照序列分组排队,会出现源目标IP相同会话的数据包无法走同一条线路,nat后多线的出口IP是不一样的,这样会出现要求对源IP校验的站点无法访问的情况,例如,淘宝验证,银行网银验证等等,线路越多出错的概率越大,因为序列分组后,指定的IP数据包走之前线路的几率越小,当时通过指定443端口走一条固定线路来解决,那时https网站还不流行,勉强能解决。
估计在很多用户的强烈要求下MikroTik在v3.24(2009年发布)开发了PCC功能。
IP Hash取余算法
在说PCC之前,先讨论下IP Hash,IP_hash是根据用户请求的IP地址和端口,映射成哈希值,并做取余算法,根据余数进行的策略分配。使用ip_hash这种负载均衡以后,可以保证用户相同请求的IP会话保证走同一条线路。Ip_hash算法在网络通信方面应用非常广泛 ,很多CDN缓存技术通过对url进行哈希算法,完成内容访问和存储的负载均衡。
上图是3条线路的负载均衡,根据源和目标IP做哈希取余算法,3作为分母。如果4线就用4作为分母,以此类推。可以看到,同一组源和目标IP地址请求,哈希值是一样的,取余数的结果也会是一样,因此相同的源和目标IP会话始终会走相同线路。
PCC负载均衡
Hash算法说清楚了,来看看RouterOS的PCC(per-connection-classifier),每次连接分类器。PCC从IP报头中获取选定的字段,并通过哈希算法将选定的字段转换为32位值。然后将此值除以指定的分母,将余数与指定的余数进行比较,如果相等,则标记该数据包。PCC支持从数据包头选定的字段包括src-address、dst-address、src-port、dst-port,进行哈希算法操作。
明白哈希算法后,我们举例下面的一个实例,这是有3条线路的PCC连接标记规则:
/ip firewall mangle add chain=prerouting action=mark-connection \ new-connection-mark=1st_conn per-connection-classifier=both-addresses:3/0 /ip firewall mangle add chain=prerouting action=mark-connection \ new-connection-mark=2nd_conn per-connection-classifier= both-addresses:3/1 /ip firewall mangle add chain=prerouting action=mark-connection \ new-connection-mark=3rd_conn per-connection-classifier= both-addresses:3/2
依次看看三条规则
- 第一条:取源和目标IP地址字段(both-addresses)计算哈希值取余,得到的哈希值除以3,余数如果为0,即“both-addresses:3/0”action操作标记为1st_conn。
- 第二条:哈希值除以3 ,余数如果为1,即“both-addresses:3/1”,action标记为2nd_conn,
- 第三条:哈希值除以3,余数如果为2,即“both-addresses:3/2”,action标记为3rd_conn。
以上标记连接规则只是PCC负载均衡的一部分,还要做mangle的路由标记,和ip route的策略,这里就省略了。完整的配置可以参考
PCC可用于哈希的字段和字段组合包括:both-addresses|both-ports|dst-address-and-port| src-address|src-port|both-addresses-and-ports|dst-address|dst-port|src-address-and-port
Nginx负载均衡与PCC
Nginx在做http服务器的时候,做负载均衡的场景,可以通过IP哈希算法提高后端http服务器的负载均衡访问
以下是nginx的IP哈希的配置
upstream backend{ ip_hash; server 192.168.88.2:80 ; server 192.168.88.3:80 ; server 192.168.88.4:80 ; }
Nginx还能实现url的哈希算法,这里就不多说了,我提到nginx的负载均衡只是想延申出RouterOS的PCC,之前我们做的PCC是从内到外的多线路出口负载均衡,反过来PCC也可实现从外到内的IP哈希负载均衡,比如内网多服务器的端口映射的负载均衡调度器,PCC还能实现权重比例的调度。
例如:我们需要将80端口同时映射给内网的三台服务器192.168.88.10、192.168.88.20和192.168.88.30,并实现三台服务器端口映射的负载均衡
配置为:在mangle中标记到外网接口IP是192.88.88.2的80端口,做PCC连接标记,分为3组
/ip firewall mangle add action=mark-connection chain=prerouting dst-address=192.88.88.2 dst-port=\ 80 new-connection-mark=pcc1 passthrough=yes per-connection-classifier=\ src-address:3/0 protocol=tcp add action=mark-connection chain=prerouting dst-address=192.88.88.2 dst-port=\ 80 new-connection-mark=pcc2 passthrough=yes per-connection-classifier=\ src-address:3/1 protocol=tcp add action=mark-connection chain=prerouting dst-address=192.88.88.2 dst-port=\ 80 new-connection-mark=pcc2 passthrough=yes per-connection-classifier=\ src-address:3/2 protocol=tcp
在nat做80端口映射到内网的三台服务器192.168.88.10和192.168.88.20,连接标记分别标记为pcc1、pcc2和pcc3:
/ip firewall nat add action=dst-nat chain=dstnat connection-mark=pcc1 dst-address= 192.88.88.2 dst-port=80 log=yes log-prefix=pcc1 protocol=tcp \ to-addresses=192.168.88.10 to-ports=80 add action=dst-nat chain=dstnat comment=test1 connection-mark=pcc2 dst-address=\ 192.88.88.2 dst-port=80 log=yes log-prefix=pcc2 protocol=tcp \ to-addresses=192.168.88.20 to-ports=80 add action=dst-nat chain=dstnat comment=test1 connection-mark=pcc2 dst-address=\ 192.88.88.2 dst-port=80 log=yes log-prefix=pcc3 protocol=tcp \ to-addresses=192.168.88.30 to-ports=80
这个实例我们调整下,如果192.168.88.30下线,只有2台,192.168.88.20的服务器处理能力更强劲,我们希望3份流量,有两份都给192.168.88.20,我们的nat规则可以修改为
/ip firewall nat add action=dst-nat chain=dstnat connection-mark=pcc1 dst-address= 192.88.88.2 dst-port=80 log=yes log-prefix=pcc1 protocol=tcp \ to-addresses=192.168.88.10 to-ports=80 add action=dst-nat chain=dstnat comment=test1 connection-mark=pcc2 dst-address=\ 192.88.88.2 dst-port=80 log=yes log-prefix=pcc2 protocol=tcp \ to-addresses=192.168.88.20 to-ports=80 add action=dst-nat chain=dstnat comment=test1 connection-mark=pcc2 dst-address=\ 192.88.88.2 dst-port=80 log=yes log-prefix=pcc3 protocol=tcp \ to-addresses=192.168.88.20 to-ports=80
如果后端服务器故障,前端的RouterOS可以通过脚本ping监控判断,并调整nat映射规则,避开故障服务器。RouterOS利用PCC也可以对集群的应用提供负载均衡,上面实例通过nat方式,也算代理访问,让我又想起了haproxy。通过非nat的路由调度也是可以通过PCC实现。
RouterOS的PCC功能是不可能去代替nginx或者haproxy,nginx和haproxy是非常优秀的软件,能实现四层和七层的负载均衡,性能上来说RouterOS和他们差的很远,也只能实现四层的负载均衡,只是想告诉大家RouterOS在许多应用场景中更多的思路,感觉我把RouterOS科技树点歪了!
其实Nth和早期的ECMP有着类似的原理,基于数据包的负载均衡,在1997 年 11 月 Linux内核v2.1.68 初步支持 IPv4 ECMP多路径路由。包括对加权 ECMP 的支持,下一跳以伪随机方式选择网关,Nth只是顺序选择网关,但他们都是基于数据包分流。
MikroTik在2009年发布的v3.24,支持PCC负载均衡,基于会话连接的Hash算法,直到2016 年 1 月Linux内核 v4.4 IPv4 的多路径路由切换到基于会话哈希的路径选择,也意味着Linux的ECMP由于PCC相同的会话负载均衡功能,但PCC是基于iptables的负载均衡,非常消耗CPU资源,非常遗憾的是RouterOS直到v7才更换到了Linux 5.x的内核(2020年发布),RouterOS v5是 Linux 2.6内核,RouterOS v6发布在2013年,Linux内核是v3.3.5,也就是v6到v7用了7年时间。