Mysql性能瓶頸深度定位分析

       我們在性能測試過程中,經常會遇到Mysql出現性能瓶頸的情況,對於數據庫來說,所謂的性能瓶頸無非是慢SQL、CPU高、IO高、內存高,其中前三個舉實際例子來進行性能分析,最後內存高只是方法性說明(實際測試項目中沒遇到過):

       首先我們要保證沒有數據庫配置方面的性能問題,畢竟在性能測試前,對一些基本配置要擼一遍,避免犯低級錯誤。

       本文結合實際項目例子進行分析(絕對硬核),包括內容:一、慢SQL定位分析;二、高CPU定位分析;三、高IO定位分析;四、高內存定位分析。

一、慢SQL定位分析

       首先業務系統慢,肯定是體現在響應時間上,所以在性能測試中,如果發現慢我們就從響應時間上進行拆分,最後拆到mysql,那就是分析慢SQL,同樣如果在高併發時發現mysql進程佔CPU很高,也是優先分析是否存在慢SQL,而且判斷慢SQL還是比較簡單的,對於Mysq就是看慢日誌查詢。

1、首先是開啓慢日誌查詢:

#查看是否開啓,以及存放路徑
show variables like '%slow_query_log%';

#開啓
set global slow_query_log = 1;

#記錄慢日誌的時間,默認情況下爲10秒
show variables like '%long_query_time%'

#查看慢日誌條數
show global status like '%slow_queries%'

使用set global slow_query_log=1;開啓慢查詢日誌只對當前數據庫生效,如果MySQL重啓後則會失效。想要永久生效,就必須修改配置文件,其實沒這必要,我們都是臨時開啓,分析性能問題而已(分析完了,還得關了)。

2、測試過程獲取慢SQL

要手工分析日誌,查找和分析SQL,顯然是個體力活,MySql提供了日誌分析工具mysqldumpslow

#得到返回記錄集最多的10個SQL
Mysqldumpslow –s r –t 10 /usr/local/mysql/data/localhost-slow.log
#得到訪問次數最多的10個SQL
Mysqldumpslow –s c –t 10 /usr/local/mysql/data/localhost-slow.log
#得到按照時間排序的前10條裏面含有左連接的查詢
Mysqldumpslow –s t –t 10 –g “left join” /usr/local/mysql/data/localhost-slow.log
#另外建議在使用這些命令時結合|和more使用,否則可能出現爆破情況
Mysqldumpslow –s r –t 10 /usr/local/mysql/data/localhost-slow.log | more
參數含義
s: 表示按照何種方式排序
c:訪問次數
l:鎖定時間
r:返回記錄
t:查詢時間
al:平均鎖定時間
t:返回前面多少條的數據
g:後面搭配一個正則表達式

除此之外,你們也可以通過APM監控(全鏈路監控),也是能監控到慢SQL(當然壓測過程中不建議依賴一些重型工具):

3、初步Explain分析

這是最基礎的功能,獲取到慢SQL,當然是要實際驗證一下有多慢,是否索引配置了,拿一條實際測試項目的SQL語句來分析:

explain SELECT count(c.id)
        FROM administrative_check_content c
        LEFT JOIN administrative_check_report_enforcers e ON c.report_id=e.report_id
        LEFT JOIN administrative_check_report r ON c.report_id = r.id
        WHERE e.enforcer_id= 'ec66d95c8c6d437b9e3a460f93f1a592';

可以分析出這條語句,86%的時間是花在了Sending data(所謂的“Sending data”並不是單純的發送數據,而是包括“收集 [檢索] + 發送數據”):

通過Explain解釋也能看出索引已經加了(enforcer_id_index),而且通過索引幾乎全表檢索了30084條數據,如下:

 一般如果索引沒加或是加的不合理,通過這麼一分析也就能馬上看出來,可以說索引問題是導致慢SQL的最主要原因之一,也是影響業務系統性能的關鍵因素,以下以本次壓測項目的例子來說,沒加索引時最高TPS只有200,加了索引最高TPS達到900,如下所示:

在Explain解釋語句中,跟索引有關的列我們主要關注以下幾個:

(1)type
這列很重要,顯示了連接使用了哪種類別,有無使用到索引。一般從最好到最差的連接類型爲const、eq_reg、ref、range、indexhe和ALL。

