OpenVPN多处理之-netns容器与iptables CLUSTER

假设还是沉湎于之前的战果以及强加的感叹,不要冥想,将其升华。

1.C还是脚本

以前,我用bash组织了复杂的iptables,ip rule等逻辑来配合OpenVPN,将其应用于差点儿全部能够想象得到的复杂网络场景中,实现网间VPN隧道。后来我发现玩大了,要不是当时留下一份文档,我自己差点儿已经无法通过这些关系错综复杂的bash脚本还原当时的思路,一切太复杂了。
       我想重构它们,同一时候将其改造成“能经得起继续复杂化”的系统,因此我不得不想办法将这些关系理顺。是的,bash太复杂了,那么改用什么好呢?用Python?或者PHP?再或者Java?或者直接用C?总之,不再用bash了。
       脚本的长处在于,你能够随时实验新想法,所见即所得,不用携带不论什么装备,仅仅要有个终端就能做事。缺点在于,正是由于上述那些随时编写随时执行的任意性,bash写出的东西非常easy发散开来以后便无法收敛,Python声称自己支持OO,支持复杂数据结构组织与easy,看上去会比bash好些,可是那依赖程序猿拥有良好的设计与编码能力,让一个菜鸟比方我写Python,你能想象他能写得多么恶心吗?能用C写出好软件的不算高手,用脚本写出好软件的才叫猛士。新手写bash脚本,一般都会搞得关系错综复杂,毕竟bash脚本中并没有什么好用的容器能够容纳数据结构,所以,bash天生就是发散了。
       C的缺点正是脚本的长处,那就是你不得不随身携带一些重量级装备,比方gcc,gnu make,gdb,strace...而这些一般在运维环境中是没有的,所以你要拥有一台时刻能够派上用场的开发用虚拟机,并且随身携带,假设没有,那就非常悲慘了。上周的某天,我就中午从公司回家去调试C程序,仅仅由于我的笔记本没有带到公司。在云上架一台开发机是好主意,可是那须要你有財力支持以及你时刻都要能够接入互联网。C的长处在于它是内敛的,对于刚開始学习的人而言,一般都会将全部逻辑写到一个文件里,也不善于调用外部的库函数或者脚本,假设说bash引诱你利用其他小命令组织大程序的话,那么C就是阻止这一切的发生,仅仅有高手才倾向于写小的C程序,然后通过动态库或者脚本将其组织起来,对于新手而言,一般都是倾向于写完备的大程序,即全部的逻辑集中在一起。
       我写了蜘蛛网般的bash代码,说明我是一个新手,为了将逻辑略微集中一下,作为新手,写出来的C应该是超级内敛的,非常可能全部的用户态逻辑都在一个so中,全部的内核逻辑都在一个ko中...然而,这是大忌讳,怎么办?简单,那就是最大限度利用系统本身提供的功能而不是自己用bash组织逻辑。

2.一台机器当多台用

试想,在一台机器上启用一个OpenVPN服务端是一件多么简单的事!
可是,为何这样不行,为何我非要费劲地折腾什么多实例多进程,由于OpenVPN本身不支持这些。上一篇系列文章中,我已经在OpenVPN内部使其支持了多线程,可是假设我没有改动OpenVPN代码的能力呢?假设换另外一个人来做这件事呢。我决定又一次给出一个方案。
       既然一台机器启动一个OpenVPN服务端进程超级简单,那么假设有N台机器的话,每台机器上启1个OpenVPN服务端也就是个体力活。在实际上没有N台机器的前提下,换个思路,怎样将一台机器当N台机器使用。
       Linux的netns完美攻克了这个问题:
ip netns add vpn1
ip netns add vpn2

这样就加入?了两个命名空间。接下来就是要为这两个命名空间加入?网卡,假设我的机器上仅仅有一块网卡,给了vpn1,它就被vpn1独占了,外面以及vpn2就都看不到了,显然不行,我又不可能在机器上插物理网卡,此时veth虚拟网卡帮了忙。
ip link add veth0_vpn1 type veth peer name veth_vpn1
ip link add veth0_vpn2 type veth peer name veth_vpn2
随后将veth0_vpn1给了vpn1,将veth0_vpn2给了vpn2
ip link set veth0_vpn1 netns vpn1
ip link set veth0_vpn2 netns vpn2
然后将veth_vpn1,veth_vpn2,eth0桥接在一起:
brctl addbr br0
brctl addif br0 eth0 veth_vpn1 veth_vpn2
好了,接下来就是在这两个命名空间执行OpenVPN了:
ip netns exec vpn1 ifconfig veth0_vpn1 192.168.1.1/24
ip netns exec vpn2 ifconfig veth0_vpn2 192.168.1.1/24
ip netns exec vpn1 openvpn --config /home/zy/vpn/server.conf
ip netns exec vpn2 openvpn --config /home/zy/vpn/server.conf
两个openvpn进程读取同样的配置文件,可是此时它们的网络已经是隔离的了。能够看出,两个命名空间的veth网卡地址全然一样,其实,对于网络配置而言,vpn1和vpn2是全然同样的。由于此时两个命名空间的veth的peer已经和eth0桥接在一起了,接下来的问题是怎样将数据包分发到两个命名空间,此时iptables的CLUSTER target来帮忙了。给出结论之前,眼下的系统原理图例如以下:



3.构建分布式Cluster

