mysql案例分析

工做中,須要設計一個數據庫存儲,項目的需求大體以下:html

(1)對於每一個用戶,須要存儲一個或多個庫, 每一個庫, 由一個用戶標識來標識,這裏成爲clientFlag.mysql

(2) 對於每個庫,結構以下:linux

    1) 一個clientFlag對應多個組,組包括組名和組的描述一類的信息算法

    2)一個組中有多個成員,每一個成員包括成員名和成員描述一類的信息sql

    3)一個成員包括若干張本身喜歡的圖片,圖片有圖片的文件ID和圖片的描述信息數據庫

    4)每張圖片對應於多個版本,每一個版本下存儲使用深度學習引擎生成的特徵數組

這個需求的目的是,給出一張圖片,找出最有可能喜歡這張圖片的人。緩存

這是一個邏輯很簡單的數據庫,應該不難設計。首先,咱們須要考慮的是不一樣用戶的信息是存儲在一張表上,仍是以clientFlag作分割。從減小數據競爭考慮,使用clientFlag做爲表名的一部分來實現數據分割,爲何是一部分,由於這樣可使clientFlag不會與其餘表意外重名,這點須要注意。咱們沒有考慮繼續進行分割,例如使用clientFlag-groupName做爲表名的一部分,這裏考慮了mysql的特性,mysql在文件特別多的狀況下,表示並非太好,若是以clientFlag-groupName做爲表名的一部分,在mysql的服務器配置參數innodb_file_per_table爲ON時,很容易出現幾千幾萬個文件,這種狀況下mysql運行效果並很差. 若是真的使用了不少的文件,能夠考慮增大table_open_cache的值。具體能夠參考網頁: https://dev.mysql.com/doc/refman/5.6/en/table-cache.html。修改其中的5.6能夠查看其餘版本。安全

分割的缺點:bash

(1)在寫數據庫操做的代碼時,寫數據庫語句變得更加困難,須要更多的拼接,出錯的可能性和代碼維護,都變得比固定表名的數據庫苦難,增長了字符串拼接的時間。

(2)由於在表格層次進行分割,因此沒有辦法使用預處理語句 (prepared statement),這裏可能會致使必定程度的性能降低。

 

表格設計:

表格設計,考慮到組有組描述,成員有成員描述,圖片有圖片描述,爲了減小冗餘,知足第二範式,能夠以下設計:

(1) 組表包括: 

      1)組名  2) 組描述 (第一列惟一索引)

  (2)   成員包括:

      1) 組名,成員名,用戶描述 (前兩列惟一索引)

  (3)  圖像表包括:

     1)組名,成員名,圖像ID,圖像描述 (前三列惟一索引)

(4) 每個特徵表(一個clientFlag,一個特徵對應一張表)包括:

     1)組名,成員名,圖像ID,圖像特徵 (前三列惟一索引)

 上面設計的好處以下:

  (1)不須要存儲冗餘的組信息,成員信息,圖像描述信息

(2)在查找數據的時候,若是是按照先獲取全部組,獲取一個組的全部成員,獲取成員的全部圖片,獲取圖片的特徵的方式查找,不須要進行表的關聯,代碼運行速度比較快,並且邏輯簡單

(3)相關的數據會出如今一塊兒,對於某些語句很友好

上面設計的缺點以下:

 (1)組名,成員名,圖片ID重複出現,對組名,用戶名,圖像ID的修改代價比較大 (在不少設計中,可能會禁止修改組名)

 (2)組名,成員名,圖片ID通常都是使用字符串表示,在表中佔用比較大,並且爲了保證惟一性,須要在上述字段上面添加惟一索引,致使索引佔用也比較大

個人設計採起了下面的設計:

(1)組表包括:

     1)  自增ID  2) 組名  3)組描述  (第一列主鍵,第二列惟一索引)

  (2)成員表包括:

     1)自增ID 2)外建關聯 組ID  3) 成員名  4)成員描述 (第一列主鍵,第二三列惟一索引)

(3)圖像表包括:

     1)自增ID 2) 外健關聯 成員ID 3)文件ID 4) 文件描述  (第一列主鍵,第二三列惟一索引)

   (4) 每個特徵表(一個clientFlag,一個特徵對應一張表)包括:

     1)外鍵關聯 圖像ID 2) 特徵 (第一列主鍵)

優勢:

  (1)不須要存儲冗餘的組信息,成員信息,圖像描述信息

  (2)組名,成員名,圖片ID 不重複出現,修改組名,成員名等只須要修改一處

  (3)表格大小更小,索引大小也更小

    (4)  對於插入來講, 由於使用了自增ID, 爲順序插入, 插入速度較快, 並且數據內存佔用(或者硬盤)佔用會比較小

缺點:

   (1)在查找數據的時候,常常須要使用join或者子查詢,由於惟一索引的存在,子查詢能夠轉化爲常量表達式,性能上沒有問題,代碼會變得比較難寫(對於高併發場景, 幾乎全部的查找操做, 插入操做都會需查詢組表, 組表可能會面臨比較多的鎖爭用)

   (2)若是數據爲隨機併發插入,那麼數據存儲不存在彙集現象,對有些數據的查找不利,尤爲是硬盤查找

 

表格設計的一個注意事項:

(1)合理選擇每一列的類型和大小, 儘可能選擇可以實現功能的最小最簡單的類型.

(2)避免NULL類型,例如對於描述而言,空字符串應該是合理的默認值。

 

測試數據:

關於測試數據, 這裏使用英文數據進行測試,對於組名,能夠考慮使用隨機不重複的英文字符串,對於描述信息,可使用更長的隨機不重複的英文字符串,對於特徵,使用字節數據,這裏使用2000byte的字節數組。

(1)想要獲取不重複字符串,能夠考慮使用uuid,關於長度, 若是須要的長度較短,能夠將生成的uuid字符串截斷,獲得須要的長度。若是須要的長度更長,有兩種方式,第一種,重複獲取多個uuid字符串,而後拼接獲得須要的長度,第二種是在後面隨便添加一些字符數組,重複的應該也沒有什麼問題,由於uuid已經能夠保證惟一性了。在個人最初測試數據中,須要有1000個組,每一個組有1000個成員,每一個成員有10張圖片。對於這麼大量級的數據,按照個人測試來看,沒有出現過uuid衝突的狀況。個人名字基本爲20個字符,描述大約爲50或者60個字符。