(2)possible_keys
possible_keys列指出MySQL能使用哪個索引在該表中找到行。注意,該列完全獨立於EXPLAIN輸出所示的表的次序。這意味着在possible_keys中的某些鍵實際上不能按生成的表次序使用。
如果該列是NULL,則沒有相關的索引。在這種情況下,可以通過檢查WHERE子句看是否它引用某些列或適合索引的列來提高你的查詢性能。如果是這樣,創造一個適當的索引並且再次用EXPLAIN檢查查詢。
(3) key
key列顯示MySQL實際決定使用的鍵(索引)。如果沒有選擇索引,鍵是NULL。要想強制MySQL使用或忽視possible_keys列中的索引,在查詢中使用FORCE INDEX、USE INDEX或者IGNORE INDEX。
(4)key_len
key_len列顯示MySQL決定使用的鍵長度。如果鍵是NULL,則長度爲NULL。使用索引的長度,在不損失精確性的情況下,長度越短越好。
(5)ref
ref列顯示使用哪個列或常數與key一起從表中選擇行。這一列涉及到多表關聯的字段,const表示常數關聯。 ref很多時候也是和索引有關聯影響的地方。

4、用show profile進行sql分析

開啓分析也很簡單,使用臨時開啓執行set profiling=1即可(這個功能會緩存最近查詢的分析語句,默認15條,最多100條,適合在壓測結束後開展sql分析,用完後再設成0關閉),如下:

#顯示是否開啓Profiling,以及最多存儲多少條
show variables like '%profil%';

#開啓Profiling
set profiling=1;

#執行你的SQL
#在這裏我們主要是執行前面所找到的慢SQL

#查看分析
show profiles;

通過show profiles我們可以看到我們上面執行的那條SQL(Query_ID=18):

 執行:show profile cpu,memory,block io for query 18;

可以看出也是Sending data總共消耗0.39秒,其中CPU_user時間佔比較高(簡單的一條SQL語句消耗這些時間就算很高了),另外還能看到這條SQL的IO開銷(因爲查詢,都是ops out塊輸出)。

另外說明一下這個show profile語句:

show profile cpu, block io, memory,swaps,context switches,source for query [Query_ID];

# Show profile後面的一些參數:
# - All:顯示所有的開銷信息
# - Cpu:顯示cpu相關開銷
# - Block io:顯示塊IO相關開銷
# - Context switches: 上下文切換相關開銷
# - Memory:顯示內存相關開銷
# - Source:顯示和source_function,source_file,source_line相關的開銷信息

結論:通過一條慢SQL我們就能追本溯源找到它慢的原因,這樣就能很好的指導性能調優了(性能測試工程師可以不會調優,但是不會性能分析還真不行,一遇到問題只會說慢,卻不會告訴開發人員到底哪慢的測試工程師,應該很難讓開發人員敬仰,甚至難以收穫尊重!)。

二、高CPU定位分析

1、SQL引起的高CPU

在性能壓測過程中,導致數據庫CPU很高的原因有很多種,一般和慢SQL也有關(因爲每條SQL要麼佔CPU高,要麼佔IO高,大體是這樣),那麼如何分析到是某些SQL引起的呢?

(1)首先定位佔用CPU高的進程

通過TOP命令找到Mysql佔用CPU高,再看mysql進程下有多少線程是佔用CPU高的:

# top -p [pid] H
top -p 44662 H

可以看到有6個在Running,CPU都挺高的,36個在Sleeping,其中兩個Sleeping的CPU也挺高的。

(2)我們在mysql中使用 SHOW FULL PROCESSLIST; 查詢(通過FULL不僅能顯示所有連接的線程,而且能顯示出正在執行的完整SQL語句),如下:

可以看到有不少是Sending data狀態的,我們挑選其中最複雜的一條語句來分析:

(SELECT r.id,c.check_action_name,check_date,check_end_date,c.id AS check_content_id,c.check_object_name AS checkObjectName,c.update_time,c.verify	
        FROM administrative_check_content c	
        LEFT JOIN administrative_check_report r ON c.report_id=r.id	
        LEFT JOIN administrative_check_report_enforcers e ON r.id=e.report_id	
        WHERE e.enforcer_id= 'ec66d95c8c6d437b9e3a460f93f1a592'	      	      	
        )		
            UNION ALL	
            (	
            SELECT r.id,r.check_action_name,check_date,check_end_date,'0','無' AS checkObjectName,r.update_time,false	
            FROM administrative_check_report r	
            LEFT JOIN administrative_check_report_enforcers e ON r.id=e.report_id	
            WHERE e.enforcer_id= 'ec66d95c8c6d437b9e3a460f93f1a592'	
            AND r.check_content_id='0'	
             	
            )	    	
        ORDER BY update_time DESC,check_content_id	
        LIMIT 0, 15	