如今的问题就是怎样把数据包分发到这两个(实际环境是多个,视CPU数量而定)命名空间。难道要在Bridge这个层次再搞一个相似LVS之类的东西吗?思路是对的,可是我不会那么做,由于那样做还不如不搞命名空间直接在LVS上跑多个服务呢。其实,之所以搞命名空间,就是由于iptables提供了一种分布式的集群负载均衡算法模型,将集中式的决定“由哪个节点处理数据包”这个问题转化为分布式的“这个数据包是否由我来处理”。也就是说,计算分布化了,不再处于一个点上,详细的思想能够參见我的还有一篇文章以及早期全广播以太网的寻址思想。
       如今的问题就是怎样实现将数据包广播到全部的这些命名空间中,对于上图为例,不论什么广播到veth_vpn1和veth_vpn2这两个桥接端口中。iptables的CLUSTER target支持将veth0_vpn1和veth0_vpn2这两个网卡的MAC设置成同一个“组播MAC地址”,而我的工作就是在数据包从eth0进入后,将其目标MAC地址转换为那个组播地址,接下来在网桥forward数据的时候,看到目标是组播,便从veth_vpn1和veth_vpn2两个口都发出去了。这个难道不能通过ebtables的dnat来做吗?
       要问怎样来设置iptables的CLUSTER,也非常easy,两个命名空间除了local-node不一样之外,其余的都一样(这俩命名空间实际上相当于两台机器):
iptables -A INPUT -p udp --dport 1194 -j CLUSTERIP --new --hashmode sourceip --clustermac 01:00:5e:00:00:20 --total-nodes 2 --local-node 1
iptables -A INPUT -p udp --dport 1194 -j CLUSTERIP --new --hashmode sourceip --clustermac 01:00:5e:00:00:20 --total-nodes 2 --local-node 2
CLUSTER target是怎么将hash值映射到node-num的不重要,重要的是它确实能够将来自一个流的hash值映射到1~total-nodes中的一个,并且仅映射到那一个,这样的固定的映射方式保证了一个数据流始终被同一个命名空间处理。如今的图演示样例如以下:



4.知识的广度与深度

懂多少知识不重要,重要的是这些知识能用来干什么。其实,我觉得两类人是不同的,拥有构建能力的人不须要拥有多少知识的细节,属于比較有广度的人,而专攻一点的人往往对细节理解非常深入,属于有深度的人,对于系统project的构建阶段,我个人觉得广度比深度要来得重要,但同一时候绝不能忽略深度,相反,须要一种升华,即你须要拥有极强的洞察力,不须要深入细节的前提下迅速捕捉到关键点,做到这一点,没有对知识的深度理解与积累是非常难做到的。可是对于系统的调试,排错和优化阶段,知识深度的重要性就要大于知识的广度的重要性了。
       知识的广度能够是来自别处的,比方教科书,互联网论坛,博客等,可是知识的深度很多其他的是自己挖掘出来或者悟出来的,对于后者而言,那就是能力了。一个简单的样例,那就是TCP服务器端大量的TIME_WAIT状态套接字对系统的影响,人们提出了非常多的解决方案,比方设置recycle,reuse等,并且都是千篇一律的,是的,这样是能解决这个问题,可是有谁去挖掘过TIME_WAIT究竟带来了什么问题吗?并且是大量的TW套接字,其数量超过ESTABLISH套接字几个数量级的情况。人们普遍的回答是占用系统资源,耗尽socket资源,可是在如今服务器拼硬件的时代,动不动就几百个G的内存的情况下,这都不是什么问题。有人把思路从空间开销转向时间开销吗?有是有,但非常少。为什么呢?可能是由于他们总觉得升级内存比升级CPU划算吧,可其实,差点儿每一个人都知道数据结构的组织不仅仅影响内存占用,还影响操纵效率。仅仅须要略微想一下TCP socket的实现就会明确下面的事实:一个数据包相应到一个socket,须要一个查表的过程,对于TCP而言,首先要检查该数据包是否已经相应到了一个socket,假设没有查到再去查是否有listen socket与之相应(否则怎么办呢?)。也就是说,listen socket的查找是最后才做的。对于一个新建的连接而言,首先它不可能在ESTABLISH状态的socket链表中找到,假设ESTABLISH socket不多的话,这个开销能够忽略,即便非常多,也是必须要例行公事,因此这个查找是必定的开销,可是接下来还要看它是否和一个TIME_WAIT状态的socket相应,此时假设存在大量的TW套接字,那么这样的开销就是额外的开销,是能够避免的,可是有一个前提,那就是必须避开TW带来的问题(在取消一个机制之前,必须明确该机制的全部方方面面)。因此,大量的TW套接字除了消耗空间外,还会减少新建连接的效率,大量的时间会消耗在查表上,对于已经建立的连接的传输数据效率则影响不大,由于在查询TW状态套接字之前,它已经查到了一个ESTABLISH套接字了。假设你本身就懂Linux的TCP层的实现,那么以上的问题非常easy分析,可是假设你从没看过源代码的实现,就须要自己思考了。诚然,熟悉接口而不关注细节能够提高编码的效率,可是这并非箴言,由于熟悉实现细节更能提高出了问题后的排错效率。所以,知识的深度和广度都是必不可少的,关键是你处在哪个阶段。
       假设过度在意学到的东西,那么就会比較僵化,假设过度在意挖掘或者感悟出来的东西,就会easy钻入牛角尖且变得自负。怎样权衡知识的利用方式,十分重要。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。