高性能開發十大必須掌握的核心技術
● 前言
程序員經常要面臨的一個問題就是:如何提高程序性能?
這篇文章,我們循序漸進,從內存、磁盤 I/O、網絡 I/O、CPU、緩存、架構、算法等多層次遞進,串聯起高性能開發十大必須掌握的核心技術。
-
主線程進入一個循環,等待連接。
-
來一個連接就啓動一個工作線程來處理。
-
工作線程中,等待對方請求,然後從磁盤讀文件、往套接口發送數據。
● I/O 優化:零拷貝技術
上面的工作線程,從磁盤讀文件、再通過網絡發送數據,數據從磁盤到網絡,兜兜轉轉需要拷貝四次,其中 CPU 親自搬運都需要兩次。
Linux API:
ssize_t sendfile(
int out_fd,
int in_fd,
off_t *offset,
size_t count
);
函數名字已經把函數的功能解釋的很明顯了:發送文件。指定要發送的文件描述符和網絡套接字描述符,一個函數搞定!
用上了零拷貝技術後開發了 2.0 版本,圖片加載速度明顯有了提升。不過老闆發現同時訪問的人變多了以後,又變慢了,又讓你繼續優化。這個時候,你需要
● I/O 優化:多路複用技術
前面的版本中,每個線程都要阻塞在 recv 等待對方的請求,這來訪問的人多了,線程開的就多了,大量線程都在阻塞,系統運轉速度也隨之下降。
這個時候,你需要多路複用技術,使用 select 模型,將所有等待(accept、recv)都放在主線程裏,工作線程不需要再等待。
這個時候,你需要升級多路複用模型爲 epoll。
● select 有三弊,epoll 有三優。
-
select 底層採用數組來管理套接字描述符,同時管理的數量有上限,一般不超過幾千個,epoll 使用樹和鏈表來管理,同時管理數量可以很大。
-
select 不會告訴你到底哪個套接字來了消息,你需要一個個去詢問。epoll 直接告訴你誰來了消息,不用輪詢。
-
select 進行系統調用時還需要把套接字列表在用戶空間和內核空間來回拷貝,循環中調用 select 時簡直浪費。epoll 統一在內核管理套接字描述符,無需來回拷貝。
用上了 epoll 多路複用技術,開發了 3.0 版本,你的網站能同時處理很多用戶請求了。
● 線程池技術
創建線程池
我們可以通過自定義 ThreadPoolExecutor 或者 jdk 內置的 Executors 來創建一系列的線程池
-
newFixedThreadPool: 創建固定線程數量的線程池
-
newSingleThreadExecutor: 創建單一線程的池
-
newCachedThreadPool: 創建線程數量自動擴容, 自動銷燬的線程池
-
newScheduledThreadPool: 創建支持計劃任務的線程池
潛在宕機風險
- 使用 Executors 來創建要注意潛在宕機風險
1.FixedThreadPool 和 SingleThreadPoolPool : 允許的請求隊列長度爲 Integer.MAX_VALUE,可能因爲無限制任務隊列而耗盡資源,只是出現問題的概率較小。如果新請求的到達速率超過了線程池的處理速率,那麼新到來的請求將被累積起來可能會堆積大量的請求,從而導致 OOM(內存溢出).
解決方法:使用有界隊列可以防止資源耗盡,但也因此必須要考慮飽和策略。因爲默認的中止策略可能不是我們想要的
- CachedThreadPool 和 ScheduledThreadPool : 允許的創建線程數量爲 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM.
解決方法:這種情況可以採用固定大小的線程池來解決這個問題。
可能有大量請求的線程池場景中, 更推薦自定義 ThreadPoolExecutor 來創建線程池
線程池大小配置:
一般根據任務類型進行區分, 假設 CPU 爲 N 核
CPU 密集型任務需要減少線程數量, 降低線程切換開銷,可配置線程池大小爲 N + 1.
IO 密集型任務則可以加大線程數量, 可配置線程池大小爲 N * 2.
混合型任務則可以拆分爲 CPU 密集型與 IO 密集型, 獨立配置.
● 無鎖編程技術
多線程併發編程中,遇到公共數據時就需要進行線程同步。而這裏的同步又可以分爲阻塞型同步和非阻塞型同步。
阻塞型同步好理解,我們常用的互斥體、信號、條件變量等這些操作系統提供的機制都屬於阻塞型同步,其本質都是要加 “鎖”。
與之對應的非阻塞型同步就是在無鎖的情況下實現同步,目前有三類技術方案:
-
Wait-free
-
Lock-free
-
Obstruction-free
三類技術方案都是通過一定的算法和技術手段來實現不用阻塞等待而實現同步,這其中又以 Lock-free 最爲應用廣泛。
Lock-free 能夠廣泛應用得益於目前主流的 CPU 都提供了原子級別的 read-modify-write 原語,這就是著名的 CAS(Compare-And-Swap) 操作。在 Intel x86 系列處理器上,就是 cmpxchg 系列指令。
// 通過CAS操作實現Lock-free
do {
...
} while(!CAS(ptr,old_data,new_data ))
我們常常見到的無鎖隊列、無鎖鏈表、無鎖 HashMap 等數據結構,其無鎖的核心大都來源於此。在日常開發中,恰當的運用無鎖化編程技術,可以有效地降低多線程阻塞和切換帶來的額外開銷,提升性能。
● 進程間通信技術
提起進程間通信,你能想到的是什麼?
====================
-
管道
-
命名管道
-
socket
-
消息隊列
-
信號
-
信號量
-
共享內存
以上各種進程間通信的方式詳細介紹和比較,推薦一篇文章一文掌握進程間通信,這裏不再贅述。
對於本地進程間需要高頻次的大量數據交互,首推共享內存這種方案。
現代操作系統普遍採用了基於虛擬內存的管理方案,在這種內存管理方式之下,各個進程之間進行了強制隔離。程序代碼中使用的內存地址均是一個虛擬地址,由操作系統的內存管理算法提前分配映射到對應的物理內存頁面,CPU 在執行代碼指令時,對訪問到的內存地址再進行實時的轉換翻譯。
而共享內存這種進程間通信方案的核心在於:如果讓同一個物理內存頁面映射到兩個進程地址空間中,雙方不是就可以直接讀寫,而無需拷貝了嗎?
當然,共享內存只是最終的數據傳輸載體,雙方要實現通信還得藉助信號、信號量等其他通知機制。
用上了高性能的共享內存通信機制,多個服務進程之間就可以愉快的工作了,即便有工作進程出現 Crash,整個服務也不至於癱瘓。
滿足於只能提供靜態網頁瀏覽了,需要能夠實現動態交互。這一次老闆還算良心,給你加了一臺硬件服務器。
於是你用 Java/PHP/Python 等語言搞了一套 web 開發框架,單獨起了一個服務,用來提供動態網頁支持,和原來等靜態內容服務器配合工作。
這個時候你發現,靜態服務和動態服務之間經常需要通信。
一開始你用基於 HTTP 的 RESTful 接口在服務器之間通信,後來發現用 JSON 格式傳輸數據效率低下,你需要更高效的通信方案。
這個時候你需要:
RPC && 序列化技術
什麼是 RPC 技術?
RPC 全稱 Remote Procedure Call,遠程過程調用。我們平時編程中,隨時都在調用函數,這些函數基本上都位於本地,也就是當前進程某一個位置的代碼塊。但如果要調用的函數不在本地,而在網絡上的某個服務器上呢?這就是遠程過程調用的來源。
來源:
https://www.toutiao.com/i6946920710614909453/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/jwyvwb4VCetU04rbP7eORg