Linux accept()/epoll_wait()驚きの問題と解決策

5517 ワード

問題のソース:


参考『UNP第三版』第30章「クライアント/サーバ設計モデル」の「30.6 TCP事前出産プロセスサーバプログラム」
//        ,     
int main(int argc, char **argv)
{
    int listenfd = Tcp_Listen();

    for (int i = 0; i < nchildren; i++){
        if ((pid = fork()) == 0){
            child_main(i, listenfd, addrlen);
        }
    }

    for(;;){
        pause();
    }
}

pid_t child_main(int i, int listenfd, int addrlen)
{
    struct sockaddr clientAddr;
    socklen_t clientAddrLen;
    for(;;)
    {
        int connfd = accept(listenfd, &clientAddr, &clientAddrLen);
        web_child(connfd);
        close(connfd);
    }
}

上記のコードでは、プライマリ・プロセスがリスニング用のsocket記述子listenfdを作成し、各サブプロセスがaccept(listenfd,)を呼び出し、クライアントの新しい接続connfdの取得を待機し、web_child(connfd)で実行します.複数のサブプロセスが同じリスニングsocketに同時にブロックされるため、クライアントの新しい接続アクセスがある場合、ブロックされたすべてのサブプロセスは起動されるが、最も速いサブプロセス呼び出しaccept(listenfd)だけがクライアント接続connfdを得ることができ、他のサブプロセス呼び出しaccept(listenfd)はEGAINまで待つことができる.このような複数のサブプロセスが起動されても何もできない状態を「驚きのグループ」と呼び、プロセスごとにスケジューリングされるシステムのオーバーヘッドが比較的大きいため、高同時サーバでは頻繁な「驚きのグループ」がサーバのパフォーマンスを低下させるに違いない.
30.6サンプルに存在する問題に対して、30.7および30.8は、「ファイルロック」および「スレッド反発ロック」を使用してaccept()を保護する2つのスキームを示した.簡単に言えば、すべてのサブプロセスはaccept()を呼び出す前に、ロックを取得し、ロックを取得したプロセスだけがaccept()関数を実行し続け、他のプロセスはロックによってブロックされます.さらに,このロック方式は「驚きの群れ」問題の解決に有効であることを検証した.
for(;;) 
{    
    lock(); //            
    int client = accept(...);    
    unlock();    

    if (client < 0) continue;    
    ...  
}  

さらに、30.9には、「メインプロセスaceeptクライアント接続、サブプロセス処理にクライアント接続記述子を渡す」というスキームも示されている.しかし,ロックに比べてプロセス間伝達記述子の効率が低いことを実験で実証した.これは余談だろう.
実際、Linuxシステムのみを考慮すると、Linux 2.6以降、カーネルカーネルはaccept()関数の「驚きのグループ」の問題を解決しており、カーネルがクライアント接続を受信すると、待機キュー上の最初のプロセスまたはスレッドのみが呼び出されるという処理が一般的です.したがって,サーバがacceptブロッキング呼び出し方式を採用すれば,最新のLinuxシステムでは「驚くべきグループ」の問題はなくなった.
しかし、実際のエンジニアリングでよく見られるサーバプログラムでは、select、poll、またはepollメカニズムが使用されることが多い.この場合、サーバはacceptではなく、select、poll、またはepoll_でブロックされる.wait,この場合の「驚きの群れ」は依然として考慮しなければならない.次にepollを例に分析します.
以前のLinuxバージョンでは、カーネルはepoll_にブロックされていました.waitのプロセスも、すべてを呼び覚ますメカニズムを採用しているため、acceptに似た「驚きの群れ」の問題がある.新しいバージョンのソリューションも、待ち行列の最初のプロセスまたはスレッドを起動するだけであるため、新しいバージョンのLinuxセクションではepollの「驚きのグループ」の問題が解決されます.部分的な解決とは,一部の特殊なシーンに対してepollメカニズムを用いることで,「驚きの群れ」の問題は存在しないが,多くのシーンに対してepollメカニズムには依然として「驚きの群れ」が存在することを意味する.

1、シーン1:epoll_create()forkサブプロセスの前に:


