MySQL 效能壓測工具-sysbench,從入門到自定義測試項

iVictor發表於2022-12-06

sysbench是一個開源的、基於LuaJIT(LuaJIT 是 Lua 的即時編譯器,可將程式碼直接翻譯成機器碼,效能比原生 lua 要高) 的、可自定義指令碼的多執行緒基準測試工具,也是目前用得最多的 MySQL 效能壓測工具。

基於 sysbench,我們可以對比 MySQL 在不同版本、不同硬體配置、不同引數(作業系統和資料庫)下的效能差異。

下面會從 sysbench 的基本用法出發,逐漸延伸到 sysbench 的一些高階玩法,譬如如何閱讀自帶的測試指令碼、如何自定義測試項等。除此之外,使用 sysbench 對 CPU 進行測試,網上很多資料都語焉不詳,甚至是錯誤的,所以這次也會從原始碼的角度分析 CPU 測試的實現邏輯及 --cpu-max-prime 選項的具體含義。

本文主要包括以下幾部分:

  1. 安裝sysbench
  2. sysbench用法講解
  3. 對MySQL進行基準測試的基本步驟
  4. 如何分析MySQL基準測試結果
  5. 如何使用sysbench對伺服器進行測試
  6. MySQL常見測試場景及對應的 SQL 語句
  7. 如何自定義sysbench測試指令碼

 

安裝 sysbench

下面是 sysbench 原始碼包的安裝步驟。

# yum -y install make automake libtool pkgconfig libaio-devel openssl-devel mysql-devel
# cd /usr/src/
# wget https://github.com/akopytov/sysbench/archive/refs/tags/1.0.20.tar.gz
# tar xvf 1.0.20.tar.gz
# cd sysbench-1.0.20/
# ./autogen.sh
# ./configure
# make -j
# make install

安裝完成後,壓測指令碼預設會安裝在 /usr/local/share/sysbench 目錄下。

我們看看該目錄的內容。

# ls /usr/local/share/sysbench/
bulk_insert.lua  oltp_insert.lua        oltp_read_write.lua        oltp_write_only.lua       tests
oltp_common.lua  oltp_point_select.lua  oltp_update_index.lua      select_random_points.lua
oltp_delete.lua  oltp_read_only.lua     oltp_update_non_index.lua  select_random_ranges.lua

除了oltp_common.lua是個公共模組,其它每個 lua 指令碼都對應一個測試場景。

 

sysbench 用法講解

sysbench 命令語法如下:

sysbench [options]... [testname] [command]

命令中的testname是測試項名稱。sysbench 支援的測試項包括:

  • *.lua:資料庫效能基準測試。

  • fileio:磁碟 IO 基準測試。

  • cpu:CPU 效能基準測試。

  • memory:記憶體訪問基準測試。

  • threads:基於執行緒的排程程式基準測試。

  • mutex:POSIX 互斥量基準測試。

command是 sysbench 要執行的命令,支援的選項有:prepareprewarmruncleanuphelp。注意,不是所有的測試項都支援這些選項。

options是配置項。sysbench 中的配置項主要包括以下兩部分:

1.  通用配置項。這部分配置項可透過 sysbench --help 檢視。例如,

# sysbench --help
...
General options:
  --threads=N                     number of threads to use [1]
  --events=N                      limit for total number of events [0]
  --time=N                        limit for total execution time in seconds [10]
 ...

2.  測試項相關的配置項。各個測試項支援的配置項可透過 sysbench testname help 檢視。例如,

# sysbench memory help
sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)

memory options:
  --memory-block-size=SIZE    size of memory block for test [1K]
  --memory-total-size=SIZE    total size of data to transfer [100G]
  --memory-scope=STRING       memory access scope {global,local} [global]
  --memory-hugetlb[=on|off]   allocate memory from HugeTLB pool [off]
  --memory-oper=STRING        type of memory operations {read, write, none} [write]
  --memory-access-mode=STRING memory access mode {seq,rnd} [seq]

 

對 MySQL 進行基準測試的基本步驟