(2)對於特徵數據,這裏有兩種方式,第一個使用split截斷一個比較大的二進制文件,而後每個文件當作一個特徵。第二種是在讀取一個大文件的時候,在代碼中進行分割,從個人實際操做來看,第二種方式比較好。實際上並不須要有一千萬個不同的特徵,由於mysql並不精確比較不一樣的行的數據是否一致,因此,有數十萬個應該就足夠了。

壓測的用處:

(1) 能夠測試硬件的效果,這不是此次測試的目的

(2)能夠測試不一樣服務器參數的影響

  (3) 能夠測試在測試量級下,服務器的性能

(4)可使用測試數據,檢測所寫語句的性能,在這裏,我對個人幾乎全部的語句使用了explain查看了語句的性能,防止在寫的時候,犯一些低級的錯誤,例如將能夠索引查找的變成表掃描一類的

(5)分析在運行中出現的各類問題,例如在使用事務經過先刪除,後插入的方式實現對全部圖片和特徵替換的時候,會發現死鎖的出現,在使用percona toolkit的pt-deadlock-logger分析,能夠看出這種死鎖來源於插入和刪除的衝突, 查看innodb status的輸出,能夠看到間隙鎖,應該是致使出現出現這種問題的緣由。

(6)糾正一些本身的錯誤。

 

測試過程當中,考慮使用的助手:

(1)高性能mysql中的狀態記錄:

#!/bin/sh

INTERVAL=5
PREFIX=$INTERVAL-sec-status
RUNFILE=/home/sun/benchmarks/running
mysql -e 'SHOW GLOBAL VARIABLES' >> mysql-variables
while test -e $RUNFILE; do
    file=$(date +%F_%I)
    sleep=$(date +%s.%N | awk "{print $INTERVAL - (\$1 % $INTERVAL)}")
    sleep $sleep
    ts="$(date +"TS %s.%N %F %T")"
    loadavg="$(uptime)"
    echo "$ts $loadavg" >> $PREFIX-${file}-status
    mysql -e 'SHOW GLOBAL STATUS' >> $PREFIX-${file}-status &
    echo "$ts $loadavg" >> $PREFIX-${file}-innodbstatus
    mysql -e 'SHOW ENGINE INNODB STATUS\G' >> $PREFIX-${file}-innodbstatus &
    echo "$ts $loadavg" >> $PREFIX-${file}-processlist
    mysql -e 'SHOW FULL PROCESSLIST\G' >> $PREFIX-${file}-processlist &
    echo $ts
done
echo Exiting because $RUNFILE does not exist

注: 上述腳本中的date +%F_%I使用的是12小時制, 也就是說1點和13點的記錄會存在同一個文件中,若是想用24小時制, 能夠改成 date +%F_%H.

這段腳本,對於分析mysql狀態來講,很是有益。另外,下面分析用的腳本,一樣很重要:

#!/bin/bash
# This script converts SHOW GLOBAL STATUS into a tabulated format, one line
# per sample in the input, with the metrics divided by the time elapsed
# between samples.
awk '
    BEGIN {
    printf "#ts date time load QPS";
        fmt = " %.2f";
    }
    /^TS/ { # The timestamp lines begin with TS.
    ts = substr($2, 1, index($2, ".") - 1);
    load = NF - 2;
    diff = ts - prev_ts;
    prev_ts = ts;
    printf "\n%s %s %s %s", ts, $3, $4, substr($load, 1, length($load)-1);
    }
    /Queries/ {
        printf fmt, ($2-Queries)/diff;
        Queries=$2
    }
    ' "$@"

對於上面的bash腳本,有一點要特別主要, /Queries/ 可能須要替換成$1~/^Com_insert$/, $1~/^Com_select$/或者$1~/^Questions$/等, Queries在以後的出現,也作相應的替換,這樣纔是你須要的QPS,具體是使用Com_insert, Com_select,仍是Questions,查看一下mysql官方文檔。對於個人測試操做測試來看,我使用的go語言的客戶端prepare語句,Queries得出的數值大約是Com_insert的四倍,而Com_insert獲得的數值纔是我須要的QPS。

 

(2)慢日誌查詢

這裏能夠考慮啓用表格版的慢日誌查詢,文件版本更加精確,可是查看須要先使用腳本一類的分析比較好。啓用表格版本的慢日誌查詢以下:

(1) 將log_output設置爲TABLE, 設置以下
set global log_output="TABLE";
(2) 將slow_query_log設置爲ON, 設置以下
set global slow_query_log=ON;
(3) 設置合適的long_query_time
set global long_query_time=0.5;

show global variables like 'long_query_time';

查詢慢日誌中的內容,可使用以下語句:

select * from mysql.slow_log;

若是用於測試,注意其中的global標誌,由於基本是在另一個鏈接中啓用的慢日誌查詢,因此global標誌是必要的,並且show global variables like 'long_query_time' 和show variables like 'long_query_time'獲得的結果多是不一樣的。

慢日誌查詢的做用是告訴你,哪條語句執行比較慢,從而給你必定的指導,省得在優化的路上走錯了方向。我在寫插入語句的過程當中,使用了先獲取組列表,而後獲取成員列表,而後獲取圖片表中的自增ID,而後執行插入特徵, 在innodb_buffer_pool_size足夠大的狀況下,執行時間比較長的基本都是插入特徵的語句,根據這個,我能夠判斷對於這種操做來講,我須要優化的主要是插入特徵這個部分。

 

(3)explain語句或者explain extended,這裏我簡單介紹一下這個語法

先給出查詢語句:

explain insert into portrait_123(person_id, file_id, description) values ((select person_123.id from person_123 inner join group_123 where person_123.name = 'xxxx' and group_123.name = 'xxxxx'), 'xxxx', 'xxxx');

輸出結果爲:

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: group_123
         type: const
