Part1linux性能優化
1性能優化
性能指標
高并發和響應快對應著性能優化的兩個核心指標:吞吐和延時

-
應用負載角度:直接影響了產品終端的用戶體驗 -
系統資源角度:資源使用率、飽和度等
性能問題的本質就是系統資源已經到達瓶頸,但請求的處理還不夠快,無法支撐更多的請求。性能分析實際上就是找出應用或系統的瓶頸,設法去避免或緩解它們。
-
選擇指標評估應用程序和系統性能 -
為應用程序和系統設置性能目標 -
進行性能基準測試 -
性能分析定位瓶頸 -
性能監控和告警
對于不同的性能問題要選取不同的性能分析工具。下面是常用的Linux Performance Tools以及對應分析的性能問題類型。

到底應該怎么理解”平均負載”
平均負載:單位時間內,系統處于可運行狀態和不可中斷狀態的平均進程數,也就是平均活躍進程數。它和我們傳統意義上理解的CPU使用率并沒有直接關系。
其中不可中斷進程是正處于內核態關鍵流程中的進程(如常見的等待設備的I/O響應)。不可中斷狀態實際上是系統對進程和硬件設備的一種保護機制。
平均負載多少時合理
實際生產環境中將系統的平均負載監控起來,根據歷史數據判斷負載的變化趨勢。當負載存在明顯升高趨勢時,及時進行分析和調查。當然也可以當設置閾值(如當平均負載高于CPU數量的70%時)
現實工作中我們會經常混淆平均負載和CPU使用率的概念,其實兩者并不完全對等:
-
CPU密集型進程,大量CPU使用會導致平均負載升高,此時兩者一致 -
I/O密集型進程,等待I/O也會導致平均負載升高,此時CPU使用率并不一定高 -
大量等待CPU的進程調度會導致平均負載升高,此時CPU使用率也會比較高
平均負載高時可能是CPU密集型進程導致,也可能是I/O繁忙導致。具體分析時可以結合mpstat/pidstat工具輔助分析負載來源
2CPU
CPU上下文切換(上)
CPU上下文切換,就是把前一個任務的CPU上下文(CPU寄存器和PC)保存起來,然后加載新任務的上下文到這些寄存器和程序計數器,最后再跳轉到程序計數器所指的位置,運行新任務。其中,保存下來的上下文會存儲在系統內核中,待任務重新調度執行時再加載,保證原來的任務狀態不受影響。關注Linux中文社區
按照任務類型,CPU上下文切換分為:
-
進程上下文切換 -
線程上下文切換 -
中斷上下文切換
進程上下文切換
Linux進程按照等級權限將進程的運行空間分為內核空間和用戶空間。從用戶態向內核態轉變時需要通過系統調用來完成。
一次系統調用過程其實進行了兩次CPU上下文切換:
-
CPU寄存器中用戶態的指令位置先保存起來,CPU寄存器更新為內核態指令的位置,跳轉到內核態運行內核任務; -
系統調用結束后,CPU寄存器恢復原來保存的用戶態數據,再切換到用戶空間繼續運行。
系統調用過程中并不會涉及虛擬內存等進程用戶態資源,也不會切換進程。和傳統意義上的進程上下文切換不同。因此系統調用通常稱為特權模式切換。
進程是由內核管理和調度的,進程上下文切換只能發生在內核態。因此相比系統調用來說,在保存當前進程的內核狀態和CPU寄存器之前,需要先把該進程的虛擬內存,棧保存下來。再加載新進程的內核態后,還要刷新進程的虛擬內存和用戶棧。
進程只有在調度到CPU上運行時才需要切換上下文,有以下幾種場景:CPU時間片輪流分配,系統資源不足導致進程掛起,進程通過sleep函數主動掛起,高優先級進程搶占時間片,硬件中斷時CPU上的進程被掛起轉而執行內核中的中斷服務。
線程上下文切換
線程上下文切換分為兩種:
-
前后線程同屬于一個進程,切換時虛擬內存資源不變,只需要切換線程的私有數據,寄存器等; -
前后線程屬于不同進程,與進程上下文切換相同。
同進程的線程切換消耗資源較少,這也是多線程的優勢。
中斷上下文切換
中斷上下文切換并不涉及到進程的用戶態,因此中斷上下文只包括內核態中斷服務程序執行所必須的狀態(CPU寄存器,內核堆棧,硬件中斷參數等)。
中斷處理優先級比進程高,所以中斷上下文切換和進程上下文切換不會同時發生
CPU上下文切換(下)
通過vmstat可以查看系統總體的上下文切換情況
vmstat?5?????????#每隔5s輸出一組數據 procs?-----------memory----------?---swap--?-----io----?-system--?------cpu----- ?r??b???swpd???free???buff??cache???si???so????bi????bo???in???cs?us?sy?id?wa?st ?1??0??????0?103388?145412?511056????0????0????18????60????1????1??2??1?96??0??0 ?0??0??????0?103388?145412?511076????0????0?????0?????2??450?1176??1??1?99??0??0 ?0??0??????0?103388?145412?511076????0????0?????0?????8??429?1135??1??1?98??0??0 ?0??0??????0?103388?145412?511076????0????0?????0?????0??431?1132??1??1?98??0??0 ?0??0??????0?103388?145412?511076????0????0?????0????10??467?1195??1??1?98??0??0 ?1??0??????0?103388?145412?511076????0????0?????0?????2??426?1139??1??0?99??0??0 ?4??0??????0??95184?145412?511108????0????0?????0????74??500?1228??4??1?94??0??0 ?0??0??????0?103512?145416?511076????0????0?????0???455??723?1573?12??3?83??2??0
-
cs (context switch) 每秒上下文切換次數 -
in (interrupt) 每秒中斷次數 -
r (runnning or runnable)就緒隊列的長度,正在運行和等待CPU的進程數 -
b (Blocked) 處于不可中斷睡眠狀態的進程數
要查看每個進程的詳細情況,需要使用pidstat來查看每個進程上下文切換情況
pidstat?-w?5 14時51分16秒???UID???????PID???cswch/s?nvcswch/s??Command 14時51分21秒?????0?????????1??????0.80??????0.00??systemd 14時51分21秒?????0?????????6??????1.40??????0.00??ksoftirqd/0 14時51分21秒?????0?????????9?????32.67??????0.00??rcu_sched 14時51分21秒?????0????????11??????0.40??????0.00??watchdog/0 14時51分21秒?????0????????32??????0.20??????0.00??khugepaged 14時51分21秒?????0???????271??????0.20??????0.00??jbd2/vda1-8 14時51分21秒?????0??????1332??????0.20??????0.00??argusagent 14時51分21秒?????0??????5265?????10.02??????0.00??AliSecGuard 14時51分21秒?????0??????7439??????7.82??????0.00??kworker/0:2 14時51分21秒?????0??????7906??????0.20??????0.00??pidstat 14時51分21秒?????0??????8346??????0.20??????0.00??sshd 14時51分21秒?????0?????20654??????9.82??????0.00??AliYunDun 14時51分21秒?????0?????25766??????0.20??????0.00??kworker/u2:1 14時51分21秒?????0?????28603??????1.00??????0.00??python3
-
cswch 每秒自愿上下文切換次數 (進程無法獲取所需資源導致的上下文切換) -
nvcswch 每秒非自愿上下文切換次數 (時間片輪流等系統強制調度)
vmstat?1?1????#首先獲取空閑系統的上下文切換次數 sysbench?--threads=10?--max-time=300?threads?run?#模擬多線程切換問題 vmstat?1?1????#新終端觀察上下文切換情況 此時發現cs數據明顯升高,同時觀察其他指標: r列:?遠超系統CPU個數,說明存在大量CPU競爭 us和sy列:sy列占比80%,說明CPU主要被內核占用 in列:?中斷次數明顯上升,說明中斷處理也是潛在問題
說明運行/等待CPU的進程過多,導致大量的上下文切換,上下文切換導致系統的CPU占用率高
pidstat?-w?-u?1??#查看到底哪個進程導致的問題
從結果中看出是sysbench導致CPU使用率過高,但是pidstat輸出的上下文次數加起來也并不多。分析sysbench模擬的是線程的切換,因此需要在pidstat后加-t參數查看線程指標。
另外對于中斷次數過多,我們可以通過/proc/interrupts文件讀取
watch?-d?cat?/proc/interrupts
發現次數變化速度最快的是重調度中斷(RES),該中斷用來喚醒空閑狀態的CPU來調度新的任務運行。分析還是因為過多任務的調度問題,和上下文切換分析一致。
某個應用的CPU使用率達到100%,怎么辦?
Linux作為多任務操作系統,將CPU時間劃分為很短的時間片,通過調度器輪流分配給各個任務使用。為了維護CPU時間,Linux通過事先定義的節拍率,觸發時間中斷,并使用全局變了jiffies記錄開機以來的節拍數。時間中斷發生一次該值+1.
CPU使用率,除了空閑時間以外的其他時間占總CPU時間的百分比??梢酝ㄟ^/proc/stat中的數據來計算出CPU使用率。因為/proc/stat時開機以來的節拍數累加值,計算出來的是開機以來的平均CPU使用率,一般意義不大??梢蚤g隔取一段時間的兩次值作差來計算該段時間內的平均CPU使用率。性能分析工具給出的都是間隔一段時間的平均CPU使用率,要注意間隔時間的設置。
CPU使用率可以通過top 或 ps來查看。分析進程的CPU問題可以通過perf,它以性能事件采樣為基礎,不僅可以分析系統的各種事件和內核性能,還可以用來分析指定應用程序的性能問題。
perf top / perf record / perf report (-g 開啟調用關系的采樣)
sudo?docker?run?--name?nginx?-p?10000:80?-itd?feisky/nginx sudo?docker?run?--name?phpfpm?-itd?--network?container:nginx?feisky/php-fpm ab?-c?10?-n?100?http://XXX.XXX.XXX.XXX:10000/?#測試Nginx服務性能
發現此時每秒可承受請求給長少,此時將測試的請求數從100增加到10000。在另外一個終端運行top查看每個CPU的使用率。發現系統中幾個php-fpm進程導致CPU使用率驟升。
接著用perf來分析具體是php-fpm中哪個函數導致該問題。
perf?top?-g?-p?XXXX?#對某一個php-fpm進程進行分析
發現其中sqrt和add_function占用CPU過多, 此時查看源碼找到原來是sqrt中在發布前沒有刪除測試代碼段,存在一個百萬次的循環導致。將該無用代碼刪除后發現nginx負載能力明顯提升
系統的CPU使用率很高,為什么找不到高CPU的應用?
sudo?docker?run?--name?nginx?-p?10000:80?-itd?feisky/nginx:sp sudo?docker?run?--name?phpfpm?-itd?--network?container:nginx?feisky/php-fpm:sp ab?-c?100?-n?1000?http://XXX.XXX.XXX.XXX:10000/?#并發100個請求測試
實驗結果中每秒請求數依舊不高,我們將并發請求數降為5后,nginx負載能力依舊很低。
此時用top和pidstat發現系統CPU使用率過高,但是并沒有發現CPU使用率高的進程。
出現這種情況一般時我們分析時遺漏的什么信息,重新運行top命令并觀察一會。發現就緒隊列中處于Running狀態的進行過多,超過了我們的并發請求次數5. 再仔細查看進程運行數據,發現nginx和php-fpm都處于sleep狀態,真正處于運行的卻是幾個stress進程。
下一步就利用pidstat分析這幾個stress進程,發現沒有任何輸出。用ps aux交叉驗證發現依舊不存在該進程。說明不是工具的問題。再top查看發現stress進程的進程號變化了,此時有可能時以下兩種原因導致:
-
進程不停的崩潰重啟(如段錯誤/配置錯誤等),此時進程退出后可能又被監控系統重啟; -
短時進程導致,即其他應用內部通過exec調用的外面命令,這些命令一般只運行很短時間就結束,很難用top這種間隔較長的工具來發現
可以通過pstree來查找 stress的父進程,找出調用關系。
pstree?|?grep?stress
發現是php-fpm調用的該子進程,此時去查看源碼可以看出每個請求都會調用一個stress命令來模擬I/O壓力。之前top顯示的結果是CPU使用率升高,是否真的是由該stress命令導致的,還需要繼續分析。代碼中給每個請求加了verbose=1的參數后可以查看stress命令的輸出,在中斷測試該命令結果顯示stress命令運行時存在因權限問題導致的文件創建失敗的bug。
此時依舊只是猜測,下一步繼續通過perf工具來分析。性能報告顯示確實時stress占用了大量的CPU,通過修復權限問題來優化解決即可.
系統中出現大量不可中斷進程和僵尸進程怎么辦?
進程狀態
-
R Running/Runnable,表示進程在CPU的就緒隊列中,正在運行或者等待運行; -
D Disk Sleep,不可中斷狀態睡眠,一般表示進程正在跟硬件交互,并且交互過程中不允許被其他進程中斷; -
Z Zombie,僵尸進程,表示進程實際上已經結束,但是父進程還沒有回收它的資源; -
S Interruptible Sleep,可中斷睡眠狀態,表示進程因為等待某個事件而被系統掛起,當等待事件發生則會被喚醒并進入R狀態; -
I Idle,空閑狀態,用在不可中斷睡眠的內核線程上。該狀態不會導致平均負載升高; -
T Stop/Traced,表示進程處于暫?;蚋櫊顟B(SIGSTOP/SIGCONT, GDB調試); -
X Dead,進程已經消亡,不會在top/ps中看到。
對于不可中斷狀態,一般都是在很短時間內結束,可忽略。但是如果系統或硬件發生故障,進程可能會保持不可中斷狀態很久,甚至系統中出現大量不可中斷狀態,此時需注意是否出現了I/O性能問題。
僵尸進程一般多進程應用容易遇到,父進程來不及處理子進程狀態時子進程就提前退出,此時子進程就變成了僵尸進程。大量的僵尸進程會用盡PID進程號,導致新進程無法建立。
磁盤O_DIRECT問題
sudo?docker?run?--privileged?--name=app?-itd?feisky/app:iowait ps?aux?|?grep?'/app'
可以看到此時有多個app進程運行,狀態分別時Ss+和D+。其中后面s表示進程是一個會話的領導進程,+號表示前臺進程組。
其中進程組表示一組相互關聯的進程,子進程是父進程所在組的組員。會話指共享同一個控制終端的一個或多個進程組。
用top查看系統資源發現:1)平均負載在逐漸增加,且1分鐘內平均負載達到了CPU個數,說明系統可能已經有了性能瓶頸;2)僵尸進程比較多且在不停增加;3)us和sys CPU使用率都不高,iowait卻比較高;4)每個進程CPU使用率也不高,但有兩個進程處于D狀態,可能在等待IO。
分析目前數據可知:iowait過高導致系統平均負載升高,僵尸進程不斷增長說明有程序沒能正確清理子進程資源。
用dstat來分析,因為它可以同時查看CPU和I/O兩種資源的使用情況,便于對比分析。
dstat?1?10????#間隔1秒輸出10組數據
可以看到當wai(iowait)升高時磁盤請求read都會很大,說明iowait的升高和磁盤的讀請求有關。接下來分析到底時哪個進程在讀磁盤。
之前top查看的處于D狀態的進程號,用pidstat -d -p XXX 展示進程的I/O統計數據。發現處于D狀態的進程都沒有任何讀寫操作。在用pidstat -d 查看所有進程的I/O統計數據,看到app進程在進行磁盤讀操作,每秒讀取32MB的數據。進程訪問磁盤必須使用系統調用處于內核態,接下來重點就是找到app進程的系統調用。
sudo?strace?-p?XXX?#對app進程調用進行跟蹤
報錯沒有權限,因為已經時root權限了。所以遇到這種情況,首先要檢查進程狀態是否正常。ps命令查找該進程已經處于Z狀態,即僵尸進程。
這種情況下top pidstat之類的工具無法給出更多的信息,此時像第5篇一樣,用perf record -d和perf report進行分析,查看app進程調用棧。
看到app確實在通過系統調用sys_read()讀取數據,并且從new_sync_read和blkdev_direct_IO看出進程時進行直接讀操作,請求直接從磁盤讀,沒有通過緩存導致iowait升高。
通過層層分析后,root cause是app內部進行了磁盤的直接I/O。然后定位到具體代碼位置進行優化即可。
僵尸進程
上述優化后iowait顯著下降,但是僵尸進程數量仍舊在增加。首先要定位僵尸進程的父進程,通過pstree -aps XXX,打印出該僵尸進程的調用樹,發現父進程就是app進程。
查看app代碼,看看子進程結束的處理是否正確(是否調用wait()/waitpid(),有沒有注冊SIGCHILD信號的處理函數等)。
碰到iowait升高時,先用dstat pidstat等工具確認是否存在磁盤I/O問題,再找是哪些進程導致I/O,不能用strace直接分析進程調用時可以通過perf工具分析。
對于僵尸問題,用pstree找到父進程,然后看源碼檢查子進程結束的處理邏輯即可。
CPU性能指標
-
CPU使用率
-
用戶CPU使用率, 包括用戶態(user)和低優先級用戶態(nice). 該指標過高說明應用程序比較繁忙. -
系統CPU使用率, CPU在內核態運行的時間百分比(不含中斷). 該指標高說明內核比較繁忙. -
等待I/O的CPU使用率, iowait, 該指標高說明系統與硬件設備I/O交互時間比較長. -
軟/硬中斷CPU使用率, 該指標高說明系統中發生大量中斷. -
steal CPU / guest CPU, 表示虛擬機占用的CPU百分比. -
平均負載
理想情況下平均負載等于邏輯CPU個數,表示每個CPU都被充分利用. 若大于則說明系統負載較重.
-
進程上下文切換
包括無法獲取資源的自愿切換和系統強制調度時的非自愿切換. 上下文切換本身是保證Linux正常運行的一項核心功能. 過多的切換則會將原本運行進程的CPU時間消耗在寄存器,內核占及虛擬內存等數據保存和恢復上
-
CPU緩存命中率
CPU緩存的復用情況,命中率越高性能越好. 其中L1/L2常用在單核,L3則用在多核中
性能工具
-
平均負載案例 -
先用uptime查看系統平均負載 -
判斷負載在升高后再用mpstat和pidstat分別查看每個CPU和每個進程CPU使用情況.找出導致平均負載較高的進程.另外,搜索公眾號Linux就該這樣學后臺回復“git書籍”,獲取一份驚喜禮包。 -
上下文切換案例 -
先用vmstat查看系統上下文切換和中斷次數 -
再用pidstat觀察進程的自愿和非自愿上下文切換情況 -
最后通過pidstat觀察線程的上下文切換情況 -
進程CPU使用率高案例 -
先用top查看系統和進程的CPU使用情況,定位到進程 -
再用perf top觀察進程調用鏈,定位到具體函數 -
系統CPU使用率高案例 -
先用top查看系統和進程的CPU使用情況,top/pidstat都無法找到CPU使用率高的進程 -
重新審視top輸出 -
從CPU使用率不高,但是處于Running狀態的進程入手 -
perf record/report發現短時進程導致 (execsnoop工具) -
不可中斷和僵尸進程案例 -
先用top觀察iowait升高,發現大量不可中斷和僵尸進程 -
strace無法跟蹤進程系統調用 -
perf分析調用鏈發現根源來自磁盤直接I/O -
軟中斷案例 -
top觀察系統軟中斷CPU使用率高 -
查看/proc/softirqs找到變化速率較快的幾種軟中斷 -
sar命令發現是網絡小包問題 -
tcpdump找出網絡幀的類型和來源, 確定SYN FLOOD攻擊導致
根據不同的性能指標來找合適的工具:

在生產環境中往往開發者沒有權限安裝新的工具包,只能最大化利用好系統中已經安裝好的工具. 因此要了解一些主流工具能夠提供哪些指標分析.

先運行幾個支持指標較多的工具, 如top/vmstat/pidstat,根據它們的輸出可以得出是哪種類型的性能問題. 定位到進程后再用strace/perf分析調用情況進一步分析. 如果是軟中斷導致用/proc/softirqs

CPU優化
-
應用程序優化
-
編譯器優化: 編譯階段開啟優化選項, 如gcc -O2 -
算法優化 -
異步處理: 避免程序因為等待某個資源而一直阻塞,提升程序的并發處理能力. (將輪詢替換為事件通知) -
多線程代替多進程: 減少上下文切換成本 -
善用緩存: 加快程序處理速度 -
系統優化
-
CPU綁定: 將進程綁定要1個/多個CPU上,提高CPU緩存命中率,減少CPU調度帶來的上下文切換 -
CPU獨占: CPU親和性機制來分配進程 -
優先級調整:使用nice適當降低非核心應用的優先級 -
為進程設置資源顯示: cgroups設置使用上限,防止由某個應用自身問題耗盡系統資源 -
NUMA優化: CPU盡可能訪問本地內存 -
中斷負載均衡: irpbalance,將中斷處理過程自動負載均衡到各個CPU上 -
TPS、QPS、系統吞吐量的區別和理解
-
QPS(TPS)
-
并發數
-
響應時間
QPS(TPS)=并發數/平均相應時間
-
用戶請求服務器
-
服務器內部處理
-
服務器返回給客戶
QPS類似TPS,但是對于一個頁面的訪問形成一個TPS,但是一次頁面請求可能包含多次對服務器的請求,可能計入多次QPS
-
QPS (Queries Per Second)每秒查詢率,一臺服務器每秒能夠響應的查詢次數.
-
TPS (Transactions Per Second)每秒事務數,軟件測試的結果.
-
系統吞吐量, 包括幾個重要參數:
3內存
Linux內存是怎么工作的
內存映射
大多數計算機用的主存都是動態隨機訪問內存(DRAM),只有內核才可以直接訪問物理內存。Linux內核給每個進程提供了一個獨立的虛擬地址空間,并且這個地址空間是連續的。這樣進程就可以很方便的訪問內存(虛擬內存)。
虛擬地址空間的內部分為內核空間和用戶空間兩部分,不同字長的處理器地址空間的范圍不同。32位系統內核空間占用1G,用戶空間占3G。64位系統內核空間和用戶空間都是128T,分別占內存空間的最高和最低處,中間部分為未定義。
并不是所有的虛擬內存都會分配物理內存,只有實際使用的才會。分配后的物理內存通過內存映射管理。為了完成內存映射,內核為每個進程都維護了一個頁表,記錄虛擬地址和物理地址的映射關系。頁表實際存儲在CPU的內存管理單元MMU中,處理器可以直接通過硬件找出要訪問的內存。
當進程訪問的虛擬地址在頁表中查不到時,系統會產生一個缺頁異常,進入內核空間分配物理內存,更新進程頁表,再返回用戶空間恢復進程的運行。
MMU以頁為單位管理內存,頁大小4KB。為了解決頁表項過多問題Linux提供了多級頁表和HugePage的機制。
虛擬內存空間分布
用戶空間內存從低到高是五種不同的內存段:
-
只讀段?代碼和常量等 -
數據段?全局變量等 -
堆?動態分配的內存,從低地址開始向上增長 -
文件映射?動態庫、共享內存等,從高地址開始向下增長 -
棧?包括局部變量和函數調用的上下文等,棧的大小是固定的。一般8MB
內存分配與回收
分配
malloc對應到系統調用上有兩種實現方式:
-
brk()?針對小塊內存( -
**mmap()**針對大塊內存(>128K),直接用內存映射來分配,即在文件映射段找一塊空閑內存分配。
前者的緩存可以減少缺頁異常的發生,提高內存訪問效率。但是由于內存沒有歸還系統,在內存工作繁忙時,頻繁的內存分配/釋放會造成內存碎片。
后者在釋放時直接歸還系統,所以每次mmap都會發生缺頁異常。在內存工作繁忙時,頻繁內存分配會導致大量缺頁異常,使內核管理負擔增加。
上述兩種調用并沒有真正分配內存,這些內存只有在首次訪問時,才通過缺頁異常進入內核中,由內核來分配
回收
內存緊張時,系統通過以下方式來回收內存:
-
回收緩存:LRU算法回收最近最少使用的內存頁面;
-
回收不常訪問內存:把不常用的內存通過交換分區寫入磁盤
-
殺死進程:OOM內核保護機制 (進程消耗內存越大oom_score越大,占用CPU越多oom_score越小,可以通過/proc手動調整oom_adj)
echo?-16?>?/proc/$(pidof?XXX)/oom_adj
如何查看內存使用情況
free來查看整個系統的內存使用情況
top/ps來查看某個進程的內存使用情況
-
VIRT?進程的虛擬內存大小 -
RES?常駐內存的大小,即進程實際使用的物理內存大小,不包括swap和共享內存 -
SHR?共享內存大小,與其他進程共享的內存,加載的動態鏈接庫以及程序代碼段 -
%MEM?進程使用物理內存占系統總內存的百分比
怎樣理解內存中的Buffer和Cache?
buffer是對磁盤數據的緩存,cache是對文件數據的緩存,它們既會用在讀請求也會用在寫請求中
如何利用系統緩存優化程序的運行效率
緩存命中率
緩存命中率是指直接通過緩存獲取數據的請求次數,占所有請求次數的百分比。命中率越高說明緩存帶來的收益越高,應用程序的性能也就越好。
安裝bcc包后可以通過cachestat和cachetop來監測緩存的讀寫命中情況。
安裝pcstat后可以查看文件在內存中的緩存大小以及緩存比例
#首先安裝Go export?GOPATH=~/go export?PATH=~/go/bin:$PATH go?get?golang.org/x/sys/unix go?ge?github.com/tobert/pcstat/pcstat
dd緩存加速
dd?if=/dev/sda1?of=file?bs=1M?count=512?#生產一個512MB的臨時文件 echo?3?>?/proc/sys/vm/drop_caches?#清理緩存 pcstat?file?#確定剛才生成文件不在系統緩存中,此時cached和percent都是0 cachetop?5 dd?if=file?of=/dev/null?bs=1M?#測試文件讀取速度 #此時文件讀取性能為30+MB/s,查看cachetop結果發現并不是所有的讀都落在磁盤上,讀緩存命中率只有50%。 dd?if=file?of=/dev/null?bs=1M?#重復上述讀文件測試 #此時文件讀取性能為4+GB/s,讀緩存命中率為100% pcstat?file?#查看文件file的緩存情況,100%全部緩存
O_DIRECT選項繞過系統緩存
cachetop?5 sudo?docker?run?--privileged?--name=app?-itd?feisky/app:io-direct sudo?docker?logs?app?#確認案例啟動成功 #實驗結果表明每讀32MB數據都要花0.9s,且cachetop輸出中顯示1024次緩存全部命中
但是憑感覺可知如果緩存命中讀速度不應如此慢,讀次數時1024,頁大小為4K,五秒的時間內讀取了1024*4KB數據,即每秒0.8MB,和結果中32MB相差較大。說明該案例沒有充分利用緩存,懷疑系統調用設置了直接I/O標志繞過系統緩存。因此接下來觀察系統調用.
strace?-p?$(pgrep?app) #strace?結果可以看到openat打開磁盤分區/dev/sdb1,傳入參數為O_RDONLY|O_DIRECT
這就解釋了為什么讀32MB數據那么慢,直接從磁盤讀寫肯定遠遠慢于緩存。找出問題后我們再看案例的源代碼發現flags中指定了直接IO標志。刪除該選項后重跑,驗證性能變化。
內存泄漏,如何定位和處理?
對應用程序來說,動態內存的分配和回收是核心又復雜的一個邏輯功能模塊。管理內存的過程中會發生各種各樣的“事故”:
-
沒正確回收分配的內存,導致了泄漏 -
訪問的是已分配內存邊界外的地址,導致程序異常退出
內存的分配與回收
虛擬內存分布從低到高分別是只讀段,數據段,堆,內存映射段,棧五部分。其中會導致內存泄漏的是:
-
堆:由應用程序自己來分配和管理,除非程序退出這些堆內存不會被系統自動釋放。 -
內存映射段:包括動態鏈接庫和共享內存,其中 共享內存由程序自動分配和管理
內存泄漏的危害比較大,這些忘記釋放的內存,不僅應用程序自己不能訪問,系統也不能把它們再次分配給其他應用。?內存泄漏不斷累積甚至會耗盡系統內存.
如何檢測內存泄漏
預先安裝systat,docker,bcc
sudo?docker?run?--name=app?-itd?feisky/app:mem-leak sudo?docker?logs?app vmstat?3
可以看到free在不斷下降,buffer和cache基本保持不變。說明系統的內存一致在升高。但并不能說明存在內存泄漏。此時可以通過memleak工具來跟蹤系統或進程的內存分配/釋放請求
/usr/share/bcc/tools/memleak?-a?-p?$(pidof?app)
從memleak輸出可以看到,應用在不停地分配內存,并且這些分配的地址并沒有被回收。通過調用棧看到是fibonacci函數分配的內存沒有釋放。定位到源碼后查看源碼來修復增加內存釋放函數即可.
為什么系統的Swap變高
系統內存資源緊張時通過內存回收和OOM殺死進程來解決。其中可回收內存包括:
-
緩存/緩沖區,屬于可回收資源,在文件管理中通常叫做文件頁 -
在應用程序中通過fsync將臟頁同步到磁盤 -
交給系統,內核線程pdflush負責這些臟頁的刷新 -
被應用程序修改過暫時沒寫入磁盤的數據(臟頁),要先寫入磁盤然后才能內存釋放 -
內存映射獲取的文件映射頁,也可以被釋放掉,下次訪問時從文件重新讀取
對于程序自動分配的堆內存,也就是我們在內存管理中的匿名頁,雖然這些內存不能直接釋放,但是Linux提供了Swap機制將不常訪問的內存寫入到磁盤來釋放內存,再次訪問時從磁盤讀取到內存即可。
Swap原理
Swap本質就是把一塊磁盤空間或者一個本地文件當作內存來使用,包括換入和換出兩個過程:
-
換出:將進程暫時不用的內存數據存儲到磁盤中,并釋放這些內存 -
換入:進程再次訪問內存時,將它們從磁盤讀到內存中
Linux如何衡量內存資源是否緊張?
-
直接內存回收?新的大塊內存分配請求,但剩余內存不足。此時系統會回收一部分內存;
-
kswapd0?內核線程定期回收內存。為了衡量內存使用情況,定義了pages_min,pages_low,pages_high三個閾值,并根據其來進行內存的回收操作。
-
剩余內存
-
pages_min pages_high
-
pages_low
-
剩余內存 > pages_high,說明剩余內存較多,無內存壓力
pages_low = pages_min?5 / 4 pages_high = pages_min?3 / 2
NUMA 與 SWAP
很多情況下系統剩余內存較多,但SWAP依舊升高,這是由于處理器的NUMA架構。
在NUMA架構下多個處理器劃分到不同的Node,每個Node都擁有自己的本地內存空間。在分析內存的使用時應該針對每個Node單獨分析
numactl?--hardware?#查看處理器在Node的分布情況,以及每個Node的內存使用情況
內存三個閾值可以通過/proc/zoneinfo來查看,該文件中還包括活躍和非活躍的匿名頁/文件頁數。
當某個Node內存不足時,系統可以從其他Node尋找空閑資源,也可以從本地內存中回收內存。通過/proc/sys/vm/zone_raclaim_mode來調整。
-
0表示既可以從其他Node尋找空閑資源,也可以從本地回收內存 -
1,2,4表示只回收本地內存,2表示可以會回臟數據回收內存,4表示可以用Swap方式回收內存。
swappiness
在實際回收過程中Linux根據/proc/sys/vm/swapiness選項來調整使用Swap的積極程度,從0-100,數值越大越積極使用Swap,即更傾向于回收匿名頁;數值越小越消極使用Swap,即更傾向于回收文件頁。
注意:這只是調整Swap積極程度的權重,即使設置為0,當剩余內存+文件頁小于頁高閾值時,還是會發生Swap。
Swap升高時如何定位分析
free?#首先通過free查看swap使用情況,若swap=0表示未配置Swap #先創建并開啟swap fallocate?-l?8G?/mnt/swapfile chmod?600?/mnt/swapfile mkswap?/mnt/swapfile swapon?/mnt/swapfile free?#再次執行free確保Swap配置成功 dd?if=/dev/sda1?of=/dev/null?bs=1G?count=2048?#模擬大文件讀取 sar?-r?-S?1??#查看內存各個指標變化?-r內存?-S?swap #根據結果可以看出,%memused在不斷增長,剩余內存kbmemfress不斷減少,緩沖區kbbuffers不斷增大,由此可知剩余內存不斷分配給了緩沖區 #一段時間之后,剩余內存很小,而緩沖區占用了大部分內存。此時Swap使用之間增大,緩沖區和剩余內存只在小范圍波動 停下sar命令 cachetop5?#觀察緩存 #可以看到dd進程讀寫只有50%的命中率,未命中數為4w+頁,說明正式dd進程導致緩沖區使用升高 watch?-d?grep?-A?15?‘Normal’?/proc/zoneinfo?#觀察內存指標變化 #發現升級內存在一個小范圍不停的波動,低于頁低閾值時會突然增大到一個大于頁高閾值的值
說明剩余內存和緩沖區的波動變化正是由于內存回收和緩存再次分配的循環往復。有時候Swap用的多,有時候緩沖區波動更多。此時查看swappiness值為60,是一個相對中和的配置,系統會根據實際運行情況來選去合適的回收類型.
如何“快準狠”找到系統內存存在的問題
內存性能指標
系統內存指標
-
已用內存/剩余內存 -
共享內存 (tmpfs實現) -
可用內存:包括剩余內存和可回收內存 -
緩存:磁盤讀取文件的頁緩存,slab分配器中的可回收部分 -
緩沖區:原始磁盤塊的臨時存儲,緩存將要寫入磁盤的數據
進程內存指標
-
虛擬內存:5大部分 -
常駐內存:進程實際使用的物理內存,不包括Swap和共享內存 -
共享內存:與其他進程共享的內存,以及動態鏈接庫和程序的代碼段 -
另外,搜索公眾號技術社區后臺回復“算法”,獲取一份驚喜禮包。 -
Swap內存:通過Swap換出到磁盤的內存
缺頁異常
-
可以直接從物理內存中分配,次缺頁異常 -
需要磁盤IO介入(如Swap),主缺頁異常。此時內存訪問會慢很多
內存性能工具
根據不同的性能指標來找合適的工具:

內存分析工具包含的性能指標:

如何迅速分析內存的性能瓶頸
通常先運行幾個覆蓋面比較大的性能工具,如free,top,vmstat,pidstat等
-
先用free和top查看系統整體內存使用情況 -
再用vmstat和pidstat,查看一段時間的趨勢,從而判斷內存問題的類型 -
最后進行詳細分析,比如內存分配分析,緩存/緩沖區分析,具體進程的內存使用分析等
常見的優化思路:
-
最好禁止Swap,若必須開啟則盡量降低swappiness的值 -
減少內存的動態分配,如可以用內存池,HugePage等 -
盡量使用緩存和緩沖區來訪問數據。如用堆棧明確聲明內存空間來存儲需要緩存的數據,或者用Redis外部緩存組件來優化數據的訪問 -
cgroups等方式來限制進程的內存使用情況,確保系統內存不被異常進程耗盡 -
/proc/pid/oom_adj調整核心應用的oom_score,保證即使內存緊張核心應用也不會被OOM殺死
vmstat使用詳解
vmstat命令是最常見的Linux/Unix監控工具,可以展現給定時間間隔的服務器的狀態值,包括服務器的CPU使用率,內存使用,虛擬內存交換情況,IO讀寫情況。可以看到整個機器的CPU,內存,IO的使用情況,而不是單單看到各個進程的CPU使用率和內存使用率(使用場景不一樣)。
vmstat?2 procs?-----------memory----------?---swap--?-----io----?-system--?------cpu----- ?r??b???swpd???free???buff??cache???si???so????bi????bo???in???cs?us?sy?id?wa?st ?1??0??????0?1379064?282244?11537528????0????0?????3???104????0????0??3??0?97??0??0 ?0??0??????0?1372716?282244?11537544????0????0?????0????24?4893?8947??1??0?98??0??0 ?0??0??????0?1373404?282248?11537544????0????0?????0????96?5105?9278??2??0?98??0??0 ?0??0??????0?1374168?282248?11537556????0????0?????0?????0?5001?9208??1??0?99??0??0 ?0??0??????0?1376948?282248?11537564????0????0?????0????80?5176?9388??2??0?98??0??0 ?0??0??????0?1379356?282256?11537580????0????0?????0???202?5474?9519??2??0?98??0??0 ?1??0??????0?1368376?282256?11543696????0????0?????0?????0?5894?8940?12??0?88??0??0 ?1??0??????0?1371936?282256?11539240????0????0?????0?10554?6176?9481?14??1?85??1??0 ?1??0??????0?1366184?282260?11542292????0????0?????0??7456?6102?9983??7??1?91??0??0 ?1??0??????0?1353040?282260?11556176????0????0?????0?16924?7233?9578?18??1?80??1??0 ?0??0??????0?1359432?282260?11549124????0????0?????0?12576?5495?9271??7??0?92??1??0 ?0??0??????0?1361744?282264?11549132????0????0?????0????58?8606?15079??4??2?95??0??0 ?1??0??????0?1367120?282264?11549140????0????0?????0?????2?5716?9205??8??0?92??0??0 ?0??0??????0?1346580?282264?11562644????0????0?????0????70?6416?9944?12??0?88??0??0 ?0??0??????0?1359164?282264?11550108????0????0?????0??2922?4941?8969??3??0?97??0??0 ?1??0??????0?1353992?282264?11557044????0????0?????0?????0?6023?8917?15??0?84??0??0 #?結果說明 -?r?表示運行隊列(就是說多少個進程真的分配到CPU),我測試的服務器目前CPU比較空閑,沒什么程序在跑,當這個值超過了CPU數目,就會出現CPU瓶頸了。這個也和top的負載有關系,一般負載超過了3就比較高,超過了5就高,超過了10就不正常了,服務器的狀態很危險。top的負載類似每秒的運行隊列。如果運行隊列過大,表示你的CPU很繁忙,一般會造成CPU使用率很高。 -?b?表示阻塞的進程,這個不多說,進程阻塞,大家懂的。 -?swpd?虛擬內存已使用的大小,如果大于0,表示你的機器物理內存不足了,如果不是程序內存泄露的原因,那么你該升級內存了或者把耗內存的任務遷移到其他機器。 -?free???空閑的物理內存的大小,我的機器內存總共8G,剩余3415M。 -?buff???Linux/Unix系統是用來存儲,目錄里面有什么內容,權限等的緩存,我本機大概占用300多M -?cache?cache直接用來記憶我們打開的文件,給文件做緩沖,我本機大概占用300多M(這里是Linux/Unix的聰明之處,把空閑的物理內存的一部分拿來做文件和目錄的緩存,是為了提高?程序執行的性能,當程序使用內存時,buffer/cached會很快地被使用。) -?si??每秒從磁盤讀入虛擬內存的大小,如果這個值大于0,表示物理內存不夠用或者內存泄露了,要查找耗內存進程解決掉。我的機器內存充裕,一切正常。 -?so??每秒虛擬內存寫入磁盤的大小,如果這個值大于0,同上。 -?bi??塊設備每秒接收的塊數量,這里的塊設備是指系統上所有的磁盤和其他塊設備,默認塊大小是1024byte,我本機上沒什么IO操作,所以一直是0,但是我曾在處理拷貝大量數據(2-3T)的機器上看過可以達到140000/s,磁盤寫入速度差不多140M每秒 -?bo?塊設備每秒發送的塊數量,例如我們讀取文件,bo就要大于0。bi和bo一般都要接近0,不然就是IO過于頻繁,需要調整。 -?in?每秒CPU的中斷次數,包括時間中斷 -?cs?每秒上下文切換次數,例如我們調用系統函數,就要進行上下文切換,線程的切換,也要進程上下文切換,這個值要越小越好,太大了,要考慮調低線程或者進程的數目,例如在apache和nginx這種web服務器中,我們一般做性能測試時會進行幾千并發甚至幾萬并發的測試,選擇web服務器的進程可以由進程或者線程的峰值一直下調,壓測,直到cs到一個比較小的值,這個進程和線程數就是比較合適的值了。系統調用也是,每次調用系統函數,我們的代碼就會進入內核空間,導致上下文切換,這個是很耗資源,也要盡量避免頻繁調用系統函數。上下文切換次數過多表示你的CPU大部分浪費在上下文切換,導致CPU干正經事的時間少了,CPU沒有充分利用,是不可取的。 -?us?用戶CPU時間,我曾經在一個做加密解密很頻繁的服務器上,可以看到us接近100,r運行隊列達到80(機器在做壓力測試,性能表現不佳)。 -?sy?系統CPU時間,如果太高,表示系統調用時間長,例如是IO操作頻繁。 -?id?空閑CPU時間,一般來說,id?+?us?+?sy?=?100,一般我認為id是空閑CPU使用率,us是用戶CPU使用率,sy是系統CPU使用率。 -?wt?等待IO?CPU時間
pidstat 使用詳解
pidstat主要用于監控全部或指定進程占用系統資源的情況,如CPU,內存、設備IO、任務切換、線程等。
使用方法:
-
pidstat –d interval times 統計各個進程的IO使用情況 -
pidstat –u interval times 統計各個進程的CPU統計信息 -
pidstat –r interval times 統計各個進程的內存使用信息 -
pidstat -w interval times 統計各個進程的上下文切換 -
p PID 指定PID
1、統計IO使用情況
pidstat?-d?1?10 03:02:02?PM???UID???????PID???kB_rd/s???kB_wr/s?kB_ccwr/s??Command 03:02:03?PM?????0???????816??????0.00????918.81??????0.00??jbd2/vda1-8 03:02:03?PM?????0??????1007??????0.00??????3.96??????0.00??AliYunDun 03:02:03?PM???997??????7326??????0.00???1904.95????918.81??java 03:02:03?PM???997??????8539??????0.00??????3.96??????0.00??java 03:02:03?PM?????0?????16066??????0.00?????35.64??????0.00??cmagent 03:02:03?PM???UID???????PID???kB_rd/s???kB_wr/s?kB_ccwr/s??Command 03:02:04?PM?????0???????816??????0.00???1924.00??????0.00??jbd2/vda1-8 03:02:04?PM???997??????7326??????0.00??11156.00???1888.00??java 03:02:04?PM???997??????8539??????0.00??????4.00??????0.00??java
-
UID -
PID -
kB_rd/s: 每秒進程從磁盤讀取的數據量 KB 單位 read from disk each second KB -
kB_wr/s: 每秒進程向磁盤寫的數據量 KB 單位 write to disk each second KB -
kB_ccwr/s: 每秒進程向磁盤寫入,但是被取消的數據量,This may occur when the task truncates some dirty pagecache. -
iodelay: Block I/O delay, measured in clock ticks -
Command: 進程名 task name
2、統計CPU使用情況
#?統計CPU pidstat?-u?1?10 03:03:33?PM???UID???????PID????%usr?%system??%guest????%CPU???CPU??Command 03:03:34?PM?????0??????2321????3.96????0.00????0.00????3.96?????0??ansible 03:03:34?PM?????0??????7110????0.00????0.99????0.00????0.99?????4??pidstat 03:03:34?PM???997??????8539????0.99????0.00????0.00????0.99?????5??java 03:03:34?PM???984?????15517????0.99????0.00????0.00????0.99?????5??java 03:03:34?PM?????0?????24406????0.99????0.00????0.00????0.99?????5??java 03:03:34?PM?????0?????32158????3.96????0.00????0.00????3.96?????2??ansible
-
UID -
PID -
%usr: 進程在用戶空間占用 cpu 的百分比 -
%system: 進程在內核空間占用 CPU 百分比 -
%guest: 進程在虛擬機占用 CPU 百分比 -
%wait: 進程等待運行的百分比 -
%CPU: 進程占用 CPU 百分比 -
CPU: 處理進程的 CPU 編號 -
Command: 進程名
3、統計內存使用情況
#?統計內存 pidstat?-r?1?10 Average:??????UID???????PID??minflt/s??majflt/s?????VSZ????RSS???%MEM??Command Average:????????0?????????1??????0.20??????0.00??191256???3064???0.01??systemd Average:????????0??????1007??????1.30??????0.00??143256??22720???0.07??AliYunDun Average:????????0??????6642??????0.10??????0.00?6301904?107680???0.33??java Average:??????997??????7326?????10.89??????0.00?13468904?8395848??26.04??java Average:????????0??????7795????348.15??????0.00??108376???1233???0.00??pidstat Average:??????997??????8539??????0.50??????0.00?8242256?2062228???6.40??java Average:??????987??????9518??????0.20??????0.00?6300944?1242924???3.85??java Average:????????0?????10280??????3.70??????0.00??807372???8344???0.03??aliyun-service Average:??????984?????15517??????0.40??????0.00?6386464?1464572???4.54??java Average:????????0?????16066????236.46??????0.00?2678332??71020???0.22??cmagent Average:??????995?????20955??????0.30??????0.00?6312520?1408040???4.37??java Average:??????995?????20956??????0.20??????0.00?6093764?1505028???4.67??java Average:????????0?????23936??????0.10??????0.00?5302416?110804???0.34??java Average:????????0?????24406??????0.70??????0.00?10211672?2361304???7.32??java Average:????????0?????26870??????1.40??????0.00?1470212??36084???0.11??promtail
-
UID -
PID -
Minflt/s : 每秒次缺頁錯誤次數 (minor page faults),虛擬內存地址映射成物理內存地址產生的 page fault 次數 -
Majflt/s : 每秒主缺頁錯誤次數 (major page faults), 虛擬內存地址映射成物理內存地址時,相應 page 在 swap 中 -
VSZ virtual memory usage : 該進程使用的虛擬內存 KB 單位 -
RSS : 該進程使用的物理內存 KB 單位 -
%MEM : 內存使用率 -
Command : 該進程的命令 task name
4、查看具體進程使用情況
pidstat?-T?ALL?-r?-p?20955?1?10 03:12:16?PM???UID???????PID??minflt/s??majflt/s?????VSZ????RSS???%MEM??Command 03:12:17?PM???995?????20955??????0.00??????0.00?6312520?1408040???4.37??java 03:12:16?PM???UID???????PID?minflt-nr?majflt-nr??Command 03:12:17?PM???995?????20955?????????0?????????0??java