(3)把這條語句用前面提到show profile進行分析:

 可以看到有兩Sending data佔用時間都挺高的,花費時間也高,其實這語句用了聯合查詢,我們可以把SQL語句拆分了繼續分析(複雜語句都是由簡單語句組成,比較不爽的是有時候分離出來的語句也很慢),拆出一條語句繼續分析:

SELECT r.id,c.check_action_name,check_date,check_end_date,c.id AS check_content_id,c.check_object_name AS checkObjectName,c.update_time,c.verify	
        FROM administrative_check_content c	
        LEFT JOIN administrative_check_report r ON c.report_id=r.id	
        LEFT JOIN administrative_check_report_enforcers e ON r.id=e.report_id	
        WHERE e.enforcer_id= 'ec66d95c8c6d437b9e3a460f93f1a592'	 

執行這條拆分出來的語句,查詢時間都需要0.8秒多,如下:

執行 EXPLAIN 分析,看到通過索引查詢到的內容多達30084行,如下所示:

一般像這樣的系統進行壓測,多半是數據分佈不合理,測試數據沒有反應真實的業務場景,明顯數據分佈不均衡,一個ID號就關聯三萬條數據,使用索引的效率都沒能體現出來。

總結:通過SHOW PROCESSLIST;我們可以知道Mysql當前的線程狀態,以及主要資源消耗在哪方面;再結合show profile分析具體佔用CPU高的SQL,可以進一步定位出SQL引起高CPU的原因,到這一步無疑就能指導開發人員的優化方向了。

2、其他原因引起的高CPU

基本上和上面的分析思路差不多,排除SQL原因(SQL引起的問題主要集中於CPU或IO,IO高有時候也會間接導致CPU高),其他原因引起的高CPU,可通過mysql show processlist + show status + kill Id的方式進行定位。

(1)首先,通過SHOW PROCESSLIST查詢mysql線程狀態,我們需要重點了解State列不同狀態所代表的含義:

Checking table
 正在檢查數據表(這是自動的)。
Closing tables
 正在將表中修改的數據刷新到磁盤中,同時正在關閉已經用完的表。這是一個很快的操作,如果不是這樣的話,就應該確認磁盤空間是否已經滿了或者磁盤是否正處於重負中。
Connect Out
 複製從服務器正在連接主服務器。
Copying to tmp table on disk
 由於臨時結果集大於tmp_table_size,正在將臨時表從內存存儲轉爲磁盤存儲以此節省內存(如果臨時表過大會導致mysql將臨時表寫入硬盤的時間過長,會影響整體性能)。
Creating tmp table
 正在創建臨時表以存放部分查詢結果。
deleting from main table
 服務器正在執行多表刪除中的第一部分,剛刪除第一個表。
deleting from reference tables
 服務器正在執行多表刪除中的第二部分,正在刪除其他表的記錄。
Flushing tables
 正在執行FLUSH TABLES,等待其他線程關閉數據表。
Killed
 發送了一個kill請求給某線程,那麼這個線程將會檢查kill標誌位,同時會放棄下一個kill請求。MySQL會在每次的主循環中檢查 kill標誌位,不過有些情況下該線程可能會過一小段才能死掉。如果該線程程被其他線程鎖住了,那麼kill請求會在鎖釋放時馬上生效。
Locked
 被其他查詢鎖住了。
Sending data
 正在處理SELECT查詢的記錄,同時正在把結果發送給客戶端。
Sorting for group
 正在爲GROUP BY做排序。
 Sorting for order
 正在爲ORDER BY做排序。
Opening tables
 這個過程應該會很快,除非受到其他因素的干擾。例如,在執ALTER TABLE或LOCK TABLE語句行完以前,數據表無法被其他線程打開。正嘗試打開一個表。
Removing duplicates
 正在執行一個SELECT DISTINCT方式的查詢,但是MySQL無法在前一個階段優化掉那些重複的記錄。因此,MySQL需要再次去掉重複的記錄,然後再把結果發送給客戶端。