possible_keys: name
          key: name
      key_len: 22
          ref: const
         rows: 1
        Extra: Using index
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: person_123
         type: index
possible_keys: NULL
          key: group_id
      key_len: 26
          ref: NULL
         rows: 921849
        Extra: Using where; Using index
2 rows in set (0.00 sec)

這裏,咱們首先須要關注rows,也就是須要掃描的估計行數,若是這個值和預計的一致,基本能夠肯定這個語句是合理的。對於這個案例來講,咱們可使用組名(groupName)的惟一索引從組表中獲取組的ID,而後使用(組ID,用戶名)的惟一索引獲取用戶的ID,也就是說明,上述兩個row中,每個,咱們都應該是能夠只掃描一行就獲得結果的。而第二個結果中,能夠看出,基本掃描了整個表格,才獲得結果。下面,簡單描述一下各個字段,其實,我以爲,主要關注掃描的行數,基本能夠判斷語句是否合理。

id: select語句的標識符,用來標識是第幾個select語句。若是這一行是其餘行的union結果,這個值爲NULL. 在這種狀況下,table列顯示爲相似於<unionM, N>來表示id值爲M和N的行的union.

 

select_type: select的類型,有以下值:

(1)simple    簡單select查詢(不使用union或者子查詢)

(2)primary    最外層的select查詢

(3)union    union中第二個及之後的查詢語句

(4)dependent union  依賴於外層查詢的union中的第二個及之後的查詢語句

(5)union result    union的結果

(6)subquery     子查詢中的第一個select

(7)dependent subquery 依賴於外層查詢的子查詢中的第一個查詢

(8)derived     派生表

(9)materialized   物化子查詢

(10)uncacheable subquery    對於外層查詢的每一行,都須要從新計算的子查詢

(11)uncacheable union      uncacheable subquery包含的union語句中的第二個及之後的查詢

 

table: 掃描行(結果行)對應的表格。若是爲union查詢,派生表查詢或者子查詢,可能會顯示爲 <unionM, N>, <derivedN>或者<subqueryN>。

 

type: 官方描述是 "join type", 更合理的說法應該是「access type",我以爲你能夠理解爲掃描類型,也就是說,mysql如何查找對應的行。從好到壞依次是:

1)system  當訪問的表只有一行時(系統表),特殊類型的const。

2)const        最多隻有一行匹配。當mysql查找過程可使用主鍵索引或者其餘惟一索引匹配一個常量時,就會出現const。

3)eq_ref      當一個主鍵或者非空惟一鍵和從其餘表中獲取的常量或表達式作相等比較時。

4)ref        使用=或者<=>作相等比較,這裏的鍵不爲惟一鍵,或者只是惟一鍵的前綴,含義與(3)相同。

5)ref_or_null     與ref基本相同,只是能夠匹配NULL值。

6)  index_merge     使用index merge優化進行查詢。

7)unique_subquery   這種類型使用eq_ref來處理in子查詢。

8)index_subquery   這種類型能夠理解爲使用ref來處理in子查詢。

9)range         某個鍵在指定範圍內的值。

10) index       使用索引進行掃描。

11)ALL       全表掃描

注:從好到壞的順序基本是大體的,並非從上到下嚴格變壞。例如:對於小表來講,全表掃描的性能不少時候優於index方式,甚至range等方式。對於index和ALL兩種方式,若是index方式可使用覆蓋掃描,那麼index方式大多時候更優(當Extra列有using index顯示),其餘時候,頗有可能全表掃描更快,由於全表掃描不須要回表查詢。這些顯示的類型,最好參照掃描行數一塊兒查看。

 possible_keys: mysql可能選擇的用來查找表格中對應行的索引。這一行顯示的結果與explain輸出中顯示的表格順序無關,也就是說,在實際中,對於顯示的表格順序,possible_keys中的某些索引可能不可以使用。若是這個值爲空,能夠考慮檢查語句,而後建立一個合適的索引。

 

key: mysql實際決定使用的索引。對於key中顯示的名字,能夠參考show index獲得的結果,show index的結果中,有比較詳細的索引說明信息。當possible_keys中沒有合適的索引時,mysql也會使用某些索引來實現覆蓋索引的效果。

 

key_len: 索引大小。對於innodb來講,若是使用二級索引,計算時,不會考慮主鍵的長度。 key_len的用處在於,對於一個複合索引,能夠根據key_len來肯定使用的索引的所有,仍是某個特定前綴。關於key_len的計算,對於基本類型,如int,爲4個字節。可是,對於字符型,尤爲是varchar(n)的計算方式以下:對於utf8(或者說utf8mb3),計算索引長度的時候爲3×n+2,對於utf8mb4,計算索引長度爲4×n+2,對於其餘的字符型,能夠簡單構建一個表格進行測試,或者查詢相關文檔。

 

ref:  顯示與key列中的索引進行對比的是哪一列,或者說是一個常數。若是值是一個const,那麼對比對象應該是一個常量(或者type 爲const的select結果), 若是值是一個func,那麼對比對象應該是某個函數的結果。

 

rows: mysql認爲在執行這個語句過程當中必需要檢查的行數。我以爲這個數值很是重要,是最終要的衡量語句好壞的指示器。對於innodb來講,這個數值可能不是精確值,若是與精確值誤差很大,能夠考慮執行analyze table,來更新innodb關於表的統計信息,不過,就算是這樣,也不能保證獲取的值足夠精確。這個值在不少時候,已經足夠咱們肯定所寫語句是否足夠優秀。

 

Extra: 這一列顯示mysql如何處理查詢的額外信息。重要的一些以下:

1)Using filesort, Using temporary

這個標識獲取結果的時候須要使用臨時表排序,能夠考慮對索引進行優化(調整索引順序,添加必要的索引),或者對order by條件進行修改(例如,對於一些不展現的獲取來講,不必定須要按照名字排序),若是實在須要使用臨時表排序,考慮一下tmp_table_size和max_heap_table_size是否合理,若是你想知道內存臨時表和硬盤臨時表的信息,能夠查看Created_tmp_disk_tables和Created_tmp_tables的數目(使用show status和show global status)。

2)Using index

