从1.9.0开始,Nginx增加了stream模块用来实现四层协议的转发、代理和负载均衡。与著名的四层LB软件lvs相比,stream 模块(开源版)无论从功能还是性能上,都有一定的差距,实现也相对简单。
从性能上来说,stream模块在应用层实现四层的转发,需要与两端建立起socket连接,然后两端的数据收发进行代理转发。因此,大量的数据从内核态到用户态再从用户态到内核态传递。这些数据copy加上系统调度的开销,使得它的性能与纯内核态转发的lvs相比,有一定差距。
从功能方面看,stream模块对很多协议的alg功能几乎没有支持。这样需要alg支持的协议,比如sip, port模式的ftp等, stream模块没有很好的支持。
正是因为stream模块的这种相对简单,给了我们一个窥视它完整实现机理的好机会。
我们将试着用一个Linux平台下的dns负载均衡的例子结合下图中描述的stream模块的主要数据结构去分析如下的代码场景。
下面分析中所有相关代码都是基于我们这一dns场景假设。
在用户态做四层的proxy,从原理上讲主要有如下步骤:
与上面逻辑类似,Nginx stream模块的大体逻辑如下图所示。
在内核态实现四层的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逻辑
5) 函数ngx_stream_proxy_connect逻辑
6) 函数ngx_stream_proxy_connect_handler逻辑
当从一侧收到数据时,stream模块把数据读入缓冲区然后写入另一侧的写缓冲区进行发送。
上述的代码流程分析是在学习代码的过程中做的笔记。文章写得主观性比较强,更像是辅助自己理解代码的文章。希望对大家也能有帮助。