Reopen table
 獲得了對一個表的鎖,但是必須在表結構修改之後才能獲得這個鎖。已經釋放鎖,關閉數據表,正嘗試重新打開數據表。
Repair by sorting
 修復指令正在排序以創建索引。
Repair with keycache
 修復指令正在利用索引緩存一個一個地創建新索引。它會比Repair by sorting慢些。
Searching rows for update
 正在講符合條件的記錄找出來以備更新。它必須在UPDATE要修改相關的記錄之前就完成了。
Sleeping
 正在等待客戶端發送新請求.(Sleeping過多也是問題,比如wait_timeout設置過大,導致MySQL裏大量的SLEEP進程無法及時釋放,拖累系統性能,不過也不能把它設置的過小,否則你可能會遭遇到“MySQL has gone away”之類的問題)。
System lock
 正在等待取得一個外部的系統鎖。如果當前沒有運行多個mysqld服務器同時請求同一個表,那麼可以通過增加--skip-external-locking參數來禁止外部系統鎖。
Upgrading lock
 INSERT DELAYED正在嘗試取得一個鎖表以插入新記錄。
Updating
 正在搜索匹配的記錄,並且修改它們。
User Lock
 正在等待GET_LOCK()。
Waiting for tables
 該線程得到通知,數據表結構已經被修改了,需要重新打開數據表以取得新的結構。然後,爲了能的重新打開數據表,必須等到所有其他線程關閉這個 表。以下幾種情況下會產生這個通知:FLUSH TABLES tbl_name, ALTER TABLE, RENAME TABLE, REPAIR TABLE, ANALYZE TABLE,或OPTIMIZE TABLE。
waiting for handler insert
 INSERT DELAYED已經處理完了所有待處理的插入操作,正在等待新的請求。

        以上大部分狀態對應很快的操作,只要有一個線程保持同一個狀態好幾秒鐘,那麼可能是有問題發生了,需要檢查一下。
     還有其他的狀態沒在上面中列出來,不過它們大部分只是在查看服務器是否有存在錯誤是才用得着。

(2)其次,通過show status查詢當前Mysql的運行狀態

瞭解以下狀態值及含義,如果在日常運維過程有做這方面的記錄,那麼當系統出現性能異常時,能做個狀態值的比較對,偏離過大的就是需要關注的點(其實可以把這些參數值加入到運維監控系統,作爲關注指標),如下:

Aborted_clients 由於客戶沒有正確關閉連接已經死掉,已經放棄的連接數量。
Aborted_connects 嘗試已經失敗的MySQL服務器的連接的次數。
Connections 試圖連接MySQL服務器的次數。
Created_tmp_tables 當執行語句時,已經被創造了的隱含臨時表的數量。
Delayed_insert_threads 正在使用的延遲插入處理器線程的數量。
Delayed_writes 用INSERT DELAYED寫入的行數。
Delayed_errors 用INSERT DELAYED寫入的發生某些錯誤(可能重複鍵值)的行數。
Flush_commands 執行FLUSH命令的次數。
Handler_delete 請求從一張表中刪除行的次數。
Handler_read_first 請求讀入表中第一行的次數。
Handler_read_key 請求數字基於鍵讀行。
Handler_read_next 請求讀入基於一個鍵的一行的次數。
Handler_read_rnd 請求讀入基於一個固定位置的一行的次數。
Handler_update 請求更新表中一行的次數。
Handler_write 請求向表中插入一行的次數。
Key_blocks_used 用於關鍵字緩存的塊的數量。
Key_read_requests 請求從緩存讀入一個鍵值的次數。
Key_reads 從磁盤物理讀入一個鍵值的次數。
Key_write_requests 請求將一個關鍵字塊寫入緩存次數。
Key_writes 將一個鍵值塊物理寫入磁盤的次數。
Max_used_connections 服務器啓動後同時使用的連接的最大數目。
Not_flushed_key_blocks 在鍵緩存中已經改變但是還沒被清空到磁盤上的鍵塊。
Not_flushed_delayed_rows 在INSERT DELAY隊列中等待寫入的行的數量。
Open_tables 當前打開表的數量。
Open_files 打開文件的數量。
Open_streams 打開流的數量(主要用於日誌記載)
Opened_tables 已經打開的表的數量。
Questions 發往服務器的查詢的數量。
Slow_queries 要花超過long_query_time時間的查詢數量。
Threads_connected 當前打開的連接的數量。
Threads_running 不在睡眠(激活)的線程數量。
Uptime 服務器工作了多少秒。
Uptime_since_flush_status 最近一次使用FLUSH STATUS 的時間(以秒爲單位)

