增強你的 sysbench - 給 TiDB 添加自定義測試

對於從事數據庫相關的同學來說,對數據庫進行性能測試是一個永遠繞不開的話題。這個世界上有很多的數據庫性能測試工具,而 sysbench 可以算是大家用的最多的之一。

根據官網的介紹,sysbench 是一個基於 Luajit 的,多線程的腳本化的性能測試工具,它不光能測試數據庫的性能,也可以測試文件 I/O,CPU 這些,不過這裏,我們重點來聊聊數據庫的測試。在 sysbench 裏面,對於數據庫的測試,用的最多的就是 OLTP 相關的腳本,譬如 oltp_point_selectoltp_update_index。通常,OLTP 的腳本在絕大多數時候都夠用了,但程序員就是這麼的不滿足,尤其是對我們這些開發數據庫的程序員來說,我們不光希望能測試 OLTP 相關的 workload,還希望能通過 sysbench 測試更多的 workload。

幸運的是,隨着 sysbench 1.0 的發佈,它開始支持自定義的腳本來進行 benchmark,那麼我們要做的事情就很簡單了,只需要幹兩件事情:

  1. 學習 Lua 這門腳本語言。
  2. 給 sysbench 寫自己的插件。

對於 Lua 語言,這裏不做過多討論,即使你不會,也不用特別擔心,它是一門非常容易學習的語言。這裏我們來談談如何給 sysbench 寫插件。

基本框架

這裏,我們來實現一個 bank transfer 的 benchmark,首先我們創建一個 bank_transfer.lua 文件,在文件頭寫上

#!/usr/bin/env sysbench

然後基本的框架:

function thread_init() 
    print(string.format("start thread %d", sysbench.tid))
end

function thread_done()
    print(string.format("stop thread %d", sysbench.tid))
end

function event() 
end

在上面的文件裏面,我們實現了三個函數,thread_initthread_done 非常的直觀,就是 sysbench 在測試線程啓動和結束的時候調用,而 event 則是我們實際執行測試的地方,這裏是空的。然後我們運行這個測試程序,你可以暫時忽略命令行裏面的 MySQL 參數,這個後面我們會實際連接到 MySQL 進行測試。

sysbench --report-interval=1 --time=20 --threads=4 --mysql-host=127.0.0.1 --mysql-port=4000 --mysql-user=root --mysql-db=sbtest --db-driver=mysql bank_transfer run

start thread 0
start thread 2
start thread 1
start thread 3
Threads started!

stop thread 0
stop thread 3
stop thread 1
stop thread 2

傳遞參數

上面例子裏面的參數其實是 sysbench 自己需要的參數,對於我們的測試程序來說,它也可能需要一些參數,所以我們在腳本里面定義自己的參數,如下我們定義了兩個參數,table-sizetables,用來告訴測試腳本,我們希望創建多少張表以及每張表裏面有多少數據。

sysbench.cmdline.options = {
    table_size =
        {"Number of rows per table", 10000},
    tables =
        {"Number of tables", 1}
}

function thread_init() 
    print(sysbench.opt.table_size)
end

啓動的時候我們就可以指定自己的參數了,如下:

sysbench bank_transfer run --tables=16 --table-size=1000000

連接數據庫

因爲我們是要對數據庫進行測試,所以首先我們需要跟數據庫建立起連接,在腳本里面寫上:

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

function thread_done()
    con:disconnect()
end

function event() 
    con:query("select 1")
end

上面我們在線程初始化的時候跟數據庫建立了連接,在結束的時候關閉了連接,而在 event 裏面,則是執行了 select 1 這個操作。重新執行腳本,我們會看到如下輸出:

Threads started!

[ 1s ] thds: 4 tps: 24940.85 qps: 24940.85 (r/w/o: 24940.85/0.00/0.00) lat (ms,95%): 0.27 err/s: 0.00 reconn/s: 0.00
[ 2s ] thds: 4 tps: 28102.97 qps: 28102.97 (r/w/o: 28102.97/0.00/0.00) lat (ms,95%): 0.24 err/s: 0.00 reconn/s: 0.00

Prepare 和 cleanup

在開始測試之前,我們首先要導入數據,在 sysbench 裏面,這個是通過 prepare 來完成的,首先我們定義好 prepare 函數:

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

    for i = sysbench.tid % sysbench.opt.threads + 1, sysbench.opt.tables, sysbench.opt
        .threads do 
        create_table(drv, con, i) 
    end
