內容主要來自搜狗實驗室技術交流文檔,
編寫鏈接數巨大的高負載服務器程序時,經典的多線程模式和select模式都不再適合了.應該采用epool/kqueue/dev_pool來捕獲IO事件.
------
問題的由來:
C10K問題的最大特點就是:設計不夠良好的程序,其性能和鏈接數以及機器性能的關系是非線性的.
例子:沒有考慮過C10k問題,一個經典的基於select的程序能在就服務器上很耗處理1000並發的吞吐量,但是在2倍性能新服務器上往往處理不了並發2000的吞吐量.
因為:大量操作的消耗和當前鏈接數n成線性相關.
=-==================
基本策略:
主要有兩個方面的策略:
1,應用軟件以何種方式和操作系統合作,獲取IO事件並調度多個socket上的IO操作;
2,應用軟件以何種方式處理任務和線程/進程的關系.
前者主要有阻塞IO,費阻塞IO,異步IO三種方式
后者主要有每任務1進程,每任務1線程,單線程,多任務共享線程池以及一些更復雜的變種方案.
常用的經典策略如下:
1,serve one client with each thread/process, and use blocking IO.,
2,serve many clients with single thread, and use nonblocking IO and readiness notification.
3,serve many clients with each thread, and use nonblocking IO and readliness notification
4,serve many clienets witch each thread, and use asynchronous IO.
接下倆主要介紹策略2.
=======================
經典的單線程服務器程序結構往往如下:
do{ get readiness notification of all sockets dispatch ready handles to corresponding handlers if(readable){ read the socketsif if(read done){ handler process the request } } if(writable){ write response } if(nothing to do){ close socket } }while(True)
其中關鍵的部分就是readiness notification,找出哪一個socket上面發生了IO事件.
一般從教科書和例子程序中會學到select來實現,
select函數的定義:
int select(int n,fd_set *rd_fds,fd_set *wr_fds, fd_set *ex_fds,struct timeval *timeout);
select用到了fd_set結構,從man page里可以看到fd_set能容納的句柄和FD_SETSIZE相關.實際上fd_set在*nix下是一個bit標志數組,每個bit表示對應下標的fd是不是在fd_set中. fd_set只能容納編號小於FD_SETSIZE的那些句柄.
----
FD_SETSIZE默認是1024,如果向fd_set中放入過大的句柄,數組越界以后程序就會垮掉.系統默認限制了一個進程最大的句柄號小於 1024,但是可以通過ulimit -n命令或者setrlimit函數來擴大這一限制.如果不幸一個程序在FD_SETSIZE=1024的環境下編譯,運行時又遇到ulimit --n>1014的,會出現未定義錯誤.
-----
針對fd_set的問題,*nix提供了poll函數作為select的一個替代品,
int poll(struct poollfd *ufds, unsigned int nfds ,int timeout);
第一個參數ufds是用戶提供的一個pollfd數組,數組大小由用戶自行決定.因此避免了FD_SETSIZE帶來的麻煩.
ufds是fd_set的一個完全替代品,從select到poll的一直很方便,到此我們面對C10k,可以寫出一個能work的程序了.
------
但是select/poll在鏈接數增加時,性能急劇下降.
因為:
1,os面對每次的select/poll操作,都需要重新建立一個當前線程的關心事件列表,並把線程掛到這個復雜的等待隊列上,耗時.
2,app在select/poll返回后,也需要堆傳入的句柄列表做一次掃描來dispatch,耗時.
這兩件事,都是和並發數相關,而事件IO的密度也和並發數相關,導致cpu占用率和並發數近似成O(n2)的關系.
-----------epoll出廠了.
因為以上原因,*nix的開發者開發了epoll,kqueue,/dev/poll這3套利器來幫助大家,
epoll是linux的方案,kqueue是freebsd方案,/dev/poll是最古老的solaris方案,使用難度一次遞增.
為什么這些api是優化方案:
1,避免了每次調用select/poll時kernel分析參數建立事件等待結構的開銷,kernel維護一個長期的事件關注列表,
應用程序通過句柄修改這個列表和捕獲IO事件
2,避免了select/poll返回后,app掃描整個句柄表的開銷,kernel直接返回具體的事件列表為app.
---先了解
邊緣觸發(edge trigger):指每當狀態變化時發生一個IO事件
和條件觸發(level trigger):只要滿足條件就發生一個IO事件
舉個例子:讀socket,假設進過長時間沉默后,來了100個字節,這是無論邊緣觸發/條件觸發都會產生一個read ready notification通知應用程序可讀. app先讀了50bytes,重新調用api等待io,這時條件觸發的api因為還有50bytes刻度可立即返回用戶一個read ready notification. 而邊緣觸發的api因為這個可讀狀態沒變陷入長期等待.
使用邊緣觸發的api時,注意每次要讀到socket返回EWOULDBLOCK為止,否則這個socket就廢了.
而條件觸發的api,如果app不需要寫就不要關注socket可寫的事件,否則會無限次的立即返回一個write ready notification.
條件觸發比較常用.
int epoll_create(int size); int epool_ctl(int epfd,int op,int fd, struct epoll_event *event); int epool_wait(int epfd,struct epoll_event *events, int maxevents,int timeout);
epoll_create 創建kernel中的關注事件表,相當於創建fd_set
epoll_ctl 修改這個表,相當於FD_SET等操作
epoll_wait 等待IO事件發生,相當於select/poll函數
epoll完全是select/poll的升級版,支持的事件一致.並且epoll同時支持條件/邊緣觸發(后者較好).
struct epoll_event ev,*events; int kdpdf = epoll_create(100); ev.events = EPOOL|EPOLLET;//edge trigger ev.data.fd = listener; epoll_ctl(kdpfd,EPOLL_CTL_ADD,listener,&ev); for(;;){ nfds = epoll_wait(kdpfd,events,maxevents,-1); for(n = 0;n<nfds;n++){ if(events[n].data.fd == listener){ client = accept(listener,(struct sockaddr *)&local,&addrlen); if(client <0){ perror("accept"); continue; } setnonblocking(client); ev.events = EPOOLIN|EPOOLET; ev.data.fd = client; if(epoll_ctl(kdpfd,EPOLL_CTL_ADD,client,&ev)<0){ fprintf(stderr,"epoll set insertion error: fd=%d0",client) return -1; } }else{ do_use_fd(events[n].data.fd); } } }