freeeの開発情報ポータルサイト

Sysbench でカスタム負荷試験を書く

こんにちは、Database Reliability Engineer (DBRE) チームの寺沢です。

業務柄、負荷試験をする機会がちょこちょこあるのですが、sysbench のカスタムテストを書くのが意外に簡単であること、かつ想像よりも色々できて便利だったので、まだ使ってない方に向けて共有しようと思います。

Sysbench の使い方

前置きとして、sysbench の基本的な使い方をおさらいします。 https://github.com/akopytov/sysbench#general-syntax

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

command には prepare/run/cleanup のいずれかを指定し、それぞれ「テスト環境セットアップ」「テスト本体の実行」「テスト環境の掃除」向けの処理が実行されます。 testname には sysbench の Builtin Test Suit Name、もしくは自分で用意した Lua script path を指定します。Builtin Test Suite は、真っ先に想起されるであろう MySQL/Postgres 向けの oltp_*.lua を始めとし、他にも fileio/cpu/memory/thread/mutex など、多様な workload 向けの Test Suite が用意されています。

Run Command で実際に負荷試験を実行すると、以下のように試験結果のサマリーを出力してくれます。

SQL statistics:

  queries performed:
    read:                            25962
    write:                           0
    other:                           0
    total:                           25962
    transactions:                        25962  (2596.10 per sec.)
    queries:                             25962  (2596.10 per sec.)
    ignored errors:                      0      (0.00 per sec.)
    reconnects:                          0      (0.00 per sec.)

  Throughput:
    events/s (eps):                      2596.0960
    time elapsed:                        10.0004s
    total number of events:              25962

  Latency (ms):
    min:                                    0.28
    avg:                                    0.38
    max:                                   53.71
    95th percentile:                        0.42
    sum:                                 9965.88

  Threads fairness:
    events (avg/stddev):           25962.0000/0.00
    execution time (avg/stddev):   9.9659/0.00

option には、掛ける負荷の設定や、Test Suite の実行に必要な parameter を渡します。 例えば Load Generator となる thread の数や、DB の接続情報などが該当します。 Test Suite と option の指定を変えることで、任意の負荷試験を行えるというツールが sysbench です。

Sysbench のデザイン

sysbench は C で書かれたツールですが、テスト内容や出力に関するロジックは Lua (厳密には oltp_*.lua 以外の Builtin Test Suite は C)で書かれています。Lua で実装されたテストロジックからは FFI (Foreign Function Interface) を利用することで、DB Driver 向けのロジックなど、C で実装された sysbench の共通機構を利用することが可能になっています。oltp_*.lua 系の Builtin Test Suite はこの方式で用意されているので、実装を読むことで、自分で何を上書きできるのかイメージしやすいと思います。

-- https://github.com/akopytov/sysbench/blob/1.0.20/src/lua/internal/sysbench.sql.lua#L192-L198
function driver_methods.connect(self)
   local con = ffi.C.db_connection_create(self)
   if con == nil then
      error("connection creation failed", 2)
   end
   return ffi.gc(con, ffi.C.db_connection_free)
end

この仕組みのおかげで、DB Driver 実装や、並列実行などのロジックは sysbench が用意してくれたものに乗っかった上で、テスト内容だけを自由に Lua で記述することができます。

上書きする function について

ここから、実際に Test Scenario を用意するために、具体的に上書きすることになるであろう Lua function を oltp_*.lua での例を参考に説明します。 参照するソースコードは sysbench v1.0.20 を利用します。

Prepare Command

Prepare Command は単純で、DB connection を open し、Create Table を打ち込み、Bulk Insert を行っています。この実装は oltp_common.lua というファイルに実装されています。oltp_common.luaoltp_point_select.lua 等の oltp_*.lua ファイルから include されているので、testname に include 先の oltp_point_select.lua 等を指定して Prepare Command を打つと、デフォルトの処理が実行されます。

-- https://github.com/akopytov/sysbench/blob/1.0.20/src/lua/oltp_point_select.lua
require("oltp_common")

(snip)

デフォルトでは sysbench が用意した DB Schema において負荷試験が実行されます。

-- https://github.com/akopytov/sysbench/blob/1.0.20/src/lua/oltp_common.lua#L81-L89
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

