epoll程式設計,單epoll+執行緒池?執行緒池+epoll?nginx實現高併發的原理?

菠蘿科技發表於2019-01-03

草稿未驗證

1 epoll程式設計,如何實現高併發伺服器開發?

- 知乎 https://www.zhihu.com/question/21516827/answer/55127881

nginx 多程式網路程式設計的巔峰
memcached 多執行緒網路程式設計的巔峰
redis單執行緒網路程式設計的巔峰~~

2 為什麼說 event-loop 在 IO 密集型場景中比執行緒模型更高效?

  https://www.zhihu.com/question/67751355/answer/256921689

 3 單個 epoll + 執行緒池與每個執行緒一個 epoll 這兩種架構哪個更適合大量短連線的場景?

3.1

    單個 epoll + 執行緒池與每個執行緒一個 epoll 這兩種架構哪個更適合大量短連線的場景? - kanmars的回答 - 知乎 https://www.zhihu.com/question/271561199/answer/374711772

相關知識可以檢視書籍《UNIX環境高階程式設計》中的重點章節。

單epoll+執行緒池?執行緒池+epoll?

3.2  https://www.zhihu.com/question/271561199/answer/374621127

不需要執行緒池。執行緒池的應用場景是執行緒數量不斷增減的情況,因此需要用一個池來不斷維護申請與釋放。

現在主流做法是按cpu核心數量開執行緒數量,執行緒數量恆定不變,等於epoll數量。這種情況下,由於執行緒數量固定,執行緒池毫無優勢。

4 nginx實現高併發的原理

nginx是以多程式的方式來工作的。
nginx在啟動後,會有一個master程式和多個worker程式。

這裡寫圖片描述

 

多程式的工作模式


 
  1. 1、Nginx 在啟動後,會有一個 master 程式和多個相互獨立的 worker 程式。

  2. 2、接收來自外界的訊號,向各worker程式傳送訊號,每個程式都有可能來處理這個連線。

  3. 3、 master 程式能監控 worker 程式的執行狀態,當 worker 程式退出後(異常情況下),會自動啟動新的 worker 程式。

注意 worker 程式數,一般會設定成機器 cpu 核數。因為更多的worker 數,只會導致程式相互競爭 cpu,從而帶來不必要的上下文切換。

使用多程式模式,不僅能提高併發率,而且程式之間相互獨立,一個 worker 程式掛了不會影響到其他 worker 程式。

 

驚群現象

主程式(master 程式)首先通過 socket() 來建立一個 sock 檔案描述符用來監聽,然後fork生成子程式(workers 程式),子程式將繼承父程式的 sockfd(socket 檔案描述符),之後子程式 accept() 後將建立已連線描述符(connected descriptor)),然後通過已連線描述符來與客戶端通訊。

那麼,由於所有子程式都繼承了父程式的 sockfd,那麼當連線進來時,所有子程式都將收到通知並“爭著”與它建立連線,這就叫“驚群現象”。大量的程式被啟用又掛起,只有一個程式可以accept() 到這個連線,這當然會消耗系統資源。

 

Nginx對驚群現象的處理

Nginx 提供了一個 accept_mutex 這個東西,這是一個加在accept上的一把共享鎖。即每個 worker 程式在執行 accept 之前都需要先獲取鎖,獲取不到就放棄執行 accept()。有了這把鎖之後,同一時刻,就只會有一個程式去 accpet(),這樣就不會有驚群問題了。accept_mutex 是一個可控選項,我們可以顯示地關掉,預設是開啟的。

Nginx程式詳解

 

Nginx在啟動後,會有一個master程式和多個worker程式。

master程式

主要用來管理worker程式,包含:接收來自外界的訊號,向各worker程式傳送訊號,監控worker程式的執行狀態,當worker程式退出後(異常情況下),會自動重新啟動新的worker程式。

master程式充當整個程式組與使用者的互動介面,同時對程式進行監護。它不需要處理網路事件,不負責業務的執行,只會通過管理worker程式來實現重啟服務、平滑升級、更換日誌檔案、配置檔案實時生效等功能。

