nginx 框架學習(一)啟動流程

Aurora_L發表於2017-03-02

nginx在啟動後,在unix系統中會以daemon的方式在後臺執行,後臺程式包含一個master程式和多個worker程式。我們也可以手動地關掉後臺模式,讓nginx在前臺執行,並且通過配置讓nginx取消master程式,從而可以使nginx以單程式方式執行(方便除錯)

[root@localhost ~]# vim /usr/local/nginx/conf/nginx.conf  //開啟nginx配置

enter image description here

nginx在啟動後,會有一個master程式和多個worker程式。master程式主要用來管理worker程式,包含:接收來自外界的訊號,向各worker程式傳送訊號,監控worker程式的執行狀態,當worker程式退出後(異常情況下),會自動重新啟動新的worker程式。而基本的網路事件,則是放在worker程式中來處理了。多個worker程式之間是對等的,他們同等競爭來自客戶端的請求,各程式互相之間是獨立的。一個請求,只可能在一個worker程式中處理,一個worker程式,不可能處理其它程式的請求。worker程式的個數是可以設定的,一般我們會設定與機器cpu核數一致.nginx的程式模型,可以由下圖來表示: enter image description here

master怎麼來worker程式,通過我們向master程式通訊。master程式會接收來自外界發來的訊號,再根據訊號做不同的事情。所以我們要控制nginx,只需要通過kill向master程式傳送訊號就行了。比如kill -HUP pid,則是告訴nginx,從容地重啟nginx,我們一般用這個訊號來重啟nginx,或重新載入配置,因為是從容地重啟,因此服務是不中斷的。master程式在接收到HUP訊號後是怎麼做的呢?首先master程式在接到訊號後,會先重新載入配置檔案,然後再啟動新的worker程式,並向所有老的worker程式傳送訊號,告訴他們可以光榮退休了

那麼worker程式是如何產生,又是如何處理請求的呢?每個worker程式之間是平等的,每個程式,處理請求的機會也是一樣的。當我們提供80埠的http服務時,一個連線請求過來,每個程式都有可能處理這個連線,怎麼做到的呢?首先,每個worker程式都是從master程式fork過來,在master程式裡面,先建立好需要listen的socket(listenfd)之後,然後再fork出多個worker程式。所有worker程式的listenfd會在新連線到來時變得可讀,為保證只有一個程式處理該連線,所有worker程式在註冊listenfd讀事件前搶accept_mutex,搶到互斥鎖的那個程式註冊listenfd讀事件,在讀事件裡呼叫accept接受該連線。當一個worker程式在accept這個連線之後,就開始讀取請求,解析請求,處理請求,產生資料後,再返回給客戶端,最後才斷開連線,這樣一個完整的請求就是這樣的了。我們可以看到,一個請求,完全由worker程式來處理,而且只在一個worker程式中處理。

nginx為什麼採用這種程式模型呢?首先,對於每個worker程式來說,獨立的程式,不需要加鎖,所以省掉了鎖帶來的開銷,同時在程式設計以及問題查詢時,也會方便很多。其次,採用獨立的程式,可以讓互相之間不會影響,一個程式退出後,其它程式還在工作,服務不會中斷,master程式則很快啟動新的worker程式。當然,worker程式的異常退出,肯定是程式有bug了,異常退出,會導致當前worker上的所有請求失敗,不過不會影響到所有請求,所以降低了風險。

上文說道worker的個數為cpu的核數,在這裡就很容易理解了,更多的worker數,只會導致程式來競爭cpu資源了,從而帶來不必要的上下文切換。而且,nginx為了更好的利用多核特性,提供了cpu親緣性的繫結選項,我們可以將某一個程式繫結在某一個核上,這樣就不會因為程式的切換帶來cache的失效。像這種小的優化在nginx中非常常見,同時也說明了nginx作者的苦心孤詣。比如,nginx在做4個位元組的字串比較時,會將4個字元轉換成一個int型,再作比較,以減少cpu的指令數等等。

還有,一個客戶端連線過來後,多個空閒的程式,會競爭這個連線,很容易看到,這種競爭會導致不公平,如果某個程式得到accept的機會比較多,它的空閒連線很快就用完了,如果不提前做一些控制,當accept到一個新的tcp連線後,因為無法得到空閒連線,而且無法將此連線轉交給其它程式,最終會導致此tcp連線得不到處理,就中止掉了。很顯然,這是不公平的,有的程式有空餘連線,卻沒有處理機會,有的程式因為沒有空餘連線,卻人為地丟棄連線。那麼,如何解決這個問題呢?首先,nginx的處理得先開啟accept_mutex選項,此時,只有獲得了accept_mutex的程式才會去新增accept事件,也就是說,nginx會控制程式是否新增accept事件。nginx使用一個叫ngx_accept_disabled的變數來控制是否去競爭accept_mutex鎖。在第一段程式碼中,計算ngx_accept_disabled的值,這個值是nginx單程式的所有連線總數的八分之一,減去剩下的空閒連線數量,得到的這個ngx_accept_disabled有一個規律,當剩餘連線數小於總連線數的八分之一時,其值才大於0,而且剩餘的連線數越小,這個值越大。再看第二段程式碼,當ngx_accept_disabled大於0時,不會去嘗試獲取accept_mutex鎖,並且將ngx_accept_disabled減1,於是,每次執行到此處時,都會去減1,直到小於0。不去獲取accept_mutex鎖,就是等於讓出獲取連線的機會,很顯然可以看出,當空餘連線越少時,ngx_accept_disable越大,於是讓出的機會就越多,這樣其它程式獲取鎖的機會也就越大。不去accept,自己的連線就控制下來了,其它程式的連線池就會得到利用,這樣,nginx就控制了多程式間連線的平衡了

ngx_accept_disabled = ngx_cycle->connection_n / 8
- ngx_cycle->free_connection_n;
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;

} else {
  if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
    return;
  }

  if (ngx_accept_mutex_held) {
      flags |= NGX_POST_EVENTS;

  } else {
      if (timer == NGX_TIMER_INFINITE
            || timer > ngx_accept_mutex_delay)
      {
          timer = ngx_accept_mutex_delay;
      }
  }
}

相關文章