Linux 開發,使用多線程還是用 IO 復用 select/epoll?

每分鐘有2K用戶訪問,伺服器端處理請求選擇用多線程(每個用戶一個線程),還是用I/O復用?


我是來指出排名第一的回答的BUG的:
"

  • 多線程模型適用於處理短連接,且連接的打開關閉非常頻繁的情形,但不適合處理長連接。多線程模型默認情況下,(在Linux)每個線程會開8M的棧空間,再TCP長連接的情況下,2000/分鐘的請求,幾乎可以假定有上萬甚至十幾萬的並發連接,假定有10000個連接,開這麼多個線程需要10000*8M=80G的內存空間!即使調整每個線程的棧空間,也很難滿足更多的需求。甚至攻擊者可以利用這一點發動DDoS,只要一個連接連上伺服器什麼也不做,就能吃掉伺服器幾M的內存,這不同於多進程模型,線程間內存無法共享,因為所有線程處在同一個地址空間中。內存是多線程模型的軟肋。
  • "

你這個說的是Per-connection-Per-thread的方式,不要以此來說這是多線程的模式.

多線程的模式有很多的,leader-follow還有Half-Sync/Half-Async.

現在這個年代,以我見過的代碼,真沒見過幾個是一個連接上來就一個線程的了.


我也同意event loop + thread pool的做法,
epoll + 多線程 + 多進程部署 效率真的不錯。
先用select介面(poll/epoll,kq,iocp)接受請求,這樣可以保證並發,在這個環節他只管收,不處理業務,把FD放到一個buffer(一個q裡面),然後業務處理模型對接線程池。可以使複雜業務處理上的負擔被分擔。select+線程池,這樣兼顧了並發(犧牲了一點性能),又保證了因為邏輯代碼的簡潔性。如果選擇完全非同步的方式,你就要在業務處理裡面使用完全的非同步API,至少很多資料庫驅動,緩存驅動等等你需要用的到技術都沒有提供非同步API,很多業務要保障流程的正確是需要同步操作的,而且業務如果全部使用非同步API,各種不明確回調和閉包導致內存暴棧的危險上升(我想各位應該被nodejs折磨過吧),對開發人員思考方式和技術實力都有較高的要求。一個部門裡面有兩個了解epoll就算技術非常NB的核心部門了吧,假若有能正確駕馭epoll,了解各種觸發方式,狀態機,特別是要能正確讀寫完整的信息,而沒有造成大量的CLOSE_WAIT,是特別特別不易的。
我曾在tornado上面搭建過一個線程池。原型參見:nikoloss/iceworld · GitHub
雖然不算最完美的解決方案,但是也在工作中省去了很多煩惱。他的效率雖沒有原生tornado高,但是非常適合多人合作(儘管如此效率還是要暴webpy幾條街)。


鐘有2K?差點看成每秒鐘。
這2K個用戶是長連接呢還是短連接呢?如果是短連接,或者連接後通信不頻繁的話,線程(池)就可以,如果一直是2k的並發,那還是非同步(感謝@程ocean提醒,補充完整:用epoll配合非阻塞IO實現,epoll本身並非非同步)靠譜點。再高就要非同步加並行(多線程/多進程)了。


2k個很少,業務若不複雜就隨便選一個。量大一般會結合使用。


在Linux下我個人以往的經驗是多進程加非同步最好。當然了,這個經驗不一定在所有場景都適用……


有幾種網路伺服器模型分析如下

1. 一個連接一個線程模型:適用場景,連接少,且邏輯複雜。例如mysql採用此模型,一個連接一個線程。模型的一些小變體是線程採用線程池,避免創建銷毀線程的開銷

2. 半同步半非同步模型:單獨一個IO線程來非同步處理網路IO,使用線程池來同步處理請求,業務邏輯的編寫就會變得簡單。適用於並發連接較多,但是每秒的請求量不太大的業務。可以參考
半同步半非同步I/O的設計模式(half sync/half async)
也可以參考我的
handy/hsha.cc at master · yedf/handy · GitHub
Leader/Follower模式屬於這個模型的變體

