LOB写入性能优化
操作场景
LOB(Large Object)是数据库中用于存储大容量二进制或字符数据(如图片、长文本等)的特殊字段类型。在数据库管理中,对于需要频繁进行插入、更新和删除操作的LOB数据,用户通常期望能够获得高效且稳定的性能。
一条带 LOB 的记录,从产生到被淘汰,可能会涉及三类写操作 —— LOB INSERT、LOB UPDATE、PURGE 旧LOB,这3类操作在MySQL原生内核的执行路径上均需要持有大粒度的互斥锁,导致并发流程被串行化,极大影响了并发写入性能。
- LOB INSERT流程:在持有索引级+表空间级的互斥锁之后,进行逐页的页面分配和数据写入;
- LOB UPDATE流程:只支持悲观更新,整个路径持有索引级互斥锁;
- Purge 旧LOB流程:在持有索引级+表空间级互斥锁之后,进行页面加载,触发同步IO;
针对上述问题,可以通过优化互斥锁机制,减少锁的粒度,以及采用异步IO操作,来提高LOB数据处理的并发性能。
优化LOB写入性能
LOB 的大容量特性使其在写入、更新、清理过程中容易成为性能瓶颈。以下从三个流程逐一说明问题与优化思路。
从而降低LOB 写入路径上的持锁的级别(互斥→共享),将持锁状态中执行的重操作(数据写入、页面加载等)移动到持锁前或者释放锁之后进行;并通过省略不必要的页面加载,避免无效IO和Buffer Pool污染。
开启LOB写入优化
在管理控制台的参数修改页面,通过设置参数,开启LOB写入优化。
| 参数名称 | 级别 | 参数说明 |
|---|---|---|
| innodb_rds_lob_prealloc | GLOBAL | 批量预分配开关,默认值为OFF。
|
| innodb_rds_lob_prealloc_data_batch_size | GLOBAL | 批量预过程中,每批写入LOB DATA 页面的上限,默认值是8页。 取值范围:1~32。 |
| innodb_rds_lob_latch_downgrade | GLOBAL | 锁降级开关,默认值为OFF。
|
| innodb_rds_lob_optimistic_update | GLOBAL | 乐观更新开关,默认值为OFF。
|
| innodb_rds_lob_purge_prefetch | GLOBAL | Purge预读LOB页面开关,默认值为OFF。
|
| innodb_rds_lob_purge_skip_load | GLOBAL | Purge省略对LOB DataPages的加载,默认值为OFF。
|
性能测试
将优化项的开关全部打开后,LOB 并发写入的吞吐会真正跃升。
测试步骤
- 准备脚本文件lob_update.lua、lob_insert.lua,用于后续性能测试。以下脚本文件供参考。
- lob_update.lua
#!/usr/bin/env sysbench -- LOB UPDATE benchmark: batched INSERT at prepare, PK UPDATE on blob_col at run. -- Default 1M rows per table; MEDIUMBLOB supports up to 16MB. sysbench.cmdline.options = { blob_size = {"BLOB payload size in bytes (insert + update)", 10240}, table_size = {"Number of rows per table", 1000000}, tables = {"Number of tables", 1}, } local c_value_template = "###########-###########-###########-" .. "###########-###########-###########-" .. "###########-###########-###########-" .. "###########" local pad_value_template = "###########-###########-###########-" .. "###########-###########" local function get_c_value() return sysbench.rand.string(c_value_template) end local function get_pad_value() return sysbench.rand.string(pad_value_template) end -- Target ~400KB per INSERT statement to stay under typical max_allowed_packet. local function rows_per_batch(blob_size) local approx = blob_size + 200 local n = math.floor(400000 / approx) return math.max(1, math.min(n, 150)) end function prepare() local drv = sysbench.sql.driver() assert(drv:name() == "mysql", "this script is MySQL-only") local con = drv:connect() local blob_size = sysbench.opt.blob_size local table_size = sysbench.opt.table_size local ntables = sysbench.opt.tables local batch = rows_per_batch(blob_size) for t = 1, ntables do print(string.format("Dropping old table sbtest%d if exists...", t)) con:query("DROP TABLE IF EXISTS sbtest" .. t) end for t = 1, ntables do print(string.format("Creating table sbtest%d...", t)) con:query(string.format([[ CREATE TABLE sbtest%d ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, k INT UNSIGNED NOT NULL, c CHAR(120) NOT NULL, pad CHAR(60) NOT NULL, blob_col MEDIUMBLOB NOT NULL, PRIMARY KEY (id), KEY k_%d (k) ) ENGINE=InnoDB]], t, t)) print(string.format( "Inserting %d rows into sbtest%d (blob REPEAT in SQL, size=%d, ~%d rows/INSERT)...", table_size, t, blob_size, batch)) local parts = {} local n = 0 for i = 1, table_size do local c_val = get_c_value() local pad_val = get_pad_value() local k = sysbench.rand.default(1, table_size) local row = string.format( "(%u, '%s', '%s', REPEAT('x', %d))", k, c_val, pad_val, blob_size) table.insert(parts, row) if #parts >= batch then con:query("INSERT INTO sbtest" .. t .. " (k, c, pad, blob_col) VALUES " .. table.concat(parts, ",")) n = n + #parts parts = {} if n % 100000 == 0 then print(string.format(" ... inserted %d / %d", n, table_size)) end end end if #parts > 0 then con:query("INSERT INTO sbtest" .. t .. " (k, c, pad, blob_col) VALUES " .. table.concat(parts, ",")) n = n + #parts end print(string.format("Done inserting into sbtest%d (%d rows).", t, n)) end end function cleanup() local drv = sysbench.sql.driver() local con = drv:connect() for t = 1, sysbench.opt.tables do print(string.format("Dropping table sbtest%d...", t)) con:query("DROP TABLE IF EXISTS sbtest" .. t) end end function thread_init() drv = sysbench.sql.driver() con = drv:connect() blob_template = string.rep("#", sysbench.opt.blob_size) end function thread_done() con:disconnect() end function event() local id = sysbench.rand.uniform(1, sysbench.opt.table_size) local t = sysbench.rand.uniform(1, sysbench.opt.tables) local payload = sysbench.rand.string(blob_template) con:query(string.format( "UPDATE sbtest%d SET blob_col='%s' WHERE id=%d", t, payload, id)) end - lob_insert.lua
#!/usr/bin/env sysbench -- LOB INSERT benchmark: run phase inserts MEDIUMBLOB rows with sysbench-generated payload. require("oltp_common") sysbench.cmdline.options.blob_size = {"LOB payload size in bytes", 1048576} sysbench.cmdline.options.lob_file_name = {"Value for file_name column", "name"} sysbench.cmdline.options.lob_file_size = {"Value for file_size column", 1048576} local function sql_quote(s) return (s:gsub("'", "''")) end function create_table(drv, con, table_num) print(string.format("Creating table 'lobtest%d'...", table_num)) local query = string.format([[ CREATE TABLE lobtest%d ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, file_name VARCHAR(255) NOT NULL, file_size BIGINT DEFAULT 0, file_content MEDIUMBLOB, upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) )]], table_num) con:query(query) end 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 } function thread_init() drv = sysbench.sql.driver() con = drv:connect() blob_template = string.rep("#", sysbench.opt.blob_size) end function thread_done() con:disconnect() end function prepare_statements() -- We use plain SQL execution in event(). end function event() local table_name = "lobtest" .. sysbench.rand.uniform(1, sysbench.opt.tables) local payload = sysbench.rand.string(blob_template) local query = string.format( "INSERT INTO %s (file_name, file_size, file_content, upload_time) " .. "VALUES ('%s', %d, '%s', NOW())", table_name, sql_quote(sysbench.opt.lob_file_name), tonumber(sysbench.opt.lob_file_size), payload ) con:query(query) end function cleanup() local drv = sysbench.sql.driver() local con = drv:connect() for i = 1, sysbench.opt.tables do print(string.format("Dropping table 'lobtest%d'...", i)) con:query("DROP TABLE IF EXISTS lobtest" .. i) end end
- lob_update.lua
- 准备数据。
sysbench --db-driver=mysql --mysql-host=host --mysql-port=port --mysql-user=user --mysql-password=password --mysql-db=sbtest --table_size=100000 --tables=1 --blob-size={LOB大小} {lob_update.lua或者lob_insert.lua} prepare
- 执行以下命令,测试性能。
sysbench --db-driver=mysql --mysql-host=host --mysql-port=port --mysql-user=user --mysql-password=password --mysql-db=sbtest --table_size=100000 --tables=1 --blob-size={LOB大小} --time=300 --threads=threadnum --percentile=99 --report-interval=5 {lob_update.lua或者 lob_insert.lua} run
- 执行以下命令,清理数据。
sysbench --db-driver=mysql --mysql-host=host --mysql-port=port --mysql-user=user --mysql-password=password --mysql-db=sbtest --table_size=100000 --tables=1 {lob_update.lua或者 lob_insert.lua} cleanup
测试结果
UPDATE优化前后对比如下:
INSERT 负载同样受益(16 KB 约 +50%、64 KB 约 +35%),量级比 UPDATE 小一些。