用:從阻塞IO到epoll與Reactor模式的高并發(fā)實踐)
1. 從“堵車”到“調(diào)度”理解IO多路復(fù)用的核心價值想象一下你是一個餐廳的服務(wù)員。傳統(tǒng)的服務(wù)模式是你為一個客人點完單就必須站在廚房門口一直等到他的菜做好親手端給他然后才能去服務(wù)下一個客人。如果廚房做菜慢或者客人多你就會發(fā)現(xiàn)大部分時間你都在“干等”其他客人餓得嗷嗷叫你也無能為力。這種模式就是阻塞式IO。你的線程服務(wù)員被一個IO操作等菜完全“阻塞”住了無法處理其他請求CPU資源大量閑置。那怎么提高效率呢一個粗暴的辦法是給每個客人都配一個專屬服務(wù)員。這就是多線程/多進程模型。雖然理論上能同時服務(wù)很多人但服務(wù)員線程/進程本身是有成本的創(chuàng)建、銷毀、上下文切換開銷大當(dāng)客人成千上萬時餐廳光發(fā)工資就破產(chǎn)了。這顯然不是高并發(fā)場景下的好方案。IO多路復(fù)用要解決的就是這個“一人等一菜”的低效問題。它的核心思想是讓一個服務(wù)員線程能夠同時照看多個客人IO連接的狀態(tài)。這個服務(wù)員不再傻等而是拿一個“呼叫器列表”內(nèi)核提供的多路復(fù)用器如select、poll、epoll。他定期去前臺內(nèi)核查看這個列表看看哪些客人的菜好了IO事件就緒然后只去為那些菜好了的客人服務(wù)。這樣一個服務(wù)員就能高效管理成百上千的客人。在計算機領(lǐng)域這解決了C10K甚至C10M問題的核心瓶頸如何用有限的線程資源管理海量的網(wǎng)絡(luò)連接。無論是你的微信后臺同時保持?jǐn)?shù)百萬長連接還是Nginx每秒處理數(shù)萬請求其高并發(fā)的基石就是IO多路復(fù)用技術(shù)。2. 核心原理內(nèi)核如何幫你“看場子”要理解IO多路復(fù)用必須搞明白用戶空間和內(nèi)核空間的關(guān)系以及“事件就緒”這個概念。當(dāng)我們程序通過socket.read()去讀數(shù)據(jù)時數(shù)據(jù)并不是直接從網(wǎng)卡飛到你的應(yīng)用程序內(nèi)存的。它要經(jīng)歷網(wǎng)卡 - 內(nèi)核緩沖區(qū) - 用戶緩沖區(qū)。“阻塞”就發(fā)生在數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū)的這個過程中。如果內(nèi)核緩沖區(qū)是空的你的線程就會被操作系統(tǒng)掛起放到等待隊列里直到數(shù)據(jù)到來。IO多路復(fù)用器select/poll/epoll做的工作就是幫你的應(yīng)用程序去詢問內(nèi)核“我關(guān)心的那一堆socket連接里現(xiàn)在有哪些已經(jīng)有數(shù)據(jù)可讀了或者可以寫了或者出錯了” 這個“詢問”的過程是系統(tǒng)調(diào)用會從用戶態(tài)切換到內(nèi)核態(tài)。內(nèi)核會遍歷你提交給它的一堆文件描述符fd檢查它們各自內(nèi)核緩沖區(qū)的情況。這個檢查是在內(nèi)核里完成的效率很高。檢查完畢后內(nèi)核會告訴你一個結(jié)果列表“fd 5, fd 12, fd 19 這幾個現(xiàn)在可以無阻塞地讀/寫了。”于是你的應(yīng)用程序拿到這個“就緒列表”后就可以有針對性地、一個接一個地對這些就緒的fd進行真正的IO操作read/write。因為內(nèi)核已經(jīng)保證了這些操作不會阻塞所以整個過程是快速的。這樣一來你的一個線程就實現(xiàn)了對多個IO連接的管理從“主動輪詢每個連接”變成了“被動接收事件通知”大大降低了空轉(zhuǎn)的CPU消耗。注意IO多路復(fù)用本身是同步IO的一種實現(xiàn)方式。因為真正的數(shù)據(jù)讀寫從內(nèi)核緩沖區(qū)拷貝到用戶空間仍然是需要你的線程自己來完成的這個拷貝過程是同步的、阻塞的盡管時間極短。它和異步IOAIO有本質(zhì)區(qū)別AIO是內(nèi)核幫你完成數(shù)據(jù)拷貝然后通知你“一切搞定”你的線程在數(shù)據(jù)拷貝期間完全不用等待。3. 三代“調(diào)度員”的演進select、poll、epollIO多路復(fù)用有幾個具體的實現(xiàn)它們可以看作是不斷升級的“調(diào)度系統(tǒng)”。3.1 select初代調(diào)度員能力有限select是最早的通用多路復(fù)用接口。它的工作方式很直接你把所有需要監(jiān)視的socket fd通過三個fd_set分別對應(yīng)讀、寫、異常事件傳給內(nèi)核。內(nèi)核線性掃描所有你傳入的fd檢查它們的狀態(tài)。內(nèi)核修改這些fd_set將其標(biāo)識為“就緒”或“未就緒”然后返回。你的程序需要再次遍歷所有fd找出哪些被置位了就緒了。它有幾個明顯的缺點監(jiān)聽的fd數(shù)量有上限通常是1024由FD_SETSIZE宏定義這在現(xiàn)代高并發(fā)下完全不夠用。每次調(diào)用都需要在用戶態(tài)和內(nèi)核態(tài)之間拷貝整個fd集合當(dāng)fd很多時開銷不小。內(nèi)核和用戶程序都需要遍歷所有fd時間復(fù)雜度是O(n)。萬級連接時每次調(diào)用select內(nèi)核都要遍歷上萬次成為性能瓶頸。fd_set被內(nèi)核修改后是“破壞性”的你下次調(diào)用前必須重置它。// 偽代碼示意 fd_set read_fds; FD_ZERO(read_fds); FD_SET(socket_fd1, read_fds); FD_SET(socket_fd2, read_fds); ... int ret select(max_fd1, read_fds, NULL, NULL, timeout); if (ret 0) { for (int i 0; i max_fd; i) { if (FD_ISSET(i, read_fds)) { // 處理socket i的數(shù)據(jù) } } }3.2 poll改進版解除數(shù)量限制poll的出現(xiàn)解決了select的fd數(shù)量限制問題。它不再使用fd_set而是使用一個pollfd結(jié)構(gòu)體數(shù)組。struct pollfd { int fd; // 文件描述符 short events; // 關(guān)心的事件POLLIN, POLLOUT... short revents; // 返回的事件由內(nèi)核填充 };它的流程和select類似但優(yōu)勢在于沒有最大連接數(shù)限制理論上受系統(tǒng)打開文件數(shù)限制。將“關(guān)心事件”和“返回事件”分開events和revents內(nèi)核只修改revents所以每次調(diào)用不需要像select那樣重置整個參數(shù)。但是它和select一樣內(nèi)核仍需遍歷所有fd用戶程序收到返回后也需要遍歷所有fd來查找就緒項。性能瓶頸在大量空閑連接時依然存在。同時用戶態(tài)和內(nèi)核態(tài)之間傳遞整個數(shù)組拷貝開銷依然存在。3.3 epoll現(xiàn)代高性能調(diào)度核心epoll是Linux下性能最優(yōu)的多路復(fù)用機制徹底解決了select/poll的性能瓶頸。它的設(shè)計哲學(xué)是“我只關(guān)心活躍的連接”。它使用了三個關(guān)鍵的系統(tǒng)調(diào)用epoll_create,epoll_ctl,epoll_wait。1.epoll_create創(chuàng)建監(jiān)控中心創(chuàng)建一個epoll實例返回一個文件描述符epfd。這個實例在內(nèi)核中對應(yīng)一塊空間用來存儲后續(xù)要監(jiān)控的fd列表。2.epoll_ctl管理監(jiān)控列表用于向epoll實例epfd中添加、修改或刪除需要監(jiān)控的fd。這是增量式的。你只需要在連接建立或關(guān)閉時調(diào)用它告訴內(nèi)核“這個新fd請幫我監(jiān)控一下”或者“這個fd不用監(jiān)控了”。內(nèi)核會將這些fd維護在一棵高效的數(shù)據(jù)結(jié)構(gòu)紅黑樹中。3.epoll_wait等待事件發(fā)生這是核心的等待調(diào)用。它阻塞或超時等待直到有事件發(fā)生。當(dāng)它返回時它只給你一個就緒事件的數(shù)組這個數(shù)組里全是已經(jīng)活躍的fd。你拿到后直接處理即可無需遍歷所有監(jiān)聽的fd。epoll的關(guān)鍵優(yōu)勢事件驅(qū)動無需遍歷epoll_wait直接返回就緒的fd列表時間復(fù)雜度O(1)。內(nèi)存拷貝優(yōu)化通過epoll_ctl提前將fd信息注冊到內(nèi)核epoll_wait調(diào)用時內(nèi)核通過共享內(nèi)存mmap的方式將就緒事件通知給用戶空間避免了大量的內(nèi)存拷貝。支持邊緣觸發(fā)ET和水平觸發(fā)LT模式水平觸發(fā)LT默認(rèn)只要fd對應(yīng)的緩沖區(qū)還有數(shù)據(jù)可讀每次epoll_wait都會報告這個事件。編程更簡單不容易遺漏事件。邊緣觸發(fā)ET只有當(dāng)fd狀態(tài)發(fā)生變化時比如從無數(shù)據(jù)到有數(shù)據(jù)才會報告一次事件。如果這次報告后你沒有一次性把緩沖區(qū)數(shù)據(jù)讀完除非再有新數(shù)據(jù)到來否則不會再報告。ET模式效率更高但要求程序員必須使用非阻塞IO并且一次循環(huán)讀完所有數(shù)據(jù)否則會丟事件。// 偽代碼示意 int epfd epoll_create1(0); struct epoll_event ev, events[MAX_EVENTS]; // 添加socket到epoll監(jiān)控 ev.events EPOLLIN; // 監(jiān)聽讀事件 ev.data.fd server_sock; epoll_ctl(epfd, EPOLL_CTL_ADD, server_sock, ev); while(1) { // 等待事件 nfds是就緒的事件數(shù)量 int nfds epoll_wait(epfd, events, MAX_EVENTS, -1); for (int i 0; i nfds; i) { if (events[i].data.fd server_sock) { // 接受新連接并將新連接的fd用epoll_ctl加入epoll } else { // 處理客戶端socket的數(shù)據(jù) handle_client(events[i].data.fd); } } }實操心得在Linux服務(wù)器開發(fā)中epoll幾乎是高并發(fā)網(wǎng)絡(luò)編程的標(biāo)配。理解ET和LT的區(qū)別至關(guān)重要。新手建議從LT模式開始更安全。使用ET模式時務(wù)必確保將socket設(shè)為非阻塞模式并在收到讀事件后循環(huán)調(diào)用read直到返回EAGAIN或EWOULDBLOCK錯誤表示本次內(nèi)核緩沖區(qū)數(shù)據(jù)已讀完。4. Reactor模式IO多路復(fù)用的經(jīng)典應(yīng)用架構(gòu)理解了epoll這樣的底層機制如何用它來構(gòu)建一個高效的服務(wù)程序呢這就引出了Reactor模式。它不是一個具體庫而是一種利用IO多路復(fù)用的設(shè)計模式。你可以把Reactor模式想象成醫(yī)院的分診臺Reactor反應(yīng)器相當(dāng)于分診臺的護士。她只有一個核心任務(wù)坐在那里監(jiān)聽所有病人的呼叫鈴epoll_wait。當(dāng)有病人按鈴IO事件就緒她就記錄下來是誰哪個fd以及是什么事讀/寫。Demultiplexer多路事件分離器這就是epoll_wait本身。護士用來監(jiān)聽呼叫鈴的工具。Dispatcher分發(fā)器護士根據(jù)記錄將不同的病人事件分配給不同的醫(yī)生Handler。比如腹痛的分給內(nèi)科醫(yī)生外傷的分給外科醫(yī)生。EventHandler/Handler事件處理器就是各個醫(yī)生。他們負(fù)責(zé)具體的業(yè)務(wù)處理讀數(shù)據(jù)、邏輯計算、寫回數(shù)據(jù)。在一個典型的單Reactor線程模型中流程是這樣的Reactor線程通過epoll_wait監(jiān)聽所有客戶端連接。當(dāng)有連接到來EPOLLINon listen socketReactor調(diào)用accept接受連接并將新連接的socket fd注冊到epoll中監(jiān)聽讀事件。當(dāng)某個客戶端發(fā)來數(shù)據(jù)EPOLLINon client socketReactor線程被喚醒它發(fā)現(xiàn)這是一個讀事件然后將這個“讀數(shù)據(jù)”的任務(wù)分發(fā)給一個工作線程池中的某個工作線程去處理。工作線程進行真正的業(yè)務(wù)處理解碼、計算、查詢數(shù)據(jù)庫等。處理完畢后工作線程將需要回復(fù)的數(shù)據(jù)準(zhǔn)備好。此時它可以通過某種方式比如將socket fd再次注冊寫事件或者通過隊列通知Reactor線程觸發(fā)一個寫事件。Reactor線程監(jiān)聽到寫事件就緒再負(fù)責(zé)將數(shù)據(jù)寫回給客戶端。這樣設(shè)計的好處是IO操作等待、讀、寫這個最耗時的部分被Reactor這個單一線程高效地管理了起來而CPU密集型的業(yè)務(wù)計算被剝離到線程池中避免了IO阻塞計算也避免了計算阻塞IO。Reactor線程本身只做事件分發(fā)快進快出保證了高并發(fā)下的響應(yīng)能力。Netty、Redis、Nginx等高性能中間件的網(wǎng)絡(luò)核心模塊都是Reactor模式的典范實現(xiàn)。5. 實戰(zhàn)用C語言實現(xiàn)一個簡易的epoll服務(wù)器理論說再多不如動手寫一遍。下面我們實現(xiàn)一個最簡單的echo服務(wù)器它使用epoll的LT模式接受客戶端連接并將客戶端發(fā)來的任何數(shù)據(jù)原樣返回。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include arpa/inet.h #include sys/socket.h #include sys/epoll.h #include fcntl.h #include errno.h #define MAX_EVENTS 1024 #define BUFFER_SIZE 4096 #define PORT 8080 // 設(shè)置socket為非阻塞 int set_nonblocking(int sockfd) { int flags fcntl(sockfd, F_GETFL, 0); if (flags -1) return -1; return fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); } int main() { int server_fd, epoll_fd; struct sockaddr_in server_addr; struct epoll_event ev, events[MAX_EVENTS]; // 1. 創(chuàng)建TCP socket server_fd socket(AF_INET, SOCK_STREAM, 0); if (server_fd -1) { perror(socket creation failed); exit(EXIT_FAILURE); } // 設(shè)置SO_REUSEADDR避免TIME_WAIT狀態(tài)導(dǎo)致bind失敗 int opt 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt))) { perror(setsockopt failed); close(server_fd); exit(EXIT_FAILURE); } // 2. 綁定地址和端口 memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_addr.s_addr INADDR_ANY; server_addr.sin_port htons(PORT); if (bind(server_fd, (struct sockaddr*)server_addr, sizeof(server_addr)) -1) { perror(bind failed); close(server_fd); exit(EXIT_FAILURE); } // 3. 開始監(jiān)聽 if (listen(server_fd, SOMAXCONN) -1) { perror(listen failed); close(server_fd); exit(EXIT_FAILURE); } printf(Echo server listening on port %d...\n, PORT); // 4. 創(chuàng)建epoll實例 epoll_fd epoll_create1(0); if (epoll_fd -1) { perror(epoll_create1 failed); close(server_fd); exit(EXIT_FAILURE); } // 5. 將server socket添加到epoll監(jiān)控監(jiān)聽讀事件新連接 ev.events EPOLLIN; // 水平觸發(fā)模式 ev.data.fd server_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, ev) -1) { perror(epoll_ctl: server_fd add failed); close(server_fd); close(epoll_fd); exit(EXIT_FAILURE); } // 主循環(huán) while (1) { // 6. 等待事件發(fā)生 超時時間設(shè)為-1表示一直阻塞 int nfds epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (nfds -1) { perror(epoll_wait error); // 通常被信號中斷可以繼續(xù) if (errno EINTR) continue; break; } // 7. 處理所有就緒的事件 for (int i 0; i nfds; i) { int current_fd events[i].data.fd; // 如果是server socket就緒表示有新連接 if (current_fd server_fd) { struct sockaddr_in client_addr; socklen_t client_len sizeof(client_addr); int client_fd accept(server_fd, (struct sockaddr*)client_addr, client_len); if (client_fd -1) { perror(accept failed); continue; } // 可選獲取客戶端IP char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, client_addr.sin_addr, client_ip, sizeof(client_ip)); printf(New connection from %s:%d, fd%d\n, client_ip, ntohs(client_addr.sin_port), client_fd); // 將新客戶端socket設(shè)為非阻塞為未來ET模式做準(zhǔn)備LT模式非必須但推薦 set_nonblocking(client_fd); // 將新客戶端socket加入epoll監(jiān)控監(jiān)聽讀事件 ev.events EPOLLIN; // 默認(rèn)LT模式 ev.data.fd client_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, ev) -1) { perror(epoll_ctl: client_fd add failed); close(client_fd); } } else { // 否則是客戶端socket有數(shù)據(jù)可讀或發(fā)生錯誤 if (events[i].events EPOLLIN) { char buffer[BUFFER_SIZE]; ssize_t bytes_read; // 循環(huán)讀取直到讀完內(nèi)核緩沖區(qū)所有數(shù)據(jù)LT模式這樣寫安全ET模式必須這樣寫 while ((bytes_read read(current_fd, buffer, sizeof(buffer))) 0) { // Echo將讀到的數(shù)據(jù)原樣寫回 if (write(current_fd, buffer, bytes_read) ! bytes_read) { perror(write failed); break; } printf(Echoed %zd bytes back to fd%d\n, bytes_read, current_fd); } // 處理讀取結(jié)果 if (bytes_read 0) { // 客戶端正常關(guān)閉連接 printf(Client fd%d disconnected.\n, current_fd); close(current_fd); // 關(guān)閉時會自動從epoll中移除 } else if (bytes_read -1) { // 讀取錯誤 if (errno ! EAGAIN errno ! EWOULDBLOCK) { // 非阻塞IO下的正?!盁o數(shù)據(jù)”錯誤 perror(read error); close(current_fd); } // 如果是EAGAIN/EWOULDBLOCK說明本次數(shù)據(jù)已讀完在ET模式下常見LT模式下可能不會出現(xiàn) } } // 可以在這里處理EPOLLOUT寫就緒和EPOLLERR等事件 if (events[i].events (EPOLLERR | EPOLLHUP)) { printf(Error or hangup on fd%d, closing.\n, current_fd); close(current_fd); } } } } // 清理通常不會執(zhí)行到這里 close(server_fd); close(epoll_fd); return 0; }代碼關(guān)鍵點解析水平觸發(fā)LT本例使用默認(rèn)的LT模式。當(dāng)客戶端發(fā)來數(shù)據(jù)epoll_wait會返回這個fd的EPOLLIN事件。即使我們一次read沒有把內(nèi)核緩沖區(qū)數(shù)據(jù)全部讀完比如數(shù)據(jù)比buffer大只要緩沖區(qū)還有數(shù)據(jù)下次調(diào)用epoll_wait時這個事件依然會被報告。這使得編程邏輯簡單。非阻塞socket雖然LT模式不強制要求非阻塞socket但我們依然將客戶端socket設(shè)為非阻塞。這是一個好習(xí)慣可以防止在某些邊緣情況如對端關(guān)閉、數(shù)據(jù)異常下read/write調(diào)用意外阻塞整個線程。事件循環(huán)程序核心是一個while(1)循環(huán)不斷調(diào)用epoll_wait等待事件然后處理就緒事件。這是所有基于事件驅(qū)動服務(wù)器的通用結(jié)構(gòu)。連接管理當(dāng)read返回0表示客戶端主動關(guān)閉連接發(fā)送了FIN我們需要關(guān)閉本地的socket fd。關(guān)閉后內(nèi)核會自動將其從epoll的監(jiān)控列表中移除。你可以用gcc -o epoll_echo epoll_echo.c編譯然后運行./epoll_echo。用telnet 127.0.0.1 8080或者nc 127.0.0.1 8080命令連接測試輸入任何字符服務(wù)器都會將其回顯。6. 常見問題與性能調(diào)優(yōu)實戰(zhàn)在實際使用epoll構(gòu)建高并發(fā)服務(wù)時你會遇到各種坑。下面記錄一些典型問題和我的排查經(jīng)驗。6.1 為什么我的epoll服務(wù)器在高壓下連接數(shù)上不去可能原因及排查文件描述符限制這是最常見的原因。每個socket連接都是一個fd。系統(tǒng)對單個進程和全局有fd數(shù)量限制。檢查命令ulimit -n查看當(dāng)前shell的進程限制。cat /proc/sys/fs/file-max查看系統(tǒng)全局總限制。解決方法程序啟動前用ulimit -n 1000000臨時提高。在程序中用setrlimit系統(tǒng)調(diào)用動態(tài)提高。修改系統(tǒng)配置文件/etc/security/limits.conf永久提高。端口耗盡作為服務(wù)器端口一般固定。但作為客戶端發(fā)起連接或者服務(wù)器主動連接后端服務(wù)時會受本地端口范圍限制。檢查命令cat /proc/sys/net/ipv4/ip_local_port_range。解決方法擴大端口范圍echo “1024 65535” /proc/sys/net/ipv4/ip_local_port_range。更根本的是優(yōu)化架構(gòu)使用連接池、長連接。線程模型或業(yè)務(wù)邏輯阻塞如果你使用了Reactor線程池模型但工作線程池的任務(wù)隊列滿或者某個業(yè)務(wù)處理如慢SQL查詢耗時過長會導(dǎo)致新連接無法被及時accept因為Reactor線程可能在處理別的事情或者工作線程全被占用。排查使用top -Hp [pid]查看進程內(nèi)線程CPU使用率用strace或perf分析線程卡在哪個系統(tǒng)調(diào)用或函數(shù)。解決優(yōu)化慢業(yè)務(wù)增加工作線程數(shù)或使用異步非阻塞的客戶端如異步MySQL驅(qū)動。6.2 ET模式 vs LT模式到底怎么選這是一個經(jīng)典爭論。我的經(jīng)驗是LT水平觸發(fā)默認(rèn)推薦新手和大多數(shù)業(yè)務(wù)使用。編程模型簡單不容易漏事件。即使你某次沒有處理完數(shù)據(jù)下次epoll_wait還會提醒你。代價是可能帶來額外的系統(tǒng)調(diào)用開銷如果就緒事件你一直不處理內(nèi)核會反復(fù)通知。ET邊緣觸發(fā)高性能場景的利器但編程復(fù)雜。它只在狀態(tài)變化時通知一次。這要求你必須使用非阻塞IO。在收到讀事件時必須循環(huán)read直到返回EAGAIN確保把本次內(nèi)核緩沖區(qū)的數(shù)據(jù)全部讀完。寫事件處理也更復(fù)雜需要自己管理輸出緩沖區(qū)當(dāng)可寫時EPOLLOUT才寫入。適用場景需要極致性能、你對自己的代碼有絕對控制力、且連接非?;钴S如高頻交易系統(tǒng)。對于普通Web后端LT模式的開銷微乎其微ET帶來的復(fù)雜性得不償失。6.3 epoll驚群問題什么是驚群當(dāng)多個進程/線程同時阻塞在同一個epoll_wait上監(jiān)聽同一個端口比如通過fork共享listen socket當(dāng)一個新連接到來時內(nèi)核會喚醒所有等待的進程/線程但最終只有一個能成功accept其他都被喚醒后又無事可做白白消耗CPU資源。解決方案SO_REUSEPORTLinux 3.9這是現(xiàn)代最優(yōu)雅的解決方案。允許多個進程綁定到相同的IP和端口。內(nèi)核會負(fù)責(zé)將新連接負(fù)載均衡到不同的監(jiān)聽socket上從而每個進程有自己的epoll實例從根本上避免了驚群。Nginx就支持這種模式。EPOLLEXCLUSIVELinux 4.5在epoll_ctl添加監(jiān)聽socket時使用EPOLLEXCLUSIVE標(biāo)志。這可以保證一個連接事件只會喚醒一個正在epoll_wait的進程避免了accept驚群。應(yīng)用層互斥鎖老式方法。多個進程競爭一個全局鎖拿到鎖的進程才能去accept。效率較低。6.4 連接關(guān)閉與資源釋放這是一個極易出錯的地方。對端正常關(guān)閉read返回0。你應(yīng)該關(guān)閉本地fd并調(diào)用epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL)將其從epoll監(jiān)控中移除實際上close(fd)后內(nèi)核會自動移除但顯式刪除是好習(xí)慣。本端主動關(guān)閉先調(diào)用epoll_ctl刪除監(jiān)控再shutdown和close。注意關(guān)閉后epoll可能還會返回這個fd的EPOLLHUP等事件你的代碼需要能優(yōu)雅處理。半關(guān)閉shutdown(fd, SHUT_WR)關(guān)閉寫端但還可以讀。這在協(xié)議處理中有時用到需要根據(jù)業(yè)務(wù)邏輯正確處理epoll的事件監(jiān)聽移除EPOLLOUT。6.5 性能監(jiān)控與調(diào)試命令ss -tanp比netstat更高效查看所有TCP連接狀態(tài)以及對應(yīng)的進程。觀察ESTAB狀態(tài)的連接數(shù)是否正常。cat /proc/net/sockstat查看系統(tǒng)級別的socket分配情況。perf top/strace -p [pid]分析進程的系統(tǒng)調(diào)用和函數(shù)熱點。vmstat 1/mpstat 1查看系統(tǒng)整體CPU、中斷、上下文切換情況。如果cs上下文切換過高可能線程模型有問題。最后理解IO多路復(fù)用是構(gòu)建高性能網(wǎng)絡(luò)服務(wù)的基石。它不是一個孤立的API而是需要和線程模型、緩沖區(qū)管理、協(xié)議解析等結(jié)合起來。從最簡單的echo服務(wù)器開始逐步增加連接超時管理、協(xié)議處理、線程池你就能慢慢體會到像Redis、Nginx這樣的軟件是如何設(shè)計出來的。記住高并發(fā)的核心秘密就是用最少的線程去管理最多的IO等待把CPU時間片留給真正的計算。