下面以oltp_read_write為例,看看使用 sysbench 對 MySQL 進行基準測試的四個標準步驟:

prepare

生成壓測資料。預設情況下,sysbench 是透過 INSERT INTO 命令來匯入測試資料的。如果是使用 LOAD DATA LOCAL INFILE 命令來匯入,sysbench 導數速度能提升30%,具體可參考:使用 LOAD DATA LOCAL INFILE,sysbench 導數速度提升30%

# sysbench oltp_read_write --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=30 prepare

命令中各個選項的具體含義如下:

  • oltp_read_write:測試項,對應的是/usr/local/share/sysbench/oltp_read_write.lua。這裡也可指定指令碼的絕對路徑名。
  • --mysql-host、--mysql-port、--mysql-user、--mysql-password:分別代表 MySQL 例項的主機名、埠、使用者名稱和密碼。
  • --mysql-db:庫名。不指定則預設為sbtest
  • --tables :表的數量,預設為 1。
  • --table-size :單表的大小,預設為 10000。
  • --threads :併發執行緒數,預設為 1。注意,匯入時,單表只能使用一個執行緒。
  • prepare:執行準備工作。

oltp_read_write 用來壓測 OLTP 場景。在 sysbench 1.0 之前, 該場景是透過 oltp.lua 這個指令碼來測試的。不過該指令碼在 sysbench 1.0 之後就被廢棄了,但為了跟之前的版本相容,該指令碼放到了 /usr/local/share/sysbench/tests/include/oltp_legacy/ 目錄下。

鑑於 oltp_read_write.lua 和 oltp.lua 兩者的壓測內容完全一致。從 sysbench 1.0 開始,壓測 OLTP 場景建議直接使用 oltp_read_write。

prewarm

預熱。主要是將磁碟中的資料載入到記憶體中。

# sysbench oltp_read_write --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=30 prewarm

除了需要將命令設定為 prewarm,其它配置與 prepare 中一樣。

run

壓測。

# sysbench oltp_read_write --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run

其中,

  • --time :壓測時間。不指定則預設為 10 秒。除了 --time,也可透過 --events 限制需要執行的 event 的數量。

  • --report-interval=10 :每 10 秒輸出一次測試結果,預設為 0,不輸出。

cleanup

清理資料。

# sysbench oltp_read_write --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 cleanup

這裡只需指定 --tables ,sysbench 會序列執行 DROP TABLE IF EXISTS sbtest 操作。

 

如何分析 MySQL 基準測試結果

下面我們分析下 oltp_read_write 場景下的壓測結果。注:右滑可以看到每個指標的具體含義。

Threads started!

[ 10s ] thds: 64 tps: 5028.08 qps: 100641.26 (r/w/o: 70457.59/20121.51/10062.16) lat (ms,95%): 17.32 err/s: 0.00 reconn/s: 0.00
# thds 是併發執行緒數。tps 是每秒事務數。qps 是每秒運算元,等於 r(讀操作)加上 w(寫操作)加上 o(其他操作,主要包括 BEGIN 和 COMMIT)。lat 是延遲,(ms,95%) 是 95% 的查詢時間小於或等於該值,單位毫秒。err/s 是每秒錯誤數。reconn/s 是每秒重試的次數。
[ 20s ] thds: 64 tps: 5108.93 qps: 102192.09 (r/w/o: 71533.28/20440.64/10218.17) lat (ms,95%): 17.32 err/s: 0.00 reconn/s: 0.00
[ 30s ] thds: 64 tps: 5126.50 qps: 102505.50 (r/w/o: 71756.30/20496.60/10252.60) lat (ms,95%): 17.32 err/s: 0.00 reconn/s: 0.00
[ 40s ] thds: 64 tps: 5144.50 qps: 102907.20 (r/w/o: 72034.07/20583.72/10289.41) lat (ms,95%): 17.01 err/s: 0.00 reconn/s: 0.00
[ 50s ] thds: 64 tps: 5137.29 qps: 102739.80 (r/w/o: 71916.99/20548.64/10274.17) lat (ms,95%): 17.01 err/s: 0.00 reconn/s: 0.00
[ 60s ] thds: 64 tps: 4995.38 qps: 99896.35 (r/w/o: 69925.98/19979.61/9990.75) lat (ms,95%): 17.95 err/s: 0.00 reconn/s: 0.00
SQL statistics:
    queries performed:
        read:                            4276622 # 讀操作的數量
        write:                           1221892 # 寫操作的數量
        other:                           610946  # 其它操作的數量
        total:                           6109460 # 總的運算元量,total = read + write + other
    transactions:                        305473 (5088.63 per sec.)    # 總的事務數(每秒事務數)
    queries:                             6109460 (101772.64 per sec.) # 總的運算元(每秒運算元)
    ignored errors:                      0      (0.00 per sec.)       # 忽略的錯誤數(每秒忽略的錯誤數)
    reconnects:                          0      (0.00 per sec.)       # 重試次數(每秒重試的次數)

