使用ZeroMQ彻底重构OpenVPN的设想以及一些新想法

在一个迟到的雨夜,我怀着无比激动的心情写了不到20行代码...但是这不到20行代码却是一个新发现,它彻底解决了OpenVPN的三个重大问题,是的,彻底解决。

ZeroMQ的到来

我接触ZeroMQ这玩意确实有点晚,那是上一个下雨的周日,我自己宅在家里看罗马史,畅想着这个辉煌的帝国,伟大的制度。
       ZeroMQ彻底颠覆了以往的socket编程模型。它使得底层的BSD socket对程序员不再可见,程序员只需要处理自己业务即可,即收到某个消息,将其做一些处理,然后要么回复一个消息,要么转发,而根本不必管它从哪里来,要到哪里去,具体的路径是怎样的。
       总的来讲,ZeroMQ正如其文档作者所说,它旨在修复这个世界。ZeroMQ通过代码来组网,彻底将程序员从底层的BSD socket及以下的网络细节中解放出来,程序员可以通过消息组建一个新的网络,可以满足任意点到任意点的消息可达性。从ZeroMQ提供的API可以看出,它几乎完全抛弃了“网络相关”的一切机制,再也找不到sockaddr结构体,当然你也再也不可能获取什么IP地址了,实际上在ZeroMQ看来,根本就没有IP地址,也没有传输层,以太网的概念,程序员可以完全不懂这些。这就好比即便是顶级网络工程师也可以不懂物理层编码规范以及PHY规范一样。这是一个真实的抽象!
       当然,由于ZeroMQ作为一个通信库而不是一个标准,它目前还要构建于既有的TCP等协议之上 ,然而,你可以看到,能体现TCP的API将TCP协议,IP地址,端口等概念退化成了一个字符串标号,比如在ZeroMQ中,"inproc://step2","tcp://localhost:5671"等是并列的,前者仅仅是在说,底层的通信协议是进程内部的通信协议,后者是使用TCP,如果不看zmq_bind,zmq_connect调用,比如它们被封装了起来,你是无法区分两个zmq_socket的,不管是通过IPC,还是管道,还是TCP,ZeroMQ唯一要保证的事就是,消息的可达性。至于底层的BSD socket是在跟谁进行着连接,至于IPC具体过程,ZeroMQ是不管的。
       在云计算,物联网时代,分工细化继续进行着,必须把程序员从网络的困境中解放出来!这一直都是一个愿景,早在10年前,我还在上大学,那个时候就不断有人推出一系列的中间件,声称“屏蔽了底层细节,让程序员只关注业务逻辑”,可是效果都不大好,几乎都是将程序员从网络的复杂性引到了中间件本身的复杂性,这个趋势在云时代是可悲的。直到有了ZeroMQ类似的东西,它足够简单,实现了针对程序员的真正减负。
       ZeroMQ,作为一个MQ,它真的和别的MQ一样吗?完全不一样!它甚至是一个库而不是一个系统,它是让程序员用的,它和系统管理员关系不大。它不需要你搭建什么系统,不需要你去做任何配置,不需要特殊的服务器,它只是一个库,在Debian上可以轻松地被./configure & make & make install,然后就可以基于它的API编程了,它的API可以被man到,只需要man -k zmq就知道大多数的API了,最后gcc test.c -o test -lzmq,然后运行它即可。对于使用TCP的zmq而言,通过抓包,你会发现它在TCP之上它封装了很多新的协议,是的,ZeroMQ拥有自己的一套协议。
       我相对认真地考虑了一下ZeroMQ对OpenVPN而言意味着什么。OpenVPN可以说是个代理,然而它又和一般的应用层代理截然不同,即,它的连接不由应用层协议决定,事实上,在OpenVPN隧道里填充的是一个以太帧或者IP数据报文,除非隧道自己想断开,否则在无故障的情况下,连接是长期存在的。在编程模型上,OpenVPN实则一个转发器,不考虑加密/解密时,它其实就是从一个BSD socket接收一段数据,然后解封装后将其送入TUN网卡字符设备,或者反过来,从TUN网卡字符设备接收一段数据,将其封装后送入BSD socket。使用ZeroMQ的各种模式组合,这个是很容易完成的。似乎可以直接套用ZeroMQ的多线程模型,事实上真的可以。
       不得不提到的几个美中不足,倒不是说ZeroMQ多么的不好,毕竟我还没有精通它,无权过问和指责ZeroMQ的过多细节,这是一点自己的想法。不过应该事先说明的是,ZeroMQ压根不是设计出来让人满足特殊需求的,针对于它所擅长的领域,它已经做得很不错了。

ZeroMQ美中不足

1.ZeroMQ无法在底层使用UDP进行传输层