這個標識是覆蓋索引掃描的標識。說明查詢過程當中只須要檢測索引內容就能夠,而不用回表查詢。

 

 (4)set profiling=1; show profiles; show profile for query N; set profiling=0;

或者:使用performance shema來進行查詢剖析。

咱們先介紹set profiling相關的語句,這個語句是Session級別的,也就是說,單個鏈接有效,若是想要在一個Session中記錄profiling信息,必需要在這個鏈接中啓用profiling, 這種方式,有本身的便捷性,尤爲適合使用mysql提供的客戶端進行一些簡單的profiling,而對於代碼實現的一些調用來講,每每不那麼友好。具體使用方法以下:

set profiling = 1;
select count(*) from group_123;
show profiles\G
*************************** 1. row ***************************
Query_ID: 1
Duration: 0.00122225
   Query: select count(*) from group_123
1 row in set, 1 warning (0.00 sec)
show profile for query 1;
+--------------------------------+----------+
| Status                         | Duration |
+--------------------------------+----------+
| starting                       | 0.000133 |
| Executing hook on transaction  | 0.000017 |
| starting                       | 0.000020 |
| checking permissions           | 0.000017 |
| Opening tables                 | 0.000073 |
| init                           | 0.000022 |
| System lock                    | 0.000027 |
| optimizing                     | 0.000017 |
| statistics                     | 0.000041 |
| preparing                      | 0.000039 |
| executing                      | 0.000013 |
| Sending data                   | 0.000699 |
| end                            | 0.000016 |
| query end                      | 0.000009 |
| waiting for handler commit     | 0.000019 |
| closing tables                 | 0.000017 |
| freeing items                  | 0.000028 |
| cleaning up                    | 0.000019 |
+--------------------------------+----------+
18 rows in set, 1 warning (0.00 sec)
set profiling=0;

經過上述表格輸出,能夠看到調用這條語句須要總的時間,已經在每一個步驟中須要的時間,能夠針對性的進行優化。若是但願能夠直接看到耗時最多一些步驟,也能夠考慮使用以下語句,按照耗時從多到少顯示:

set @query_id = 1;

SELECT STATE, SUM(DURATION) as Total_R,  ROUND( 100 * SUM(DURATION)/ (select SUM(DURATION) from INFORMATION_SCHEMA.PROFILING where QUERY_ID = @query_id), 2) AS Pct_R, COUNT(*) AS Calls, SUM(DURATION) / COUNT(*) AS "R/Call" FROM INFORMATION_SCHEMA.PROFILING where QUERY_ID = @query_id group by STATE ORDER BY Total_R DESC;

輸出結果以下:

+--------------------------------+----------+-------+-------+--------------+
| STATE                          | Total_R  | Pct_R | Calls | R/Call       |
+--------------------------------+----------+-------+-------+--------------+
| Sending data                   | 0.000699 | 57.01 |     1 | 0.0006990000 |
| starting                       | 0.000153 | 12.48 |     2 | 0.0000765000 |
| Opening tables                 | 0.000073 |  5.95 |     1 | 0.0000730000 |
| statistics                     | 0.000041 |  3.34 |     1 | 0.0000410000 |
| preparing                      | 0.000039 |  3.18 |     1 | 0.0000390000 |
| freeing items                  | 0.000028 |  2.28 |     1 | 0.0000280000 |
| System lock                    | 0.000027 |  2.20 |     1 | 0.0000270000 |
| init                           | 0.000022 |  1.79 |     1 | 0.0000220000 |
| waiting for handler commit     | 0.000019 |  1.55 |     1 | 0.0000190000 |
| cleaning up                    | 0.000019 |  1.55 |     1 | 0.0000190000 |
| optimizing                     | 0.000017 |  1.39 |     1 | 0.0000170000 |
| closing tables                 | 0.000017 |  1.39 |     1 | 0.0000170000 |
| Executing hook on transaction  | 0.000017 |  1.39 |     1 | 0.0000170000 |
| checking permissions           | 0.000017 |  1.39 |     1 | 0.0000170000 |
| end                            | 0.000016 |  1.31 |     1 | 0.0000160000 |
| executing                      | 0.000013 |  1.06 |     1 | 0.0000130000 |
| query end                      | 0.000009 |  0.73 |     1 | 0.0000090000 |
+--------------------------------+----------+-------+-------+--------------+
17 rows in set, 18 warnings (0.01 sec)

下面介紹使用performance schema來進行查詢剖析:

 peformance_schema中,存儲相關信息的表格是events_statements_history_long和events_stages_history_long。開啓方式以下:

1)肯定相關語句和階段測量已經開啓,開啓方式是經過setup_instruments表格。一些測試會被默認開啓。

update performance_schema.setup_instruments set ENABLED = 'YES', TIMED = 'YES' where name like '%statement/%';
update performance_schema.setup_instruments set ENABLED = 'YES', TIMED = 'YES' where name like '%stage/%';

2) 確保events_statements_*和events_stages_*消費者已經被啓用。一些消費者會默認開啓。

update performance_schema.setup_consumers set ENABLED = 'YES' where name like '%events_statements_%';
update performance_schema.setup_consumers set ENABLED = 'YES' where name like '%events_stages_%';

(3) 運行你打算profile的語句,這裏開始的profiling是全局模式的,因此,你能夠在其餘鏈接中調用語句。例如調用以下語句:

select count(*) from group;

(4)使用performance_schema中的表格進行查詢,注意其中的EVENT_ID,這個字段和Query_ID相似,這裏的時間單位是皮秒,轉化成秒,須要除以10的12次方。