epoll_create()呼び出しforkサブプロセスの前にepoll_create()で作成されたepfdは、すべてのサブプロセスによって継承されます.次に、すべてのサブプロセスブロック呼び出しepoll_wait()は、監視される記述子(顧客接続を傍受するための傍受記述子を含む)に新しいイベントが発生するのを待つ.リスニング記述子に読み取り可能なイベントが発生すると、カーネルはブロックキューの1番目のプロセス/スレッドを起動し、起動されたプロセス/スレッドはaccept()関数を実行し続け、新しく確立されたクライアント接続記述子connfdを得る.この場合、どのサブプロセスも起動されaccept()関数を実行しても問題ありません.
しかしながら、次に、サブプロセスの動作が、epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);を呼び出して新しい接続の記述子connfdをepfdに統合モニタリングに追加する場合、現在のepfdはforkの前に作成されるため、システムにはepollモニタリングファイルが1つしかなく、すなわち、すべてのサブプロセスが1つのepollモニタリングファイルを共有する.いずれかのプロセス(親プロセスまたはサブプロセス)がepollモニタファイルにファイル記述子を追加、変更、削除すると、他のプロセスのepoll_に影響します.wait. その後、connfd記述子にクライアント情報が受信されると、カーネルは、毎回同じプロセス/スレッドを起動して、この接続記述子connfd上の読み書き情報を処理することを保証することができず(他のプロセスはconnfdを全く認識していない可能性があり、または異なるプロセスでは、同じ記述子が異なるクライアント接続に対応している可能性がある)、最終的に接続処理エラーを引き起こす.(また、異なるスレッドが同じ接続記述子を処理すると、送信される情報が乱れてしまうこともあります)
だから、epoll_を避けるべきだ.create()はforkサブプロセスの前にあります.この点について、libeventのドキュメントには専門的な説明があるそうです.

2、シーン2:epoll_create()forkサブプロセスの後:


epoll_create()forkサブプロセスの後、各プロセスは独自のepollモニタリングファイルを持っている(あるプロセスが新しい接続の記述子connfdを本プロセスのepfdに追加して統一的にモニタリングし、他のプロセスのepoll_waitに影響を与えない)が、同時リスニングを実現するために、すべてのサブプロセスはepoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);を呼び出してリスニング記述子をモニタリングファイルに追加する.すなわち、すべてのサブプロセスはepollメカニズムによって同じリスニング記述子をポーリングしている.新しいクライアントがアクセスを要求すると、リスニング記述子にPOLLINイベント(記述子が読み取り可能で、新しい接続アクセスがあることを示す)が発生し、カーネルがすべてのプロセスを起動するため、「驚きのグループ」の問題は依然として存在します.
この場合の「驚きのグループ」の問題に対して、Nginxの解決策は「UNP第3版」第30章の30.7と30.8で与えられたロックスキームと類似しており、おそらく反発ロックによって各プロセスをepoll_waitからacceptまでの処理は反発量によって保護される.このようなロック操作では、epoll_は、サブプロセスごとに1つしか実行できないことに注意してください.waitとaccept、具体的にどのプロセスが実行されるかは、カーネルスケジューリングによって決まります.したがって、負荷分散の問題を解決するために、Nginxのソリューションでは、各プロセスに現在の接続カウントがあり、現在の接続カウントが最大接続の7/8を超えると、そのプロセスは新しい接続の受信を停止します.
lock()    
epoll_wait(...);   
accept(...);    
unlock(...);   

また、マルチプロセスを考慮せずにマルチスレッドで実現すれば、スレッドスケジューリングのオーバーヘッドがプロセスよりずっと小さいので、マルチスレッドの下で、驚きのグループの問題を考慮する必要はありません.もちろん、この結論には具体的なテストデータが必要で、後でテストの準備をする暇があります.

3、SO_を利用するREUSEPORTはepollの驚異的なグループ問題を解決する


ネット上のこの方面の内容もとても多くて、みんなは自分で検索することができます.カーネルの新しいバージョンの新しい特性を利用したソリューションなので、究極のソリューションと言えるでしょう.利用SO_REUSEPORTはepollの驚異的なグループ問題を解決する