MySQL源码(2)同步io相关模块行为

同步io相关模块的行为

gdb调试mysql io部分代码

  1. mysql重启下,然后启动之前把rm -rf ib_buffer_pool删了,否则缓存了不会走io逻辑。
  2. gdb加个缓存,挂上去会快很多,alias gdbp='gdb -iex "set index-cache on" -iex "set index-cache directory /tmp/gdb-cache" -iex "set pagination off" -p '
  3. 执行一个insert即可,执行流程会涉及几层逻辑,这里关注后两层 :fil、os

断点位置

c 复制代码
# ======= SQL → InnoDB Handler =======
b ha_innobase::write_row
# ======= Buffer Pool =======
b buf_page_get_gen
b Buf_fetch_normal::get
b buf_read_page
b buf_read_page_low
# ======= fil 层 =======
b fil_io
b Fil_shard::do_io
b pfs_os_aio_func
# ======= os 层 =======
b os_aio_func
b os_file_read_func
b os_file_read_page
b os_file_pread
b os_file_io
b SyncFileIO::execute

这里关注后两层 fil层 和 os 层

堆栈整理:

层级 # 函数 源文件 说明
线程入口 #39 clone() clone.S:95 系统调用,创建线程
#38 start_thread() pthread_create.c:479 pthread 线程启动
#37 pfs_spawn_thread() pfs.cc:3050 PFS 包装的线程入口
连接处理 #36 handle_connection() connection_handler_per_thread.cc:303 每连接一个线程,处理连接
#35 do_command() sql_parse.cc:1440 读取并分发客户端命令
#34 dispatch_command() sql_parse.cc:2055 分发 COM_QUERY 命令
SQL 解析执行 #33 dispatch_sql_command() sql_parse.cc:5385 解析 SQL 文本
#32 mysql_execute_command() sql_parse.cc:3691 执行 SQL 命令
#31 Sql_cmd_dml::execute() sql_select.cc:785 DML 通用执行框架
#30 Sql_cmd_insert_values::execute_inner() sql_insert.cc:653 INSERT VALUES 执行内核
#29 write_record() sql_insert.cc:2119 写入一条记录
存储引擎接口 #28 handler::ha_write_row() handler.cc:8020 Handler API 写行入口
#27 ha_innobase::write_row() ha_innodb.cc:9109 InnoDB Handler 写行
InnoDB 行操作 #26 row_insert_for_mysql() row0mysql.cc:1716 行插入入口
#25 row_insert_for_mysql_using_ins_graph() row0mysql.cc:1586 使用插入图执行插入
#24 row_ins_step() row0ins.cc:3730 行插入执行步骤
#23 row_ins() row0ins.cc:3606 行插入主逻辑
#22 row_ins_index_entry_step() row0ins.cc:3488 插入索引项步骤
#21 row_ins_index_entry() row0ins.cc:3351 插入索引项
#20 row_ins_clust_index_entry() row0ins.cc:3159 插入聚簇索引项
#19 row_ins_clust_index_entry_low() row0ins.cc:2417 聚簇索引插入底层实现
B-Tree 操作 #18 btr_height_get() btr0btr.cc:209 获取 B-Tree 高度
#17 btr_root_block_get() btr0btr.cc:164 获取 B-Tree 根页
#16 btr_block_get() btr0btr.h:205 获取 B-Tree 页面(内联)
#15 btr_block_get_func() btr0btr.ic:56 获取 B-Tree 页面实现
Buffer Pool #14 buf_page_get_gen() buf0buf.cc:4481 获取页面通用入口
#13 Buf_fetch_normal::get() buf0buf.cc:3734 正常模式获取页面
#12 Buf_fetch<Buf_fetch_normal>::single_page() buf0buf.cc:4288 单页获取
#11 Buf_fetch<Buf_fetch_normal>::read_page() buf0buf.cc:4107 Buffer Pool 未命中,触发磁盘读
页面读取 #10 buf_read_page() buf0rea.cc:292 页面读取入口
#9 buf_read_page_low() buf0rea.cc:127 页面读取底层
文件 I/O 层 #8 fil_io() fil0fil.cc:7983 文件 I/O 分发(page_id → 物理文件)
#7 Fil_shard::do_io() fil0fil.cc:7869 Fil_shard 执行 I/O
OS AIO 层 #6 pfs_os_aio_func() os0file.ic:148 PFS 埋点包装
#5 os_aio_func() os0file.cc:6965 AIO 入口(本次 mode=SYNC
#4 os_file_read_func() os0file.cc:5532 同步读文件
#3 os_file_read_page() os0file.cc:5115 读一个页面
#2 os_file_pread() os0file.cc:5073 pread 封装
#1 os_file_io() os0file.cc:4893 I/O 重试循环
磁盘 I/O #0 SyncFileIO::execute() os0file.cc:2049 最终调用 pread() 系统调用