select event_id, truncate(timer_wait/1000000000000,6) as Duration, SQL_TEXT From performance_schema.events_statements_history_long;
+----------+----------+------------------------------------------------------------+
| event_id | Duration | SQL_TEXT                                                   |
+----------+----------+------------------------------------------------------------+
|      312 | 0.003801 | truncate performance_schema.events_statements_history_long |
|      325 | 0.000861 | select count(*) from group_123                             |
+----------+----------+------------------------------------------------------------+
2 rows in set (0.00 sec)
SELECT event_name AS Stage, TRUNCATE(TIMER_WAIT/1000000000000,6) AS Duration FROM performance_schema.events_stages_history_long WHERE NESTING_EVENT_ID=325;
+------------------------------------------------+----------+
| Stage                                          | Duration |
+------------------------------------------------+----------+
| stage/sql/starting                             | 0.000122 |
| stage/sql/Executing hook on transaction begin. | 0.000001 |
| stage/sql/starting                             | 0.000009 |
| stage/sql/checking permissions                 | 0.000006 |
| stage/sql/Opening tables                       | 0.000061 |
| stage/sql/init                                 | 0.000007 |
| stage/sql/System lock                          | 0.000016 |
| stage/sql/optimizing                           | 0.000006 |
| stage/sql/statistics                           | 0.000030 |
| stage/sql/preparing                            | 0.000025 |
| stage/sql/executing                            | 0.000001 |
| stage/sql/Sending data                         | 0.000503 |
| stage/sql/end                                  | 0.000002 |
| stage/sql/query end                            | 0.000002 |
| stage/sql/waiting for handler commit           | 0.000014 |
| stage/sql/closing tables                       | 0.000012 |
| stage/sql/freeing items                        | 0.000029 |
| stage/sql/cleaning up                          | 0.000001 |
+------------------------------------------------+----------+
18 rows in set (0.00 sec)

對於上述語句,能夠參考profiling的處理,進行必定的順序排列和彙總。

關於上述的profiling在使用觸發器的狀況下,會有必定的問題,我在這裏簡單介紹一下。場景以下:

1)有group表存儲組的信息,person表存儲用戶信息,group_member存儲組的成員關係,也就是成員關係。group表的設計以下:

CREATE TABLE `group_123` (
  `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL,
  `description` varchar(60) NOT NULL DEFAULT '',PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `person_123` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL,
  `description` varchar(60) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `group_member_123` (
  `group_id` mediumint(8) unsigned NOT NULL,
  `person_id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`group_id`,`person_id`),
  KEY `person_id` (`person_id`),
  CONSTRAINT `group_member_123_ibfk_1` FOREIGN KEY (`group_id`) REFERENCES `group_123` (`id`) ON DELETE CASCADE,
  CONSTRAINT `group_member_123_ibfk_2` FOREIGN KEY (`person_id`) REFERENCES `person_123` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2) 觸發器以下,

CREATE TRIGGER `instri_member` AFTER INSERT ON `group_member` FOR EACH ROW insert into group_count_123 values (NEW.group_id, 1) on duplicate key update count = count+1
CREATE TRIGGER `deltri_member` AFTER DELETE ON `group_member` FOR EACH ROW update group_count_123 set count = count-1 where group_id = OLD.group_id

3) 進行以下的插入語句操做:

insert into group_member_123 values((select id from group_123 where name = 'xxx'), (select id from person_123 where name = 'yyy'));

調用show profiles,顯示結果以下:

+----------+------------+----------------------------------------------------------------------------------------------+
| Query_ID | Duration   | Query                                                                                        |
+----------+------------+----------------------------------------------------------------------------------------------+
|        1 | 0.01630200 | insert into group_count_123 values (NEW.group_id, 1) on duplicate key update count = count+1 |
+----------+------------+----------------------------------------------------------------------------------------------+
1 row in set, 1 warning (0.00 sec) 

這裏須要注意兩點:

(1)咱們調用的語句沒有出現,而觸發器的語句被記錄。

(2)觸發器觸發的這條語句時間很短。

而後咱們調用show profile for query 1, 結果以下:

+--------------------------------+----------+
| Status                         | Duration |
+--------------------------------+----------+
| continuing inside routine      | 0.000034 |
| Executing hook on transaction  | 0.000010 |
| Sending data                   | 0.000013 |
| checking permissions           | 0.000012 |
| Opening tables                 | 0.000049 |
| init                           | 0.000021 |
| update                         | 0.016120 |
| end                            | 0.000018 |
| query end                      | 0.000005 |
| closing tables                 | 0.000022 |
+--------------------------------+----------+
10 rows in set, 1 warning (0.00 sec)

與show profiles的結果基本一致。

下面,咱們再看一下,使用performance_schema的統計信息:

SELECT EVENT_ID, TRUNCATE(TIMER_WAIT/1000000000000,6) as Duration, SQL_TEXT FROM performance_schema.events_statements_history_long;
+----------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EVENT_ID | Duration | SQL_TEXT                                                                                                                                                             |
+----------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|      252 | 0.004271 | truncate table performance_schema.events_statements_history_long                                                                                                     |
|      198 | 0.016278 | insert into group_count_123 values (NEW.group_id, 1) on duplicate key update count = count+1                                                                         |
|      176 | 0.050943 | insert into group_member_123 values((select id from group_123 where name = '69be790a-332a-400e-8'), (select id from person_123 where name = 'dca83c7d-b1f6-4193-b')) |
+----------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+
3 rows in set (0.00 sec)
SELECT event_name AS Stage, TRUNCATE(TIMER_WAIT/1000000000000,6) AS Duration
    -> FROM performance_schema.events_stages_history_long WHERE NESTING_EVENT_ID=176;