-- https://github.com/akopytov/sysbench/blob/1.0.20/src/lua/oltp_common.lua#L150-L241
function create_table(drv, con, table_num)
    (snip)

    query = string.format([[CREATE TABLE sbtest%d(
        id %s,
        k INTEGER DEFAULT '0' NOT NULL,
        c CHAR(120) DEFAULT '' NOT NULL,
        pad CHAR(60) DEFAULT '' NOT NULL,
        %s (id)) %s %s]],
        table_num,
        id_def, id_index_def, engine_def, extra_table_options)

    con:query(query)

    (snip)

    con:bulk_insert_init(query)

    local c_val
    local pad_val
    for i = 1, sysbench.opt.table_size do
        c_val = get_c_value()
        pad_val = get_pad_value()
        query = string.format("(%d, '%s', '%s')",
                              sb_rand(1, sysbench.opt.table_size),
                              c_val,
                              pad_val)
        con:bulk_insert_next(query)
    end
    con:bulk_insert_done()

    (snip)
end

例えば任意の DB Schema に対して負荷試験を行いたい場合があると思います。そんな時、任意の Lua script を用意し、oltp_common.lua を include してから create_table function を上書きすることで実現できます。

-- mytest.lua
#!/usr/bin/env sysbench

require("oltp_common")

function create_table(drv, con, table_num)
    (snip)

    -- select 処理もこの Schema 用に変更する必要があるのに注意
    query = string.format([[CREATE TABLE sbtest%d(
        id CHAR(10) NOT NULL,
        c CHAR(120) DEFAULT '' NOT NULL,
        pad CHAR(60) DEFAULT '' NOT NULL,
        %s (id)) %s %s]],
        table_num,
        id_index_def, engine_def, extra_table_options)

    con:query(query)

    (snip)
end

Run Command

続いて Run Command の説明です。実際に workload を実行するのはこの部分なので、おそらく最も上書きしたくなる部分だと思います。

Run Command が実行されると、main thread から option で指定した任意数の worker threads が作られ、worker thread 上で Test コードを実行します。 worker thread は大まかに thread_init function で初期設定を行い、thread_run function で loop をしながら負荷を掛ける処理(event function)を実行します。 一定時間の経過、もしくは一定数の event 処理を終えたことを契機とし、thread_run function の loop は終了し、thread_done function で後始末を行い、worker thread は終了します。main thread は worker thread の終了を待ち、最後にテスト結果の統計情報を report して終了します。

oltp_*.lua においては、thread_init function で DB connection を open し、Prepared Statement の準備などを行います。 thread_run function では loop しながら event function の中でテストしたい workload を実行します。latency などの計測はこの event function ごとに行われるので、計測したいクエリを指定して投げ込むといった形で上書きを行います。 最後に thread_done function で DB connection を close するといった具合です。

上記の図で緑色になっている function がありますが、これらは上書き可能であることを示しています。 Builtin Test Suite の event function の中身を見てみましょう。Prepared statement を実行するだけのとても単純な実装になっていることがわかると思います。

--  https://github.com/akopytov/sysbench/blob/1.0.20/src/lua/oltp_point_select.lua#L32-L34
function event()
    execute_point_selects()
end

-- https://github.com/akopytov/sysbench/blob/1.0.20/src/lua/oltp_common.lua#L412-L421
function execute_point_selects()
   local tnum = get_table_num()
   local i
   for i = 1, sysbench.opt.point_selects do
       param[tnum].point_selects[1]:set(get_id())
       stmt[tnum].point_selects:execute()
   end
end

-- https://github.com/akopytov/sysbench/blob/1.0.20/src/lua/oltp_common.lua#L245-L247
local stmt_defs = {
   point_selects = {
      "SELECT c FROM sbtest%u WHERE id=?",
      t.INT},

    (snip)
}

-- https://github.com/akopytov/sysbench/blob/1.0.20/src/lua/oltp_common.lua#L412-L421
function execute_point_selects()
   local tnum = get_table_num()
   local i

   for i = 1, sysbench.opt.point_selects do
      param[tnum].point_selects[1]:set(get_id())

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

先程の図において、緑色になっている function は上書き可能と説明しましたが、実際の所、oltp_*.lua において local function として定義されていなければ、どの関数も上書き可能です。そのため、execute_point_selects functionSELECT 1 するだけというロジックに書き換えると、SELECT 1 クエリの負荷をテストすることができます。

-- mytest.lua
#!/usr/bin/env sysbench

function event()
  execute_point_selects()
end

function execute_point_selects()
   con:query("SELECT 1")
