图片来自宝途网Nginx基础设施启动后,Nginx作为守护进程在后台运行。后台进程包括一个master进程和多个worker进程。如下图:master和workerNginx是一个master管理进程,多个worker进程处理工作的多进程模型。Infrastructuredesign,如下图所示:infrastructuredesignmaster负责管理worker进程,worker进程负责处理网络事件。整个框架设计为事件驱动、异步、非阻塞模式。这种设计的优点:可以充分利用多核机器,增强并发处理能力。多个worker之间可以实现负载均衡。Master对worker行为进行统一监控和管理。worker异常后,可以主动拉起worker进程,从而提高系统的可靠性。此外,Master进程控制服务运行过程中的程序升级、配置项修改等操作,从而增强整体的动态扩展和热更新能力。Master进程①核心逻辑Master进程的主要逻辑在ngx_master_process_cycle中,核心重点源码:ngx_master_process_cycle(ngx_cycle_t*cycle){...ngx_start_worker_processes(cycle,ccf->worker_processes,NGX_PROCESS_RESPAWN);...for(;;){if(延迟){...}ngx_log_debug0(NGX_LOG_DEBUG_EVENT,cycle->log,0,"sigsuspend");sigsuspend(&set);ngx_time_update();ngx_log_debug1(NGX_LOG_DEBUG_EVENT,cycle->log,0,"唤醒,sigio%i",sigio);if(ngx_reap){ngx_reap=0;ngx_log_debug0(NGX_LOG_DEBUG_EVENT,cycle->log,0,"reapchildren");live=ngx_reap_children(cycle);}if(!live&&(ngx_terminate||ngx_quit)){。..}if(ngx_terminate){...}if(ngx_quit){...}if(ngx_reconfigure){...}if(ngx_restart){...}if(ngx_reopen){...}if(ngx_change_binary){...}if(ngx_noaccept){ngx_noaccept=0;ngx_noaccepting=1;ngx_signal_worker_processes(cycle,ngx_signal_value(NGX_SHUTDOWN_SIGNAL));}}}从上面的代码可以理解master进程主要是用来管理worker进程,具体包括以下四个主要功能:接受来自外界的信号。mastercycle中的flags对应着各种信号,比如:ngx_quit代表QUIT信号,表示整个服务优雅关闭。向每个工作进程发送一封信。例如ngx_noaccept代表WINCH信号,表示所有子进程不再接受和处理新的连接,master向所有子进程发送QUIT信号量。监控工作进程的运行状态。比如ngx_reap代表CHILD信号,表示一个子进程意外结束。这时候就需要监控所有子进程的运行状态,主要由ngx_reap_children完成。当woker进程退出时(异常情况下),会自动重启一个新的woker进程。主要也是在ngx_reap_children。②热更新、hotreload-configuration热更新:hotreloadNginx热更新配置时,可以保持运行并平滑更新配置。具体过程如下:更新nginx.conf配置文件,向master发送SIGHUP信号或者执行nginx-sreloadmaster进程使用新配置,启动新的worker进程,使用旧配置worker进程,nolonger接受新的连接请求,完成现有的连接-程序热升级后退出热升级:热升级Nginx热升级流程如下:用新的Nginx文件替换旧的Nginx文件(注意备份)发送USR2信号到master进程(顺利升级到新版本的Nginx程序)master进程修改pid文件号,加上后缀.oldbinmaster进程使用新的Nginx文件启动一个新的master进程,新旧master/worker同时存在。向旧master发送WINCH信号,关闭旧worker进程,观察新worker进程的工作情况。如果升级成功,向旧master进程发送QUIT信号,关闭旧master进程;如果升级失败,需要回滚,向旧master发送HUP信号(重新读取配置文件),向新master发送QUIT信号,关闭新master和worker。Worker进程①核心逻辑Worker进程的主要逻辑在ngx_worker_process_cycle中,核心关注源码:ngx_worker_processwork_init);(循环,ngx_setproctitle("workerprocess");for(;;){if(ngx_exiting){...}ngx_log_debug0(NGX_LOG_DEBUG_EVENT,cycle->log,0,"workercycle");ngx_process_events_and_timers(cycle);if(ngx_terminate){...}if(ngx_quit){...}if(ngx_reopen){...}}}从上面的代码可以理解worker进程主要是处理网络事件,实现通过ngx_process_events_and_timers方法,事件主要包括:网络事件,定时器事件。blocking等特性,如下图:图reinfographic-Inside-NGINX_nonblocking通常在海量并发连接的过程中,在每个时刻(一个比较短的时间段),往往只有少量有事件的连接需要处理,即活跃连接。基于以上现象,epoll通过将连接管理与主动连接管理分离,实现了高效稳定的网络IO处理能力。网络模型对比其中,epoll利用红黑树高效的增删改查效率来管理连接,使用双向链表维护活动连接。epoll数据结构③惊群由于worker都是master进程的fork产生的,worker都会监听同一个端口。这样,当accept建立连接时,多个子进程就会发生争抢,从而产生著名的“shockinggroup”问题。worker核心处理逻辑ngx_process_events_and_timers核心代码如下:voidngx_process_events_and_timers(ngx_cycle_t*cycle){//这里会处理监听的socket...if(ngx_accept_disabled>0){ngx_accept_disabled--;}else{//获取锁和加入waitset,if(ngx_trylock_accept_mutex(cycle)==NGX_ERROR){return;}...//设置网络读写事件延迟处理标志,即释放锁后处理if(ngx_accept_mutex_held){flags|=NGX_POST_EVENTS;}}...//这里epollwait等待网络事件//网络连接事件,放入ngx_posted_accept_events队列//网络读写事件,放入ngx_posted_events队列(void)ngx_process_events(cycle,timer,flags);...//先处理网络连接事件,只有获取到锁后,才会有连接事件ngx_event_process_posted(cycle,&ngx_posted_accept_events);//释放锁,让其他进程也能拿到ngx_shmtx_unlock(&ngx_accept_mutex);}//处理网络读写事件ngx_event_process_posted(cycle,&ngx_posted_events);}从上面的代码可以看出,Nginx通过将连接事件和读写事件分离来解决惊群。连接事件存储为ngx_posted_accept_events,读写事件存储为ngx_posted_events。设置ngx_accept_mutex锁,只有获得锁的进程才能处理连接事件。④worker之间负载均衡的关键是有多少connections相互连接。访问连接锁的前提是ngx_accept_disabled>0,所以ngx_accept_disabled是负载均衡机制实现的关键阈值。ngx_int_tngx_accept_disabled;ngx_accept_disabled=ngx_cycle->connection_n/8-ngx_cycle->free_connection_n;所以nginx启动时,ngx_accept_disabled的值为负数,其值为总连接数的7/8。当进程的连接数达到总连接数的7/8时,进程将不再处理新的连接。同时,每次调用'ngx_process_events_and_timers'时,ngx_accept_disabled都会减1,直到其值低于阈值,然后再尝试重新处理新的连接。因此,只有当一个worker进程处理的连接数达到其最大连接总数的7/8时,才会触发Nginxworker子进程之间的负载均衡,并且在任何情况下都不满足其负载均衡。如下图:实际工作情况,pid为1211的进程为master进程,其余为worker进程。想想为什么不用多线程模型来管理连接呢?①无状态服务,不需要共享进程内存。②使用独立进程可以防止相互影响。如果一个进程异常崩溃,其他进程的服务不会中断,提高了架构的可靠性。③进程不共享资源,不需要加锁,省去了加锁带来的开销。为什么不用多线程来处理逻辑业务呢?①进程数已经等于核心数,再创建新线程处理任务只会抢占已有进程,增加切换成本。②作为接入层,基本是数据转发业务。网络IO任务的等待耗时部分已经采用非阻塞/全异步/事件驱动的方式处理。在没有更多CPU的情况下,用多线程处理,没有意义。而如果流程中有阻塞处理逻辑,则需要各业务自行解决。例如,OpenResty使用Lua协程来优化阻塞业务。作者:handyli,腾讯IEG后台开发工程师编辑:陶家龙来源:转载自公众号腾讯技术工程(ID:Tencent_TEG)
