更新时间:2026-11-03 GMT+08:00
分享

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写入优化。

表1 参数说明

参数名称

级别

参数说明

innodb_rds_lob_prealloc

GLOBAL

批量预分配开关,默认值为OFF。

  • ON:开启批量预分配。
  • OFF:关闭批量预分配。

innodb_rds_lob_prealloc_data_batch_size

GLOBAL

批量预过程中,每批写入LOB DATA 页面的上限,默认值是8页。

取值范围:1~32。

innodb_rds_lob_latch_downgrade

GLOBAL

锁降级开关,默认值为OFF。

  • ON:关闭锁降级。
  • OFF:开启锁降级。

innodb_rds_lob_optimistic_update

GLOBAL

乐观更新开关,默认值为OFF。

  • ON:开启乐观更新。
  • OFF:关闭乐观更新。

innodb_rds_lob_purge_prefetch

GLOBAL

Purge预读LOB页面开关,默认值为OFF。

  • ON:开启Purge预读LOB页面。
  • OFF:关闭Purge预读LOB页面。

innodb_rds_lob_purge_skip_load

GLOBAL

Purge省略对LOB DataPages的加载,默认值为OFF。

  • ON:开启Purge免加载。
  • OFF:关闭Purge免加载。

性能测试

将优化项的开关全部打开后,LOB 并发写入的吞吐会真正跃升。

测试步骤

  1. 准备脚本文件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

  2. 准备数据。

    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

  3. 执行以下命令,测试性能。

    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

  4. 执行以下命令,清理数据。

    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优化前后对比如下:

图1 UPDATE性能结果

INSERT 负载同样受益(16 KB 约 +50%、64 KB 约 +35%),量级比 UPDATE 小一些。

相关文档