General statistics:
    total time:                          60.0301s  # 總的執行時間
    total number of events:              305473    # 執行的 event 的數量
                                                   # 在 oltp_read_write 中,預設引數下,一個 event 其實就是一個事務

Latency (ms):
         min:                                    5.81 # 最小耗時
         avg:                                   12.57 # 平均耗時
         max:                                  228.87 # 最大耗時
         95th percentile:                       17.32 # 95% event 的執行耗時
         sum:                              3840044.28 # 總耗時

Threads fairness:
    events (avg/stddev):           4773.0156/30.77  # 平均每個執行緒執行 event 的數量
                                                    # stddev 是標準差,值越小,代表結果越穩定。
    execution time (avg/stddev):   60.0007/0.01     # 平均每個執行緒的執行時間

輸出中,重點關注三個指標:

  1. 每秒事務數,即我們常說的 TPS。
  2. 每秒運算元,即我們常說的 QPS。
  3. 95% event 的執行耗時。

TPS 和 QPS 反映了系統的吞吐量,越大越好。執行耗時代表了事務的執行時長,越小越好。在一定範圍內,併發執行緒數指定得越大,TPS 和 QPS 也會越高。

 

使用 sysbench 對伺服器進行測試

除了資料庫基準測試,sysbench 還能對伺服器的效能進行測試。伺服器資源一般包括四大類:CPU、記憶體、IO和網路。sysbench 可對CPU、記憶體和磁碟IO進行測試。下面我們具體來看看。

cpu

CPU 效能測試。支援的選項只有一個,即--cpu-max-prime

CPU 測試的命令如下:

# sysbench cpu --cpu-max-prime=20000 --threads=32 run

輸出中,重點關注events per second。值越大,代表 CPU 的計算效能越強。

CPU speed:
    events per second: 25058.08

下面是 CPU 測試相關的程式碼,可以看到,sysbench 是透過計算--cpu-max-prime範圍內的質數來衡量 CPU 的計算能力的。

質數(prime number)又稱素數,指的是大於 1,且只能被 1 和自身整除的自然數。在程式碼實現時,對於自然數 n,一般會用 2 到根號 n 之間的整數去除,如果都無法整除,則意味著 n 是個質數。

int cpu_execute_event(sb_event_t *r, int thread_id)
{
  unsigned long long c;
  unsigned long long l;
  double t;
  unsigned long long n=0;

  (void)thread_id; /* unused */
  (void)r; /* unused */

  // max_prime 即命令列中指定的 --cpu-max-prime
  for(c=3; c < max_prime; c++)
  {
    t = sqrt((double)c);
    for(l = 2; l <= t; l++)
      if (c % l == 0)
        break;
    if (l > t )
      n++;
  }

  return 0;
}

memory

