原文作者:皮皮鲁
原文链接: NGINX Stream模块原理及代码分析 - NGINX开源社区
转载来源:NGINX开源社区
从1.9.0开始,NGINX增加了stream模块用来实现四层协议的转发、代理和负载均衡。与著名的四层LB软件lvs相比,stream 模块(开源版)无论从功能还是性能上,都有一定的差距,实现也相对简单。
从性能上来说,stream模块在应用层实现四层的转发,需要与两端建立起socket连接,然后两端的数据收发进行代理转发。因此,大量的数据从内核态到用户态再从用户态到内核态传递。这些数据copy加上系统调度的开销,使得它的性能与纯内核态转发的lvs相比,有一定差距。
从功能方面看,stream模块对很多协议的alg功能几乎没有支持。这样需要alg支持的协议,比如sip, port模式的ftp等, stream模块没有很好的支持。
正是因为stream模块的这种相对简单,给了我们一个窥视它完整实现机理的好机会。
我们将试着用一个Linux平台下的dns负载均衡的例子结合下图中描述的stream模块的主要数据结构去分析如下的代码场景。
一个dns 的stream的连接流程是怎样从图中上侧小的红色方框中的socket监听句柄被接收,一直到下侧红色方框中所有的stream相关子模块的handler函数被调用。
在这个过程中,上游的dns服务是如何通过什么样的负载均衡算法被选中进行服务的。
数据是如何从client到NGINX然后又怎么样从NGINX转发到upstream的某一个server中去的。
下面分析中所有相关代码都是基于我们这一dns场景假设。
在用户态做四层的proxy,从原理上讲主要有如下步骤:
创建一个socket 监听client连接。
Client连接到来以后,使用load balancer算法选取一个正确的upstream 服务器并且建立socket连接。
把从client收到的数据转发给upstream server,同时把upstream server收到的数据转发给client。
与上面逻辑类似,NGINX stream模块的大体逻辑如下图所示。
首先NGINX承担服务器角色和下游的client建立里连接。
然后承担客户端的角色,根据自己的配置从上游服务器中通过特定的负载均衡算法选取一个服务器建立连接。
两端连接建立以后,NGINX就把一端收到的数据发送到另外一端从而实现了代理功能。
在内核态实现四层的proxy,不需要创建listening socket,也不需要与远端的服务器建立socket连接。所有的数据包都在内核态进行转发。但也正是因为如此,大多数情况下,内核态的四层proxy需要做NAT。
在系统启动阶段,每一个模块都调用函数负责解析与自己模块相关的配置。stream模块对应的函数就是ngx_stream_block,它主要完成以下的工作。
1)子模块配置文件解析
在ngx_stream_block函数的最开始,需要生成stream模块本身需要的context。然后调用各个子模块的create_main_conf, create_srv_conf, preconfigureation, init_main_conf以及merge_srv_conf回调函数生成和初始化各个子模块的所需的配置信息。并把这些信息存放在stream模块的context中。
同时在解析过程中,各个子模块所有的配置指令都有相对于的回调函数进行处理。比如proxy子模块在处理proxy_pass指令时,就会把upstream和正在解析的server关联起来。在比如在解析upstream时,会根据配置把upstream和它针对内部服务器采用的负载均衡算法关联起来。
整个解析过程完成以后,与stream模块所有的listening socket,配置的全部server, upstream已经对某一个upstream内部所有的server采用的负载均衡的算法都会被解析并且相互关联起来,为数据层面运行提供支持。
2)子模块处理函数组织
和HTTP模块类似,stream模块把所有的这子模块安装处理的流程分成了如下7个阶段。
NGX_STREAM_POST_ACCEPT_PHASE = 0, NGX_STREAM_PREACCESS_PHASE, NGX_STREAM_ACCESS_PHASE, NGX_STREAM_SSL_PHASE, NGX_STREAM_PREREAD_PHASE, NGX_STREAM_CONTENT_PHASE, NGX_STREAM_LOG_PHASE
所有的stream子模块如下,我们可以发现,很多HTTP模块对应的子模块,都能在stream模块中找到。
"ngx_stream_module", "ngx_stream_core_module", "ngx_stream_log_module", "ngx_stream_proxy_module", "ngx_stream_upstream_module", "ngx_stream_write_filter_module", "ngx_stream_ssl_module", "ngx_stream_limit_conn_module", "ngx_stream_access_module", "ngx_stream_geo_module", "ngx_stream_map_module", "ngx_stream_split_clients_module", "ngx_stream_return_module", "ngx_stream_upstream_hash_module", "ngx_stream_upstream_least_conn_module", "ngx_stream_upstream_random_module", "ngx_stream_upstream_zone_module",
函数ngx_stream_block调用ngx_stream_init_phase_handlers初始化结构体phase_engine用来存放stream所有的子模块处理函数。这些处理函数组成了stream模块处理的核心引擎。
在ngx_stream_block完成各个子模块的配置解析后,再调用各个子模块的postconfiguration函数把自身的handler函数存放到对应的handler数组中。这样在每一个stream连接处理的过程中,可以按照特定的逻辑处理phase_engine中存放的各个模块的handler进行处理。
3)创建listening socket
在解析配置文件的过程中,会生成各个server对应的listening socket.并且把所有的这些socket添加到全局的listening socket列表里面。
在event 模块的初始化函数ngx_event_proces_init中,对于系统的每一个listening socket,会首先通过ngx_get_connection函数得到一个connection结构体,用来处理和客户端的连接。同时把此listening socket和这connection结构体连接起来。然后把connection自身的读事件的回调函数设置为ngx_event_recvmsg。最后把此事件添加到全局的poll红黑树中。当一个连接到达此listening socket时,ngx_event_recvmsg函数就会被调用。
在ngx_stream_block函数的最后会调用ngx_stream_optimize_servers函数把listening socket的回调函数设置为ngx_stream_init_connection。 它会在listening socket的新连接处理函数ngx_event_recvmsg中被调用。
当ngx_stream_block执行完毕,整个的stream模块相关的控制平面的数据结构已经搭建好。
1) 新连接处理
当一个listening socket收到数据以后,操作系统通知epoll返回,对应listening socket的event回调函数ngx_event_recvmsg就会被调用。在ngx_event_recvmsg函数中,对应的listening socket的处理函数, ngx_stream_init_connection会被调用,至此就进入了stream的数据处理阶段。
2)函数ngx_stream_init_connection逻辑
在ngx_stream_init_connection函数中,会生成一个重要的数据结构,ngx_stream_session_t,并且与对应客户端的连接的ngx_connection_t结构连接起来。然后把这一客户端的连接相关的读事件回调函数设置为ngx_stream_session_handler。然后执行这个回调函数。
3)函数ngx_stream_session_handler逻辑
在ngx_stream_session_handler中,主要任务是调用ngx_stream_core_run_phases执行各个stream子模块的回调函数。在我们这个dns 的例子中,ngx_stream_limit_conn_handler, ngx_stream_access_handler,ngx_stream_proxy_handler依次被调用。其中ngx_stream_proxy_handler主要完成和选定上游的server并且进行连接通信的工作。
4)函数ngx_stream_proxy_handler逻辑
在ngx_stream_proxy_handler函数中,会生成一个ngx_stream_upstream_t结构挂接到ngx_stream_session_t结构的upstream成员中。在这个ngx_stream_upstream_t的结构中,存放着与下游客户端和上游服务器两侧的收发缓冲区。其中针对下游客户端的收发缓冲区就是在这个函数中生成并且连接到ngx_stream_upstream_t这个结构中的。
设置ngx_stream_session_t结构的upstream成员。此upstream成员是在启动过程中,通过配置解析获得的,在解析过程中,同时把server和对应的upstream连接起来。
把listening_socket对应的与客户端连接的ngx_connection_t结构的读写事件的回调函数设置为ngx_stream_proxy_downstream_handler。
至此,于此listening_socket对应的客户端连接的ngx_connection_t的读事件回调函数第三次被改变。当下游的client与NGINX有数据交互时,此函数就会被调用。
调用upstream的peer的init函数进行loadbalance算法选择对应的上游server。在我们的这个例子中,我们配置了默认的round robin算法,此时对应的init函数就是ngx_stream_upstream_init_round_robin_peer 。在这个函数中,设置ngx_upstream_session_t结构的peer对应的回调函数。 分别设置get,free,notify为ngx_stream_upstream_get_round_robin_peer ngx_stream_upstream_free_round_robin_peer ngx_stream_upstream_notify_round_robin_peer 。
设置完成以后,调用ngx_stream_proxy_connect来连接上游服务器。
5) 函数ngx_stream_proxy_connect逻辑
在函数ngx_stream_proxy_connect函数中,在get回调函数中调用ngx_stream_upstream_get_peer函数。此函数是upstream模块执行loadbalance的核心。它的主要功能是选取最合适的上游服务器。
获得upstream server以后,调用网络编程的API创建socket并且connect到upstream server。同时为upstream server的通信分配一个ngx_connection_t。这个结构体对应的就是NGINX与上游服务器之间的连接。
设置与上游服务器的connection的读写事件的handler为ngx_udp_recv(ugx_udp_unix_recv) 和ngx_send (ngx_unix_send) 。
把connection的读写事件的handler函数设置为ngx_stream_proxy_connect_handler 。
当upstream 服务器有读写事件时,ngx_stream_proxy_connect_handler就会得到调用。
6)函数ngx_stream_proxy_connect_handler逻辑
调用ngx_stream_proxy_init_upstream函数用来初始化与upstream server的连接。其中包括,设置接收和发送缓冲器,设置upstream server对应的ngx_connection_t的读写事件的回调函数设置为ngx_stream_proxy_upstream_handler。
调用ngx_stream_proxy_process处理downstream和upstream服务器之间的数据proxy。
至此,stream模块针对下游的客户端的连接和针对上游服务器的连接都已经建立起来。两端连接的读写函数也分别设置成了 ngx_stream_proxy_downstream_handler 和ngx_stream_proxy_upstream_handler 。当两侧连接有收发数据时,相应的函数就得到调用。这侧的调用函数本质上是调用ngx_stream_proxy_process_connection 然后调用ngx_stream_proxy_process。
当从一侧收到数据时,stream模块把数据读入缓冲区然后写入另一侧的写缓冲区进行发送。
上述的代码流程分析是在学习代码的过程中做的笔记。文章写得主观性比较强,更像是辅助自己理解代码的文章。希望对大家也能有帮助。
想要更及时全面地获取NGINX相关的技术干货、互动问答、系列课程、活动资源?请前往NGINX开源社区官方网站 。