+------------------------------------------------+----------+
| Stage                                          | Duration |
+------------------------------------------------+----------+
| stage/sql/starting                             | 0.000249 |
| stage/sql/Executing hook on transaction begin. | 0.000009 |
| stage/sql/starting                             | 0.000020 |
| stage/sql/checking permissions                 | 0.000013 |
| stage/sql/checking permissions                 | 0.000011 |
| stage/sql/checking permissions                 | 0.000011 |
| stage/sql/Opening tables                       | 0.000177 |
| stage/sql/init                                 | 0.000017 |
| stage/sql/System lock                          | 0.000028 |
| stage/sql/update                               | 0.000014 |
| stage/sql/optimizing                           | 0.000018 |
| stage/sql/statistics                           | 0.000116 |
| stage/sql/preparing                            | 0.000022 |
| stage/sql/executing                            | 0.000007 |
| stage/sql/Sending data                         | 0.000015 |
| stage/sql/optimizing                           | 0.000011 |
| stage/sql/statistics                           | 0.000055 |
| stage/sql/preparing                            | 0.000014 |
| stage/sql/executing                            | 0.000006 |
+------------------------------------------------+----------+
19 rows in set (0.00 sec) 
SELECT event_name AS Stage, TRUNCATE(TIMER_WAIT/1000000000000,6) AS Duration FROM performance_schema.events_stages_history_long WHERE NESTING_EVENT_ID=198;
+------------------------------------------------+----------+
| Stage                                          | Duration |
+------------------------------------------------+----------+
| stage/sql/Sending data                         | 0.000330 |
| stage/sql/Executing hook on transaction begin. | 0.000007 |
| stage/sql/Sending data                         | 0.000013 |
| stage/sql/checking permissions                 | 0.000011 |
| stage/sql/Opening tables                       | 0.000051 |
| stage/sql/init                                 | 0.000019 |
| stage/sql/update                               | 0.016126 |
| stage/sql/end                                  | 0.000007 |
| stage/sql/query end                            | 0.000004 |
| stage/sql/closing tables                       | 0.000010 |
| stage/sql/end                                  | 0.000005 |
| stage/sql/query end                            | 0.000005 |
| stage/sql/waiting for handler commit           | 0.033397 |
| stage/sql/closing tables                       | 0.000043 |
| stage/sql/freeing items                        | 0.000047 |
| stage/sql/cleaning up                          | 0.000002 |
+------------------------------------------------+----------+
16 rows in set (0.00 sec)

4) 總結:

由上面輸出結果能夠看出來,在有觸發器存在的狀況下, 使用performance_schema查看運行時間是更加明智的選擇,由於set profiling的方式只會顯示觸發器操做須要的時間,而這個時間不少時候是耗時比較少的操做。

 

服務器參數調整

先給出比較合理的初始數據配置參數:

[mysqld]
# GENERAL
datadir                              = /var/lib/mysql
socket                               = /var/lib/mysql/mysql.sock
pid_file                             = /var/lib/mysql/mysql.pid
user                                 = mysql
port                                 = 3306
default_storage_engine               = InnoDB
#INNODB
innodb_buffer_pool_size              = <value>
innodb_log_file_size                 = <value>
innodb_file_per_table                = 1
innodb_flush_method                  = O_DIRECT
# MyISAM
key_buffer_size                      = <value>
# LOGGING
log_error                            = /var/lib/mysql/mysql-error.log
slow_query_log_file                  = /var/lib/mysql/mysql-slow.log
# OTHER
tmp_table_size                       = 32M
max_heap_table_size                  = 32M
query_cache_type                     = 0
query_cache_size                     = 0
max_connections                      = <value>
thread_cache_size                    = <value>
table_open_cache                     = <value>
open_files_limit                     = 65535
[client]
socket                               = /var/lib/mysql/mysql.sock
port                                 = 3306

簡單說明一下,我下面描述的插入測試數據基於的插入方案是,先獲取大約100個組,而後啓用100個go程(能夠理解爲線程)併發對100個組下全部的用戶的全部圖片特徵進行插入,而後壓測插入速度等信息。個人插入操做在兩臺不一樣的計算機上進行,內網環境,發送2K數據須要的時間小於1ms.

(1)首先須要設置的是innodb_buffer_pool_size,這個值應該在保證系統穩定的狀況下,設置的儘可能大,計算方式比較複雜,能夠參考《高性能mysql》或者官方的建議,設置爲內存總量的50%到75%。這是一個很是重要的配置參數,因此說提供比較大的內存對mysql來講很重要,由於只有內存足夠大,才能夠將這個值設置的比較大。按照個人測試,個人innodb_buffer_pool_size設置爲8G的樣子,在數據量達到二十多個G(其中feature表大約佔80%左右的數據量,有上千萬的數據,每條數據大約2K的樣子)的時候,使用select count(*) from feature__123_1; 須要耗時三到四個小時,這其中與沒有secondary index有關,因此不能使用索引覆蓋掃描,可是,若是innodb_buffer_pool_size足夠大,數據基本都在內存中,那麼執行上述指令的速度應該快的多。

(2)其次,innodb_log_file_size對於插入操做來講,也很重要。在innodb_buffer_pool_size足夠容納全部數據的狀況下,按照個人測試過程來講, 在innodb_log_file_size由50M改成1G(其中innodb_log_files_in_group都爲2),插入速度大約提高了70%的樣子,在innodb_log_file_size爲50M時,插入速度大約爲500次/s(平均值),在innodb_log_file_size爲1G時,插入速度大約爲850次/s(平均值)。實際上,我這裏設置的innodb_log_file_size仍是比較小,按照官方說明,對於個人這裏的狀況,若是我但願每秒能夠插入1000次,那麼須要的innodb_log_file_size爲1000×2K×3600(一小時)/ 2(innodb_log_files_in_group) = 3.5G。我沒有將這個值設置這麼大測試過,有條件的狀況下,能夠嘗試。

關於更新innodb_log_file_size,方法以下(對於mysql版本大於5.6.7):

1) 中止mysql服務器,而且肯定服務器在關閉過程當中沒有出現錯誤。

2)修改my.cnf改變log file的配置。想要修改log file的大小,配置innodb_log_file_size,想要增長log files的個數,配置innodb_log_files_in_group.

3) 從新啓動mysql服務器。

在mysql小於等於5.6.7的時候,更新innodb_log_file_size的值比較複雜,建議參考官網:

https://dev.mysql.com/doc/refman/5.6/en/innodb-redo-log.html

