I/O
I/O 其實是挺復雜的一個邏輯,但我們今天只說在做性能分析的時候,應該如何定位問題。
對性能優化比較有經驗的人(或者說見過世面比較多的人)都會知道,當一個系統調到非常精致的程度時,基本上會卡在兩個環節上,對計算密集型的應用來說,會卡在 CPU 上;對 I/O 密集型的應用來說,瓶頸會卡在 I/O 上。
我們對 I/O 的判斷邏輯關系是什么呢?
我們先畫一個 I/O 基本的邏輯過程。我們很多人嘴上說 I/O,其實腦子里想的都是 Disk I/O,但實際上一個數據要想寫到磁盤當中,沒那么容易,步驟並不簡單。
I/O 有很多原理細節,那我們如何能快速地做出相應的判斷呢?首先要祭出的一個工具就是iostat。
在這張圖中,我們取出一條數據來做詳細看下:
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz
vda 0.00 0.67 18.33 114.33 540.00 54073.33 823.32
avgqu-sz await r_await w_await svctm %util
127.01 776.75 1.76 901.01 7.54 100.00
我解釋一下其中幾個關鍵計數器的含義。
svctm代表 I/O 平均響應時間。請注意,這個計數器,有很多人還把它當個寶一樣,實際上在 man 手冊中已經明確說了:“Warning! Do not trust this field any more. This field will be removed in a future sysstat version.” 也就是說,這個數據你愛看就愛,不一定准。
w_await表示寫入的平均響應時間;r_await表示讀取的平均響應時間;r/s表示每秒讀取次數;w/s表示每秒寫入次數。
而 IO/s 的關鍵計算是這樣的:
IO/s = r/s + w/s = 18.33+114.33 = 132.66
%util = ( (IO/s * svctm) /1000) * 100% = 100.02564%
這個%util是用svctm算來的,既然svctm都不一定准了,那這個值也只能參考了。還好我們還有其他工具可以接着往深了去定位,那就是iotop。
Total DISK READ : 2.27 M/s | Total DISK WRITE : 574.86 M/s
Actual DISK READ: 3.86 M/s | Actual DISK WRITE: 34.13 M/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
394 be/3 root 0.00 B/s 441.15 M/s 0.00 % 85.47 % [jbd2/vda1-8]
32616 be/4 root 1984.69 K/s 3.40 K/s 0.00 % 42.89 % kube-controllers
13787 be/4 root 0.00 B/s 0.00 B/s 0.00 % 35.41 % [kworker/u4:1]
...............................
從上面的Total DISK WRITE/READ就可以知道當前的讀寫到底有多少了,默認是按照I/O列來排序的,這里有Total,也有Actual,並且這兩個並不相等,為什么呢?
因為 Total 的值顯示的是用戶態進程與內核態進程之間的速度,而 Actual 顯示的是內核塊設備子系統與硬件之間的速度。
而在I/O交互中,由於存在cache和在內核中會做I/O排序,因此這兩個值並不會相同。那如果你要說磁盤的讀寫能力怎么樣,我們應該看的是Actual。這個沒啥好說的,因為Total再大,不能真實寫到硬盤上也是沒用的。在下面的線程列表中,通過排序,就可以知道是哪個線程(注意在第一列是 TID 哦)占的I/O高了。
Memory
關於內存,要說操作系統的內存管理,那大概開一個新專欄也不為過。但是在性能測試的項目中,如果不做底層的測試,基本上在上層語言開發的系統中,比如說 Java、Go、C++ 等,在分析過程中都直接看業務系統就好了。在操作系統中,分析業務應用的時候,我們會關注的內存內容如下面的命令所示:
[root@7dgroup ~]# free -m
total used free shared buff/cache available Mem: 3791 1873 421 174 1495 1512 Swap: 0 0 0 [root@7dgroup ~]#
total肯定是要優先看的,其次是available,這個值才是系統真正可用的內存,而不是free。
因為 Linux 通常都會把用的內存給cache,但是不一定會用,所以free肯定會越來越少,但是available是計算了buff和cache中不用的內存的,所以只要available多,就表示內存夠用。
當出現內存泄露或因其他原因導致物理內存不夠用的時候,操作系統就會調用OOM Killer,這個進程會強制殺死消耗內存大的應用。這個過程是不商量的,然后你在“dmesg”中就會看到如下信息。
[12766211.187745] Out of memory: Kill process 32188 (java) score 177 or sacrifice child [12766211.190964] Killed process 32188 (java) total-vm:5861784kB, anon-rss:1416044kB, file-rss:0kB, shmem-rss:0kB
這種情況只要出現,TPS 肯定會掉下來,如果你有負載均衡的話,壓力工具中的事務還是可能有成功的。
但如果你只有一個應用節點,或者所有應用節點都被OOM Killer給干掉了,那 TPS 就會是這樣的結果。
對內存監控,可以看到這樣的趨勢:
內存慢慢被耗光,但是殺掉應用進程之后,free內存立即就有了。你看上面這個圖,就是一個機器上有兩個節點,先被殺了一個,另一個接着泄露,又把內存耗光了,於是又被殺掉,最后內存全都空閑了。
這樣的例子還挺常見。當然對這種情況的分析定位,只看物理內存已經沒有意義了,更重要的是看應用的內存是如何被消耗光的。
對於內存的分析,你還可以用nmon和cat/proc/meminfo看到更多信息。如果你的應用是需要大頁處理的,特別是大數據類的應用,需要關注下HugePages相關的計數器。
內存我們就說到這里,總之,要關注available內存的值。
NetWork
這里我們就來到了網絡分析的部分了,在說握手之前,我們先看網絡的分析決策鏈。
請看上圖中,在判斷了瓶頸在網絡上之后,如果知道某個進程的網絡流量大,首先肯定是要考慮減少流量,當然要在保證業務正常運行,TPS 也不降低的情況下。
Recv_Q 和 Send_Q
當然我們還要干一件事,就是可能你並不知道是在哪個具體的環節上出了問題,那就要學會判斷了。網絡I/O棧也並不簡單,看下圖:
數據發送過程是這樣的:
應用把數據給到tcp_wmem就結束它的工作了,由內核接過來之后,經過傳輸層,再經過隊列、環形緩沖區,最后通過網卡發出去。
數據接收過程則是這樣的:
網卡把數據接過來,經過隊列、環形緩沖區,再經過傳輸層,最后通過tcp_rmem給到應用。你似乎懂了對不對?那么在這個過程中,我們有什么需要關注的呢?首先肯定是看隊列,通過netstat或其他命令可以看到Recv_Q和Send_Q,這兩項至少可以告訴你瓶頸會在哪一端。如下圖所示:
我畫個表清晰地判斷一下瓶頸點。
其實這個過程中,我還沒有把防火牆加進去,甚至我都沒說NAT的邏輯,這些基礎知識你需要自己先做足功課。在我們做性能分析的過程中,基本上,基於上面這個表格就夠通過接收和發送判斷瓶頸點發生在誰身上了。但是,要是這些隊列都沒有值,是不是網絡就算好了呢?還不是。
三次握手和四次揮手
我們先看握手圖:
我發現一點,很多人以為三次握手是非常容易理解的,但是沒幾個人能判斷出和它相關的問題。
握手的過程,我就不說了,主要看這里面的兩個隊列:半連接隊列和全連接隊列。
在 B 只接到第一個syn包的時候,把這個連接放到半連接隊列中,當接到ack的時候才放到全連接隊列中。這兩個隊列如果有問題,都到不了發送接收數據的時候,你就看到報錯了。
查看半連接全連接溢出的手段也很簡單,像下面這種情況就是半連接沒建立起來,半連接隊列滿了,syn包都被扔掉了。
[root@7dgroup ~]# netstat -s |grep -i listen 8866 SYNs to LISTEN sockets dropped
那么半連接隊列和什么參數有關呢?
- 代碼中的backlog:你是不是想起來了ServerSocket(int port, int backlog)中的backlog?是的,它就是半連接的隊列長度,如果它不夠了,就會丟掉syn包了。
- 還有操作系統的內核參數net.ipv4.tcp_max_syn_backlog。
而像下面這樣的情況呢,就是全連接隊列已經滿了,但是還有連接要進來,已經超過負荷了。
[root@7dgroup2 ~]# netstat -s |grep overflow 154864 times the listen queue of a socket overflowed
這是在性能分析過程中經常遇到的連接出各種錯的原因之一,它和哪些參數有關呢?我列在這里。
net.core.somaxconn:系統中每一個端口最大的監聽隊列的長度。
net.core.netdev_max_backlog:每個網絡接口接收數據包的速率比內核處理這些包的速率快時,允許送到隊列的數據包的最大數目。
open_file:文件句柄數。
我們再來看下四次揮手。我遇到性能測試過程中的揮手問題,有很多都是做性能分析的人在不了解的情況下就去做各種優化動作而產生的。
先看一下 TCP 揮手圖:
在揮手的邏輯中,和性能相關的問題真的非常少。但有一個點是經常會問到的,那就是TIME_WAIT。不知道為什么,很多人看到TIME_WAIT就緊張,就想去處理掉,於是搜索一圈,哦,要改recycle/reuse的 TCP 參數,要改fin_time_out值。
至於為什么要處理TIME_WAIT,卻沒幾個人能回答得上來。在我的性能工作經驗中,只有一種情況要處理TIME_WAIT,那就是端口不夠用的時候。
TCP/IPv4的標准中,端口最大是 65535,還有一些被用了的,所以當我們做壓力測試的時候,有些應用由於響應時間非常快,端口就會不夠用,這時我們去處理TIME_WAIT的端口,讓它復用或盡快釋放掉,以支持更多的壓力。
所以處理TIME_WAIT的端口要先判斷清楚,如果是其他原因導致的,即使你處理了TIME_WAIT,也沒有提升性能的希望。
如果還有人說,還有一種情況,就是內存不夠用。我必須得說,那是我沒見過世面了,我至今沒見過因為TIME_WAIT的連接數把內存耗光了的。一個 TCP 連接大概占 3KB,創建 10 萬個連接,才100000x3KB≈300M左右,何況最多才 65535 呢?服務器有那么窮嗎?
System
確切地說,在性能測試分析的領域里,System 似乎實在是沒有什么可寫的地方。我們最常見的 System 的計數器是in(interrupts:中斷)和cs(context switch:上下文切換)。
因為這是我能找得到的最瘋狂的 System 計數器了。中斷的邏輯在前面跟你說過了。
cs也比較容易理解,就是 CPU 不得不轉到另一件事情上,聽這一句你就會知道,中斷時肯定會有cs。但是不止中斷會引起 cs,還有多任務處理也會導致cs。因為cs是被動的,這個值的高和低都不會是問題的原因,只會是一種表現,所以它只能用來做性能分析中的證據數據。在我們的這個圖中,顯然是由於in引起的cs,CPU 隊列那么高也是由in導致的。像這樣的問題,你可以去看我們在上篇文章中提到的si CPU高的那個分析鏈了。
Swap
Swap 的邏輯是什么呢?它是在磁盤上創建的一個空間,當物理內存不夠的時候,可以保存物理內存里的數據。如下圖所示:
先看和它相關的幾個參數。
在操作系統中,vm.swappiness 是用來定義使用 swap 的傾向性。
- 值越高,則使用 swap 的傾向性越大。
- 值越低,則使用 swap 的傾向性越小。
但這個傾向性是誰跟誰比呢?簡單地說,在內存中有 anon 內存 (匿名而鏈表,分為:inactive/active) 和 file 內存 (映射頁鏈表,也分為:inactive/active),而 swappiness 是定義了對 anon 頁鏈表掃描的傾向性。
也就是說如果 swappiness 設置為 100 時,則 anon 和 file 內存會同等的掃描;如果設置為 0 時,則 file 內存掃描的優先級會高。但是這並不是說設置為了 0 就沒有 swap 了,在操作系統中還有其他的邏輯使用 swap。
swapiness默認是 60%。注意,下面還有一個參數叫vm.min_free_kbytes。即使把vm.swappiness改為 0,當內存用到小於vm.min_free_kbytes時照樣會發生 Swap。
想關掉 Swap 就swapoff -a。和 Swap 相關的計數器有:top中的Total、free、used和vmstat里的si、so。
說到 Swap,在性能測試和分析中,我的建議是直接把它關了。為什么呢?因為當物理內存不足的時候,不管怎么交換性能都是會下降的,不管是 Swap 還是磁盤上的其他空間,都是從磁盤上取數據,性能肯定會刷刷往下掉。
總結
對操作系統的監控及常用計數器的分析會涉及到很多的內容,所以兩篇文章可能也是覆蓋不全的,我只把在性能測試分析工作中經常見到的計數器解析了一遍。總體來說,你需要記住以下三點:
- 監控平台再花哨,都只是提供數據來給你分析的。只要知道了數據的來源、原理、含義,用什么工具都不重要。
- 性能分析的時候,不會只看操作系統一個模塊或哪幾個固定計數器的。這些動態的數據,需要有分析鏈把它們串起來。
- 操作系統提供的監控數據是分析鏈路中不可缺少的一環,除非你能繞過操作系統,又能很確切地定位出根本原因。