IO多路複用與進/線程池的優劣對比

首先提一下驚羣

驚羣現象

主進程(master 進程)首先通過 socket() 來創建一個 sock 文件描述符用來監聽,然後fork生成子進程(workers 進程),子進程將繼承父進程的 sockfd(socket 文件描述符),之後子進程 accept() 後將創建已連接描述符(connected descriptor)),然後通過已連接描述符來與客戶端通信。

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

Nginx(engine x) 是一個高性能的HTTP和反向代理服務器,其特點是佔有內存少,併發能力強,事實上nginx的併發能力確實在同類型的網頁服務器中表現較好,中國大陸使用nginx網站用戶有:百度、京東、新浪、網易、騰訊、淘寶等。

Nginx對驚羣現象的處理

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

worker進程工作流程
當一個 worker 進程在 accept() 這個連接之後,就開始讀取請求,解析請求,處理請求,產生數據後,再返回給客戶端,最後才斷開連接,一個完整的請求。

一個請求,完全由 worker 進程來處理,而且只能在一個 worker 進程中處理。

這樣做帶來的好處:

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

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

多進程模型每個進程/線程只能處理一路IO,那麼 Nginx是如何處理多路IO呢?

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

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

再來看IO多路複用

IO多路複用是利用select、poll、epoll同時監視多個流的IO事件的狀態:
1   空閒時把當前進/線程阻塞
2   當有一個或多個流由IO事件發生時,就從阻塞態中喚醒,select 和 poll會輪詢一遍所有的流, 而epoll只輪詢那些真正發生變化的流,並且依次處理就緒了的流。
  1. select 服務端一直在輪詢,如果有客戶端鏈接上來就創建一個連接放到數組array中,並繼續輪詢,如果在輪詢的過程中有客戶端發生IO事件就去處理;select只能監視1024個連接(一個進程只能創建1024個文件);而且存在線程安全問題;

  2. epoll:也是監測IO事件,如果發生IO事件,它會告訴你是哪個連接發生了事件,而不是輪詢訪問。而且它epoll 線程安全,但是隻有linux平臺支持

使用epoll 框架

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
             {
                 //其他的處理
             }
         }
     }

O多路複用的優勢在於,當處理消耗 相比IO消耗 , 幾乎可以忽略不計時,這時處理大量的併發IO,而不會消耗太多CPU/內存。

**典型的例子是nginx做代理,**代理的轉發邏輯相對比較簡單直接,那麼IO多路複用很適合。相反,如果是一個做複雜計算的場景,計算本身可能是個 指數複雜度的東西,IO不是瓶頸。那麼怎麼充分利用CPU或者顯卡的核心多幹活纔是關鍵。

IO多路複用適合處理很多閒置的IO,因爲IO socket的數量的增加並不會導致進/線程數的增加,也就不會引起stack內存,內核對象,切換時間的損耗。

因此 IO多路複用非常適合長鏈接場景。

另外,IO多路複用不會遇到併發編程的一系列問題。

如果做不到“處理過程相對於IO可以忽略不計”,IO多路複用的並不一定比線程池方案更好。

總結

IO多路複用適合用於:處理過程簡單,進/線程池適合處理過程複雜 的情況

IO多路複用+單進(線)程比較省資源適合處理大量的閒置的IO

IO多路複用+多單進(線)程與線程池方案相比有好處,但是並不會
有太大的優勢如果壓力很大,什麼方案都得跪,這時就得擴容。當然因爲IO多路複用+單進(線)程比較省資源,所以擴容時能省錢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章