使用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年之前很久的事情吧...

时间: 2024-08-25 08:40:49

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

OpenVPN移动性改造-靠新的session iD而不是IP/Port识别客户端

设备移动性的挑战 1.设备会经常由于小区或模式切换而更改IP地址. 这种地址更新是移动网络的正常行为,不应作为故障或事故看待,因此理应对应用程序透明,应用不应被此类事件打扰,更无责做善后处理. 2.移动设备存在多张3G/4G/2.75G网卡时,希望这些网卡同时收发数据. 由于这些网卡一般属于不同运营商网络,其网络架构又不同,一般要求数据包携带本运营商网卡的IP地址作为源(这一般是为了在该运营商核心网终点处做NAT),因此为了支持多运营商多网卡负载均衡,一个应用程序业务流数据包必然要支持不同的IP

ZeroMQ接口函数之 :zmq_curve_keypair - 生成一个新的CURVE 密钥对

ZeroMQ 官方地址 :http://api.zeromq.org/4-0:zmq_curve_keypair zmq_curve_keypair(3) ØMQ Manual - ØMQ/4.1.0 Name zmq_curve_keypair - 生成一个新的CURVE 密钥对 Synopsis int zmq_curve_keypair (char *z85_public_key, char *z85_secret_key); Description 函数zmq_curve_keypair

ZeroMQ接口函数之 :zmq_ctx_new – 创建一个新的ZMQ 环境上下文

ZeroMQ 官方地址 :http://api.zeromq.org/4-0:zmq_ctx_new zmq_ctx_new(3)               ØMQ Manual - ØMQ/3.2.5 Name zmq_ctx_new – 创建一个新的ZMQ 环境上下文 Synopsis void *zmq_ctx_new (); Description zmq_ctx_new()函数创建一个新的ZMQ 环境上下文. 本函数取代了已经不再被赞成使用的函数 zmq_init(3). Threa

OpenVPN多处理之-多队列TUN多线程

1.有一点不正确劲 在改动了那个TUN驱动后,我在想,为何我总是对一些驱动程序进行修修补补而从来不从应用程序找解决方式呢?我改动了那个TUN驱动,可是能保证我的改动对别的应用一样可用吗?难道TUN驱动就OpenVPN一家在用?这绝不可能,既然我想到了这个方法,肯定别人也想到了,仅仅所以网上没有资料,是由于这些牛人不屑于此罢了.       使用原生的没有改动的TUN驱动,怎样?Well,let's go on!       问题在哪里,问题在假设我启动多个OpenVPN进程,那么它们每个的mul

什么是重构

     重构( Refactoring)就是在不改变 软件现有功能的基础上,通过调整 程序代码改善软件的质量.性能,使其程序的 设计模式和 架构更趋合理,提高软件的扩展性和维护性. 也许有人会问,为什么不在项目开始时多花些时间把设计做好,而要以后花时间来重构呢?要知道一个完 美得可以预见未来任何变化的设计,或一个灵活得可以容纳任何扩展的设计是不存在的.系统设计人员对即将着手的项目往往只能从大方向予以把控,而无法知道每 个细枝末节,其次永远不变的就是变化,提出 需求的用户往往要在软件成型后,始才

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

如果还是沉湎于之前的战果以及强加的感叹,不要冥想,将其升华. 1.C还是脚本 曾经,我用bash组织了复杂的iptables,ip rule等逻辑来配合OpenVPN,将其应用于几乎所有可以想象得到的复杂网络场景中,实现网间VPN隧道.后来我发现玩大了,要不是当时留下一份文档,我自己几乎已经无法通过这些关系错综复杂的bash脚本还原当时的思路,一切太复杂了.       我想重构它们,同时将其改造成"能经得起继续复杂化"的系统,因此我不得不想办法将这些关系理顺.是的,bash太复杂了,

OpenVPN多处理-多队列TUN多线程

1.有一件事是不正确的力量 中的变化TUN后轮驱动,我在想,为什么我总是修修补补,而有些驾驶员再也找不到从它的应用程序的解决方案?我做出改变TUN驱动器,但我可以保证的变化,因为它是提供给其他应用程序?是TUN开车OpenVPN一个在使用?这是不可能的,现在,我觉得这个方法.当然有些人认为,仅所以网上没有资料.是由于这些牛人不屑于此罢了. 使用原生的没有改动的TUN驱动,怎样?Well,let's go on!       问题在哪里,问题在假设我启动多个OpenVPN进程.那么它们每个的mul

项目设计&amp;重构&amp;性能优化

漫谈项目设计&重构&性能优化 重构的好处:重构能够改进软件设计,随着项目需求的变更,项目体积的变大早已与最初的设计大相径庭,代码结构变得凌乱.复杂,如果不进行重构,则很难添加新的功能. 1.使项目代码更容易理解很多情况下是由于项目赶进度和不注重质量导致的.那么通过重构可以帮助代码维持自己该有的形态.项目开始的时候,设计并没有考虑到方方面面,因为你不可能预测到后面的所有需求.同时你也不能把每个功能都做预留,做成灵活可变,如果最后你预测失败,那么意味着你所做的灵活性是多余的,浪费了时间且增加了

读书笔记--《大话重构》

       整体鸟瞰       最近小编读了一本书,叫做<大话重构>,这本书运用大量源于实践的示例,从编码.设计.组织.架构.测试.评估.应对需求变更等方面,深入而多角度地讲述了我们应该如何重构,建设性地提出了高效可行的重构七步.读完本书,实践重构不再卡壳,需求变更不再纠结.全面领悟重构之美,遗留系统不再是梦魇,自动化测试原来可以这样做.本书帮助程序员告别劣质代码步入精妙设计,让遗留系统的维护者逐步改善原有设计,指导重构实践者走出困惑步步坚定.同时,也为管理者加强软件质量的管理与监督,提供