mysql pointer_MySQL-Innodb-Hazard Pointer的设计与实现

本文详细介绍了InnoDB存储引擎在处理刷脏页过程中的性能问题。早期版本的算法可能导致在磁盘速度较慢时,刷脏操作退化为O(N*N)复杂度。为了解决这个问题,引入了Hazard Pointer技术。Hazard Pointer允许在释放锁之前保留对下一个待处理节点的引用,从而在释放锁后仍能快速定位到有效节点,避免重复扫描,保证刷脏操作始终维持在O(N)复杂度,提高了系统的效率。这一优化同样适用于LRU List驱逐算法,提升了整体系统性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前提

flush_list是包含dirty page并按修改时间有序的链表,在刷脏时选择从链表的尾部进行遍历淘汰

异步IO,当异步写page完成后,io helper线程会调buf_flush_write_complete,将写入的Page从flush list上移除。

调用栈

----fil_aio_wait

----buf_page_io_complete(static_cast(message));

----buf_page_io_complete

--------buf_flush_write_complete

-------------buf_flush_remove

-----------------UT_LIST_REMOVE(buf_pool->flush_list, bpage);

以前的代码实现(版本5.7.1):

刷脏线程刷完一个数据页,就需要回到Flush List末尾,重新扫描新的可刷盘的脏页。

buf_do_flush_list_batch{

do {

/* Start from the end of the list looking for a suitable

block to be flushed. */

buf_flush_list_mutex_enter(buf_pool);

/* We use len here because theoretically insertions can

happen in the flush_list below while we are traversing

it for a suitable candidate for flushing. We'd like to

set a limit on how farther we are willing to traverse

the list. */

len = UT_LIST_GET_LEN(buf_pool->flush_list);

bpage = UT_LIST_GET_LAST(buf_pool->flush_list);

if (bpage) {

ut_a(bpage->oldest_modification > 0);

}

if (!bpage || bpage->oldest_modification >= lsn_limit) {

/* We have flushed enough */

buf_flush_list_mutex_exit(buf_pool);

break;

}

ut_a(bpage->oldest_modification > 0);

ut_ad(bpage->in_flush_list);

buf_flush_list_mutex_exit(buf_pool);

/* The list may change during the flushing and we cannot

safely preserve within this function a pointer to a

block in the list! */

while (bpage != NULL

&& len > 0

&& !buf_flush_page_and_try_neighbors(

bpage, BUF_FLUSH_LIST, min_n, &count)) {

++scanned;

buf_flush_list_mutex_enter(buf_pool);

/* If we are here that means that buf_pool->mutex

was not released in buf_flush_page_and_try_neighbors()

above and this guarantees that bpage didn't get

relocated since we released the flush_list

mutex above. There is a chance, however, that

the bpage got removed from flush_list (not

currently possible because flush_list_remove()

also obtains buf_pool mutex but that may change

in future). To avoid this scenario we check

the oldest_modification and if it is zero

we start all over again. */

if (bpage->oldest_modification == 0) {

buf_flush_list_mutex_exit(buf_pool);

break;

}

bpage = UT_LIST_GET_PREV(list, bpage);

ut_ad(!bpage || bpage->in_flush_list);

buf_flush_list_mutex_exit(buf_pool);

--len;

}

} while (count < min_n && bpage != NULL && len > 0);

}

为什么这样设计?

因为刷脏线程刷的这个脏页之前的脏页可能被其他线程给刷走了,之前的脏页可能已经不在Flush list中。

造成这个现象的原因:

用户线程也会进行刷脏,虽然是从lru队列中找到的脏页,当脏页被flush到磁盘后,用户线程也会将写入的Page从flush list上移除。具体的调用栈参见:

https://www.jianshu.com/p/080056afa842

同时,为了减少锁占用的时间,InnoDB在进行写盘的时候都会把之前占用的锁给释放掉。

带来了什么后果

我们的某一个刷脏线程拿到队尾最后一个数据页,IO fixed,发送给IO线程,最后再从队尾扫描寻找可刷盘的脏页。在这次扫描中,它发现最后一个数据页(也就是刚刚发送到IO线程中的数据页)状态为IO fixed(磁盘很慢,还没处理完)所以不能刷,跳过,开始刷倒数第二个数据页,同样IO fixed,发送给IO线程,然后再次重新扫描Flush List。它又发现尾部的两个数据页都不能刷新(因为磁盘很慢,可能还没刷完),直到扫描到倒数第三个数据页。所以,存在一种极端的情况,如果磁盘比较缓慢,刷脏算法性能会从O(N)退化成O(N*N)。

https://dev.mysql.com/worklog/task/?id=7047

Hazard Pointer版本的代码实现(5.7.28)

