# 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 INSERT 流程
如果您在业务高峰期遇到 LOB 表的写入吞吐量瓶颈、并发写入时延高，通常是由于写入过程中的锁竞争导致。TaurusDB 提供两项优化来解决此问题。
- **优化1：批量预分配**
  原生 InnoDB 每写一页就申请一页空间，每次申请都需要持有全局锁（索引根页锁 + 表空间锁），整个 LOB 写完之前锁一直不释放，并发写入只能排队等待。
  TaurusDB将写入过程拆成三步，只在第一步短暂持锁：
  1. 批量分配：一次性申请好所有页面，然后立即释放全局锁。
  
  2. 逐页写入：不持全局锁，每页只持该页自身的短锁，写完即释放。
  
  3. 收尾确认：短暂持有记录页锁，确认无冲突后写回最终长度。
   
  全局锁持有时间从"整个 LOB 写完"缩短到"分配那一小会儿"，并发写入几乎不再互相阻塞。
  
- **优化2：索引锁降级**
  原生写入路径持有索引排他锁，同一索引上只能有一个LOB在写入，高并发下写入被完全串行化。
  TaurusDB将索引锁从排他锁降级为共享锁，多个写入可同时持有共享锁，不再互相阻塞，并发写入不再串行化。
  
 
#### 优化LOB UPDATE 流程
如果您在更新含 LOB 字段的记录时发现并发性能差，通常是因为原生 InnoDB一刀切走悲观更新路径导致的。
只要满足以下 3 个条件之一，就回退到悲观路径：
1. 更新前的旧记录包含溢出字段；
2. 待更新的字段的新值是溢出字段：此场景仅出现在rollback流程中，基于undo record进行反向更新时，undo record 中记录的被更新的字段的旧值已经是一个溢出字段。
3. 乐观更新执行过程中发现页面剩余空间不足以inline 存放更新后的新行：作为对比，悲观路径会尝试将行中字段进行溢出存放；
**优化3：支持乐观更新**
原生 InnoDB 只要记录含 LOB，就一律回退到悲观路径------持有排他锁，高并发下整个索引的更新被串行化。但实际上，是否需要悲观更新只取决于记录是否放得下，跟有没有 LOB 并无必然关系。
对于不会导致页面分裂的 LOB 更新，TaurusDB允许走乐观路径（持共享锁即可），仅在真正需要时才回退悲观路径。大量原本被串行化的更新操作可以并发执行，吞吐显著提升。
#### 优化Purge旧LOB流程
如果您在频繁删除或更新 LOB 数据后发现表空间回收缓慢、文件持续膨胀，根源在于原生 Purge 进程效率低下。TaurusDB 提供两项优化来解决此问题。
- **优化4：Purge 预读**
  原生 Purge 在持锁期间逐页同步读取待清理的 LOB 页面，如果页面不在内存中就要从磁盘加载，持锁时间随 IO 延迟成倍增长，其他写入一直被阻塞。
  TaurusDB将IO操作与加锁过程分离，持锁期间将不再有磁盘 IO，锁持有时间缩短到仅剩内存操作，前台写入几乎不受影响。
  1. 识别目标：加锁前，先找出需要清理的 LOB 页面列表。
  
  2. 异步预读：发起异步 IO，将页面提前从磁盘加载到内存。
  
  3. 快速清理：页面就绪后再拿锁，直接在内存中完成释放操作。
   
- **优化5：Purge 免加载**
  释放一个 LOB 数据页，实际只需要在空间位图上标记"空闲"，根本不需要看页面内容。但原生流程仍会把整个大页面加载到内存，既浪费 IO，又占用缓存。
  释放LOB数据页时，TaurusDB仅加载必要的元数据页获取页号，直接通过页号更新位图标记为空闲，无需加载大数据页本身，省去无效IO，避免缓存污染。
  
 
#### 开启LOB写入优化
在管理控制台的参数修改页面，通过[设置参数](https://support.huaweicloud.com/usermanual-taurusdb/taurusdb_configuration.html)，开启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优化前后对比如下：
图1UPDATE性能结果   
![](https://support.huaweicloud.com/kerneldesc-taurusdb/figure/zh-cn_image_0000002797488347.png "点击放大")
**INSERT 负载同样受益**（16 KB 约 +50%、64 KB 约 +35%），量级比 UPDATE 小一些。
