社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  NGINX

一文看懂 Nginx 架构

Linux爱好者 • 5 年前 • 710 次点击  

导语 | 最近使用了基于Nginx的OpenResty的框架,于是对Nginx相关内容进行了学习,现将一些理解撰写成文,和大家探讨。


一、Nginx基础架构


Nginx启动后以daemon形式在后台运行,后台进程包含一个master进程和多个worker进程。如下图所示:



Nginx是由一个master管理进程,多个worker进程处理工作的多进程模型。基础架构设计,如下图所示:



Master负责管理worker进程,worker进程负责处理网络事件。整个框架被设计为一种依赖事件驱动、异步、非阻塞的模式


如此设计的优点有:


  • 可以充分利用多核机器,增强并发处理能力。

  • 多worker间可以实现负载均衡。

  • Master监控并统一管理worker行为。在worker异常后,可以主动拉起worker进程,从而提升了系统的可靠性。并且由Master进程控制服务运行中的程序升级、配置项修改等操作,从而增强了整体的动态可扩展与热更的能力。


二、Master进程


1. 核心逻辑


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 (delay) {...}
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, "wake up, sigio %i", sigio);
if (ngx_reap) { ngx_reap = 0; ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "reap children"); 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进程,具体包括如下4个主要功能:


1)接受来自外界的信号。其中master循环中的各项标志位就对应着各种信号,如:ngx_quit代表QUIT信号,表示优雅的关闭整个服务。


2)向各个worker进程发送信。比如ngx_noaccept代表WINCH信号,表示所有子进程不再接受处理新的连接,由master向所有的子进程发送QUIT信号量。


3)监控worker进程的运行状态。比如ngx_reap代表CHILD信号,表示有子进程意外结束,这时需要监控所有子进程的运行状态,主要由ngx_reap_children完成。


4)当woker进程退出后(异常情况下),会自动重新启动新的woker进程。主要也是在ngx_reap_children。


2. 热更


1)热重载-配置热更

Nginx热更配置时,可以保持运行中平滑更新配置,具体流程如下:


  • 更新nginx.conf配置文件,向master发送SIGHUP信号或执行nginx -s reload
  • Master进程使用新配置,启动新的worker进程
  • 使用旧配置的worker进程,不再接受新的连接请求,并在完成已存在的连接后退出


2)热升级-程序热更


Nginx热升级过程如下:



  • 将旧Nginx文件换成新Nginx文件(注意备份)
  • 向master进程发送USR2信号(平滑升级到新版本的Nginx程序)
  • master进程修改pid文件号,加后缀.oldbin
  • master进程用新Nginx文件启动新master进程,此时新老master/worker同时存在。
  • 向老master发送WINCH信号,关闭旧worker进程,观察新worker进程工作情况。若升级成功,则向老master进程发送QUIT信号,关闭老master进程;若升级失败,则需要回滚,向老master发送HUP信号(重读配置文件),向新master发送QUIT信号,关闭新master及worker。


三、Worker进程


1. 核心逻辑


Worker进程的主逻辑在ngx_worker_process_cycle,核心关注源码:


ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data){    ngx_int_t worker = (intptr_t) data;

ngx_process = NGX_PROCESS_WORKER; ngx_worker = worker;

ngx_worker_process_init(cycle, worker);

ngx_setproctitle("worker process");

for ( ;; ) {

if (ngx_exiting) {...}

ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "worker cycle");

ngx_process_events_and_timers(cycle);

if (ngx_terminate) {...}

if (ngx_quit) {...}

if (ngx_reopen) {...} }}


由上述代码,可以理解,worker进程主要在处理网络事件,通过ngx_process_events_and_timers方法实现,其中事件主要包括:网络事件、定时器事件。


2. 事件驱动-epoll


Worker进程在处理网络事件时,依靠epoll模型,来管理并发连接,实现了事件驱动、异步、非阻塞等特性。如下图所示:



通常海量并发连接过程中,每一时刻(相对较短的一段时间),往往只需要处理一小部分有事件的连接即活跃连接。基于以上现象,epoll通过将连接管理与活跃连接管理进行分离,实现了高效、稳定的网络IO处理能力。



其中,epoll利用红黑树高效的增删查效率来管理连接,利用一个双向链表来维护活跃连接。



3. 惊群


由于worker都是由master进程fork产生,所以worker都会监听相同端口。这样多个子进程在accept建立连接时会发生争抢,带来著名的“惊群”问题。


Worker核心处理逻辑ngx_process_events_and_timers核心代码如下:


void ngx_process_events_and_timers(ngx_cycle_t *cycle){    //这里面会对监听socket处理    ...
if (ngx_accept_disabled > 0) { ngx_accept_disabled--; } else { //获得锁则加入wait集合, 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); //释放锁,让其他进程也能够拿到 if (ngx_accept_mutex_held) { ngx_shmtx_unlock(&ngx_accept_mutex); } //处理网络读写事件 ngx_event_process_posted(cycle, &ngx_posted_events);}


由上述代码可知,Nginx解决惊群的方法:


1)将连接事件与读写事件进行分离。连接事件存放为ngx_posted_accept_events,读写事件存放为ngx_posted_events。


2)设置ngx_accept_mutex锁,只有获得锁的进程,才可以处理连接事件。


4. 负载均衡


Worker间的负载关键在于各自接入了多少连接,其中接入连接抢锁的前置条件是ngx_accept_disabled > 0,所以ngx_accept_disabled就是负载均衡机制实现的关键阈值。


ngx_int_t             ngx_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,直到其值低于阈值时,才试图重新处理新的连接。


因此,nginx各worker子进程间的负载均衡仅在某个worker进程处理的连接数达到它最大处理总数的7/8时才会触发,其负载均衡并不是在任意条件都满足。如下图所示:



其中'pid'为1211的进程为master进程,其余为worker进程。


四、思考


1. 为什么不采用多线程模型管理连接?


  • 无状态服务,无需共享进程内存
  • 采用独立的进程,可以让互相之间不会影响。一个进程异常崩溃,其他进程的服务不会中断,提升了架构的可靠性。
  • 进程之间不共享资源,不需要加锁,所以省掉了锁带来的开销。


2. 为什么不采用多线程处理逻辑业务?


  • 进程数已经等于核心数,再新建线程处理任务,只会抢占现有进程,增加切换代价。
  • 作为接入层,基本上都是数据转发业务,网络IO任务的等待耗时部分,已经被处理为非阻塞/全异步/事件驱动模式,在没有更多CPU的情况下,再利用多线程处理,意义不大。并且如果进程中有阻塞的处理逻辑,应该由各个业务进行解决,比如openResty中利用了Lua协程,对阻塞业务进行了优化。


- EOF -

推荐阅读  点击标题可跳转

1、万字整理,肝翻 Linux 内存管理所有知识点

2、Linux 进程管理之调度和进程切换

3、Linux 系统结构详解


看完本文有收获?请分享给更多人

推荐关注「Linux 爱好者」,提升Linux技能

点赞和在看就是最大的支持❤️

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/115659