(3)innodb_flush_method這個參數,按照個人測試結果來講,O_DIRECT效果很好,至少遠比O_DSYNC(官方推薦過這種刷新方式,見https://dev.mysql.com/doc/refman/5.6/en/optimizing-innodb-diskio.html)好。使用O_DSYNC的時候,數據預熱的速度明顯變慢,在執行完查詢,而後插入以後,再次執行相似的操做,速度提高的速度明顯慢於O_DIRECT,並且插入速度也不如O_DIRECT。我在使用O_DSYNC的時候,甚至出現select id from feature_1 比 select count(*) from feature_1慢不少的狀況。因此,對於想要使用O_DSYNC的,建議進行詳細的測試再說。

(4)I/O 調度算法,這個參數實際上不是服務器參數,而是linux系統調度算法。根據《高性能mysql》和官方推薦(Use a noop or deadline I/O scheduler with native AIO on Linux),使用deadline應該優於cfq。在我上面的插入測試條件下,使用deadline時,插入速度大約比使用cfq快10%的樣子。使用deadline時,插入速度大約爲930次/s(平均值)。我沒有測試過noop方法,由於按照《高性能mysql》上的描述,對於單硬盤來講,deadline應該比noop更合適,對於磁盤冗餘陣列(線上經常使用方案),能夠對noop和deadline同時進行壓測,查看哪種調度算法更加合適。固然,也能夠同時測試cfq,或者其餘可用的調用算法。

(5)max_connections根據實際同時會有的鏈接數進行設置,這個值能夠設置的稍微大一點,具體根據實際應用來考慮,也能夠監測Connection_errors_max_connections的值,查看這個值在實際運行中的變化,若是出現較多的這個錯誤,能夠考慮調大max_connections的值。具體能夠參考《高性能mysql》中文版No.370或者官網的說明(https://dev.mysql.com/doc/refman/5.6/en/client-connections.html)。

(6)thread_cache_size這個值表示mysql緩存的線程數。若是Threads_connected(用戶鏈接)的變化比較大的話,能夠考慮將這個值設置的大一些,也能夠監測Threads_created的數值變化,若是增加的速度比較快,則應該也須要調大thread_cache_size的值。具體能夠參考《高性能mysql》中文版的No.370。

(7)table_open_cache這個值表示緩存的用於打開表的文件描述符。在mysql5.6.8之後的版本中,這個值已經很大了,應該不須要調整,除非你以爲你的表不少不少。

 

壓測和測試過程當中的問題:

1. 插入速度比較慢的問題。

對於這個問題,天然能夠經過將innodb_flush_log_at_trx_commit改成2來顯著提升mysql的插入速度,這種方式是不安全的。最好的方式應該是將innodb_flush_log_at_trx_commit設置爲1,而後將innodb的日誌文件放在有電池保護的寫緩存的RAID卷中,同時將innodb_flush_method設置爲O_DIRECT。

另外,使用自增鍵做爲主鍵,也能夠提升插入的速度。

2. 讀取速度比較慢。

對於個人例子來講,當mysql存儲的數據大約在24G,而innodb_buffer_pool_size爲8G時,對於feature表(佔據數據的80%)左右,調用select count(*) from feature;這樣的語句,都須要運行三到四個小時,一方面,由於feature表中沒有二級索引,因此沒有辦法使用覆蓋索引掃描(對於二級索引來講,基本都在內存中,並且二級索引數據量每每比較小),另外一方面,若是innodb_buffer_pool_size的量足夠大,那麼獲取表的條數應該仍是會比較快,前提是數據已經獲得了預熱。關於讀取,我有以下的建議:

(1)若是自己數據就會被隨機訪問,例如我這裏的例子,在最初的設計中,咱們沒有辦法肯定group中成員特徵被訪問的狀況,默認爲每一個group都是會被隨機訪問的,那麼幾乎全部的特徵被一樣可能性的訪問到,在這種狀況下,數據訪問沒有熱點,咱們能夠作的一種方式是,經過數據分割,提供比較大的內存,來保證innodb_buffer_pool_size的大小和數據量差很少或者略大,而後經過事先預熱來保證讀取速度知足咱們的需求。

(2)若是innodb_buffer_pool_size比數據量小,可是相差不是很大。咱們能夠考慮是否可使用數據壓縮,在我這個例子中,我測試過對特徵數據的壓縮,壓縮率能夠達到50%的樣子。若是能夠經過壓縮的方式使得innodb_buffer_pool_size能夠容納經常使用數據的話,增長必定的CPU開銷(也可能有邏輯複雜度開銷),應該是值得的,對於個人例子來講,我觀察過mysql的CPU利用率,CPU利用率比較低。

(3)若是存在某些不多檢查的大數據,對於這類數據,放在單獨的表格上比較合適,而後使用外健關聯,這樣也能夠減小緩存的負擔。

(4)若是數據量很大,遠遠超過innodb_buffer_pool_size的大小,那麼能夠實際測試使用不一樣大小的innodb_buffer_pool_size的大小,和查詢QPS一類數據的關係,而後找到一個比較合適的innodb_buffer_pool_size的大小。

提升讀取硬盤的速度,也是一個能夠採起的手段。

1)使用SSD的RAID卡,若是沒有這個條件,可使用RAID卡,將讀取壓力分擔到不一樣的磁盤上。

2)修改表的結構。將feature的主鍵改成(personID+fileID)。對於個人這個例子來講,由於每次調用基本都是一我的的特徵和一個組內的特徵進行比較,那麼一次獲取同一我的的全部特徵這種狀況,應該是比較合理的訪問數據庫的方式,將同一我的的全部特徵存儲在一塊兒,那麼就能夠順序獲取這些特徵,將減小不少的隨機訪問,使得讀取速度能夠提升不少。

(5)查看是否必須存儲這麼多的數據,或者說是否必須存儲這麼多的熱數據。對於我這個例子來講,後來改成根據全部圖片的特徵生成一個總的特徵,經過這種方式,數據庫中減小了大量的存儲,至少減小了大量的熱數據。

3. 不要隨便執行一些修改表的結構的語句。

在《高性能mysql》中,咱們看到有一條語句叫作optimize table。對於這類語句,不要隨便調用,在我以前出現的數據量爲24G,innodb_buffer_pool_size爲8G的狀況下,我調用了這個語句,mysql花費了七八個小時才執行完成。

4. mysql的觸發器在外健刪除的狀況下不會觸發。

咱們存在一個統計組內有多少成員的需求,按照考慮,有兩種方式實現,

一種方式是使用查詢緩存(若是打算使用查詢緩存的話, 最好將query_cache_type設置爲DEMAND, 而後在語句中使用SQL_CACHE修飾符, 不過在8.0.3中, 這個服務器參數已經被移除),當修改組內成員的操做不多的狀況下,能夠考慮對select count(*)語句使用查詢緩存,而後下一次訪問就能夠不實際計算,這個適用於修改不多,或者組內成員通常不太多的狀況。

