如果还是沉湎于之前的战果以及强加的感叹,不要冥想,将其升华。
1.C还是脚本
曾经,我用bash组织了复杂的iptables,ip rule等逻辑来配合OpenVPN,将其应用于几乎所有可以想象得到的复杂网络场景中,实现网间VPN隧道。后来我发现玩大了,要不是当时留下一份文档,我自己几乎已经无法通过这些关系错综复杂的bash脚本还原当时的思路,一切太复杂了。
我想重构它们,同时将其改造成“能经得起继续复杂化”的系统,因此我不得不想办法将这些关系理顺。是的,bash太复杂了,那么改用什么好呢?用Python?或者PHP?再或者Java?或者直接用C?总之,不再用bash了。
脚本的好处在于,你可以随时实验新想法,所见即所得,不用携带任何装备,只要有个终端就能做事。缺点在于,正是因为上述那些随时编写随时运行的随意性,bash写出的东西很容易发散开来以后便无法收敛,Python声称自己支持OO,支持复杂数据结构组织与容易,看上去会比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,也很简单,两个命名空间除了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.知识的广度与深度
懂多少知识不重要,重要的是这些知识能用来干什么。事实上,我认为两类人是不同的,拥有构建能力的人不需要拥有多少知识的细节,属于比较有广度的人,而专攻一点的人往往对细节理解很深入,属于有深度的人,对于系统工程的构建阶段,我个人认为广度比深度要来得重要,但同时绝不能忽略深度,相反,需要一种升华,即你需要拥有极强的洞察力,不需要深入细节的前提下迅速捕捉到关键点,做到这一点,没有对知识的深度理解与积累是很难做到的。但是对于系统的调试,排错和优化阶段,知识深度的重要性就要大于知识的广度的重要性了。
知识的广度可以是来自别处的,比如教科书,互联网论坛,博客等,但是知识的深度更多的是自己挖掘出来或者悟出来的,对于后者而言,那就是能力了。一个简单的例子,那就是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层的实现,那么以上的问题很容易分析,但是如果你从没看过源码的实现,就需要自己思考了。诚然,熟悉接口而不关注细节可以提高编码的效率,但是这并不是箴言,因为熟悉实现细节更能提高出了问题后的排错效率。所以,知识的深度和广度都是不可缺少的,关键是你处在哪个阶段。
如果过度在意学到的东西,那么就会比较僵化,如果过度在意挖掘或者感悟出来的东西,就会容易钻入牛角尖且变得自负。如何权衡知识的利用方式,十分重要。
OpenVPN多处理之-netns容器与iptables CLUSTER,布布扣,bubuko.com