end

また、report の出力方式も自分でカスタマイズ可能です。 例えば、試験結果を SpreadSheet などにコピペしていく際に、CSV 形式にしたいケースがあると思います。 sysbench にはテスト終了時のみならず、interval ごとに統計情報を出力する --report-interval option があります。 こちらの interval report 向けには CSV Format, JSON Format 等の report function が用意されています。 これらの report function の実装を参考にすることで、テスト終了時の report 出力形式も任意形式に変更できます。

-- mytest.lua
function sysbench.hooks.report_cumulative(stat)
    print(string.format("%s,%s,%s,%s," ..
                        "%s,%s," ..
                        "%s,%s," ..
                        "%s,%s," ..
                        "%s,%s," ..
                        "%s,%s,%s,%2dth_%s,%s,",
                    "reads", "writes", "other", "queries",
                    "events", "events/sec",
                    "queries", "queries/sec",
                    "errors", "errors/sec",
                    "reconnects", "reconnects/sec",
                    "latency_min", "latency_avg", "latency_max", sysbench.opt.percentile, "latency_pct", "latency_sum"
    ))

    local queries = stat.reads + stat.writes + stat.other
    local seconds = stat.time_total
    print(string.format("%u,%u,%u,%u," ..
                        "%u,%.2f," ..
                        "%u,%.2f," ..
                        "%u,%.2f," ..
                        "%u,%.2f," ..
                        "%.2f,%.2f,%.2f,%.2f,%.2f",
                    stat.reads, stat.writes, stat.other, queries,
                    stat.events, stat.events / seconds,
                    queries, queries / seconds,
                    stat.errors, stat.errors / seconds,
                    stat.reconnects, stat.reconnects / seconds,
                    stat.latency_min, stat.latency_avg, stat.latency_max, stat.latency_pct, stat.latency_sum
    ))
end

function event()

  (snip)

end

Cleanup Command

Cleanup Command は単純で、DB connection を open し、Drop Table を打ち込みます。この実装も Prepare Command と同様に oltp_common.lua に実装されています。Prepare Command の部分で説明したとおり、oltp_common.luaoltp_point_select.lua 等の oltp_*.lua ファイルから include されているので、それら include 先の Test Suite を指定して Cleanup Command を打つことで実行されます。

Run Command Option

ちなみに Run Command は多様な option をサポートしており、option 有無によって実行の仕組みが切り替わったりします。 例えば、txn rate を固定する rate option を指定した場合は、先程共有した main/worker thread のみの構成とは異なり、少し複雑な構成で実行されます。

流量を固定するために queue としての ring buffer とそこに event を詰める eventgen thread が追加されます。 eventgen thread は流量を一定にするために sleep をはさみながら queue に event を詰めていきます。ring buffer がいっぱいになったら、queue が空くまで条件変数を使って wait します。 worker thread 側は queue から event を取り出して event function を実行していきます。queue が空になると worker thread は条件変数を使って signal を送り、眠っている eventgen thread を起こして event を詰めさせます。

他にも一定 interval ごとに report を出力させる option を有効化すると、report 報告用の thread が吐き出してくれる option もあり、その場合 report 用の thread が生えてきます。 こういった thread 周りの実装は sysbench.c に集まっています。

Lua 周り

Third Party Module

sysbench のカスタムテスト上でも LuaRocks を利用することで、3rd party library が利用可能です。

# example on ubuntu
$ sudo apt -y install libmysqlclient-dev libssl-dev sysbench luarocks
$ sudo luarocks install luasocket

install した library を require することで利用可能になります。

-- mytest.lua
#!/usr/bin/env sysbench

require("oltp_common")
local socket = require ("socket")

function event()

    (snip)

end

Lua の心理的ハードル

自分は Lua を書いたことがなかったのですが、あくまで sysbench という framework に差し込む script を書くだけなので、実施したいテストを実装する限りには困りませんでした。 自分は無駄に Lua に対する心理的ハードルを上げてしまっていたので、以下を眺めてもらって、心理的ハードルを下げてもらえればなと思います。

learnxinyminutes.com

Playground も web 上で利用可能なので、是非お試し下さい。

www.lua.org

まとめ

sysbench のカスタムテストの作り方を、sysbench 自体の設計を踏まえつつ説明しました。 基本的に小さなコードベースなので、不明な点があれば気軽に読んでみてもらえたらと思います。