static

ulint

buf_do_flush_list_batch(

buf_pool_t* buf_pool,

ulint min_n,

lsn_t lsn_limit)

{

ulint count = 0;

ulint scanned = 0;

ut_ad(buf_pool_mutex_own(buf_pool));

/* Start from the end of the list looking for a suitable

block to be flushed. */

buf_flush_list_mutex_enter(buf_pool);

ulint len = UT_LIST_GET_LEN(buf_pool->flush_list);

/* In order not to degenerate this scan to O(n*n) we attempt

to preserve pointer of previous block in the flush list. To do

so we declare it a hazard pointer. Any thread working on the

flush list must check the hazard pointer and if it is removing

the same block then it must reset it. */

for (buf_page_t* bpage = UT_LIST_GET_LAST(buf_pool->flush_list);

count < min_n && bpage != NULL && len > 0

&& bpage->oldest_modification < lsn_limit;

bpage = buf_pool->flush_hp.get(),

++scanned) {

buf_page_t* prev;

ut_a(bpage->oldest_modification > 0);

ut_ad(bpage->in_flush_list);

prev = UT_LIST_GET_PREV(list, bpage);

buf_pool->flush_hp.set(prev);

buf_flush_list_mutex_exit(buf_pool);

#ifdef UNIV_DEBUG

bool flushed =

#endif /* UNIV_DEBUG */

buf_flush_page_and_try_neighbors(

bpage, BUF_FLUSH_LIST, min_n, &count);

buf_flush_list_mutex_enter(buf_pool);

ut_ad(flushed || buf_pool->flush_hp.is_hp(prev));

--len;

}

buf_pool->flush_hp.set(NULL);

buf_flush_list_mutex_exit(buf_pool);

if (scanned) {

MONITOR_INC_VALUE_CUMULATIVE(

MONITOR_FLUSH_BATCH_SCANNED,

MONITOR_FLUSH_BATCH_SCANNED_NUM_CALL,

MONITOR_FLUSH_BATCH_SCANNED_PER_CALL,

scanned);

}

if (count) {

MONITOR_INC_VALUE_CUMULATIVE(

MONITOR_FLUSH_BATCH_TOTAL_PAGE,

MONITOR_FLUSH_BATCH_COUNT,

MONITOR_FLUSH_BATCH_PAGES,

count);

}

ut_ad(buf_pool_mutex_own(buf_pool));

return(count);

}

原理

要解决这个问题,最本质的方法就是当刷完一个脏页的时候不要每次都从队尾重新扫描。我们可以使用Hazard Pointer来解决,方法如下:遍历找到一个可刷盘的数据页,在锁释放之前,调整Hazard Pointer使之指向Flush List中下一个节点,注意一定要在持有锁的情况下修改。然后释放锁,进行刷盘,刷完盘后,重新获取锁,读取Hazard Pointer并设置下一个节点,然后释放锁,进行刷盘,如此重复。当这个线程在刷盘的时候,另外一个线程需要刷盘,也是通过Hazard Pointer来获取可靠的节点,并重置下一个有效的节点。通过这种机制,保证每次读到的Hazard Pointer是一个有效的Flush List节点,即使磁盘再慢,刷脏算法效率依然是O(N)。 这个解法同样可以用到LRU List驱逐算法上,提高驱逐的效率。

http://mysql.taobao.org/monthly/2017/05/01/

