系統的性能瓶頸,排查該從哪些方面入手,如何定位?
如何排查系統的性能瓶頸點?
梳理系統的性能瓶頸點這件事應該不是一件簡單的事情,需要針對不同設計的系統來進行單獨分析。
首先一套完整可用的系統應該是有ui介面的(這裡強調的是一套完整的,可用的系統,而並不是指單獨的一個中台系統),系統分為了前端模組和後端模組。
這裡由於我個人的擅長領域更多是處於後端模組,所以對於系統的瓶頸點梳理我會從後端進行分析。
這裡我結合常用的nginx+tomcat+redis+mysql這類常見架構進行分析:
請求入口 所有的請求打入到後台的服務當中,首先需要考慮的一個點就是:
頻寬因素:
假設有200m的流量同時請求進入伺服器,但是頻寬只有1m,這麼來算光是接收這批數據量資訊也要消耗大約200s的時間。頻寬可以理解為在指定時間內從一端請求到另一端的流量總量。而且區域網和廣域網的頻寬計算其實也是不一樣的,
伺服器的ulimit
通常我們使用的線上伺服器都是centos系列,這裡我列舉centos7相關的系統配置:ulimit配置 查看伺服器允許的最大打開文件數目(linux系統中設計概念為一切皆文件) 通常如果我們的java程式需要增大一些socket的鏈接數目,可以通過調整ulimit 裡面的open參數進行配置。
[root@izwz9ic9ggky8kub9x1ptuz ~]# ulimit -a | grep open open files (-n) 1000
查看用戶的最大進程數目
[root@izwz9ic9ggky8kub9x1ptuz ~]# ulimit -a | grep user max user processes (-u) 7284
相關的配置存放在了/etc/security/limits.conf文件中。
系統的一些內核參數配置
如果是在一些壓力測試場景中,我們通常會預見到這種報錯:
apr_socket_recv: Connection reset by peer (54)
通常這種情況是因為系統內部的一些防範參數設置導致的,需要調整/etc/sysctl.conf 文件中的相關參數:
net.ipv4.tcp_syncookies = 0 #當並發請求數目超過了1000之後,伺服器自身可能會認為是收到了syn泛洪攻擊,但對於高並發系統,要禁用此設置 net.ipv4.tcp_max_syn_backlog #參數決定了SYN_RECV狀態隊列的數量,一般默認值為512或者1024,即超過這個數量,系統將不再接受新的TCP連接請求,一定程度上可以防止系統資源耗盡。可根據情況增加該值以接受更多的連接請求。 net.ipv4.tcp_tw_recycle #參數決定是否加速TIME_WAIT的sockets的回收,默認為0。 net.ipv4.tcp_tw_reuse #參數決定是否可將TIME_WAIT狀態的sockets用於新的TCP連接,默認為0。 net.ipv4.tcp_max_tw_buckets #參數決定TIME_WAIT狀態的sockets總數量,可根據連接數和系統資源需要進行設置。
對於防範參數還可以如下修改查看:
cd /proc/sys/net/ipv4 echo "0" > tcp_syncookies
通常企業中使用的都是nginx進行接收請求,然後進行負載均衡轉發。在nginx層裡面會有幾個核心參數配置:最大連接數,最大並發訪問數
#指定同一個ip的每次請求數量都限制為10次 limit_conn_zone $binary_remote_addr zone=perip:10m; limit_conn perip 10;
Tomcat部分分析
Tomcat支援三種接收請求的處理方式:BIO、NIO、APR 。
1、Bio方式,阻塞式I/O操作即使用的是傳統Java I/O操作,Tomcat7以下版本默認情況下是以bio模式運行的,由於每個請求都要創建一個執行緒來處理,執行緒開銷較大,不能處理高並發的場景,在三種模式中性能也最低
2、Nio方式,是Java SE 1.4及後續版本提供的一種新的I/O操作方式(即java.nio包及其子包),是一個基於緩衝區、並能提供非阻塞I/O操作的Java API,它擁有比傳統I/O操作(bio)更好的並發運行性能。tomcat 8版本及以上默認nio模式
3、apr模式,簡單理解,就是從作業系統級別解決非同步IO問題,大幅度的提高伺服器的處理和響應性能, 也是Tomcat運行高並發應用的首選模式。啟用這種模式稍微麻煩一些,需要安裝一些依賴庫, 而apr的本質就是使用jni技術調用作業系統底層的IO介面,所以需要提前安裝所需要的依賴,首先是需要安裝openssl和apr
tomcat連接參數調整
在tomcat中有這麼一段經典的配置參數:
<Connector port="80" maxHttpHeaderSize="8192" maxThreads="4000" minSpareThreads="1000" maxSpareThreads="2000" enableLookups="false" redirectPort="8443" acceptCount="2000" connectionTimeout="20000" disableUploadTimeout="true" />
maxThreads表示tomcat最多可以創建多少個執行緒來處理請求。
minSpareThread表示tomcat一開始啟動的時候會創建多少個執行緒,即使是閑著也會創建。
maxSpareThread表示tomcat創建的最大閑置執行緒數目。一旦tomcat創建的執行緒數目達到這個瓶頸,那麼就需要進行執行緒的回收了。
connectionTimeout表示連接的超時時長。
假設我們同時有1000個請求並發訪問,但是一台tomcat的maxThreads只設置為了500,那麼此時就會出現請求擁塞的情況,也就是瓶頸點之一。
Redis部分性能瓶頸分析
一些大key的查詢,導致網路出現擁塞情況
例如說往一個list集合中存儲了50m的數據,一旦發生list全量查詢,同時又有其他指令在進行訪問的時候,就容易會導致網路堵塞。因為redis的設計為單執行緒處理請求,所以其他指令發送到redis服務端的時候,都需要等待redis將之前的任務處理完畢之後才能繼續執行。
線上環境出現了一些」違規操作「
比較常見的違規操作:批量執行keys指令
在redis處於高qps的狀態下,隨意一個keys指令都可能是致命的。keys指令的時間複雜度是O(n)級別,容易導致一時間系統的卡頓。
記憶體空間不足
當redis處於記憶體空間不足的時候,基本就是整個系統處於癱瘓作用。因此我們在對每個存儲在redis中的數值都需要設置一個合理的過期時間,以及需要思考存儲數據的體積大小。
MySQL部分性能瓶頸分析
通常我們在分析sql查詢方面都容易出現一個誤區,就是上來直接進行explian分析,但是卻忽略了系統的運作上下文環境。
假設有一張t_user表,已經存儲了幾千萬的數據,並且也對用戶的id進行了索引建立,但是sql執行速度依舊是超過1s時長,這個時候就需要換一種思路進行分析了。
例如從表的拆分方面進行思考,是否該對錶進行橫向拆分,拆解為t_user_01,t_user_02……
以下是我總結的一些對於資料庫層面可能出現性能瓶頸的幾點總結:
1.鎖
排查是否會存在鎖表的情況導致資料庫響應緩慢。
2.sql查詢還有優化空間,有待完善
通常我們對於sql的執行分析都會使用explain命令進行查看:
這裡我貼出了一張關於explain的常用參數含義表供大家參考:
id | SELECT識別符。這是SELECT的查詢序列號 |
---|---|
select_type | SIMPLE:簡單SELECT(不使用UNION或子查詢) PRIMARY:最外面的SELECT UNION:UNION中的第二個或後面的SELECT語句 DEPENDENT UNION:UNION中的第二個或後面的SELECT語句,取決於外面的查詢 UNION RESULT:UNION 的結果 SUBQUERY:子查詢中的第一個SELECT DEPENDENT SUBQUERY:子查詢中的第一個SELECT,取決於外面的查詢 DERIVED:導出表的SELECT(FROM子句的子查詢) |
table | 查詢sql過程中關聯的表名稱 |
type | 連接類型。下面給出各種聯接類型,按照從最佳類型到最壞類型進行排序: system:表僅有一行(=系統表)。這是const聯接類型的一個特例。 const:表最多有一個匹配行,它將在查詢開始時被讀取。因為僅有一行,在這行的列值可被優化器剩餘部分認為是常數。const表很快,因為它們只讀取一次! eq_ref:對於每個來自於前面的表的行組合,從該表中讀取一行,性能僅次於const。 ref:對於每個來自於前面的表的行組合,所有有匹配索引值的行將從這張表中讀取。 ref_or_null:該聯接類型如同ref,但是添加了MySQL可以專門搜索包含NULL值的行。 index_merge:該聯接類型表示使用了索引合併優化方法。 unique_subquery:該類型替換了下面形式的IN子查詢的ref: value IN (SELECT primary_key FROM single_table WHERE some_expr) unique_subquery是一個索引查找函數,可以完全替換子查詢,效率更高。 index_subquery:該聯接類型類似於unique_subquery。可以替換IN子查詢,但只適合下列形式的子查詢中的非唯一索引: value IN (SELECT key_column FROM single_table WHERE some_expr) range:只檢索給定範圍的行,使用一個索引來選擇行。 index:該聯接類型與ALL相同,除了只有索引樹被掃描。這通常比ALL快,因為索引文件通常比數據文件小。 ALL:對於每個來自於先前的表的行組合,進行完整的表掃描。 |
possible_keys | 這個參數更像是mysql的一次預測,預測指定sql可能執行過程中哪些索引會生效 |
key | sql在執行過程中實際生效的索引列 |
key_len | 顯示MySQL決定使用的鍵長度。 |
ref | 顯示使用哪個列或常數與key一起從表中選擇行。 |
rows | 顯示MySQL認為它執行查詢時必須檢查的行數。多行之間的數據相乘可以估算要處理的行數。 |
filtered | 顯示了通過條件過濾出的行數的百分比估計值。 |
Extra | Distinct:MySQL發現第1個匹配行後,停止為當前的行組合搜索更多的行。 Not exists:MySQL能夠對查詢進行LEFT JOIN優化,發現1個匹配LEFT JOIN標準的行後,不再為前面的的行組合在該表內檢查更多的行。 range checked for each record (index map: #):MySQL沒有發現好的可以使用的索引,但發現如果來自前面的表的列值已知,可能部分索引可以使用。 Using filesort:MySQL需要額外的一次傳遞,以找出如何按排序順序檢索行。 Using index:從只使用索引樹中的資訊而不需要進一步搜索讀取實際的行來檢索表中的列資訊。 Using temporary:為了解決查詢,MySQL需要創建一個臨時表來容納結果。 Using where:WHERE 子句用於限制哪一個行匹配下一個表或發送到客戶。 Using sort_union(…), Using union(…), Using intersect(…):這些函數說明如何為index_merge聯接類型合併索引掃描。 Using index for group-by:類似於訪問表的Using index方式,Using index for group-by表示MySQL發現了一個索引,可以用來查 詢GROUP BY或DISTINCT查詢的所有列,而不要額外搜索硬碟訪問實際的表。 |
3.查詢出的數據量過大
例如說一條sql直接查詢了全表的數據資訊量,直接佔滿了網路頻寬,因此訪問時候出現了網路擁塞。
4.硬體設備不足
例如在面對一些高qps的查詢時候,資料庫本身的機器硬體配置較低,自然處理速度會比較慢。
5.自適應hash出現鎖衝突
AHI是innodb存儲引擎特有的屬性,innodb存儲引擎會針對索引數據的查詢結果做自適應的優化,當某些特定的索引查詢頻率特別高的時候會自動為其建立hash索引,從而提升查詢的效率。相比於B+Tree索引來說,hash索引能夠大大減少對於io的訪問次數,「一擊命中」 查詢數據,具備更加高效的性能,而且hash索引是由mysql內部自動適配的,無需dba在外部做過多的干預。
早期版本的hash索引是採用了單鎖模式來防範並發訪問問題,這對於程式自身的一個運作高效性有一定的」折扣「,後期通過對hash索引進行了分區,不同頁的數據用不同的hashtable,每個分區有對應的鎖來做並發訪問的預防。
如果某天你發現了有很多執行緒都被堵塞在了RW-latches的時候,有可能就是因為hash索引的並發訪問負載過高導致的堵塞,這個時候可以通過增大hash索引的分區參數,或者關閉自適應hash索引特性來進行處理。
作者:Java知音-idea