fil层

当前来到fil_io

复制代码
(gdb) f 8
#8  0x000000000518094d in fil_io (type=..., sync=true, page_id=..., page_size=..., byte_offset=0, len=16384, buf=0x7f6738ff8000, message=0x7f6737b21a70) at /data/my/myroot15/mysrc/storage/innobase/fil/fil0fil.cc:7983
7983	  auto const err = shard->do_io(type, sync, page_id, page_size, byte_offset,

fil_io函数很简单,核心逻辑:

  1. 通过page_id.space确定由哪个Fil_shard处理
  2. 将io请求委托给shard->do_io()
c 复制代码
dberr_t fil_io(const IORequest &type, bool sync, const page_id_t &page_id,
               const page_size_t &page_size, ulint byte_offset, ulint len,
               void *buf, void *message) {
  auto shard = fil_system->shard_by_id(page_id.space());
  auto const err = shard->do_io(type, sync, page_id, page_size, byte_offset,
                                len, buf, message);
  return err;
}

进入fil_io后来一个方便的调试命令:

复制代码
(gdb) printf "=== fil_io: space=%u page=%u %s sync=%d ===\n", page_id.m_space, page_id.m_page_no, type.m_type == 0 ? "READ" : "WRITE", sync

=== fil_io: space=12 page=4 WRITE sync=1 ===

看下shard关键数据:m_spaces保存了spaceid到fil_space_t的映射关系

Fil_shard数据结构:

复制代码
class Fil_shard {
	...
	using Spaces = std::unordered_map<space_id_t, fil_space_t *>;
	...
private:
	Spaces m_spaces;
	...
}

shard->m_spaces信息:

复制代码
(gdb) p shard->m_spaces
$11 = std::unordered_map with 1 element = {
  [12] = 0x7f674b400b80
}

# 注意这里是 fil_space_t
(gdb) p *(fil_space_t*)0x7f674b400b80
$12 = {
	...
	name = 0x7f674b400d70 "d1/t_debug",
	...
	files = ...
	size = 7,
    flags = 16417,
    free_len = 0,
    free_limit = 64,
    latch = ...
    unflushed_spaces = ...
    m_header_page_flush_lsn = 0,
    magic_n = 89472,
    static s_sys_space = 0x7f67483d3220

# 注意这里是fil_node_t
(gdb) p ((fil_space_t*)0x7f674b400b80)->files[0]
$15 = {
  space = 0x7f674b400b80,
  name = 0x7f674b400ef0 "./d1/t_debug.ibd",
  is_open = true,
  handle = {
    m_psi = 0x7f674fca8900,
    m_file = 36
  },
  sync_event = 0x7f674b400f30,
  is_raw_disk = false,
  size = 7,
  flush_size = 0,
  init_size = 0,
  max_size = 4294967294,
  n_pending_ios = 1,
  n_pending_flushes = 0,
  is_being_extended = false,
  modification_counter = 0,
  flush_counter = 0,
  LRU = {
    prev = 0x0,
    next = 0x0
  },
  punch_hole = true,
  block_size = 4096,
  atomic_write = false,
  magic_n = 89389
}

(gdb) ptype ((fil_space_t*)0x7f674b400b80)->files[0]
type = struct fil_node_t {
    fil_space_t *space;
    char *name;
    bool is_open;
    pfs_os_file_t handle;
    os_event_t sync_event;
    bool is_raw_disk;
    page_no_t size;
    page_no_t flush_size;
    page_no_t init_size;
    page_no_t max_size;
    size_t n_pending_ios;
    size_t n_pending_flushes;
    bool is_being_extended;
    int64_t modification_counter;
    int64_t flush_counter;
    List_node LRU;
    bool punch_hole;
    size_t block_size;
    bool atomic_write;
    size_t magic_n;
  public:
    bool can_be_closed(void) const;
    bool is_flushed(void) const;
    void set_flushed(void);

    typedef ut_list_node<fil_node_t> List_node;
}

整理下这里的关系

复制代码
fil_system (全局单例)
  ├─ Fil_shard[0]
  │    ├─ m_spaces (hash_map: space_id → fil_space_t*)
  │    │    ├─ fil_space_t (space_id=12, name="d1/t_debug")
  │    │    │    └─ files: [fil_node_t(./d1/t_debug.ibd)]
  │    │    └─ ...
  │    └─ m_LRU (文件描述符 LRU 列表, 控制同时打开的文件数)
  ├─ Fil_shard[1]
  │    └─ ...
  ├─ Fil_shard[2]
  │    └─ ...
  └─ Fil_shard[3]  (默认68个 shard,MAX_SHARDS = 68)
       └─ ...

fil_space_t                          fil_node_t
┌─────────────---------───────────┐           ┌──────────────────────────────┐
│ id         = 12                 │           │ name   = "./d1/t_debug.ibd"  │
│ name       = "d1/t_debug"       │           │ handle = {m_file = 36}       │
│ size       = 7 (pages)          │           │ is_open = true               │
│ purpose    = FIL_TYPE_TABLESPACE│           │ size    = 7 (pages)          │
│ flags      = 16417              │           │ block_size = 4096            │
│ n_reserved_extents              │           │ n_pending_ios = 1            │
│ files[]             ────────────┼─files────→│ space   = (指回 fil_space_t)  │
│ encryption_metadata             │           │ punch_hole = true            │
└────────────────---------────────┘           └──────────────────────────────┘

这里filesystem为什么设计多个Fil_shard呢?核心目的是减少 mutex 竞争,每个 Fil_shard 都有自己独立的 mutex。

复制代码
/** Constructor
@param[in]      shard_id        Shard ID  */
Fil_shard::Fil_shard(size_t shard_id)
    : m_id(shard_id),
      m_spaces(),
      m_names(),
      m_LRU(),
      m_unflushed_spaces(),
      m_modification_counter() {
  mutex_create(LATCH_ID_FIL_SHARD, &m_mutex);
}

8.0 之前整个 fil_system 只有一把全局 mutex(fil_system_mutex)。这导致了严重的锁竞争瓶颈:

复制代码
旧版本(单锁):
Thread-1 写表 A  ──→ ┐
Thread-2 读表 B  ──→ ├─→ 争抢同一把 fil_system_mutex ──→ 串行化!
Thread-3 读表 C  ──→ ┘

新版本(分片锁):
Thread-1 写表 A (space_id=3)  ──→ shard[3].mutex   ──→ 并行!
Thread-2 读表 B (space_id=5)  ──→ shard[5].mutex   ──→ 并行!
Thread-3 读表 C (space_id=3)  ──→ shard[3].mutex   ──→ 仅这两个竞争
  • shard[0] ~ shard[63]:共 64 个,用于普通表空间(用户表、系统表空间、临时表空间等)
  • shard[64] ~ shard[67]:共 4 个,专门用于 Undo 表空间

映射逻辑

复制代码
Fil_shard *shard_by_id(space_id_t space_id) const {
    if (fsp_is_undo_tablespace(space_id)) {
        // Undo 表空间 → 映射到后 4 个 shard
        const size_t limit = space_id % UNDO_SHARDS;        // space_id % 4
        return m_shards[UNDO_SHARDS_START + limit];         // shard[64..67]
    }
    // 普通表空间 → 映射到前 64 个 shard
    return m_shards[space_id % UNDO_SHARDS_START];          // space_id % 64
}

每个 Fil_shard 是一个迷你版的表空间管理器,拥有自己独立的:

成员 含义
m_mutex 独立的互斥锁,保护本 shard 内的所有数据结构
m_spaces unordered_map<space_id, fil_space_t*>,本 shard 管辖的所有表空间
m_names unordered_map<name, fil_space_t*>,按名字索引的表空间
m_LRU 本 shard 内最近使用的打开文件的 LRU 链表
m_unflushed_spaces 本 shard 内有未刷盘脏数据的表空间链表
m_modification_counter 本 shard 内的写操作计数器

分配:

复制代码
fil_system (Fil_system)
├── m_shards[0]  ──→ Fil_shard { mutex, spaces, LRU, ... }  ← space_id % 64 == 0 的普通表
├── m_shards[1]  ──→ Fil_shard { mutex, spaces, LRU, ... }  ← space_id % 64 == 1 的普通表
├── m_shards[2]  ──→ Fil_shard { mutex, spaces, LRU, ... }  ← space_id % 64 == 2
├── ...
├── m_shards[63] ──→ Fil_shard { mutex, spaces, LRU, ... }  ← space_id % 64 == 63
├── m_shards[64] ──→ Fil_shard { mutex, spaces, LRU, ... }  ← Undo (space_id % 4 == 0)
├── m_shards[65] ──→ Fil_shard { mutex, spaces, LRU, ... }  ← Undo (space_id % 4 == 1)
├── m_shards[66] ──→ Fil_shard { mutex, spaces, LRU, ... }  ← Undo (space_id % 4 == 2)
└── m_shards[67] ──→ Fil_shard { mutex, spaces, LRU, ... }  ← Undo (space_id % 4 == 3)

os层

os层堆栈:

这里只分析下同步IO的场景,进入os_aio_func后,同步IO直接路由到os_file_read_func或os_file_write_func函数。

复制代码
pfs_os_aio_func(SYNC, ...)              ← PFS 包装层(Performance Schema)
  │  register_pfs_file_io_begin()
  ▼
os_aio_func(SYNC, ...)                  ← os 层入口: SYNC 直接走同步路径
  │  if (aio_mode == SYNC) → os_file_read_func()
  ▼
os_file_read_func(type, name, fd, ...)   ← 同步读入口(thin wrapper)
  │
  ▼
os_file_read_page(type, name, fd, ...)   ← 页面读取 + 错误恢复外层循环
  │  内含 for(;;) 无限重试
  ▼
os_file_pread(type, fd, buf, n, offset)  ← 计数器 + 监控统计
  │  ++os_n_file_reads
  │  MONITOR_ATOMIC_INC(MONITOR_OS_PENDING_READS)
  ▼
os_file_io(type, fd, buf, n, offset)     ← 核心: 压缩/加密 + 部分I/O重试
  │  SyncFileIO sync_file_io(fd, buf, n, offset)
  │  for (i = 0; i < 10; ++i)  ← NUM_RETRIES_ON_PARTIAL_IO
  ▼
SyncFileIO::execute(request)             ← 最终: pread() 系统调用 !!!
  │
  ▼
pread(fd=36, buf=0x7f6738ff8000, 16384, 65536)   ← Linux 内核

os_file_read_page读页面,要求页面读出来的字节必须是n否则报错。

os_file_io真正要读了

复制代码
os_file_io
  ...
  // ===== 创建 SyncFileIO 对象, 执行实际 I/O =====
  SyncFileIO sync_file_io(file,  buf,    n,        offset);
  //                      ^fd    ^buf    ^16384    ^65536

  for (ulint i = 0; i < NUM_RETRIES_ON_PARTIAL_IO; ++i) {  // 最多重试 10 次
    ssize_t n_bytes = sync_file_io.execute(type);
    //                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                最终调用 pread()/pwrite()

    if (n_bytes < 0) {
      break;  // 硬错误, 退出
    }

    if ((ulint)n_bytes + bytes_returned == n) {
      bytes_returned += n_bytes;
      *err = DB_SUCCESS;
      return original_n;
    }

    // ===== Partial I/O: 只完成了部分字节 =====
    bytes_returned += n_bytes;
    ib::warn() << n << " bytes should have been read. Only "
               << bytes_returned << " bytes read. Retrying...";

    sync_file_io.advance(n_bytes);
    //   m_offset += n_bytes;  // 文件偏移前进
    //   m_n -= n_bytes;       // 剩余字节减少
    //   m_buf += n_bytes;     // 缓冲区指针前进
  }

  *err = DB_IO_ERROR;
  return bytes_returned;
}

入参in os_file_io (file=36, buf=0x7f6738ff8000, n=16384, offset=65536) 的含义,从d1/t_debug.ibd文件的第65536字节处开始读取16384字节。

复制代码
offset = page_no × page_size
       = 4       × 16384
       = 65536

从堆栈可以看到,这次读取的是 B-tree 根页面(page_no = 4):

ibd文件格式

复制代码
文件偏移(字节)    page_no    用途
─────────────────────────────────────────
0                0          FSP_HDR (文件空间头, 管理区段分配)
16384            1          IBUF_BITMAP (Insert Buffer 位图)
32768            2          INODE (段信息节点)
49152            3          SDI (Serialized Dictionary Info, 8.0新增)
65536            4          ★★★★★★★★★★★★★★★★ B-tree 根页面 (INDEX page) ★★★★★★★★★★★★★★★★★
81920            5          第一个数据/叶子页面
...
相关推荐
Web极客码2 小时前
修复Discuz 迁移后页面全部变成“????”乱码的问题
数据库·mysql·discuz·mariadb
霖霖总总2 小时前
[小技巧71]从回滚到 MVCC:全面解析 MySQL Undo Log 机制
数据库·mysql
知识即是力量ol2 小时前
口语八股:MySQL 核心原理系列(一):索引篇
java·数据库·mysql·八股·索引·面试技巧
e***89010 小时前
MySQL 8.0版本JDBC驱动Jar包
数据库·mysql·jar
p***199414 小时前
MySQL——内置函数
android·数据库·mysql
橘颂TA15 小时前
【MySQL】90% 开发者都踩过的坑:数据库数据类型选错有多可怕?
数据库·mysql·算法与结构
认真的薛薛18 小时前
数据库-MHA-读写分离-redis
数据库·sql·mysql
YIN_尹18 小时前
【MySQL】数据处理的瑞士军刀——常用的内置函数(下)
android·mysql
霖霖总总19 小时前
[小技巧70]深入COUNT(*)、COUNT(1) 与 COUNT(字段):你以为的快,其实慢了
数据库·mysql