这个似乎是因为UDP难以追踪并映射客户端导致的,但是要想做到这点似乎不难,使用内核的conntrack类似的机制只是其中一法,最好的办法还是在协议层面解决。对于UDP的ZeroMQ,在ZeroMQ的数据传输协议中增加一个字段用来做TCP五元组类似的事情,当然这需要维护一张map,你也许觉得查找这张map的开销有多大,但是往往而言,在做到一个低效率的版本之前,不要考虑优化。
       不过,从ZeroMQ的设计初衷来看,它需要消息本身的可靠传输,兼顾有边界的短消息,高并发,这也许解释了为什么ZeroMQ使用TCP而不是UDP。也许,ZeroMQ本身的协议就是基于考虑传输协议的,如果使用了UDP,它也还是要自己做Reliable层的。然而我要说的是,为何不能更进一步呢?我记得当初UDP从TCP/IP中剥离出来的过程走的就是类似的一条路,毕竟,如果将功能有限的ZeroMQ发展成一个全面的通信库,就该考虑众口难调的各种传输需求,而这,确实需要重新审视和设计ZeroMQ的协议了。

2.ZeroMQ不支持文件描述符

和问题1一样,ZeroMQ并不支持通过文件描述符机制之间进行的传输。如果把ZeroMQ定位成一个I/O库而不是通信库,可能一开始就应该支持文件描述符了,要知道,对于操作系统接口而言,一个socket和一个TUN文件描述符没有任何区别,大家都可以被poll/select。既然旨在让ZeroMQ成为一个代码联网的库,那何不将所有通信机制都支持掉呢?数据可以来自socket,也可以来自文件,数据可以发往socket,可以发往同一进程的不同线程,当然也可以发往TUN网卡。如果这样,OpenVPN的改造简直不用写什么代码了。

3.Zero不支持IP地址变更

ZeroMQ作为REQ方时支持坚持不懈地重连探测,直到REP方启动或者断开后又恢复。但是一旦REQ连接成功,其IP地址变化了,它无法将这个变化告诉REP方,这等于说还是没有完全屏蔽底层网络的变化。如果REQ可以将这个变化信息作为协议的一部分通告REP方,那么REP方将可以及时更新map消息,保证期间消息的传递继续进行。

       不管怎么样,这个支持IP地址变化的机制在移动的时代是极其有用的,我自己在去年就测试过。它可以直接替代复杂的移动IP问题。本来,IP层就是管理所在点到目的地的连通性的,既然所在点变化了,IP也应该变化,不管怎样,都不应该影响到应用层的数据传输。如果非要做什么诸如计费之类的事情,请别基于IP层信息做,这是应用的一部分,请在应用层完成。一个传输层的端到端连接对应一个应用的连接,而一个端到端连接对应一个不变的五元组-不变的IP地址,不变的协议/端口...这种时代已经过去了,并且再也不会回来。

4.ZMQ socket的线程传递问题

这个问题直接导致了分发瓶颈,但这部分目前和OpenVPN的重构无关

ISO的OSI模型

在网络分层模型看来,ZeroMQ对应传输层之上,那么会话层可以构建于ZeroMQ之上也可以构建于其下。对于OpenVPN而言,将OpenVPN协议构建于ZeroMQ之上是比较方便的。
       ZeroMQ最终可能会进入协议栈,取代BSD socket接口,但是这需要一个标准化的过程。如果真的是这样,这将是程序员最大的福音。

OpenVPN的问题1以及解决

OpenVPN一直以来都没有多处理,原因是合理的,但不是绝对的,我已经写了不下10篇文章讨论这个主题,感觉这种纠结就是自己和自己辩论,十分不和谐。我曾经试着启动多个OpenVPN服务然后通过外部封装的方式实现多处理,还尝试过使用random NAT来做负载均衡,最终,我将OpenVPN本身改成了多线程的,同时使用了内核的基于HASH算法的REUSEPORT机制,甚至将它的数据路径放在内核中...在这4年多的时间,没有连续的5天没有思考过OpenVPN的问题。
       OpenVPN多处理的问题难点不在问题本身,而在于OpenVPN的代码十分不好修改,也就是在最近,我决定放弃OpenVPN本身,写一个和OpenVPN协议兼容的实现。从此不再折腾OpenVPN了。既然要自己从头写一个兼容OpenVPN协议的,那么最重要的工作就是设计一套MPM模型了,我曾经想着向Apache取点经,但是发现了更好用的ZeroMQ。它完全适合做这个,因为再也不用管理server模式复杂的连接了,再也不用自己设计select/poll/epoll了,再也不用面临将某个连接分发到某个线程这种问题了。
       遗留的问题就是UDP和设备文件描述符的支持。在将ZeroMQ用得得心应手之前,我还没看它的底层实现,可以猜想,它底层一定使用了复杂的机制来管理连接,这才让基于ZeroMQ的应用可以直接处理消息而不必管理网络。

