入門架構——單機高性能

clipboard.png

協做方式前端

在高併發場景中,必需要讓服務器同時維護大量請求鏈接,多是一個服務進程建立另外一個進程,也多是一個服務線程去建立另外一個線程,但鏈接結束後進程或線程就銷燬了,這是一個巨大的浪費json

一個天然的想法就是經過建立一個進程/線程池從而達到資源複用,一個進程/線程能夠處理多個鏈接性能優化

那麼如何處理多個鏈接?服務器

同步阻塞網絡

clipboard.png

一個請求佔用一個進程處理,先等待數據準備好,而後從內核向進程複製數據,最後處理完數據後返回數據結構

若是一個進程處理一個請求,再來請求再開進程,雖然會有CPU在等待IO時的浪費和進程數量限制,但仍是能夠作到必定的高性能。若是一個進程處理多個鏈接,那麼其餘鏈接會在第一個鏈接致使的IO操做時被阻塞,這樣沒法作到高性能,因此不會選擇該模式實現高性能多線程

同步非阻塞架構

clipboard.png

進程先將一個套接字在內核中設置成非阻塞再等待數據準備好,在這個過程當中反覆輪詢內核數據是否準備好,準備好以後最後處理數據返回併發

一個進程處理一個請求不太實際,一個進程處理多個請求的性能上限會更高,因此簡單的處理同步阻塞中的阻塞問題的方式就是一個進程輪詢多個鏈接,但輪詢是有CPU開銷的,且若是一個進程有成千上萬的鏈接時效率很低,也不會選擇該模式實現高性能框架

I/O多路複用

clipboard.png

至關於對同步非阻塞的優化版本,區別在於I/O多路複用阻塞在select,epoll這樣的系統調用之上,而沒有阻塞在真正的I/O系統調用如recvfrom之上。換句話說,輪詢機制被優化成通知機制,多個鏈接公用一個阻塞對象,進程只須要在一個阻塞對象上等待,無需再輪詢全部鏈接

當某條鏈接有新的數據能夠處理時,操做系統會通知進程,進程從阻塞狀態返回,開始處理業務,這是高性能的基礎,但仍不算高效,由於讓一個進程/線程進行select是不夠的,還須要某種機制來分配進程/線程去負責監聽、處理數據這個兩個過程才能實現高性能

Reactor

I/O多路複用結合線程池就是Reactor

Reactor的核心包括Reactor(監聽和分配事件)和處理資源池(負責處理事件),具體實現能夠多變,體如今:

  • Reactor的數量能夠變化
  • 處理資源池的數量能夠變化,能夠是單個進程/線程,也能夠是多個進程/線程

單Reactor單進程/線程

clipboard.png

Reactor對象經過select監控鏈接事件,收到事件後經過dispatch分發
若是是創建鏈接,交給Acceptor處理,經過accept接收鏈接,建立一個Handler來處理鏈接後續的事件
若是是否是創建鏈接事件,交給以前創建鏈接階段建立的對應的Handler處理
優勢是簡單,沒有進程間通訊、競爭,缺點是隻有一個進程,沒法發揮多核CPU性能,且Handler上處理某個鏈接的業務時,整個進程沒法處理任何其餘事件

因此適用場景很少,適合於業務處理很是快的場景,如Redis

單Reactor多線程

clipboard.png

與單Reactor單進程/線程在於Handler只負責響應事件,業務處理交給Processor,且Processor會在獨立的子線程中處理,而後將結果發給主進程的Handler處理

優勢是充分發揮了多核CPU的能力,缺點是多線程數據共享複雜,且Reactor承擔全部事件的監聽和響應,高併發會成爲瓶頸

多Reactor多進程/線程

clipboard.png

爲了解決單Reactor多線程的問題,這個模式的區別:

父進程的select監聽到鏈接創建事件後經過Acceptor將新的鏈接分配給子進程
子進程的Reactor將新的鏈接加入本身的鏈接隊列進行監聽,並建立一個Handler用於處理鏈接的事件
當有新的事件發生,子Reactor會調用鏈接的Handler
Handler完成read->業務處理->send的業務流程
看起來比單Reactor多線程更復雜,但實現更簡單,由於:

