郭大伟,张 伟,2,姜晓艳
(1.北京信息科技大学 计算机学院, 北京 100192;2.北京信息科技大学 北京材料基因工程高精尖创新中心, 北京 100192)
Nginx[1]在1.9.0版本之后推出了stream模块,这是一个重要的里程碑,因为它加入了TCP/UDP的反向代理和负载均衡的功能,使得Nginx不仅可以作为单一的HTTP/HTTPS的Web服务器,还可以作为TCP/UDP的反向代理服务器。Nginx同时具有出色的高并发性、高稳定性,以及较低的资源占用率等优点。
在实时性要求较高的特殊场景下,简单的UDP协议仍然是我们的主要手段。UDP协议没有重传机制,还适用于同时向多台主机广播,因此在诸如多人会议、实时竞技游戏、DNS查询等场景里很适用,视频、音频每一帧允许丢失但绝对不能重传。使用UDP协议作为信息承载的传输层协议时,就要面临反向代理如何选择的挑战。
反向代理方式与普通的代理方式有所不同。标准代理方式是客户使用代理访问多个外部Web 服务器;反向代理方式是多个客户使用它访问内部Web服务器。使用反向代理服务器可以将请求转发给内部的Web 服务器,从而提升静态网页的访问速度。因此可以使用这种技术,让代理服务器将请求均匀转发给多台内部Web 服务器上,从而达到负载均衡的目的。图1是Nginx处理UDP数据包的反向代理的默认流程,其中Nginx作为后台服务器的反向代理服务器提供高并发控制[3]、多连接的UDP数据包传输服务,前端用户可以使用手机和电脑请求后台服务器的数据。利用Nginx的千万级并发连接数,可以保证所有用户稳定、高效地请求后台服务器的数据而不会出现卡顿、延迟的现象。
图1 Nginx反向代理
如图2所示,传统的Nginx对UDP用户传输过来的数据包的处理过程如下:当用户A发来一帧UDP数据包,Nginx会从事先创建好的系统socket连接池中取出一个可用的有效连接套接字socket,然后创建与后台游戏服务器的UDP连接,成功之后将从用户A收到的UDP数据包,原封不动地发送给游戏服务器。当用户A又发送另一帧UDP数据包时,由于UDP传输协议是面向无连接的,所以转发第二帧UDP数据包时,Nginx会再次从自己的socket连接池中取出另一个有效的连接套接字socket,接着将第二帧数据包发给游戏服务器。Nginx可以同时处理成千上万个请求[4],但Nginx连接池中连接的数量是有限的,由于缺少有效的连接套接字,后续的用户B、C、D发送的UDP数据无法被转发到后台,而是被丢弃,增加网络传输成本。
图2 Nginx连接池
根据Nginx的官方文档可以得知Nginx的listen指令有一个backlog参数,这个参数在Linux系统中默认是511,这就意味着在进行UDP的反向代理时Nginx最多只能转发511个数据包,超出的数据会被丢弃,而backlog参数和UDP参数不能同时使用。当有超出511个数据包的大流量数据同时需要转发时,优化之前的模型将出现大量的数据包丢失现象。
本文所研究的是在实际的网络应用需求当中使用Nginx来代理转发UDP数据包业务,保证传输过程中的稳定性、高并发、高效率。主要应用的场景为网络直播、游戏直播等对数据的实时性要求比较高的应用。
如何保证传输过程中的稳定性、高并发、高效率是网络传输中的一个关键问题。围绕Nginx的负载均衡功能,研究者们提出了多种策略和方法。
文献[5-7]介绍了反向代理在Web服务中的应用。徐长君等[7]针对Nginx固有负载均衡方式不能根据需要灵活调整的现状,提出了WLC(weighted least connection)调度算法,对Nginx的请求分发方式进行了优化;冯贵兰等[5]利用Nginx实现校园网环境下的Web反向代理方案,节约了IP地址,加快了网站访问速度;王利萍等[8]提出一种应用于Nginx服务器集群的动态自适应负载均衡算法,实现了根据服务器负载状况动态调整权值,提高了集群性能;戴伟等[9]通过对Nginx自带的负载均衡算法进行分析,优化出一种具有实时反馈能力的负载均衡算法。
本文利用Nginx作为反向代理服务器[10],开发了一款Nginx的过滤模型,在网络层上对往来于上下游的UDP连接进行复用,提高Nginx的UDP反向代理效率,为Nginx增添了新的功能,配置简单实用。
图3所示为本文设计的一种基于哈希表的UDP数据转发策略。该数据转发策略与优化之前的模型相比较,主要区别是在Nginx的过滤模块中新增一张哈希表,哈希表的结构如表1所示。其中Key是后台游戏服务器的IP地址和端口,Value是从Nginx连接池中取出的socket套接字的句柄。通过新增哈希表的映射关系,使得优化之后的模型可以复用socket套接字[2]向后台服务器转发UDP数据包,从而避免 Nginx的连接池中的有效socket套接字很快被耗尽,提高了UDP转发代理的效率。
图3 过滤模块增加哈希表
KEYValue202.104.33.20:51000Socket A 句柄202.104.33.21:51001Socket B 句柄202.104.33.22:51002Socket C 句柄202.104.33.23:51003Socket D 句柄
在Nginx官方网站中实现了一种基于红黑树的哈希表。红黑树是一种自平衡的二叉查找树。选择红黑树是因为一般的容器是顺序容器,通常情况下检索效率比较低,一般只能遍历检索指定元素,而红黑树[11]的检索速度快、效率高,而且Nginx提供了红黑树的具体实现供开发模块时来使用。
模型的框架结构如图4所示,工作流程为:读取用户在Nginx配置文件中对该模块的配置信息,申请内存空间一系列操作,并且创建一棵红黑树来保存IP地址+端口与socket套接字的映射关系。
图4 红黑树处理UDP数据流程
当用户的第一帧UDP数据包最先到达Nginx的接收缓冲区时,Nginx通过调用RecvFrom函数已经解析出它将要被转发到的地方,即后台游戏或直播服务器的IP地址和端口,例如192.168.1.128∶50043,然后去模块创建的红黑树中查找是否已经存在Key是192.168.1.128∶50043的数据项。如果找到就从红黑树中的这一项中取出对应的Value,此时的Value是一个有效的socket套接字,然后利用刚刚取出来的socket套接字,调用SendTo函数转发用户A发送过来的UDP数据包;如果从表中没有找到匹配的数据项,则从Nginx的连接池中取出一个有效的socket套接字,利用它转发UDP数据包向后台的游戏服务器,最后再将这个socket套接字和要转发的后台服务器的IP地址和端口一起组成一对Key:Value,保存到红黑树中,使得接下来的所有要发往这个IP地址和端口的UDP数据包可以完美地复用这个既有的socket套接字去转发数据,不需要再从连接池中重新取出一个全新的socket套接字来调用SendTo函数,从而提高了Nginx系统连接池中的socket套接字的使用效率,避免了连接池中的socket套接字很快被耗尽的尴尬。
本文采用的主要技术包括Nginx过滤模块的开发与调试、Nginx加载自定义过滤模块的编译与运行测试、Nginx反向代理服务器的配置与测试3部分。
过滤模块对外提供了一个netpas_udp_forward的配置指令,它是一个只接受on|off 作为参数的布尔值,意思是启用或关闭Nginx的UDP包转发功能。下面是一个配置的示例:
server
{
listen 999 udp;
netpas_udp_forward on;
proxy_pass 20.172.30.155∶998;
}
这个配置是让Nginx在本机的999商品帧听UDP连接请求,并开启UDP包转发功能,即启用UDP过滤模块的功能,然后将999端口接收到的UDP数据包转发给20.172.30.155的998端口,同时将20.172.30.155的998端口返回的UDP数据包转发回最初连接本机999端口的IP地址和端口上,实现UDP数据包的反向代理。
在优化之后的模型中,当Nginx框架加载到UDP转发模块的时候调用handler函数ngx_stream_netpas_udp_handler,将优化之前的模型中的Nginx框架的网络接收的事件处理函数ngx_event_recvmsg替换成ngx_netpas_event_recvmsg。
优化之前的模型中ngx_event_recvmsg函数是在Nginx内部实现的,它不停地在本地的UDP帧听端口上接收数据。当从帧听端口上接收到数据时,无论一次性接收到的数据大小是多少,Nginx都会马上调用公共的接口ngx_get_connection函数,从系统的连接池中取出一个连接结构体对像ngx_connection_t,利用这个连接结构体的socket成员变量向上游服务器进行转发。这将会导致系统连接池中的连接结构体对象很快被耗尽,后续的数据拿不到连接结构体对象,后续的数据报文因得不到处理而被丢弃。
优化之后的模型中过滤模块实现的ngx_netpas_event_recvmsg与ngx_event_recvmsg函数的主要区别是新增了如下功能:首先在红黑树中查找相同客户端IP地址和端口的连接结构体指针,如果找到了就复用它,否则再从系统的连接池中取一个来使用,这样就避免了连接结构体被用尽的情况。做法是:首先要新增一个全局的红黑树的链表数据结构,声明:static ngx_rbtree_t ngx_netpas_udp_connection_rbtree;红黑树要想正常工作还需要一个哨兵结点,这棵红黑树调用ngx_rbtree_init函数来初始化,它以红黑树指针、哨兵结点指针、插入函数指针作为参数。所有来自不同客户端IP地址和端口的,想要连接到Nginx帧听端口的UDP连接都会被保存在这棵红黑树上。因为红黑树是Key-Value结构的,可以把从系统连接池中获取的连接结构体对象和请求的客户端IP地址与端口信息做一个映射关系。具体的做法是:将请求的客户端IP地址和端口的字符串传入ngx_hash_key函数作参数,返回的结果是IP地址和端口对应的唯一的哈希值,用这个哈希值作为Key,用连接结构体指针作为Value,保存在全局的红黑树中。
为验证优化效果,分别使用优化之前的模型和优化之后的模型在Nginx下进行UDP数据包的转发,然后进行对比。
用Nginx做UDP的反向代理,将从本地端口3000上接收的UDP数据包转发到本地的3001端口上去,Nginx的配置文件nginx.conf中的listen 3000 udp,其功能是在本地的3000端口上监听UDP连接,而proxy_pass 127.0.0.1∶3001,功能是将本地端口3000上收到的数据包都转发到本机的3001端口上去[1]。
实验环境如下:
虚拟机:Oracle VM VirtualBox
操作系统: Ubuntu Kylin 15.10(64-bit)6G内存
Linux版本: Linux VirtualBox 4.2.0-16-generic #19-Ubuntu SMP UTC 2015 x86_64 GNU/Linux
Nginx版本: Nginx 1.11.6
Python版本: Python 2.7.10
GCC版本: gcc version 5.2.1 20151010
测试服务器使用Python语言实现一个server侦听在本地的3001端口,用于接收外部发送的UDP数据,test_server.py代码如下:
import socket
socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
sock.bind(′127.0.0.1′,3001)
print(′start server on [%s]:[%s]′%(′127.0.0.1′,3001))
while True:
data,addr=sock.recvfrom(1024)
print(′Received from %s:%s′ % (addr,data))
sock.sendto(′Hello,%s!′ % data,addr)
测试服务器程序的功能是将本地3001端口上收到的数据打印出来,并原包发回给发送端。
客户端的功能是向本机的3000端口上发送2000个UDP数据包,并打印回显的内容。
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
#for data in range(2000):
while True:
s.sendto(str(data).encode(),(′127.0.0.1′,
3000))
print (s.recv(1024).decode(′utf-8′))
先运行服务器,再运行客户端。图5为优化之前的测试结果,测试客户端只成功发送了511个数据包,而没有转发2000个测试数据包。可见,在超出511个数据包的大流量数据的情况下,其中大部分数据包被丢弃了。
图5 优化前测试结果
优化之后的基于Nginx的UDP反向代理服务器测试效果如下:优化之后的模型从最大转发511个数据包,提高到有效支撑大流量下全部转发2000个数据包,而且2000个数据包都能够正确、稳定地被传输并由服务器返回。
为验证优化之后的模型在大数据流量下的UDP包转发的性能,实验设置客户端一直不间断向服务器发送数据包,5 min后观察实验结果如图6所示。
图6 优化后测试结果
从图6可以看出,系统连接池没有被快速耗尽,数据转发量远远高于优化之前的模型中的转发量511条。数据达到36万时仍然能够正确转发,没有出现丢包现象,使得UDP数据的转发问题不再成为Nginx对UDP数据包反向代理的瓶颈,能够充分发挥Nginx的反向代理功能,实现正确转发百万级UDP数据包,有效支撑大流量、长时间的UDP业务数据的反向代理。
本文针对Nginx的UDP反向代理服务器的实现,使用新增哈希表的数据转发策略对其做出了改进。优化之前的模型,每次数据转发都需要从连接池中取出一个新的socket套接字,当数据转发请求数量大于连接池内socket套接字的数量时,会因获取不到socket套接字而发生丢包现象;优化之后的模型主要利用Nginx的高级数据结构红黑树ngx_rbtree_t,实现了上下游服务器的地址端口映射关系的哈希数据结构,缓存UDP的连接池,复用UDP的端口连接,有效解决了系统连接池被快速耗尽的现象,优化了UDP的处理流程,保证了请求与响应数据的稳定、高效、可靠的传输,满足大流量、长时间的UDP业务数据的反向代理。