3)最後,可以嘗試kill id(id在SHOW PROCESSLIST中顯示 ),關掉疑似佔CPU高的線程,以確認是否能讓CPU降下來。

對於mysql來說,慢SQL及死鎖以外的CPU問題確實不好定位,要求對數據庫系統及性能非常瞭解,而對於我們做性能測試的,能做的就是逐層分析,縮小問題範圍,實在不行,只能用kill id的方式來試錯排查。

三、高IO定位分析

       其實高IO也可能導致CPU高,因爲磁盤I/O比較慢,會導致CPU一直等待磁盤I/O請求。分析數據庫IO屬於基本技能(畢竟大部分數據庫調優到了極致,最後的瓶頸也可能會是IO,而且IO調優的難度會高一些)。

(1)首先用萬能的top命令查看進程

[root@localhost ~]# top
top - 11:53:04 up 702 days, 56 min,  1 user,  load average: 7.18, 6.70, 6.47
Tasks: 576 total,   1 running, 575 sleeping,   0 stopped,   0 zombie
Cpu(s):  7.7%us,  3.4%sy,  0.0%ni, 77.6%id, 11.0%wa,  0.0%hi,  0.3%si,  0.0%st
Mem:  49374024k total, 32018844k used, 17355180k free,   115416k buffers
Swap: 16777208k total,   117612k used, 16659596k free,  5689020k cached
 
  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
14165 mysql     20   0 8822m 3.1g 4672 S 162.3  6.6  89839:59 mysqld
40610 mysql     20   0 25.6g  14g 8336 S 121.7 31.5 282809:08 mysqld
49023 mysql     20   0 16.9g 5.1g 4772 S  4.6 10.8   34940:09 mysqld

很明顯是前面兩個mysqld進程導致整體負載較高。而且,從 Cpu(s) 這行的統計結果也能看的出來,%us 和 %wa 的值較高,表示當前比較大的瓶頸可能是在用戶進程消耗的CPU以及磁盤I/O等待上。
(2)我們先分析下磁盤I/O的情況

執行 sar -d 1或(iostat -d -x -k 1)命令(每秒刷新) 確認磁盤I/O是否真的較大:

[root@localhost ~]# sar -d 1
Linux 2.6.32-431.el6.x86_64 (localhost.localdomain)     06/05/2020  _x86_64_        (8 CPU)
11:54:31 AM     DEV      tps    rd_sec/s    wr_sec/s    avgrq-sz   avgqu-sz  await     svctm   %util
11:54:32 AM    dev8-0   5338.00 162784.00   1394.00     30.76      5.24      0.98      0.19    100.00
11:54:33 AM    dev8-0   5134.00 148032.00  32365.00     35.14      6.93      1.34      0.19    100.10
11:54:34 AM    dev8-0   5233.00 161376.00    996.00     31.03      9.77      1.88      0.19    100.00
11:54:35 AM    dev8-0   4566.00 139232.00   1166.00     30.75      5.37      1.18      0.22    100.00
11:54:36 AM    dev8-0   4665.00 145920.00    630.00     31.41      5.94      1.27      0.21    100.00
11:54:37 AM    dev8-0   4994.00 156544.00    546.00     31.46      7.07      1.42      0.20    100.00

%util 達到或接近100%,說明產生的I/O請求太多,I/O系統已經滿負荷。 

(3)再利用 iotop 確認到底哪些進程消耗的磁盤I/O資源最多:

[root@localhost ~]# iotop
Total DISK READ: 59.52 M/s | Total DISK WRITE: 598.63 K/s
TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
16397 be/4 mysql       7.98 M/s    0.00 B/s  0.00 % 95.67 % mysqld --basedir=/usr/local/mysql5.7 --datadir=/usr/local/mysql5.7/data --port=3306
 7295 be/4 mysql      9.95 M/s    0.00 B/s  0.00 % 93.72 % mysqld --basedir=/usr/local/mysql5.7 --datadir=/usr/local/mysql5.7/data --port=3306