3. 全非同步模型:網路IO和業務處理都是非同步的,一個線程可以處理所有的任務,程序不會阻塞在任何一個網路IO或者磁碟IO上。這種模型能夠最大程度利用計算機的性能,但是全非同步的處理讓業務的編寫變得非常複雜。nginx這是這種模型,他是多進程,每個worker都是一樣的,沒有不同。memcache也是類似的,它的多線程完全是為了利用多個cpu能力,單線程也能夠完整的跑整個業務的。

樓主的每分鐘2k用戶,按照通常的訪問情況,每秒也就4,5個請求,最適合應該是半同步半非同步模型。
----------------------------------------------
2017.1.22更新

全非同步模型是性能最好,同時也是開發難度最高的模型。為了追求更好的性能,許多語言例如C++,C#,GO,nodejs,python都嘗試簡化此模型的編程,推出了支持非同步編程的語言特性。

C++的future,promise
C#的async/await
GO的goroutine
nodejs的Promise=&>generator=&>async/await
python的yield

在這些特性的支持下,全非同步編程可以做到與同步編程非常接近,可讀性良好。其中GoLang是近年來新推出的語言,專門為高並發設計了goroutine,對此模型的支持最佳,使用體驗也最好。


「每個連接一個線程」和「多線程」是兩個完全不同的概念,所以這個問題的描述是有毛病的。多線程與否和epoll/kqueue沒有直接關係,這兩個問題是正交關係。有one eventloop per thread/threadpool等設計,應視情況而定。

如果說的是os提供的native thread,第一種做法是不可取的,除非是cpu密集的服務,比如十幾個進程就吃光了cpu那種,這時候可以簡化設計。對於io密集的程序,如果是go或者lua那種coroutine,則可以考慮這種模式,這個可能涉及scheduler的profiling,我不懂就不展開了。

c語言沒有原生的coroutine,在沒有專門庫的輔助下做第一種模式是很難的,基本上都要採用基於io multiplex的設計。但上層用戶代碼一般不直接調用這種底層api,「epoll=伺服器程序」的思路是錯誤的。

另外,@藍形參 認為epoll是非同步api,難以苟同。linux的非同步api應當是aio函數系列。


  • 多線程模型適用於處理短連接,且連接的打開關閉非常頻繁的情形,但不適合處理長連接。多線程模型默認情況下,(在Linux)每個線程會開8M的棧空間,再TCP長連接的情況下,2000/分鐘的請求,幾乎可以假定有上萬甚至十幾萬的並發連接,假定有10000個連接,開這麼多個線程需要10000*8M=80G的內存空間!即使調整每個線程的棧空間,也很難滿足更多的需求。甚至攻擊者可以利用這一點發動DDoS,只要一個連接連上伺服器什麼也不做,就能吃掉伺服器幾M的內存,這不同於多進程模型,線程間內存無法共享,因為所有線程處在同一個地址空間中。內存是多線程模型的軟肋。
  • 在UNIX平台下多進程模型擅長處理並髮長連接,但卻不適用於連接頻繁產生和關閉的情形。Windows平台忽略此項。 同樣的連接需要的內存數量並不比多線程模型少,但是得益於操作系統虛擬內存的Copy on Write機制,fork產生的進程和父進程共享了很大一部分物理內存。但是多進程模型在執行效率上太低,接受一個連接需要幾百個時鐘周期,產生一個進程 可能消耗幾萬個CPU時鐘周期,兩者的開銷不成比例。而且由於每個進程的地址空間是獨立的,如果需要進行進程間通信的話,只能使用IPC進行進程間通 信,而不能直接對內存進行訪問。在CPU能力不足的情況下同樣容易遭受DDos,攻擊者只需要連上伺服器,然後立刻關閉連接,服務端則需要打開一個進程再關閉。
  • 同時需要保持很多的長連接,而且連接的開關很頻繁,最高效的模型是非阻塞、非同步IO模型。而且不要用select/poll,這兩個API的有著O(N)的時間複雜度。在Linux用epoll,BSD用kqueue,Windows用IOCP,或者用libevent封裝的統一介面(對於不同平台libevent實現時採用各個平台特有的API),這些平台特有的API時間複雜度為O(1)。 然而在非阻塞,非同步I/O模型下的編程是非常痛苦的。由於I/O操作不再阻塞,報文的解析需要小心翼翼,並且需要親自管理維護每個鏈接的狀態。並且為了充分利用CPU,還應結合線程池,避免在輪詢線程中處理業務邏輯。
    但這種模型的效率是極高的。以知名的http伺服器nginx為例,可以輕鬆應付上千萬的空連接+少量活動鏈接,每個連接連接僅需要幾K的內核緩衝區,想要應付更多的空連接,只需簡單的增加內存(數據來源為淘寶一位工程師的一次技術講座,並未實測)。這使得DDoS攻擊者的成本大大增加,這種模型攻擊者只能將伺服器的帶寬全部佔用,才能達到目的,而兩方的投入是不成比例的。