end

sysbench.cmdline.commands = {
    prepare = {cmd_prepare, sysbench.cmdline.PARALLEL_COMMAND}
}

上面我們定義了 cmd_prepare,並且告訴 sysbench 這個是 PARALLEL_COMMAND 類型,也就是 sysbench 會併發的調用 prepare。而在 cmd_prepare 裏面,我們也是讓每個線程負責給不同的 table 導入數據。

我們在 create_table 裏面創建表結構,以及使用 bulk_insert_* 相關的接口來導入數據:

function create_table(drv, con, table_num)
    print(string.format("Creating table 'account%d'...", table_num))

    local query = string.format([[
CREATE TABLE account%d(
  id INTEGER NOT NULL,
  balance INTEGER DEFAULT '1000' NOT NULL,
  PRIMARY KEY (id)
)]], table_num)

    con:query(query)

    if (sysbench.opt.table_size > 0) then
        print(string.format("Inserting %d records into 'account%d'",
                            sysbench.opt.table_size, table_num))
    end

    query = "INSERT INTO account" .. table_num .. "(id, balance) VALUES"

    con:bulk_insert_init(query)

    for i = 1, sysbench.opt.table_size do
        query = string.format("(%d, %d)", i, 1000)

        con:bulk_insert_next(query)
    end

    con:bulk_insert_done()
end

我們能 prepare,自然也會有對應的 cleanup:

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

    for i = 1, sysbench.opt.tables do
        print(string.format("Dropping table 'account%d'...", i))
        con:query("DROP TABLE IF EXISTS account" .. i)
    end
end

當定義好上面這些函數之後,我們就可以使用 sysbench bank_transfer prepare 以及 sysbench bank_transfer cleanup 來導入或者清理數據了。

Transfer

好了,現在到了最激動人心的時刻,我們開始寫真正的測試邏輯。我們這裏要模擬的是 transfer,代碼如下:

function event() 
    local from = get_id()
    local to = get_id()
    local table_num = get_table_num()
    local amount = sysbench.rand.default(1, 100)
    while(from == to)
    do
        to = get_id()
    end

    con:query("BEGIN")

    local rs = con:query(string.format([[
SELECT id, balance FROM account%d WHERE id IN (%d, %d) FOR UPDATE
]], table_num, from, to))

    assert(rs.nrows == 2)

    local row_from = rs:fetch_row()
    local row_to = rs:fetch_row()

    if row_from[1] ~= from then
        row_from, row_to = row_to, row_from
    end 

    if row_from[2] - amount < 0 then 
        con:query("ROLLBACK")
        return 
    end

    con:query(string.format([[
UPDATE account%d SET balance = balance - %d WHERE id = %d
]], table_num, amount, from))

    con:query(string.format([[
UPDATE account%d SET balance = balance + %d WHERE id = %d
]], table_num, amount, to))

    con:query("COMMIT") 
end

可以看到,邏輯還是非常簡單的,主要流程是:

  1. 開啓事務
  2. 隨機選擇兩個賬戶 from 和 to
  3. 查詢兩個賬戶的餘額
  4. 如果 from 的餘額不夠,轉賬失敗,回滾事務
  5. 執行轉賬操作,from 減去 amount,to 增加 amount
  6. 提交事務

運行測試,可以看到如下輸出:

[ 1s ] thds: 4 tps: 661.92 qps: 3325.56 (r/w/o: 665.91/1331.82/1327.83) lat (ms,95%): 10.09 err/s: 0.00 reconn/s: 0.00
[ 2s ] thds: 4 tps: 678.85 qps: 3394.23 (r/w/o: 678.85/1357.69/1357.69) lat (ms,95%): 7.98 err/s: 0.00 reconn/s: 0.00
[ 3s ] thds: 4 tps: 894.98 qps: 4465.85 (r/w/o: 891.96/1783.93/1789.95) lat (ms,95%): 5.18 err/s: 0.00 reconn/s: 0.00

總結

上面通過一個簡單的例子,告訴大家如何在 sysbench 裏面寫自己的測試,我個人認爲,作爲一個非常通用的測試框架,會有越來越多的開發者給 sysbench 添加新的測試用例,譬如 Percona 已經添加了 tpccblob,我自己後續也會嘗試在 sysbench 裏面加入更多的測試 case,來對 TiDB 進行各種的性能測試。

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