nginx架構模型分析

1、Nginx架構


Nginx由內核和模塊組成,從官方文檔http://nginx.org/en/docs/下的Modules reference能夠看到一些比較重要的模塊,通常分爲核心、基礎模塊以及第三方模塊。nginx

第三方模塊意味着你也能夠按照nginx標準去開發符合本身業務的模塊插件。web

核心主要用於提供Web Server的基本功能,以及Web和Mail反向代理的功能;還用於啓用網絡協議,建立必要的運行時環境以及確保不一樣的模塊之間平滑地進行交互。sql

不過,大多跟協議相關的功能和某應用特有的功能都是由nginx的模塊實現的。這些功能模塊大體能夠分爲:事件模塊、階段性處理器、輸出過濾器、變量處理器、協議、upstream和負載均衡幾個類別,這些共同組成了nginx的http功能。事件模塊主要用於提供OS獨立的(不一樣操做系統的事件機制有所不一樣)事件通知機制如kqueue或epoll等。協議模塊則負責實現nginx經過http、tls/ssl、smtp、pop3以及imap與對應的客戶端創建會話。在Nginx內部,進程間的通訊是經過模塊的pipeline或chain實現的;換句話說,每個功能或操做都由一個模塊來實現。例如,壓縮、經過FastCGI或uwsgi協議與upstream服務器通訊,以及與memcached創建會話等。apache

2、nginx進程模型


2.1 多進程模型

Nginx之因此爲廣大碼農喜好,除了其高性能外,還有其優雅的系統架構。與Memcached的經典多線程模型相比,Nginx是經典的多進程模型。Nginx啓動後以daemon的方式在後臺運行,後臺進程包含一個master進程和多個worker進程,具體以下圖:編程

2.2 多進程模型的好處 對於每一個worker進程來講,獨立的進程,不須要加鎖,因此省掉了鎖帶來的開銷,同時在編程以及問題查找時,也會方便不少。其次,採用獨立的進程,可讓互相之間不會影響,一個進程退出後,其它進程還在工做,服務不會中斷,master進程則很快啓動新的worker進程,而且獨立的進程,可讓互相之間不會影響,一個進程退出後,其它進程還在,以上也是Nginx高效的另外一個緣由了。bash

2.3 master與worker功能

2.3.1 master進程主要用來管理worker進程,具體包括以下4個主要功能服務器

  • 接收來自外界的信號。
  • 向各worker進程發送信號。
  • 監控woker進程的運行狀態。
  • 當woker進程退出後(異常狀況下),會自動從新啓動新的woker進程。

2.3.2 woker進程主要用來處理基本的網絡事件網絡

  • 多個worker進程之間是對等且相互獨立的,他們同等競爭來自客戶端的請求。
  • 一個請求,只可能在一個worker進程中處理,一個worker進程,不可能處理其它進程的請求。
  • worker進程的個數是能夠設置的,通常咱們會設置與機器cpu核數一致。更多的worker數,只會致使進程來競爭cpu資源了,從而帶來沒必要要的上下文切換。並且,nginx爲了更好的利用多核特性,具備cpu綁定選項,咱們能夠將某一個進程綁定在某一個核上,這樣就不會由於進程的切換帶來cache的失效。

3、進程控制方式


對Nginx進程的控制主要是經過master進程來作到的,主要有兩種方式:多線程

3.1 手動發送信號

從圖1能夠看出,master接收信號以管理衆woker進程,那麼,能夠經過kill向master進程發送信號,好比kill -HUP pid用以通知Nginx從容重啓。所謂從容重啓就是不中斷服務:master進程在接收到信號後,會先從新加載配置,而後再啓動新進程開始接收新請求,並向全部老進程發送信號告知再也不接收新請求並在處理完全部未處理完的請求後自動退出。架構

3.2 自動發送信號

能夠經過帶命令行參數啓動新進程來發送信號給master進程,好比./nginx -s reload用以啓動一個新的Nginx進程,而新進程在解析到reload參數後會向master進程發送信號(新進程會幫咱們把手動發送信號中的動做自動完成)。固然也能夠這樣./nginx -s stop來中止Nginx。

4、守護線程 daemon


4.1 守護線程

nginx在啓動後,在unix系統中會以daemon的方式在後臺運行,後臺進程包含一個master進程和多個worker進程。固然nginx也是支持多線程的方式的,只是咱們主流的方式仍是多進程的方式,也是nginx的默認方式。

5、網絡事件模塊