OpenVPN的问题2以及解决

OpenVPN的问题2就是不好做负载均衡,这也是由于问题1引起的,在此不深究,可以采用ROUTER模式来分发消息。

OpenVPN的问题3以及解决

这个其实不是OpenVPN的问题,这个说的是现如今的socket编程模型对移动性的不适应。移动性会导致终端的IP地址可能会不断发生变化,而这会导致socket的断开和重新连接,以往的很多应用程序session直接和一个socket连接绑定,这会导致应用的重连。
       移动性要求即便底层的五元组发生变化,应用层数据依然可以在新的五元组上连续正常传输,一个TCP的断开重连应该对应用透明,就像IP路径对TCP透明一样。我写的那不到20行代码就是做这个的。我基于ZeroMQ guide的example的例子hwclient.c/hwserver.c修改。其中server的代码如下:
//  Hello World server

#include <zmq.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>

int main (void)
{
    //  Socket to talk to clients
    static int i = 0;
    void *context = zmq_ctx_new ();
    void *responder = zmq_socket (context, ZMQ_REP);
    int rc = zmq_bind (responder, "tcp://1.2.3.4:5555");
    assert (rc == 0);

    while (1) {
        char buffer [10];
        char b2 [10];
        zmq_recv (responder, buffer, 10, 0);
        printf ("Received Hello:%s\n", buffer);
        sleep (1);          //  Do some ‘work‘
        sprintf(b2, "%d\n", i++);
        zmq_send (responder, b2, 5, 0);
    }  
    return 0;
}

它不断接受客户端的连接,然后返回一个不断递增的数。客户端的代码如下:

//  Hello World client
#include <zmq.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void *context;
void *requester;

/*
 * 由于ZeroMQ不支持tuple变更协议,只好在这里
 * 通过外部的方式重新初始化zmq socket了
 */
void re_init_socket(int unused)
{
        if (context) {
                printf("reinit\n");
                zmq_close (requester);
                requester = zmq_socket (context, ZMQ_REQ);
                zmq_connect (requester, "tcp://1.2.3.4:5555");
        }
}

int main (void)
{
        int iv = 1;
        static int k = 1;
        printf ("Connecting to hello world server...\n");
        signal(SIGHUP, re_init_socket);
        context = zmq_ctx_new ();
        requester = zmq_socket (context, ZMQ_REQ);
        zmq_connect (requester, "tcp://1.2.3.4:5555");

        int request_nbr;
        for (request_nbr = 0; request_nbr != 150; request_nbr++, k++) {
                char buffer [10];
                printf ("Sending Hello %d...\n", request_nbr);
                sprintf(buffer, "t:%d\n", k);
                zmq_send (requester, buffer, 5, 0);
                sleep(1);
                zmq_recv (requester, buffer, 5, 0);
                printf ("Received World %d, %s\n", request_nbr, buffer);
        }
        zmq_close (requester);
        zmq_ctx_destroy (context);
        return 0;
}

当客户端的IP地址发生变化的时候,发送一个信号SIGHUP给客户端,客户端就会重新初始化zmq底层的socket。此时数据传输并没有断开。值得注意的是,千万不要指望在这种客户端变更了IP的情况下ZeroMQ依然可以对应到同一个客户端,因为ZeroMQ根本没有做tuple变更协议,而这原本是它本来该做的。我们可以指望的是,客户端的数据在IP地址变化了的情况下依然在继续往服务端传递,并且真的传到了服务端,对于服务端来讲,它会认为IP变更后的连接是一个新的连接,虽然这是不应该的。
       在没有tuple变更协议的时代,我们不得不在消息体内部自定义字段来关联IP变更前后的两个连接为同一个session。其实对于OpenVPN的使用场景来讲,关联与否是无所谓的,因为ZeroMQ消息体内部肯定需要有一个字段用来查找multi_instance。

一点遗憾-关于tuple变更(比如IP地址变更)

我个人认为,ZeroMQ既然将网络抽象到了编程层次,旨在隐藏底层网络连接,为什么不做的更加彻底一些呢?可以猜想,ZeroMQ在底层用一个map保存了一个socket上下文和tuple的对应关系,即它依然是通过5元组来找到该往哪里发送数据的。这并不是什么缺点,也没有什么不好。当然如果在ZeroMQ的协议层面上加一个64位的字段用来保存一个唯一的ID值来做对应,那当然也可以,只是浪费了数据包的数据空间。更好的方案是设计一个新的协议,即tuple变更协议。一旦客户端发现IP地址发生变化,则主动发送一个控制报文,内容是自己原来的tuple的信息以及新的tuple信息,客户端序列如下:
客户端保存tuple信息
客户端断开并重新打开底层socket
客户端在新的socket上封装并发送一个tuple控制包
服务端序列如下:
收到一个tuple控制包
更新老的tuple映射为新的tuple映射,即更新底层socket
以上的思路我已经在OpenVPN中尝试过了,我觉得很不错,应用到ZeroMQ中,应该也比较赞。