14295 be/4 mysql      9.86 M/s    0.00 B/s  0.00 % 94.53 % mysqld --basedir=/usr/local/mysql5.7 --datadir=/usr/local/mysql5.7/data --port=3306
14288 be/4 mysql      13.38 M/s    0.00 B/s  0.00 % 92.21 % mysqld --basedir=/usr/local/mysql5.7 --datadir=/usr/local/mysql5.7/data --port=3306
14292 be/4 mysql      13.54 M/s    0.00 B/s  0.00 % 91.96 % mysqld --basedir=/usr/local/mysql5.7 --datadir=/usr/local/mysql5.7/data --port=3306

可以看到,端口號是3306的實例消耗的磁盤I/O資源比較多,那就看看這個實例裏都有什麼查詢在跑。

(4)可以用上面提到的SHOW PROCESSLIST方法,也可以用mysqladmin命令工具

我們需要看到當前都有哪些SQL在運行:

(以下用mysqladmin的方式,該命令mysql自帶,可創建軟鏈接方便調用,ln -s /usr/local/mysql/bin/mysqladmin /usr/bin):

[root@localhost ~]# mysqladmin -uroot -p123456 pr|grep -v Sleep
+----+----+----------+----+-------+-----+--------------+-----------------------------------------------------------------------------------------------+
| Id |User| Host     | db |Command|Time | State        | Info                                                                                          |
+----+----+----------+----+-------+-----+--------------+-----------------------------------------------------------------------------------------------+
| 25 |root| 172.16.1.133:45921 | db | Query | 68  | Sending data | select max(Fvideoid) from (select Fvideoid from t where Fvideoid>404612 order by Fvideoid) t1 |
| 26 |root| 172.16.1.133:45923 | db | Query | 65  | Sending data | select max(Fvideoid) from (select Fvideoid from t where Fvideoid>484915 order by Fvideoid) t1 |
| 28 |root| 172.16.1.133:45928 | db | Query | 130 | Sending data | select max(Fvideoid) from (select Fvideoid from t where Fvideoid>404641 order by Fvideoid) t1 |
| 27 |root| 172.16.1.133:45930 | db | Query | 167 | Sending data | select max(Fvideoid) from (select Fvideoid from t where Fvideoid>324157 order by Fvideoid) t1 |
| 36 |root| 172.16.1.133:45937 | db | Query | 174 | Sending data | select max(Fvideoid) from (select Fvideoid from t where Fvideoid>324346 order by Fvideoid) t1 |
+----+----+----------+----+-------+-----+--------------+-----------------------------------------------------------------------------------------------+

可以看到有不少慢查詢還未完成,從slow query log中也能發現,這類SQL發生的頻率很高。
這是一個非常低效的SQL寫法,導致需要對整個主鍵進行掃描,但實際上只需要取得一個最大值而已,從slow query log中可看到:

Rows_sent: 1  Rows_examined: 5413058

每次都要掃描500多萬行數據,卻只爲讀取一個最大值,效率非常低。

經過分析,這個SQL稍做簡單改造即可在個位數毫秒級內完成,提升了N次方。
改造的方法是:對查詢結果做一次倒序排序,取得第一條記錄即可。而原先的做法是對結果正序排序,取最後一條記錄。

總結:mysql的IO分析思路挺簡單,首先通過top關注%wa(指CPU等待磁盤寫入完成的時間,平時爲0,越高表示磁盤越忙)的波動是否較大;其次分析下磁盤I/O情況,並找到哪些進程佔用IO資源最多;最後還是用SHOW PROCESSLIST或mysqladmin查看哪些語句的頻繁調用在佔用IO。

四、高內存定位分析

       要在linux下分析內存佔用高低,對於新手來說不太容易,因爲涉及MemFree、虛擬內存(Swap)和Buffers、Cached的概念要搞明白,直觀性不如windows,另外mysql本身默認沒有開啓內存 / 緩存監控(只對performance_schema進行了內存開銷的統計),一般的mysql監控軟件在這方面也很難直觀的暴露問題。

(1)首先分析內存也可以用萬能的TOP命令,看看是否mysql進程佔用內存高 :

       以上這張圖,比較容易看出是內存佔用高,因爲free、buffers、cached都不高,那麼大部分被used掉的內存就屬於被進程佔用,而且沒有Swap(被禁用了,一般正常情況下,swap交換分區的used值如果在不斷的變化,就說明內核在不斷進行內存和swap的數據交換,很可能是內存不夠用了),那麼現在mysql佔用63.3%就可以判斷確實內存高了。