我們要控制nginx,只需要通過kill向master程式傳送訊號就行了。比如kill -HUP pid,則是告訴nginx,從容地重啟nginx,我們一般用這個訊號來重啟nginx,或重新載入配置,因為是從容地重啟,因此服務是不中斷的。master程式在接收到HUP訊號後是怎麼做的呢?首先master程式在接到訊號後,會先重新載入配置檔案,然後再啟動新的worker程式,並向所有老的worker程式傳送訊號,告訴他們可以光榮退休了。新的worker在啟動後,就開始接收新的請求,而老的worker在收到來自master的訊號後,就不再接收新的請求,並且在當前程式中的所有未處理完的請求處理完成後,再退出。當然,直接給master程式傳送訊號,這是比較老的操作方式,nginx在0.8版本之後,引入了一系列命令列引數,來方便我們管理。比如,./nginx -s reload,就是來重啟nginx,./nginx -s stop,就是來停止nginx的執行。如何做到的呢?我們還是拿reload來說,我們看到,執行命令時,我們是啟動一個新的nginx程式,而新的nginx程式在解析到reload引數後,就知道我們的目的是控制nginx來重新載入配置檔案了,它會向master程式傳送訊號,然後接下來的動作,就和我們直接向master程式傳送訊號一樣了。

worker程式

而基本的網路事件,則是放在worker程式中來處理了。多個worker程式之間是對等的,他們同等競爭來自客戶端的請求,各程式互相之間是獨立的。一個請求,只可能在一個worker程式中處理,一個worker程式,不可能處理其它程式的請求。worker程式的個數是可以設定的,一般我們會設定與機器cpu核數一致,這裡面的原因與nginx的程式模型以及事件處理模型是分不開的。

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

 

worker程式工作流程

當一個 worker 程式在 accept() 這個連線之後,就開始讀取請求,解析請求,處理請求,產生資料後,再返回給客戶端,最後才斷開連線,一個完整的請求。一個請求,完全由 worker 程式來處理,而且只能在一個 worker 程式中處理。

這樣做帶來的好處:

1、節省鎖帶來的開銷。每個 worker 程式都是獨立的程式,不共享資源,不需要加鎖。同時在程式設計以及問題查上時,也會方便很多。

2、獨立程式,減少風險。採用獨立的程式,可以讓互相之間不會影響,一個程式退出後,其它程式還在工作,服務不會中斷,master 程式則很快重新啟動新的 worker 程式。當然,worker 程式的也能發生意外退出。

多程式模型每個程式/執行緒只能處理一路IO,那麼 Nginx是如何處理多路IO呢?

如果不使用 IO 多路複用,那麼在一個程式中,同時只能處理一個請求,比如執行 accept(),如果沒有連線過來,那麼程式會阻塞在這裡,直到有一個連線過來,才能繼續向下執行。

而多路複用,允許我們只在事件發生時才將控制返回給程式,而其他時候核心都掛起程式,隨時待命。

 

核心:Nginx採用的 IO多路複用模型epoll

epoll通過在Linux核心中申請一個簡易的檔案系統(檔案系統一般用什麼資料結構實現?B+樹),其工作流程分為三部分:


 
  1. 1、呼叫 int epoll_create(int size)建立一個epoll物件,核心會建立一個eventpoll結構體,用於存放通過epoll_ctl()向epoll物件中新增進來

  2. 的事件,這些事件都會掛載在紅黑樹中。

  3. 2、呼叫 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 在 epoll 物件中為 fd 註冊事件,所有新增到epoll中的件

  4. 都會與裝置驅動程式建立回撥關係,也就是說,當相應的事件發生時會呼叫這個sockfd的回撥方法,將sockfd新增到eventpoll 中的雙連結串列

  5. 3、呼叫 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) 來等待事件的發生,timeout 為 -1 時,該

  6. 呼叫會阻塞知道有事件發生

  7.  

這樣,註冊好事件之後,只要有 fd 上事件發生,epoll_wait() 就能檢測到並返回給使用者,使用者就能”非阻塞“地進行 I/O 了。

epoll() 中核心則維護一個連結串列,epoll_wait 直接檢查連結串列是不是空就知道是否有檔案描述符準備好了。(epoll 與 select 相比最大的優點是不會隨著 sockfd 數目增長而降低效率,使用 select() 時,核心採用輪訓的方法來檢視是否有fd 準備好,其中的儲存 sockfd 的是類似陣列的資料結構 fd_set,key 為 fd,value 為 0 或者 1。)