Nginx(多進程)採用異步非阻塞的方式來處理網絡事件,相似於Libevent(單進程單線程),具體過程以下圖:

master進程先建好須要listen的socket後,而後再fork出多個woker進程,這樣每一個work進程均可以去accept這個socket。當一個client鏈接到來時,全部accept的work進程都會受到通知,但只有一個進程能夠accept成功,其它的則會accept失敗。Nginx提供了一把共享鎖accept_mutex來保證同一時刻只有一個work進程在accept鏈接,從而解決驚羣問題。當一個worker進程accept這個鏈接後,就開始讀取請求,解析請求,處理請求,產生數據後,再返回給客戶端,最後才斷開鏈接,這樣一個完成的請求就結束了。

對於一個基本的web服務器來講,事件一般有三種類型,網絡事件、信號、定時器。從上面的講解中知道,網絡事件經過異步非阻塞能夠很好的解決掉。如何處理信號與定時器?

  • 信號的處理。(待補充)
  • 定時器。nginx裏面的定時器事件是放在一顆維護定時器的紅黑樹裏面,每次在進入epoll_wait前,先從該紅黑樹裏面拿到全部定時器事件的最小時間,在計算出epoll_wait的超時時間後進入epoll_wait。(待補充)

6、驚羣現象


6.1 什麼事驚羣現象

驚羣簡單來講就是多個進程或者線程在等待同一個事件,當事件發生時,全部線程和進程都會被內核喚醒。喚醒後一般只有一個進程得到了該事件並進行處理,其餘進程發現獲取事件失敗後又繼續進入了等待狀態,在必定程度上下降了系統性能。具體來講驚羣一般發生在服務器的監聽等待調用上,服務器建立監聽socket,後fork多個進程,在每一個進程中調用accept或者epoll_wait等待終端的鏈接。

6.2 nginx的驚羣現象

每一個worker進程都是從master進程fork過來。在master進程裏面,先創建好須要listen的socket之 後,而後再fork出多個worker進程,這樣每一個worker進程均可以去accept這個socket(固然不是同一個socket,只是每一個進程 的這個socket會監控在同一個ip地址與端口,這個在網絡協議裏面是容許的)。通常來講,當一個鏈接進來後,全部在accept在這個socket上 面的進程,都會收到通知,而只有一個進程能夠accept這個鏈接,其它的則accept失敗。

6.3 nginx如何處理驚羣

內核解決epoll的驚羣效應是比較晚的,所以nginx自身解決了該問題(更準確的說是避免了)。其具體思路是:不讓多個進程在同一時間監聽接受鏈接的socket,而是讓每一個進程輪流監聽,這樣當有鏈接過來的時候,就只有一個進程在監聽那確定就沒有驚羣的問題。具體作法是:利用一把進程間鎖,每一個進程中都嘗試得到這把鎖,若是獲取成功將監聽socket加入wait集合中,並設置超時等待鏈接到來,沒有得到所的進程則將監聽socket從wait集合去除。這裏只是簡單討論nginx在處理驚羣問題基本作法,實際其代碼還處理了不少細節問題,例如簡單的鏈接的負載均衡、定時事件處理等等。 核心的代碼以下

void ngx_process_events_and_timers(ngx_cycle_t *cycle){
	...
	    //這裏面會對監聽socket處理
	//一、得到鎖則加入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);
}
複製代碼

7、相對於線程,採用進程的優勢


  • 進程之間不共享資源,不須要加鎖,因此省掉了鎖帶來的開銷。
  • 採用獨立的進程,可讓互相之間不會影響,一個進程退出後,其它進程還在工做,服務不會中斷,master進程則很快從新啓動新的worker進程。
  • 編程上更加容易。

8、多線程的問題


而多線程在多併發狀況下,線程的內存佔用大,線程上下文切換形成CPU大量的開銷。想一想apache的經常使用工做方式(apache 也有異步非阻塞版本,但因其與自帶某些模塊衝突,因此不經常使用),每一個請求會獨佔一個工做線程,當併發數上到幾千時,就同時有幾千的線程在處理請求了。這對 操做系統來講,是個不小的挑戰,線程帶來的內存佔用很是大,線程的上下文切換帶來的cpu開銷很大,天然性能就上不去了,而這些開銷徹底是沒有意義的。

9、異步非阻塞


9.1. 什麼是異步?

異步的概念和同步相對的,也就是否是事件之間不是同時發生的。

9.2 什麼事阻塞非阻塞?