記憶體測試,支援的選項有:

  • --memory-block-size:記憶體塊的大小,預設為 1KB。測試時建議設定為 1MB。
  • --memory-total-size:要傳輸的資料的總大小。預設為 100GB。
  • --memory-scope:記憶體訪問範圍,可指定 global、local,預設為 global。
  • --memory-hugetlb:是否從 HugeTLB 池中分配記憶體,預設為 off。
  • --memory-oper:記憶體操作型別,可指定 read、write、none,預設為 write。
  • --memory-access-mode:記憶體訪問模式,可指定 seq(順序訪問)、rnd(隨機訪問),預設為 seq。

記憶體測試的命令如下:

# sysbench --test=memory --memory-block-size=1M --memory-total-size=100G --num-threads=1 run

輸出中,重點關注以下部分:

102400.00 MiB transferred (23335.96 MiB/sec)

23335.96 MiB/sec 即資料在記憶體中的順序寫入速率。


fileio

磁碟 IO 測試。支援的選項有:

  • --file-num:需要建立的檔案數,預設為128。
  • --file-block-size:資料塊的大小,預設為16384,即16KB。
  • --file-total-size:需要建立的檔案總大小,預設為2GB。
  • --file-test-mode:測試模式,可指定 seqwr(順序寫)、seqrewr(順序重寫)、seqrd(順序讀)、rndrd(隨機讀)、rndwr(隨機寫)、rndrw(隨機讀寫)。
  • --file-io-mode:檔案的操作模式,可指定 sync(同步 IO)、async(非同步 IO)、mmap,預設為 sync。
  • --file-async-backlog:每個執行緒非同步 IO 佇列的長度,預設為 128。
  • --file-extra-flags:開啟檔案時指定的標誌,可指定 sync、dsync、direct,預設為空,沒指定。
  • --file-fsync-freq:指定持久化操作的頻率,預設為 100,即每執行 100 個 IO 請求,則會進行一次持久化操作。
  • --file-fsync-all:在每次寫入操作後執行持久化操作,預設為 off。
  • --file-fsync-end:在測試結束時執行持久化操作,預設為 on。
  • --file-fsync-mode:持久化操作的模式,可指定 fsync、fdatasync,預設為 fsync。fdatasync 和 fsync類似,只不過 fdatasync 只會更新資料,而 fsync 還會同步更新檔案的屬性。
  • --file-merged-requests:允許合併的最多 IO 請求數,預設為0,不合並。
  • --file-rw-ratio:混合測試中的讀寫比例,預設為1.5。

磁碟 IO 測試主要分為以下三步:

# 準備測試檔案
# sysbench fileio --file-num=1 --file-total-size=10G --file-test-mode=rndrw prepare

# 測試
# sysbench fileio --file-num=1 --file-total-size=10G --file-test-mode=rndrw run

# 刪除測試檔案
# sysbench fileio --file-num=1 --file-total-size=10G --file-test-mode=rndrw cleanup

輸出中,重點關注以下兩部分:

File operations:
    reads/s:                      4978.26
    writes/s:                     3318.84
    fsyncs/s:                     83.07

Throughput:
    read, MiB/s:                  77.79
    written, MiB/s:               51.86

其中,reads/s 加上 writes/s 即我們常說的 IOPS。read, MiB/s 加上 written, MiB/s 即我們常說的吞吐量。

 

MySQL 常見測試場景及對應的 SQL 語句

接下來會列舉 MySQL 常見的測試場景及各個場景對應的 SQL 語句。

為了讓大家清晰的知道 SQL 語句的含義,首先我們看看測試表的表結構。

除了 bulk_insert 會建立單獨的測試表,其它場景都會使用下面的表結構。

mysql> show create table sbtest.sbtest1\G
*************************** 1. row ***************************
       Table: sbtest1
