【架構入門 - 高效能篇】單機高效能
協作方式
在高併發場景中,必須要讓伺服器同時維護大量請求連線,可能是一個服務程序建立另一個程序,也可能是一個服務執行緒去建立另一個執行緒,但連線結束後進程或執行緒就銷燬了,這是一個巨大的浪費
一個自然的想法就是通過建立一個程序/執行緒池從而達到資源複用,一個程序/執行緒可以處理多個連線
那麼如何處理多個連線?
同步阻塞
一個請求佔用一個程序處理,先等待資料準備好,然後從核心向程序複製資料,最後處理完資料後返回
如果一個程序處理一個請求,再來請求再開程序,雖然會有CPU在等待IO時的浪費和程序數量限制,但還是可以做到一定的高效能。如果一個程序處理多個連線,那麼其他連線會在第一個連線導致的IO操作時被阻塞,這樣無法做到高效能,所以不會選擇該模式實現高效能
同步非阻塞
程序先將一個套接字在核心中設定成非阻塞再等待資料準備好,在這個過程中反覆輪詢核心資料是否準備好,準備好之後最後處理資料返回
一個程序處理一個請求不太實際,一個程序處理多個請求的效能上限會更高,所以簡單的處理 同步阻塞 中的阻塞問題的方式就是一個程序輪詢多個連線,但輪詢是有CPU開銷的,且如果一個程序有成千上萬的連線時效率很低,也不會選擇該模式實現高效能
I/O多路複用
相當於對 同步非阻塞 的優化版本,區別在於 I/O多路複用 阻塞在select,epoll這樣的系統呼叫之上,而沒有阻塞在真正的I/O系統呼叫如recvfrom之上。換句話說,輪詢機制被優化成通知機制,多個連線公用一個阻塞物件,程序只需要在一個阻塞物件上等待,無需再輪詢所有連線
當某條連線有新的資料可以處理時,作業系統會通知程序,程序從阻塞狀態返回,開始處理業務,這是高效能的基礎,但仍不算高效,因為讓一個程序/執行緒進行select是不夠的,還需要某種機制來分配程序/執行緒去負責監聽、處理資料這個兩個過程才能實現高效能
Reactor
I/O多路複用結合 執行緒池 就是 Reactor
Reactor的核心包括Reactor(監聽和分配事件)和處理資源池(負責處理事件),具體實現可以多變,體現在:
- Reactor的數量可以變化
- 處理資源池的數量可以變化,可以是單個程序/執行緒,也可以是多個程序/執行緒
單Reactor單程序/執行緒
- Reactor物件通過select監控連線事件,收到事件後通過dispatch分發
- 如果是建立連線,交給Acceptor處理,通過accept接收連線,建立一個Handler來處理連線後續的事件
- 如果是不是建立連線事件,交給之前建立連線階段建立的對應的Handler處理
優點是簡單,沒有程序間通訊、競爭,缺點是隻有一個程序,無法發揮多核CPU效能,且Handler上處理某個連線的業務時,整個程序無法處理任何其他事件
所以適用場景不多,適合於業務處理非常快的場景,如Redis
單Reactor多執行緒
與 單Reactor單程序/執行緒 在於Handler只負責響應事件,業務處理交給Processor,且Processor會在獨立的子執行緒中處理,然後將結果發給主程序的Handler處理
優點是充分發揮了多核CPU的能力,缺點是多執行緒資料共享複雜,且Reactor承擔所有事件的監聽和響應,高併發會成為瓶頸
多Reactor多程序/執行緒
為了解決 單Reactor多執行緒 的問題,這個模式的區別:
- 父程序的select監聽到連線建立事件後通過Acceptor將新的連線分配給子程序
- 子程序的Reactor將新的連線加入自己的連線佇列進行監聽,並建立一個Handler用於處理連線的事件
- 當有新的事件發生,子Reactor會呼叫連線的Handler
- Handler完成read->業務處理->send的業務流程
看起來比 單Reactor多執行緒 更復雜,但實現更簡單,因為:
- 父程序只負責接收並建立新連線,子程序只負責業務處理
- 父子程序之間的互動只有父程序把連線交給子程序,子程序不需要把結果返回給父程序
Nginx、Memcache、Netty使用的就是該模式
Proactor
實踐方式
高效能的程式碼
效能 日誌
合適的伺服器
規格 配置
參考
號外號外
最近在總結一些針對 Java 面試相關的知識點,感興趣的朋友可以一起維護~