一、基礎知識
1、一個主機的端口號為所有進程所共享,但普通用戶進程綁定bind不了一些特殊端口號如20、80等。
多個進程不能同時監聽listen同一個端口,會失敗。當然父進程可以先listen然后fork多個子進程,多個子進程都可以accept這個sock,即搶奪式響應(驚群效應)。
關注4元組是否能唯一確定一個連接?
2、每個進程都有自己的文件描述符(包括file fd, socket fd, timer fd, event fd, signal fd),一般是1024,可以通過ulimit -n 設置,但所有進程打開的文件描述符總數有上限,跟主機的內存有關。
3、一個進程內的所有線程共享進程的文件描述符。
二、并發服務器方案:
1、循環式/迭代式( iterative )服務器
無法充分利用多核CPU,不適合執行時間較長的服務,即適用于短連接。如果是長連接則需要在read/write之間循環,那么只能服務一個客戶端。
2、并發式(concurrent)服務器
one connection per process/one connection per thread
適合執行時間比較長的服務
one connection per process : 主進程每次fork 之后要關閉connfd,子進程要關閉listenfd
one connection per thread : 主線程每次accept 回來就創建一個子線程服務,由于線程共享文件描述符,故不用關閉。
3、prefork or pre threaded(容易發生“驚群”現象,即多個子進程都處于accept狀態)
4、反應式( reactive )服務器 (reactor模式)(select/poll/epoll)
并發處理多個請求,實際上是在一個線程中完成。無法充分利用多核CPU
不適合執行時間比較長的服務,所以為了讓客戶感覺是在“并發”處理而不是“循環”處理,每個請求必須在相對較短時間內執行。
5、reactor + thread per request(過渡方案)
6、reactor + worker thread(過渡方案)
7、reactor + thread pool(能適應密集計算)
muduo庫中的/example/suduku/ 中有這樣一個例子,因為數獨求解是計算密集型任務。
在實踐中為了reactor能快速回到事件循環去響應請求,經常將讀到的數據put到一個環形內存隊列(一般內存or共享內存),而thread pool的線程則從中讀取進行數據計算。
需要C/C++ linux服務器架構師學習資料私信“資料”(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享
8、multiple reactors(能適應更大的突發I/O)
reactors in threads(one loop per thread)
reactors in processes
一般來說一個subReactor適用于一個千兆網口
9、multiple reactors + thread pool(one loop per thread + threadpool)(突發I/O與密集計算)
subReactor可以有多個,但threadpool只有一個。
10、proactor服務器(proactor模式,基于異步I/O)
理論上proactor比reactor效率要高一些
異步I/O能夠讓I/O操作與計算重疊。充分利用DMA特性。
Linux異步IO
glibc aio(aio_*),有bug
kernel native aio(io_*),也不完美。目前僅支持 O_DIRECT 方式來對磁盤讀寫,跳過系統緩存。要自已實現緩存,難度不小。
boost asio實現的proactor,實際上不是真正意義上的異步I/O,底層是用epoll來實現的,模擬異步I/O的。
常見并發服務器方案比較:
三、一些常見問題
1、Linux能同時啟動多少個線程?
對于 32-bit Linux,一個進程的地址空間是 4G,其中用戶態能訪問 3G 左右,而一個線程的默認棧 (stack) 大小是 8M,心算可知,一個進程大約最多能同時啟動 350 個線程左右。
2、多線程能提高并發度嗎?
如果指的是“并發連接數”,不能。
假如單純采用 thread per connection 的模型,那么并發連接數大約350,這遠遠低于基于事件的單線程程序所能輕松達到的并發連接數(幾千上萬,甚至幾萬)。所謂“基于事件”,指的是用 IO multiplexing event loop 的編程模型,又稱 Reactor 模式。
3、多線程能提高吞吐量嗎?
對于計算密集型服務,不能。
如果要在一個8核的機器上壓縮100個1G的文本文件,每個core的處理能力為200MB/s,那么“每次起8個進程,一個進程壓縮一個文件”與“只啟動一個進程(8個線程并發壓縮一個文件)”,這兩種方式總耗時相當,但是第二種方式能較快的拿到第一個壓縮完的文件。
4、多線程能提高響應時間嗎?
可以。參考問題3
5、多線程程序日志庫要求
線程安全,即多個線程可以并發寫日志,兩個線程的日志消息不會出現交織。
用一個全局的mutex保護IO
每個線程單獨寫一個日志文件
前者造成全部線程搶占一個鎖(串行寫入)
后者有可能讓業務線程阻塞在寫磁盤操作上。(磁盤IO時間比較長)
解決辦法:用一個logging線程負責收集日志消息,并寫入日志文件,其他業務線程只管往這個“日志線程”發送日志消息(如通過BlockingQueue提供接口),這稱為“異步日志”,也是一個經典的生產者消費者模型。
6、線程池大小的選擇
如果池中執行任務時,密集計算所占時間比重為P(0<P<=1),而系統一共有C個CPU,為了讓C個CPU跑滿而不過載,線程池大小的經驗公式T=C/P,即T*P=C(讓CPU剛好跑滿 )
假設C=8,P=1.0,線程池的任務完全密集計算,只要8個活動線程就能讓CPU飽和
假設C=8,P=0.5,線程池的任務有一半是計算,一半是IO,那么T=16,也就是16個“50%繁忙的線程”能讓8個CPU忙個不停。
7、線程分類
I/O線程(這里特指網絡I/O)
計算線程
第三方庫所用線程,如logging,又比如database