父進程只負責接收並創建新鏈接,子進程只負責業務處理
父子進程之間的交互只有父進程把鏈接交給子進程,子進程不須要把結果返回給父進程
Nginx、Memcache、Netty使用的就是該模式

Proactor

Reactor是同步非阻塞的網絡模型,由於真正的read和send這樣的IO操做都須要用戶進程同步操做,若是把IO操做改成異步就能進一步提高性能,這就是Proactor

clipboard.png

初始化器Initiator負責建立通知組件Proactor和處理器Handler,而且都註冊到內核
內核負責處理註冊請求,並完成IO操做
內核完成IO操做後通知Proactor
Proactor回調到Handler
Handler完成業務處理,Handler也能夠註冊新的Handler到內核
理論上Proactor的效率高於Reactor,讓IO操做與計算重疊,但要實現真正的異步IO,須要操做系統支持,Windows支持而Linux不完善

實踐方式

以上是操做系統或Nginx或高性能服務器軟件已經幫咱們解決了,咱們在編碼的時候除非達到了代碼的性能極限,通常不須要擔憂這方面

因此下面談到的是一些做爲開發人員,爲了提高單體服務的性能而須要注意的地方

高性能的代碼

性能

選用高性能的框架。好比Java方面考慮用Netty,Go方面考慮用Gin
代碼細節。這塊是與咱們最息息相關的了,如何寫出高性能代碼,每種語言都有本身的最佳實踐,反而這裏沒辦法講到,須要平常學習積累。好比字符串拼接效率如何最高?哪一個數據結構適合在某個業務場景使用?
IO細節。因爲磁盤的讀寫速度遠低於CPU、內存,因此對磁盤的讀寫每每會嚴重拖慢性能,好比寫日誌,不注意的話可能本地寫了一份日誌文件,控制檯也在輸出日誌信息,另一個文件上傳流也在寫入信息,那麼log會成倍地拖慢速度,因此須要統一日誌輸出方式,好比只往日誌收集流中寫入到EFK系統中查看

單體服務器壓測

寫出了自認爲高性能的代碼?趕忙來壓測試一遍,壓測就一個目的:尋找瓶頸

在接近於生產環境下的機器作壓測纔是最真實的,還須要使用專門的壓測機來避免環境的影響,最簡單的方式是經過ab工具測試QPS是多少,同時檢測CPU、內存、網絡流量是否達到了瓶頸,而後再根據瓶頸,尋找解決方案,這就是大致壓測以及優化的思路,單體應用的壓測還挺簡單,至於集羣的壓測就須要考慮更多,往後再說

最近我對一個服務進行了壓測,QPS是1200,而且是跑在3臺虛擬機上的,瓶頸在於CPU,因此很明顯單體服務的性能過低或者是總路由出現了轉發問題,這裏不考慮後者,咱們先分析這個服務的接口是拿來幹什麼的,這個接口僅僅作了一件事,從Redis獲取數據,轉發給前端,這裏也不考慮Redis的性能問題,那麼就多是在處理數據的時候性能過低。因此同事將返回的json壓縮了一下,從40kb壓縮到了20kb,QPS直接提高到2500。這就是一個簡單的壓測後調優的例子,還能夠參考這裏。

合適的服務器

規格

若是你用過雲服務,那麼確定會在啓動實例的時候被強迫去選擇一個規格的實例,以下

clipboard.png

那麼請根據你的服務是哪一種性能須要,選擇對應的服務器呢,固然還要考慮你滴錢包夠不夠

配置

在Linux平臺上,在進行高併發TCP鏈接處理時,最高的併發數量都要受到系統對用戶單一進程同時可打開文件數量的限制(這是由於系統爲每一個TCP鏈接都要建立一個socket句柄,每一個socket句柄同時也是一個文件句柄)。可以使用ulimit命令查看系統容許當前用戶進程打開的文件數限制

相似的,對Linux系統配置也會影響到性能的參數須要格外注意,但也須要聽從一個方式:按需調整

感謝您耐心看完的文章

順便給你們推薦一個Java技術交流羣:710373545裏面會分享一些資深架構師錄製的視頻資料:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多!

相關文章
相關標籤/搜索