(2)排查是否大量SQL運行佔用內存高

查看mysql裏的線程,觀察是否有長期運行或阻塞的sql,也是用到萬能的show full processlist;,如果沒有發現相關線程(具體參考上面提到的State列不同狀態含義)異常現象,就可以排除該原因。

(3)查看mysql內存/緩存的相關配置,以便排查mysql連接使用完後是否沒有真正釋放內存

Mysql的內存消耗一般分爲兩種:global級共享內存、session級私有內存。

執行如下命令,即可查詢global級共享內存分配情況:

show variables where variable_name in (
'innodb_buffer_pool_size','innodb_log_buffer_size','innodb_additional_mem_pool_size','query_cache_size'
);

session級私有內存,主要是數據庫連接私有內存使用,查詢命令如下: 

show variables where variable_name in (
'tmp_table_size','sort_buffer_size','read_buffer_size','read_rnd_buffer_size','join_buffer_size','thread_stack', 'binlog_cache_size'
);

按理用mysql查詢命令,就能查到當前各項內存或緩存的使用情況,但是mysql默認是沒有開啓內存監控的,通過以下語句就能查出大部分監控項都是未開啓的:

SELECT * FROM performance_schema.setup_instruments
       WHERE  NAME LIKE '%memory%' and NAME not LIKE '%memory/performance_schema%';

我們可以用update語句批量開啓(屬於臨時性開啓,重啓mysql後又還原爲關閉):

mysql> update performance_schema.setup_instruments set enabled = 'yes'
       WHERE  NAME LIKE '%memory%' and NAME not LIKE '%memory/performance_schema%';
> Affected rows: 310
> 時間: 0.002s

然後通過以下語句就可以查出mysql所有內存的使用:

SELECT SUBSTRING_INDEX(event_name,'/',2) AS
       code_area, sys.format_bytes(SUM(current_alloc))
       AS current_alloc
       FROM sys.x$memory_global_by_current_bytes
       GROUP BY SUBSTRING_INDEX(event_name,'/',2)
       ORDER BY SUM(current_alloc) DESC;

查到佔用內存最高的是memory/innodb,如下:

 可以進一步細化查詢memory/innodb:

SELECT * FROM sys.memory_global_by_current_bytes WHERE event_name LIKE 'memory/innodb%';

對於內存使用預估,在網上有人推薦了一款內存計算器,統計網址:http://www.mysqlcalculator.com/ 

(說明:上圖左列爲mysql默認配置,右列爲當前數據庫的配置  [通過show variables可以查到],可以預估出內存使用最大值,如上圖,輕微調大一些配置,就能達到7119MB的內存量;如果預計到的結果不符合要求,就說明當前配置不合理,需要進行調整)。 

mysql的數據庫內存/緩存優化真沒什麼經驗,以下是網上提供的一個優化過程配置項(實際要設置多大,得看自己機器的內存有多大,結合內存計算器算一算,看看有沒有超標):