能達到這種效果,是因為在核心實現中 epoll 是根據每個 sockfd 上面的與裝置驅動程式建立起來的回撥函式實現的。那麼,某個 sockfd 上的事件發生時,與它對應的回撥函式就會被呼叫,來把這個 sockfd 加入連結串列,其他處於“空閒的”狀態的則不會。在這點上,epoll 實現了一個”偽”AIO。但是如果絕大部分的 I/O 都是“活躍的”,每個 socket 使用率很高的話,epoll效率不一定比 select 高(可能是要維護佇列複雜)。

可以看出,因為一個程式裡只有一個執行緒,所以一個程式同時只能做一件事,但是可以通過不斷地切換來“同時”處理多個請求。

例子:Nginx 會註冊一個事件:“如果來自一個新客戶端的連線請求到來了,再通知我”,此後只有連線請求到來,伺服器才會執行 accept() 來接收請求。又比如向上遊伺服器(比如 PHP-FPM)轉發請求,並等待請求返回時,這個處理的 worker 不會在這阻塞,它會在傳送完請求後,註冊一個事件:“如果緩衝區接收到資料了,告訴我一聲,我再將它讀進來”,於是程式就空閒下來等待事件發生。

這樣,基於 多程式+epoll, Nginx 便能實現高併發。

使用 epoll 處理事件的一個框架,程式碼轉自:http://www.cnblogs.com/fnlingnzb-learner/p/5835573.html

https://blog.csdn.net/u010412301/article/details/79353211

for( ; ; )  //  無限迴圈
      {
          nfds = epoll_wait(epfd,events,20,500);  //  最長阻塞 500s
          for(i=0;i<nfds;++i)
          {
              if(events[i].data.fd==listenfd) //有新的連線
              {
                  connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept這個連線
                  ev.data.fd=connfd;
                 ev.events=EPOLLIN|EPOLLET;
                 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd新增到epoll的監聽佇列中
             }
             else if( events[i].events&EPOLLIN ) //接收到資料,讀socket
             {
                 n = read(sockfd, line, MAXLINE)) < 0    //讀
                 ev.data.ptr = md;     //md為自定義型別,新增資料
                 ev.events=EPOLLOUT|EPOLLET;
                 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改識別符號,等待下一個迴圈時傳送資料,非同步處理的精髓
             }
             else if(events[i].events&EPOLLOUT) //有資料待傳送,寫socket
             {
                 struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取資料
                 sockfd = md->fd;
                 send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //傳送資料
                 ev.data.fd=sockfd;
                 ev.events=EPOLLIN|EPOLLET;
                 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改識別符號,等待下一個迴圈時接收資料
             }
             else
             {
                 //其他的處理
             }
         }
     }

Nginx 與 多程式模式 Apache 的比較:

對於Apache,每個請求都會獨佔一個工作執行緒,當併發數到達幾千時,就同時有幾千的執行緒在處理請求了。這對於作業系統來說,佔用的記憶體非常大,執行緒的上下文切換帶來的cpu開銷也很大,效能就難以上去,同時這些開銷是完全沒有意義的。
     對於Nginx來講,一個程式只有一個主執行緒,通過非同步非阻塞的事件處理機制,實現了迴圈處理多個準備好的事件,從而實現輕量級和高併發。

事件驅動適合於I/O密集型服務,多程式或執行緒適合於CPU密集型服務: 
1、Nginx 更主要是作為反向代理,而非Web伺服器使用。其模式是事件驅動。 
2、事件驅動伺服器,最適合做的就是這種 I/O 密集型工作,如反向代理,它在客戶端與WEB伺服器之間起一個資料中轉作用,純粹是 I/O 操作,自身並不涉及到複雜計算。因為程式在一個地方進行計算時,那麼這個程式就不能處理其他事件了。 
3、Nginx 只需要少量程式配合事件驅動,幾個程式跑 libevent,不像 Apache 多程式模型那樣動輒數百的程式數。 
5、Nginx 處理靜態檔案效果也很好,那是因為讀寫檔案和網路通訊其實都是 I/O操作,處理過程一樣。

相關文章