第二種方式是使用匯總表。這個是咱們實際使用的統計方式,參考上面說彙總表的狀況。在使用匯總表的過程當中,存在一個小的問題,就是說在刪除成員的時候,會使用外健關聯刪除成員表中的對應角色,在這個狀況下,成員表上的觸發器不會被觸發,沒辦法更新彙總表上的數據。對於這種問題,能夠在成員表中新增長一個觸發器,讓這個觸發器直接產生刪除角色表中對應數據的效果,就能夠解決這個問題,雖然更加麻煩,能夠知足要求。

在刪除角色的時候,會涉及對group_member表中的刪除角色ID所在的全部組進行計數減一處理,這個操做最直觀的語句以下:

explain update group_count_123 set count=count-1 where group_id in (select group_id from group_member_123 where person_id = 1);

這個語句的執行效果如何呢?

在group_count包含group表中的全部組ID,測試時,使用的是999個組,group_member中有1011行,其中person_id爲1的行數是8行。上述語句的結果以下:

+----+--------------------+--------------------+------------+-----------------+-------------------+---------+---------+------------+------+----------+-------------+
| id | select_type        | table              | partitions | type            | possible_keys     | key     | key_len | ref        | rows | filtered | Extra       |
+----+--------------------+--------------------+------------+-----------------+-------------------+---------+---------+------------+------+----------+-------------+
|  1 | UPDATE             | group_count_123    | NULL       | index           | NULL              | PRIMARY | 3       | NULL       |  999 |   100.00 | Using where |
|  2 | DEPENDENT SUBQUERY | group_member_123   | NULL       | unique_subquery | PRIMARY,person_id | PRIMARY | 7       | func,const |    1 |   100.00 | Using index |
+----+--------------------+--------------------+------------+-----------------+-------------------+---------+---------+------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec

如何理解這個結果呢?咱們關注一下行數,rows的行數與group_count_123表格相同,第一張表的select_type爲update,而第二張表的select_type爲dependent subquery,這裏的意思就是,這條語句會遍歷group_count_123的全部行,而後對於每一行取出的group_id和person_id聯合獲取group_member_123中的惟一一條記錄,或者沒有記錄。個人佐證是Extra中的Using where和id爲2查詢中的key_len爲7,正好是3(group_id的長度)+4(person_id的長度)。

有沒有更優的處理方式,尤爲是若是一個組內的成員很少的狀況,這種方式顯然掃描了不少無用的行,有的,語句以下:

explain update group_count_123 inner join group_member_123 using(group_id) set count=count-1 where person_id = 1;

讓咱們來看一下這個語句的輸出結果:

+----+-------------+--------------------+------------+--------+-------------------+-----------+---------+---------------------------------+------+----------+-------------+
| id | select_type | table              | partitions | type   | possible_keys     | key       | key_len | ref                             | rows | filtered | Extra       |
+----+-------------+--------------------+------------+--------+-------------------+-----------+---------+---------------------------------+------+----------+-------------+
|  1 | SIMPLE      | group_member_123   | NULL       | ref    | PRIMARY,person_id | person_id | 4       | const                           |    8 |   100.00 | Using index |
|  1 | UPDATE      | group_count_123    | NULL       | eq_ref | PRIMARY           | PRIMARY   | 3       | face_id.group_member.group_id   |    1 |   100.00 | NULL        |
+----+-------------+--------------------+------------+--------+-------------------+-----------+---------+---------------------------------+------+----------+-------------+
2 rows in set, 1 warning (0.01 sec)

這裏須要掃描的行數和組內的成員個數一致,應該是最優的語句,執行順序也發生了改變,先執行查找語句,而後使用主鍵索引,找到對應的行進行更新。在這個例子中,由於group_count_123的行數不會低於一個成員所在的組的個數,因此第二種寫法不會比第一種寫法差,並且大多數時間都是優於第一種寫法。這個語句,我是根據《高性能mysql》的6.5.9節(在同一個表上查詢和更新)中的update語句改寫來的。

5. 插入成員表中速度太慢。

我在使用十線程向成員表中插入數據的時候,發現插入速度很慢,每秒只有兩三百。對於這個問題,咱們須要作的是,查看單次插入須要的時間,以及單次插入花費在每一個階段的時間。根據測量,由於單次插入大約須要30ms,而時間主要在最後的同步階段(query end),因此這個問題能夠經過增長線程數來解決。固然,若是但願單線程插入速度顯著提高,就須要減小單次插入的時間,尤爲是最後的同步階段的時間。

6. 事務修改一個角色的全部文件ID和特徵,會出現大量的死鎖現象。

修改過程當中,我使用了刪除再從新插入的方式,這種方式有一個優勢,就算是新插入的某個角色的文件ID與以前的這個角色的文件ID存在重複的狀況,也不會有問題。在實際運行過程當中,會發現大量的死鎖回滾的錯誤,按照個人測試來講,若是使用20個線程以一樣的順序更新同一個組的全部成員的數據的話,回滾的數目和提交的數目基本一致。這個數據能夠經過查看show global status like 'Com_commit'; 和 show global status like 'Com_rollback';獲得。經過記錄show engine innodb status的數據,分析show engine innodb status的數據,或者經過pt-deadlock-logger檢測mysql消息能夠獲得。這個問題,能夠經過當返回值表示是死鎖回滾的話,那麼從新再進行幾回嘗試來解決。咱們試着先插入,而後獲得第一個插入的自增ID,而後刪除小於這個自增ID的特定用戶的fileID的方式,發現死鎖回滾的狀況有了必定程度的減小,可是更新速度更慢了,我嘗試了10線程到50線程的插入測試,發現先刪除後插入都會更快,雖然死鎖回滾的次數也更多。按照個人這個測試結果來看,至少說明,死鎖回滾並非很是致命的問題。我查看了官網對innodb鎖的介紹,仍是沒有找到能夠基本消除死鎖回滾的方式,仍是須要繼續努力。

相關文章
相關標籤/搜索