非阻塞的概念是和阻塞對應的,阻塞是事件按順序執行,每一事件都要等待上一事件的完成,而非阻塞是若是事件沒有準備好,這個事件能夠直接返回,過一段時間再進行處理詢問,這期間能夠作其餘事情。可是,屢次詢問也會帶來額外的開銷。

9.3 Nginx採用異步非阻塞的好處?

  • 不須要建立線程,每一個請求只佔用少許的內存
  • 沒有上下文切換,事件處理很是輕量

淘寶tengine團隊說測試結果是「24G內存機器上,處理併發請求可達200萬」。

10、libevent 概覽


支持Libevent運轉的就是一個大循環,這個主循環體如今event_base_loop(Event.c/1533)函數裏,該函數的執行流程以下:

上圖的簡單描述就是:

  • 校訂系統當前時間。
  • 將當前時間與存放時間的最小堆中的時間依次進行比較,將全部時間小於當前時間的定時器事件從堆中取出來加入到活動事件隊列中。
  • 調用I/O封裝(好比:Epoll)的事件分發函數dispatch函數,以當前時間與時間堆中的最小值之間的差值(最小堆取最小值複雜度爲O(1))做爲Epoll/epoll_wait(Epoll.c/dispatch/407)的timeout值,在其中將觸發的I/O和信號事件加入到活動事件隊列中。
  • 調用函數event_process_active(Event.c/1406)遍歷活動事件隊列,依次調用註冊的回調函數處理相應事件。

附上event_base_loop源碼以下:

int event_base_loop(struct event_base *base, int flags)
        {
	const struct eventop *evsel = base->evsel;
	struct timeval tv;
	struct timeval *tv_p;
	int res, done, retval = 0;
	/* Grab the lock.  We will release it inside evsel.dispatch, and again
                * as we invoke user callbacks. */
	EVBASE_ACQUIRE_LOCK(base, th_base_lock);
	if (base->running_loop) {
		event_warnx("%s: reentrant invocation. Only one event_base_loop"
		                               " can run on each event_base at once.", __func__);
		EVBASE_RELEASE_LOCK(base, th_base_lock);
		return -1;
	}
	base->running_loop = 1;
	clear_time_cache(base);
	if (base->sig.ev_signal_added && base->sig.ev_n_signals_added)
	                        evsig_set_base(base);
	done = 0;
	#ifndef _EVENT_DISABLE_THREAD_SUPPORT
	                        base->th_owner_id = EVTHREAD_GET_ID();
	#endif
	                base->event_gotterm = base->event_break = 0;
	while (!done) {
		base->event_continue = 0;
		/* Terminate the loop if we have been asked to */
		if (base->event_gotterm) {
			break;
		}
		if (base->event_break) {
			break;
		}
		timeout_correct(base, &tv);
		tv_p = &tv;
		if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) {
			timeout_next(base, &tv_p);
		} else {
			/*
                            * if we have active events, we just poll new events
                            * without waiting.
                            */
			evutil_timerclear(&tv);
		}
		/* If we have no events, we just exit */
		if (!event_haveevents(base) && !N_ACTIVE_CALLBACKS(base)) {
			event_debug(("%s: no events registered.", __func__));
			retval = 1;
			goto done;
		}
		/* update last old time */
		gettime(base, &base->event_tv);
		clear_time_cache(base);
		res = evsel->dispatch(base, tv_p);
		if (res == -1) {
			event_debug(("%s: dispatch returned unsuccessfully.",
			                                    __func__));
			retval = -1;
			goto done;
		}
		update_time_cache(base);
		timeout_process(base);
		if (N_ACTIVE_CALLBACKS(base)) {
			int n = event_process_active(base);
			if ((flags & EVLOOP_ONCE)
			                               && N_ACTIVE_CALLBACKS(base) == 0
			                               && n != 0)
			                                    done = 1;
		} else if (flags & EVLOOP_NONBLOCK)
		                            done = 1;
	}
	event_debug(("%s: asked to terminate loop.", __func__));
	done:
	            clear_time_cache(base);
	base->running_loop = 0;
	EVBASE_RELEASE_LOCK(base, th_base_lock);
	return (retval);
}
複製代碼

寫在最後

最後,歡迎作Java的工程師朋友們加入Java高級架構進階Qqun:963944895

羣內有技術大咖指點難題,還提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)

比你優秀的對手在學習,你的仇人在磨刀,你的閨蜜在減肥,隔壁老王在練腰, 咱們必須不斷學習,不然咱們將被學習者超越!

趁年輕,使勁拼,給將來的本身一個交代!

相關文章
相關標籤/搜索