Create Table: CREATE TABLE `sbtest1` (
  `id` int NOT NULL AUTO_INCREMENT,
  `k` int NOT NULL DEFAULT '0',
  `c` char(120) NOT NULL DEFAULT '',
  `pad` char(60) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `k_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

bulk_insert

批次插入測試。

# sysbench bulk_insert --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run

下面是 bulk_insert 場景下建立的測試表。

mysql> show create table sbtest.sbtest1\G
*************************** 1. row ***************************
       Table: sbtest1
Create Table: CREATE TABLE `sbtest1` (
  `id` int NOT NULL,
  `k` int NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.01 sec)

測試對應的 SQL 語句如下:

INSERT INTO sbtest1 VALUES(?, ?),(?, ?),(?, ?),(?, ?)...

oltp_delete

刪除測試。

# sysbench oltp_delete --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run

基於主鍵進行刪除。測試對應的 SQL 語句如下:

DELETE FROM sbtest1 WHERE id=?

oltp_insert

插入測試。

# sysbench oltp_insert --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run

測試對應的 SQL 語句如下:

INSERT INTO sbtest1 (id, k, c, pad) VALUES (?, ?, ?, ?)

oltp_point_select

基於主鍵進行查詢。

# sysbench oltp_point_select --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run

測試對應的 SQL 語句如下:

SELECT c FROM sbtest1 WHERE id=?

oltp_read_only

只讀測試。

# sysbench oltp_read_only --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run

測試對應的 SQL 語句如下:

SELECT c FROM sbtest1 WHERE id=? # 預設會執行 10 次,由 --point_selects 選項控制。
SELECT c FROM sbtest1 WHERE id BETWEEN ? AND ?
SELECT SUM(k) FROM sbtest1 WHERE id BETWEEN ? AND ?
SELECT c FROM sbtest1 WHERE id BETWEEN ? AND ? ORDER BY c
SELECT DISTINCT c FROM sbtest1 WHERE id BETWEEN ? AND ? ORDER BY c

oltp_read_write

讀寫測試。

測試對應的 SQL 語句如下:

SELECT c FROM sbtest1 WHERE id=? # 預設會執行 10 次,由 --point_selects 選項控制。
SELECT c FROM sbtest1 WHERE id BETWEEN ? AND ?
SELECT SUM(k) FROM sbtest1 WHERE id BETWEEN ? AND ?
SELECT c FROM sbtest1 WHERE id BETWEEN ? AND ? ORDER BY c
SELECT DISTINCT c FROM sbtest1 WHERE id BETWEEN ? AND ? ORDER BY c
UPDATE sbtest1 SET k=k+1 WHERE id=?
UPDATE sbtest1 SET c=? WHERE id=?
DELETE FROM sbtest1 WHERE id=?
INSERT INTO sbtest1 (id, k, c, pad) VALUES (?, ?, ?, ?)

oltp_update_index

基於主鍵進行更新,更新的是索引欄位。

# sysbench oltp_update_index --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run

測試對應的 SQL 語句如下:

UPDATE sbtest1 SET k=k+1 WHERE id=?

oltp_update_non_index

基於主鍵進行更新,更新的是非索引欄位。

# sysbench oltp_update_non_index --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run

測試對應的 SQL 語句如下:

UPDATE sbtest1 SET c=? WHERE id=?

oltp_write_only

只寫測試。

# sysbench oltp_write_only --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run

測試對應的 SQL 語句如下:

UPDATE sbtest1 SET k=k+1 WHERE id=?
UPDATE sbtest1 SET c=? WHERE id=?
DELETE FROM sbtest1 WHERE id=?
INSERT INTO sbtest1 (id, k, c, pad) VALUES (?, ?, ?, ?)

select_random_points

基於索引進行隨機查詢。

# sysbench select_random_points --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run

測試對應的 SQL 語句如下:

SELECT id, k, c, pad
          FROM sbtest1
          WHERE k IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

select_random_ranges

基於索引進行隨機範圍查詢。

# sysbench select_random_ranges --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run

測試對應的 SQL 語句如下:

SELECT count(k)
          FROM sbtest1
          WHERE k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ?

 

如何自定義 sysbench 測試指令碼

下面透過 bulk_insert.lua 和 oltp_point_select.lua 這兩個指令碼分析下 sysbench 測試指令碼的實現邏輯。

首先看看 bulk_insert.lua。

# cat bulk_insert.lua
#!/usr/bin/env sysbench

cursize=0
function thread_init()
   drv = sysbench.sql.driver()
   con = drv:connect()
end

function prepare()
   local i

   local drv = sysbench.sql.driver()
   local con = drv:connect()

   for i = 1, sysbench.opt.threads do
      print("Creating table 'sbtest" .. i .. "'...")
      con:query(string.format([[
        CREATE TABLE IF NOT EXISTS sbtest%d (
          id INTEGER NOT NULL,
          k INTEGER DEFAULT '0' NOT NULL,
          PRIMARY KEY (id))]], i))
   end
end

function event()
   if (cursize == 0) then
      con:bulk_insert_init("INSERT INTO sbtest" .. thread_id+1 .. " VALUES")
   end

   cursize = cursize + 1

   con:bulk_insert_next("(" .. cursize .. "," .. cursize .. ")")
end

function thread_done(thread_9d)
   con:bulk_insert_done()
   con:disconnect()
end

function cleanup()
   local i

   local drv = sysbench.sql.driver()
   local con = drv:connect()

   for i = 1, sysbench.opt.threads do
      print("Dropping table 'sbtest" .. i .. "'...")
      con:query("DROP TABLE IF EXISTS sbtest" .. i )
   end
end

下面,我們看看這幾個函式的具體作用:

  • thread_init():執行緒初始化時呼叫。這個函式常用來建立資料庫連線。
  • prepare():指定 prepare 時呼叫。這個函式常用來建立測試表,生成測試資料。
  • event():指定 run 時呼叫。這個函式會定義需要測試的 SQL 語句。
  • thread_done():執行緒退出時呼叫。這個函式常用來關閉 Prepared Statements 和資料庫連線。
  • cleanup():指定 cleanup 時呼叫。這個函式常用來刪除測試表。

如果我們要自定義測試指令碼,只需實現這幾個函式即可。

如果我們要基於 sbtest 表自定義測試項,就要分析 oltp*.lua 指令碼的實現邏輯。

下面,以 oltp_point_select.lua 指令碼為例。

#!/usr/bin/env sysbench
...
require("oltp_common")

function prepare_statements()
   -- point_selects 是 oltp_point_select 中支援的選項,預設為 10,這裡調整為了 1。
   sysbench.opt.point_selects=1

   prepare_point_selects()
end

function event()
   execute_point_selects()
end

與 bulk_insert.lua 不一樣的是,oltp_point_select.lua 只簡單的定義了兩個函式:prepare_statements()event()。實際上,不僅僅是 oltp_point_select.lua,其它 oltp*.lua 指令碼也只定義了這兩個函式。

雖然只定義了這兩個函式,但指令碼匯入了 oltp_common 模組,所以實際上,指令碼中的 prepare_point_selects(),execute_point_selects() 以及 bulk_insert.lua 中的 thread_init(),prepare(),thread_done(),cleanup() 都是在oltp_common.lua這個公共模組中定義的。

接下來,我們看看 prepare_point_selects() 和 execute_point_selects() 這兩個函式的實現邏輯。

首先看看prepare_point_selects()

它呼叫的是prepare_for_each_table()。prepare_for_each_table()是一個基礎函式。所有prepare 相關的函式都會呼叫prepare_for_each_table(), 只不過不同的 prepare 函式會傳入不同的引數名。

prepare_for_each_table()會填充兩張表(Lua 中的表既可用來表示陣列,也可用來表示集合):stmt 和 param。其中,stmt 用來儲存 Prepared Statements 語句,param 用來儲存 Prepared Statements 語句相關的引數型別。

填充完畢後,最後再透過 bind_param 函式將兩者繫結在一起。

可以看到,無論是 Prepared Statements 語句還是相關的引數型別,都是在 stmt_defs 定義的。

function prepare_point_selects()
   prepare_for_each_table("point_selects")
end

function prepare_for_each_table(key)
   for t = 1, sysbench.opt.tables do
      -- t 是表的序號,key 是測試項的名字
      stmt[t][key] = con:prepare(string.format(stmt_defs[key][1], t))
    
      local nparam = #stmt_defs[key] - 1

      if nparam > 0 then
         param[t][key] = {}
      end

      for p = 1, nparam do
         local btype = stmt_defs[key][p+1]
         local len

         if type(btype) == "table" then
            len = btype[2]
            btype = btype[1]
         end
         if btype == sysbench.sql.type.VARCHAR or
            btype == sysbench.sql.type.CHAR then
               param[t][key][p] = stmt[t][key]:bind_create(btype, len)
         else
            param[t][key][p] = stmt[t][key]:bind_create(btype)
         end
      end

      if nparam > 0 then
         stmt[t][key]:bind_param(unpack(param[t][key]))
      end
   end
end

接下來,我們看看 stmt_defs 的內容。

local stmt_defs = {
   point_selects = {
      "SELECT c FROM sbtest%u WHERE id=?",
      t.INT},
   simple_ranges = {
      "SELECT c FROM sbtest%u WHERE id BETWEEN ? AND ?",
      t.INT, t.INT},
   sum_ranges = {
      "SELECT SUM(k) FROM sbtest%u WHERE id BETWEEN ? AND ?",
       t.INT, t.INT},
   order_ranges = {
      "SELECT c FROM sbtest%u WHERE id BETWEEN ? AND ? ORDER BY c",
       t.INT, t.INT},
   distinct_ranges = {
      "SELECT DISTINCT c FROM sbtest%u WHERE id BETWEEN ? AND ? ORDER BY c",
      t.INT, t.INT},
   index_updates = {
      "UPDATE sbtest%u SET k=k+1 WHERE id=?",
      t.INT},
   non_index_updates = {
      "UPDATE sbtest%u SET c=? WHERE id=?",
      {t.CHAR, 120}, t.INT},
   deletes = {
      "DELETE FROM sbtest%u WHERE id=?",
      t.INT},
   inserts = {
      "INSERT INTO sbtest%u (id, k, c, pad) VALUES (?, ?, ?, ?)",
      t.INT, t.INT, {t.CHAR, 120}, {t.CHAR, 60}},
}

可以看到,stmt_defs 是一張表,裡面定義了不同測試項對應的 Prepared Statements 語句和引數型別。

具體到 point_selects 這個測試項,它對應的 Prepared Statements 語句是SELECT c FROM sbtest%u WHERE id=?,對應的引數型別是t.INT

梳理完 prepare_point_selects() 函式的實現邏輯。最後我們看看execute_point_selects()函式的實現邏輯。

function execute_point_selects()
   local tnum = get_table_num()
   local i
   -- point_selects 對應命令列中的 --point_selects 選項,預設為 10。
   for i = 1, sysbench.opt.point_selects do
      param[tnum].point_selects[1]:set(get_id())

      stmt[tnum].point_selects:execute()
   end
end

邏輯也非常簡單,先賦值,最後執行。

所以如果我們要基於 sbtest 表自定義測試項,最關鍵的一步其實就是在 stmt_defs 中定義 Prepared Statements 語句和相關的引數型別。至於 prepare_xxx 和 execute_xxx 函式,實現起來都非常簡單。

 

總結

1. 基準測試一般會關注三個指標:TPS/QPS、響應耗時和併發量。

2. 只有進行全鏈路壓測,我們才知道系統的瓶頸在哪裡。不能想當然的以為,資料庫不容易橫向擴充套件,系統瓶頸就一定會出在資料庫層。事實上,很多系統在設計之初就引入了快取,而快取會分擔很大一部分讀流量,這種架構下的資料庫壓力其實並不大。

3. 不能簡單的將 sysbench 的測試結果(TPS/QPS) 作為業務系統的吞吐量指標,因為兩者的業務模型並不一致。

4. 如果要自定義測試指令碼,實現的方式有兩種:

  • 自己實現測試相關的所有函式,具體實現細節可參考 bulk_insert.lua。
  • 基於 sbtest 表自定義測試項。實現過程中最關鍵的一步是在 stmt_defs 中定義 Prepared Statements 語句和相關的引數型別。

相關文章