這兩種模型並不衝突。可以預開與處理器內核數量相當的線程,然後每個線程內部再IO復用。不過每分鐘2K個請求的訪問,如果每個訪問不是持續時間很長的話,其實隨便怎麼寫都行。


epoll 多路復用肯定是要用的了,至於 accept 以後怎麼處理客戶端連接,這個可以衡量一下直接用非同步 IO 還是用線程池處理連接。

每個用戶一個線程太可怕了……


很多人關注這個問題,我相信焦點不在於每分鐘多少k用戶上,而是這兩種方式帶來在編程結構上的差異。

不談多線程在性能上的優劣勢,在單核時代就已經被普遍應用的這個技術主要帶來的變化是邏輯隔離。代碼的複雜性相對被簡化了,也減少了局部不斷重構的需求。

如今多核時代下,我們還需要多線程壓榨出足夠的性能,同時避免過多線程帶來的調度開銷。結合上述編碼的需求,需要在某些方面作出根本的變革。

Erlang和go走在正確的路線上,但還是不夠或存有技術上的瑕疵。


可別復用,直接epoll,嘎嘎好使。。。。。


這個問題真的是老生常談了,初期可以把幾個方案都做做,能學到不少東西。但是一旦你熟練掌握了,做得太多跟月經似的周期性時,我非常認真的建議你用 Erlang,實在不行用 Go 也不錯,再不行如果處理的都是些 Web 方面的小業務而且對容錯性的要求不是很高,那用 node.js 也比整天挖 C/C++ 里的 epool/kqueue/IOCP 那點老墳有趣多了。

網路 IO 模型就那麼幾種,儘管套著所謂高性能的大帽子,但不管哪種其實用起來都很蹩腳。因為根源在於 C/C++ 對並發的支持實在是可憐到髮指。只有庫級別的支持,沒有任何語義層面的機制。

C/C++ 在服務端高並發IO密集型業務方面生產效率和可維護性方面低得令人髮指,完全抵消掉了那一點點性能方面的增益。

最後再強調一遍,學習一下各種網路模型有百利無一害,但也僅此而已,在生產環境中你應該用一些更可靠的實現,這方面的輪子真的很無聊。


可以用libevent庫啊,用起來很快,然後做個簡單的測試就知道是否符合你的要求了,結合多線程的方式可以參考memcached中使用libevent的經驗


首先我想說下題主說的一個戶一個線程,在良好的設計中都不會出現的。 想要更好的使用cpu性能 ,多線程是必然的。io復用,epoll/select只是底層socket通訊,採用哪種方式都不妨礙你在業務邏輯處理時使用多線程。
消息隊列+線程池來是比較常見的業務邏輯處理方式,消息隊列是底層socket通訊收到的消息解包生成添加的


epoll和多進程一起用,一個處理網路一個處理邏輯,並不衝突
--------------------------
看了題主的需求,發現題主顯然矯枉過正了,2K每分鐘還要什麼event處理?


比較成熟的做法是IO復用+thread pool,這種框架性能好,可擴展性強,目前很多網路庫都採用了這種模式(nginx採用了IO復用+多進程模式)


我覺得event loop + thread pool 就是很好地處理模型,當然event loop就是用epoll來做比較好。


還有人選per connection per thread這個架構?搞十個線程說不準還敵不過別人非同步單線程呢,非同步+threadpool已經幾乎是標配了,不用遲疑。


我覺得作者應該根據業務情況去糾結多路復用非阻塞同步i/o還是非同步i/o,多線程是一定要讓線程池管理的,多線程不是無限線程


推薦閱讀:

如何修改shared_ptr智能指針,讓他支持多線程?
開發多線程的程序應該注意哪些問題?

TAG:編程 | Linux | 計算機網路 | epoll | 多線程 |