<think>我们正在解决innobackupex命令执行时出现的错误,尤其是关于`--apply-log`和`--copy-back`参数的使用以及datadir的指定问题。根据之前的分析,用户在执行命令时使用了通配符`*`导致路径错误。同时,用户现在提供了两个参考引用,其中都使用了`--apply-log`并指定了具体的备份目录(没有通配符)。此外,引用中使用了`--use-memory`参数来加速处理。用户之前执行的错误命令:innobackupex--defaults-file='/data/apps/mysql/my.cnf.bak'--apply-log'/data/apps/mysql/backup/*'正确的做法是:1.不要使用通配符,而是指定一个具体的备份目录(如引用中的那样)。2.可选地使用`--use-memory`参数来分配更多的内存以加速处理(如引用中使用了4G或64G)。另外,在恢复备份时,通常有两个步骤:Step1:在备份目录上应用日志(--apply-log),使备份处于一致状态。Step2:将备份复制回数据目录(--copy-back),但这一步需要在MySQL服务停止的情况下进行,并且要确保数据目录是空的。关于datadir的指定:-在`--apply-log`步骤中,不需要指定datadir,因为备份目录已经包含备份的数据文件。-在`--copy-back`步骤中,工具会从my.cnf文件中读取datadir的配置,所以确保my.cnf文件中的datadir配置正确。现在,我们针对用户的问题,给出具体的解决方案:1.修复`--apply-log`步骤:命令应指定一个具体的备份目录,例如:innobackupex--defaults-file='/data/apps/mysql/my.cnf.bak'--apply-log/data/apps/mysql/backup/full_backup_20240610如果备份目录下有多个备份,需要分别对每个备份目录执行`--apply-log`。2.调整`--copy-back`步骤:在完成`--apply-log`后,停止MySQL服务,清空数据目录(确保数据已备份或不需要),然后执行:innobackupex--defaults-file='/data/apps/mysql/my.cnf.bak'--copy-back/data/apps/mysql/backup/full_backup_20240610注意:`--copy-back`会将备份数据复制到my.cnf中指定的datadir目录。3.可选使用`--use-memory`参数来加速`--apply-log`过程,例如:innobackupex--defaults-file='/data/apps/mysql/my.cnf.bak'--apply-log--use-memory=4G/data/apps/mysql/backup/full_backup_20240610但注意不要超过系统可用内存。4.权限问题:在执行`--copy-back`后,需要确保数据目录的文件权限设置正确(通常属于mysql用户),可以使用:chown-Rmysql:mysql/path/to/datadir5.验证:在启动MySQL之前,检查错误日志,确保没有权限问题或其他错误。根据用户提供的引用[1]和[2],他们使用的命令格式都是正确的,因此我们按照这个格式来调整。下面我们给出具体的步骤:</think> ### 解决innobackupex配置错误及参数优化方案 #### 📌核心问题定位 1. **`--apply-log`路径错误** 命令中使用的通配符`*`会导致路径解析失败,需改为**具体备份目录名** ```bash # 错误格式 innobackupex ... --apply-log '/data/apps/mysql/backup/*' # 正确格式 innobackupex ... --apply-log /data/apps/mysql/backup/full_20240615 ``` 2. **数据目录(datadir)配置冲突** `my.cnf.bak`中的`datadir`必须主配置文件一致,验证方法: ```bash grep 'datadir' /etc/my.cnf /data/apps/mysql/my.cnf.bak # 输出示例: # /etc/my.cnf:datadir=/var/lib/mysql # my.cnf.bak:datadir=/data/mysql_data # 需修改为一致路径 ``` --- #### 🔧 参数配置指南(结合引用[1][2]) | **参数** | **作用** | **示例值** | **必填** | |----------|----------|------------|----------| | `--defaults-file` | 指定配置文件 | `/etc/my.cnf` | ✓ | | `--apply-log` | 准备备份目录 | `/backup/full_20240615` | ✓ | | `--copy-back` | 恢复数据到datadir | 无参数值 | ✓ | | `--use-memory` | 加速处理内存 | `4G`(≤可用内存80%) | ✗ | | `--datadir` | 覆盖配置文件设置 | `/new/mysql_data` | ✗ | --- #### ✅ 修复后操作流程 1. **准备备份(停机操作)** ```bash # 停服 systemctl stop mysqld # 应用日志(引用[1]方案) innobackupex --defaults-file=/etc/my.cnf \ --user=root \ --use-memory=4G \ --apply-log /backup/full_20240615 [^1] ``` 2. **恢复数据** ```bash # 清空原数据目录 rm -rf /var/lib/mysql/* # my.cnf的datadir一致 # 恢复数据(引用[2]方案) innobackupex --defaults-file=/etc/my.cnf \ --copy-back /backup/full_20240615 [^2] ``` 3. **权限修复 ```bash chown -R mysql:mysql /var/lib/mysql chmod 750 /var/lib/mysql ``` --- #### 🔍 验证成功的关键指标 1. **`--apply-log`成功标志** ```log InnoDB: Starting shutdown... InnoDB: Shutdown completed; log sequence number 12345678 [notice] completed OK! # 核心成功标识 ``` 2. **`--copy-back`成功标志** ```log Original datadir is not empty! Please remove. # 未清空目录会报错 ... Copying ./ib_logfile0 to /var/lib/mysql [OK] [notice] completed OK! ``` --- #### ⚠️ 特别注意事项 1. **配置文件优先级** `--defaults-file`必须作为**首个参数**,否则其他参数可能被忽略 ```bash # 正确顺序 innobackupex --defaults-file=xxx --apply-log ... # 错误顺序(可能导致配置读取失败) innobackupex --apply-log ... --defaults-file=xxx ``` 2. **内存优化公式** 安全内存值计算公式: $$ \text{use\_memory} \leq 0.8 \times \text{Available\_Mem} - \text{Buffer\_Pool} $$ 例如: ```bash # 可用内存16G,buffer_pool=8G --use-memory=$((16*0.8 - 8))G # =4.8G ≈4G ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值