动手写一个OpenVPN的wrapper来优化OpenVPN性能

晨曦之光 发布于 2012/04/10 14:56
阅读 1K+
收藏 1
OpenVPN,一个让人想说爱你不容易的VPN,曾经耗费了我大量精力的VPN,其性能,...最终还是不咋地!以下是一个大致的统计数据:
纯千兆环境,4核心至强3.0GHZ处理器,OpenVPN使用BF-CBC加密,SHA1摘要,OpenVPN不绑定特定CPU,带宽可达20-30MB/s;
纯千兆环境,4核心至强3.0GHZ处理器,OpenVPN不加密,不摘要,OpenVPN不绑定特定CPU,带宽可达40-45MB/s;
纯千兆环境,4核心至强3.0GHZ处理器,OpenVPN不加密,不摘要,OpenVPN绑定特定CPU,带宽可达45-55MB/s;
纯千兆环境,4核心至强3.0GHZ处理器,OpenVPN使用BF-CBC加密,不摘要,OpenVPN绑定特定CPU,带宽可达35-40MB/s;
纯千兆环境,绑定OpenVPN到特定CPU,该CPU会跑满,带宽无法提升受制于CPU;
纯千兆环境,不绑定OpenVPN到特定CPU,没有一个CPU跑满,带宽无法提升受制于OpenVPN的单进程模型以及操作系统多处理器调度开销;
百兆环境,无论如何,OpenVPN的加密隧道带宽基本接近物理网卡的百兆带宽。

既然是OpenVPN的软件模型远远跟不上硬件的提升,那么就要想办法以量取胜,办法是运行多个OpenVPN进程,每一个CPU上绑定一个,通过Linux的内核接口可以很容易做到这一点:
1.首先mount一个cpuset,创建3个set
mount -t cpuset none /mnt
mkdir /mnt/{vpn1,vpn2,vpn3}
2.简单配置一个cpuset
echo 0 > /mnt/vpn1/cpus
echo 0 > /mnt/vpn1/mems
echo 1 > /mnt/vpn2/cpus
echo 0 > /mnt/vpn2/mems
echo 2 > /mnt/vpn3/cpus
echo 0 > /mnt/vpn3/mems
3.以不同端口号和虚拟网段启动3个不同的OpenVPN进程
openvpn --config cfg1 --port 1234 --server 121.121.0.0 255.255.0.0
openvpn --config cfg2 --port 2234 --server 122.122.0.0 255.255.0.0
openvpn --config cfg3 --port 3234 --server 123.123.0.0 255.255.0.0
4.绑定上述3个openvpn进程到不同的cpuset
i=1;for pid in $(ps -e|grep openvpn|awk -F ' ' '{print $1}');do echo $pid > /mnt/vpn$i/tasks; ((i=i+1)); done
5.将多个客户端连接在这不同的三个OpenVPN进程上,拉取大文件,测试OpenVPN服务器的tap0-tap2的总流量
经过上述配置后,新的统计数据如下:
纯千兆环境,4核心至强3.0GHZ处理器,OpenVPN使用BF-CBC加密,SHA1摘要,带宽可达35-40MB/s;
纯千兆环境,4核心至强3.0GHZ处理器,OpenVPN不加密,不摘要,带宽可达75-80MB/s;
纯千兆环境,4核心至强3.0GHZ处理器,OpenVPN使用BF-CBC加密,不摘要,带宽可达50-55MB/s;
纯千兆环境,3个CPU会跑满,带宽无法再提升受制于CPU;
纯千兆环境,将物理网卡中断(同时也是协议栈接收处理的软中断)钉在第四个CPU上,上述值再有5左右的提升。

可见,多个处理进程会带来带宽的大幅提升。实际上上述的方案完全可以通过虚拟网卡bondding或者多VPN进程共享虚拟网卡的方式做到,然而这些配置都不简单,有时还要修改虚拟网卡的驱动程序,可扩展性很不好,因此简单的以多OpenVPN实例来说明问题是再好不过的了,如需bonding方案或者shared-tap方案,请参考《 关于OpenVPN文章的目录》。
        虽然上述方案解决了部分性能问题,然而配置还是比较复杂,能不能通过一个wrapper将上述的操作包装起来呢?既能做到多实例并行-绑定特定CPU,又能做到OpenVPN客户端动态自动获取OpenVPN服务器端口号和虚拟网段。这需要增加一个层次,就是在OpenVPN连接之前增加一个“端口/虚拟网段”协商的层。该包装器的使用如下:
服务器端:openvpn-wrapper -t server -p 1111 -v /usr/sbin/openvpn -c /home/zy/cfg ....
客户端:openvpn-wrapper -t client -p 1111 -a 192.168.1.23 -v /usr/sbin/openvpn -c /home/zy/cfg-client
参数意义如下:
-t:类型,分为server和client,和openvpn一样
-p:wrapper服务器监听的TCP端口号
-v:openvpn的路径
-c:openvpn配置文件的路径,该文件中没有那些自动协商出来的配置,比如openvpn的端口,虚拟网段等等。

openvpn-wrapper的代码结构如下:
struct vpn_opt {
    char vpn_path[255]; //openvpn的路径
    char cfg_path[255]; //openvpn配置文件路径,配置文件不含server指令和port指令
    int port_base;      //起始端口号
    int num_process;    //openvpn进程数量
    int num_cpu;        //CPU数量
    int sintr_cpu;      //单独分配的处理物理网卡软中断的CPU
};

struct vpn_opt vopt;
int vpn_servers()
{
    int pid;
    int ps = vopt.num_process;
    //创建ps个openvpn服务器进程
    while (ps--) {
        pid = fork();
        if (pid == 0) {
            //每一个openvpn服务器进程绑定在一个CPU上
            sched_setaffinity(getpid(), ..., ps%vopt.num_cpu);
            execve(vopt.vpn_path --config vopt.cfg_path --port vopt.port_base+ps --server $不同虚拟网段...);
        } else if (pid > 0) {
        } else {
        }
    }
}
 
int accept_clients()
{
    //创建TCP服务器,接收client请求,回送端口信息
    //端口调度算法:
    //1.轮转调度:N个客户端按照先后顺序在vopt.num_process个进程之间轮转
    //2.空闲优先:纪录M个VPN进程之间的负载情况或client连接数,取最小值
    //3.其它的算法...

    //对应客户端的wrapper先连接这个TCP server,得到VPN端口信息后调用exec启动
    //openvpn客户端进程。
}
 
int main(int argc, char **argv)
{
    char c;
    while ((c = getopt (argc, argv, "")) != -1)
        switch (c)
        {
        case 'v':
            strcpy(vopt.vpn_path, optarg);
            break;
        case 'c':
            strcpy(vopt.cfg_path, optarg);
            break;
        //可供配置的其它参数,比如CPU数量,中断处理均衡等
        default:
            abort ();
    }
    //下述参数必须由命令行提供:
    //vopt.port_base = 61195;
    //vopt.num_process = 2;
    //vopt.num_cpu = 2;
    vpn_servers();
    accept_clients();
}
有了这个wrapper,就可以将之直接替代openvpn了,但是对于大网对大网的拓扑,如此混乱的虚拟网段让客户端怎么管理呢?这可以通过将所有的配置全部集中在OpenVPN服务器端来解决,客户端需要怎么配置全由服务器端来推送,涉及到自定义信息的推送,请参考push setenv-safe这个指令。如果涉及到全网互通的路由配置,你就不能单靠OpenVPN的client-to-client了,还要在不同的虚拟子网之间配置路由,怎么办呢?还记得ICMP Redirect吗?统一配置默认网关(注意单加物理网段互通的路由)该虚拟网段的OpenVPN服务器的虚拟IP,如果目标属于同一个OpenVPN实例管辖,那么tap模式下OpenVPN服务器会直接发送ICMP Redirect,如果不是由同一个OpenVPN实例管辖,那么确保OpenVPN服务器上拥有管辖目标网段的OpenVPN实例即可,一切都可以靠单点路由配置搞定。
        总之一,将配置集中于一个点,最终由同一个人来配置,这样最不容易引起混乱,剩下的全部由机器来做。能推送下去的尽量推送下去,做到单点配置。OpenVPN提供了丰富的可推送的配置,实在满足不了的可以使用setenv-safe这个万能钥匙。
       总之二,优化无极限,如果你单看多个OpenVPN实例运行带来了性能提升就沾沾自喜了,那么你就会错过绑定单个实例到一个CPU上带来的进一步性能提升,如果你有幸看到了这一点,不要停步,看看top输出,你会发现软中断可能在和OpenVPN抢夺CPU,因此你会进入/proc/interrupts看个究竟,于是你可以通过设置/proc/irq/smp_affinity文件来分离中断,突然,你看到了以下信息:
  95:   18250769     193538      28997      45831          0          0          0          0  IR-PCI-MSI-edge      eth3-TxRx-1
  96:       4115          0          0          0          0          0          0          0  IR-PCI-MSI-edge      eth3-TxRx-2
  97:   52535493          0          0          0          0          0          0          0  IR-PCI-MSI-edge      eth3-TxRx-3
  98:   75459635          0          0          0          0          0          0          0  IR-PCI-MSI-edge      eth3-TxRx-4
  99:       4115          0          0          0          0          0          0          0  IR-PCI-MSI-edge      eth3-TxRx-5
 100:   44074216          0          0          0          0          0          0          0  IR-PCI-MSI-edge      eth3-TxRx-6
 101:   20545603          0          0          0          0          0          0          0  IR-PCI-MSI-edge      eth3-TxRx-7