key_buffer_size = 32M //key_buffer_size指定索引緩衝區的大小,它決定索引處理的速度,尤其是索引讀的速度。只對MyISAM表起作用。即使你不使用MyISAM表,但是內部的臨時磁盤表是MyISAM表,也要使用該值。由於我的數據庫引擎爲innodb,大部分表均爲innodb,此處取默認值一半32M。
query_cache_size = 64M //查詢緩存大小,當打開時候,執行查詢語句會進行緩存,讀寫都會帶來額外的內存消耗,下次再次查詢若命中該緩存會立刻返回結果。默認改選項爲關閉,打開則需要調整參數項query_cache_type=ON。此處採用默認值64M。
tmp_table_size = 64M //範圍設置爲64-256M最佳,當需要做類似group by操作生成的臨時表大小,提高聯接查詢速度的效果,調整該值直到created_tmp_disk_tables / created_tmp_tables * 100% <= 25%,處於這樣一個狀態之下,效果較好,如果網站大部分爲靜態內容,可設置爲64M,如果爲動態頁面,則設置爲100M以上,不宜過大,導致內存不足I/O堵塞。此處我們設置爲64M。
innodb_buffer_pool_size = 8196M //這個參數主要作用是緩存innodb表的索引,數據,插入數據時的緩衝。專用mysql服務器設置的大小: 操作系統內存的70%-80%最佳。由於我們的服務器還部署有其他應用,估此處設置爲8G。此外,這個參數是非動態的,要修改這個值,需要重啓mysqld服務。設置的過大,會導致system的swap空間被佔用,導致操作系統變慢,從而減低sql查詢的效率。
innodb_additional_mem_pool_size = 16M //用來存放Innodb的內部目錄,這個值不用分配太大,系統可以自動調。不用設置太高。通常比較大數據設置16M夠用了,如果表比較多,可以適當的增大。如果這個值自動增加,會在error log有中顯示的。此處我們設置爲16M。
innodb_log_buffer_size = 8M //InnoDB的寫操作,將數據寫入到內存中的日誌緩存中,由於InnoDB在事務提交前,並不將改變的日誌寫入到磁盤中,因此在大事務中,可以減輕磁盤I/O的壓力。通常情況下,如果不是寫入大量的超大二進制數據(a lot of huge blobs),4MB-8MB已經足夠了。此處我們設置爲8M。
max_connections = 800 //最大連接數,根據同時在線人數設置一個比較綜合的數字,最大不超過16384。此處我們根據系統使用量綜合評估,設置爲800。
sort_buffer_size = 2M //是一個connection級參數,在每個connection第一次需要使用這個buffer的時候,一次性分配設置的內存。並不是越大越好,由於是connection級的參數,過大的設置+高併發可能會耗盡系統內存資源。官方文檔推薦範圍爲256KB~2MB,這裏我們設置爲2M。
read_buffer_size = 2M //(數據文件存儲順序)是MySQL讀入緩衝區的大小,將對錶進行順序掃描的請求將分配一個讀入緩衝區,MySQL會爲它分配一段內存緩衝區,read_buffer_size變量控制這一緩衝區的大小,如果對錶的順序掃描非常頻繁,並你認爲頻繁掃描進行的太慢,可以通過增加該變量值以及內存緩衝區大小提高其性能,read_buffer_size變量控制這一提高表的順序掃描的效率 數據文件順序。此處我們設置得比默認值大一點,爲2M。
read_rnd_buffer_size = 250K //是MySQL的隨機讀緩衝區大小,當按任意順序讀取行時(列如按照排序順序)將分配一個隨機讀取緩衝區,進行排序查詢時,MySQL會首先掃描一遍該緩衝,以避免磁盤搜索,提高查詢速度,如果需要大量數據可適當的調整該值,但MySQL會爲每個客戶連接分配該緩衝區所以儘量適當設置該值,以免內存開銷過大。表的隨機的順序緩衝 提高讀取的效率。此處設置爲跟默認值相似,250KB。
join_buffer_size = 250K //多表參與join操作時的分配緩存,適當分配,降低內存消耗,此處我們設置爲250KB。
thread_stack = 256K //每個連接線程被創建時,MySQL給它分配的內存大小。當MySQL創建一個新的連接線程時,需要給它分配一定大小的內存堆棧空間,以便存放客戶端的請求的Query及自身的各種狀態和處理信息。Thread Cache 命中率:Thread_Cache_Hit = (Connections - Threads_created) / Connections * 100%;命中率處於90%纔算正常配置,當出現“mysql-debug: Thread stack overrun”的錯誤提示的時候需要增加該值。此處我們配置爲256K。
binlog_cache_size = 250K // 爲每個session 分配的內存,在事務過程中用來存儲二進制日誌的緩存。作用是提高記錄bin-log的效率。沒有什麼大事務,dml也不是很頻繁的情況下可以設置小一點,如果事務大而且多,dml操作也頻繁,則可以適當的調大一點。前者建議是1048576 –1M;後者建議是: 2097152 – 4194304 即 2–4M。此處我們根據系統實際,配置爲250KB。
table_definition_cache = 400 // 開發模式:從1400設置爲400,內存從150M降到90M;服務模式:從1400設置爲400,內存從324M降到227M

總結:默認情況下按默認配置,很少能出現內存不足問題(畢竟現在的數據庫產品,管理內存還是挺成熟的),因爲按照默認的配置,佔內存總計576.2MB,只是在使用過程中,很多人配置了不合理的參數(爲了追求高性能,沒有平衡好配置和硬件的關係)或是運行實例異常,導致內存爆了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章