古代与现代的互联网

罗马帝国有关的一个名词就是“我们的海”,指的是地中海。很多人将罗马帝国和同时期的秦/西汉作对比,一般而言,中国人普遍认为秦汉强于罗马,而欧美人则普遍认为罗马强于秦汉,在我看来这是毫无意义的,但是当你注意到地中海繁忙的贸易线的时候,拿中国的灵渠,秦直道相比就有点相形见绌了。不是吗?请看下罗马的道路,整个地中海的航线,那全然就是一个古代互联网啊,全网状拓扑,再看看秦汉的中国,包括现如今,基本还是星型拓扑,我不想在此讨论道路网络拓扑对政治经济制度的影响(它们本身是由地缘直接决定的),我只是想说,地中海的存在对于罗马帝国是多么的重要!
       从亚平宁南端的西西里岛到埃及亚历山大港以及小亚细亚近东的距离和从西安到江南以及岭南的距离差不多,但是在中国,对于贵族,那就是“一骑红尘妃子笑”,骑马星夜驰往皇宫送少量的荔枝,对于军队,有少量的直道(相对罗马的道路而言),而对于普通人来讲,就只能“不辞长做岭南人”了,把户口迁过去吧。因为在中国没有强大的运输系统,所以你就必须区别对待不同的人群,给与其不同的QoS。对于罗马帝国而言,地中海运输系统以及遍布全国的道路系统则全然不同,它们形成了一个互联互通的网络,任何人都可以使用的大容量大型互联网,因此便没有了使用者之间的差异,网络平坦化了。在这个平坦的网络之上,不必再单独维护节点之间的连接信息,比如罗马不关心它是怎么到亚历山大港的,它也不关心通过哪条路可以到达叙利亚,它关心的是去干什么,这就是“条条大道通罗马”的本质含义。
       罗马的道路网以及“我们的海”是一个古代互联网基础设施,中国在古代则缺少这个设施(现在呢?)。在这个设施之上,便可以构建最终的诸如船只,车辆,保险制度,保鲜措施,护航编队,股份公司,货币系统等应用层机制。在“我们的海”上,可以提供大吞吐量的物资运输服务,借助风向,速度也会提高。参照古代罗马帝国和西汉政府的道路总长度数据,可以看到,罗马帝国的道路总长要比西汉政府几乎高出一个数量级,这还不算地中海航线的长度。
       如果那句“要想富,先修路”的话是对的,那么罗马帝国和秦汉的强弱,还用多说吗?如今,我们似乎在重复着那个逝去的辉煌年代的故事,只不过是换了介质。我们依然没有“我们的海”,依然拥有诸多也许是太多的Middle check Node。当我scold网速太慢很多站点不能访问的时候,我似乎可以穿越到2000多年前看到同样的事情,当我在路上看到“前方收费站”的指示牌时,我似乎再一次地思考着如何才能忍住不去scold什么。
       ADSL在光进铜退的年代意味着什么?仅仅意味着降低了FTTH的成本?那你就错了!ADSL的A意义深远,它直接限制了个人用户或者缴的费用少的用户提供互联网服务,运营商可以让你保持慢速率下载,影响着你个人的心情,但是绝不允许你提供服务从而影响到多数的人。你的上传速率低了,别人便无法从你这里高速下载,如果你质问,回答也好利索并且令人遗憾,这是技术限制,并非我们希望的。如果FTTH为你在两个方向提供了对称的速率,再这么说就说不过去了,稍微一想就知道运营商用ACL等手段做了限制。
       再来考虑一下“本地流量”和“异地流量”的问题。是移动IP本来就没有设计好呢,还是根本就不愿意设计好。这又是一个技术与利益的博弈。近日,南车,北车的合并让人可以思考很多关于国内运营商的事情。如果不是因为两家斗狠使得中国在国外丢了单子,南北车还将继续斗下去。相关发言人似乎一开始就思维很清晰且有混乱,让人很容易就能得出结论。似乎他们竞争带来的就是绝对低价,事实上,在绝对低价的背后,是绝对的垄断,这和合并与否没有任何关系。干嘛要区分本地流量和异地流量,为什么在这个几千年的文明中,无论干什么总要说本地和异地?干嘛要把这种历史的遗毒嫁接到互联网上,从而让一个本应该完全开放的平台变得好有地域性。运营商之间的竞争不应该成为唯一的解释。什么是“跨运营商”?搞得好像流量经过了中东一样。运营商之间的竞争应该是服务质量的竞争,而不是地域垄断。就好像军阀时期,一列山西的煤车跨过了别家的领地,需要缴费...如果我没记错,这在欧洲好像是1789年之前很久的事情吧...

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