于是你不得不拿起千兆以太网卡的手册看看多队列相关的内容。这样完了吗?虚拟网卡带宽之和已经接近物理网卡的千兆带宽了--98MB/s,不要止步,记得Linux有一个renice命令,提升一下OpenVPN的优先级试一下...105MB/s。我想还可以更好,只是时间来不及了...出事了!
        如果你只是看到了这,那么你可能错过了一件大事。重新看上面的eth3-TxRx-1信息,然后思考结果。当我将每一个OpenVPN绑定于特定的不同CPU之后,所有的网卡TxRx队列却还只是由一个CPU处理,这就形成了一个多对一的关系,考虑如下的拓扑:

服务器端每一个VPN进程由一个单独的CPU来处理,它们很显然都会将包发往网卡ETH3,而该网卡ETH3的中断却只由一个CPU来处理,不管是发送中断还是接收中断。能不能均分一下任务呢?在均分之前,先要想一个方案。我有8个CPU核心,分别处于两个封装上,每个封装4个核心,于是我启动6个VPN实例,分别绑定在编号(从0开始)为1,2,3,4,5,6的CPU上,然后让CPU0和CPU1各自处理一半的软中断(大多数是接收中断的软中断),这是因为一个封装内部的Cache亲和性以及CPU核心亲和性要比属于不同封装的核心好很多,因此两个封装中各自都有一个核心处理软中断,可以高效地将数据踢给绑定于同一封装核心的VPN实例。测试下来虽然性能提升不显著,但是毕竟有了3MB/s的提升。
        说了这么多,所谓的OpenVPN的优化都是在OpenVPN外部进行的,要知道其本身也有很多的参数可以调节性能,比如sndbuf和rcvbuf以及txqueuelen这三个参数,如果rcvbuf和sndbuf不一致相差太多的话,会造成UDP以及ICMP的大量丢包,虽然TCP能调节自身的速率,但是当rcvbuf小到比TCP管道最细的部位还要小的时候,丢包就显著了,比如rcvbuf是M,而TCP的慢启动阀值为N,且N远大于M,这就会导致TCP频繁大量丢包,然后慢启动,然后再丢包,再慢启动...此时要么调小慢启动阀值,要么调小TCP发送缓冲区,对于Linux则是tcp_wmem,不过最好的办法就是调整sndbuf和rcvbuf,将其调整为一样大,并且适当比TCP管道更宽一些,让TCP可以在其中自由发挥流控以及拥塞控制,而不是将OpenVPN模拟成一段及其恶劣的线路,如果你将sndbuf设置成了30000,将rcvbuf设成了1234567,那么虽然TCP也能进入VPN隧道,但是这条隧道太恶劣了,有效速率将会很低很低。
        可是还有个小问题,为何不把中断均分到VPN实例所在的CPU呢?这是可行的,然而必须用测试结果说话,有一点可以确定的是,如果那样的话,虽然可能会得益于千兆网卡的DCA,然而VPN实例和软中断将会抢夺CPU,抢夺的激烈程度不仅仅受制于CPU的性能,还要受制于时间的串行性的本质,毕竟同一时刻一个CPU只能做一件事,因此有时候,你用top发现每一个CPU都没有泡满,然而性能却反而因为中断均分而下降了,值得注意的是,Cache亲和性虽然很重要,但是相比访问同一个核心的Cache的开销,访问同一颗封装的Cache的开销也不会差太多,毕竟现代超猛的多核心处理器的每个封装,甚至封装之间都有共享Cache的。于是这又扯到了一级Cache,二级Cache,...内存,磁盘,网络等存储设备的大小和层次问题了...
        通过测试终端的TCP重传次数计算,上述的结果非常不错,优化无极限,如果考虑到一路统一MTU,那将必须又是一个优化切入点,另外还有e1000e千兆网卡驱动的很多参数还没有调整,另外,如果你的CPU核心在绑了一个OpenVPN实例且满数据跑时top显示其idle百分比仍然很高,那么就在其上绑定两个或者多个OpenVPN实例,总之,CPU利用率达到90%以上并不是坏事,而是好事,这个和桌面系统是完全不同的...
        如果CPU跑不满,先别急着绑多个VPN实例,还有一招,那就是压缩,在OpenVPN配置中增加comp-lzo即可,非常简单,理论和测试结果均证明,启用压缩可以大大减少丢包率,且使得吞吐量得到大幅提高。最基本的一点,对于TCP而言,我觉得多个OpenVPN载荷包打包压缩要比单个载荷包单独压缩效果更好些,OpenVPN隧道一端对多个TCP包进行压缩,另一端简单进行解压缩,隧道途中,压缩包中载荷数据包顺序决不会乱掉,因此也就使得串行的协议比如TCP的顺序性得到了加强,多包加密隧道不但封装了数据加密了数据,还带着串行的TCP数据走过了最坎坷的一段路使它们不会乱序。下图说明了这一点:

但是,即使是单包压缩,也会使隧道传输速率提高,因为隧道内的数据包尺寸减小了,更有利于传输(轿车要比卡车快...?),同时通过调节中途设备的网卡驱动参数还能将隧道传输速率进一步提高,举个例子,Intel千兆网卡就有可以调节的参数使它更有利于小包的收发,或者更有利于大包的收发,或者折中。MTU导致的IP分段也是使用压缩的重要理由。压缩可以将包压小,如果传输文件,发送数据块而不是小包的可能性较大(相反的则是ssh之类的延迟敏感的程序,它们一般使用小包通信),那么起始端可能根据网卡的MTU来截取数据包,然而OpenVPN在用户态使用socket为其进行封装,肯定会超过MTU值,因此OpenVPN的封装对于端到端的MTU发现是不可见的,所以IP分段并没有发生在端到端的载荷包上,而是发生在OpenVPN的数据传输本身。通过抓包,发现大量:
10:07:26.243655 IP (tos 0x0, ttl 64, id 50897, offset 0, flags [+], proto UDP (17), length 1500)
    172.16.2.1.61195 > 172.16.2.2.43050: UDP, length 1529
10:07:26.243659 IP (tos 0x0, ttl 64, id 50897, offset 1480, flags [none], proto UDP (17), length 77)
    172.16.2.1 > 172.16.2.2: udp

因此很大一部分开销花在了IP分段/重组上,故采用压缩是明智的。一个等价的解决方案是不使用压缩(毕竟它消耗了CPU),取而代之的是将OpenVPN两个端点之间的所有链路的MTU调大那么一点点(多处一个OpenVPN协议头以及OpenVPN使用的TCP/UDP头等协议头)。
        做到这一步,现实意义上已经够了,剩下的就全属业余爱好了...
        最后要说的是,虽然以上的方式有效的提升了OpenVPN构建的VPN隧道性能,然而却不能将所有多条隧道的带宽全部供给一个OpenVPN客户端,只是说它们的和是一个不小的值,如果想实现单条隧道的带宽提升,那就需要多实例bonding或者多实例路由负载均衡了。本文所写的其实不是什么创新,很多Linux发行版自带的OpenVPN本身就提供了多实例OpenVPN并行的配置,只需要service包装命令启动一下即可。

原文链接:http://blog.csdn.net/dog250/article/details/7294234
加载中
返回顶部
顶部