因为 pt-osc 对数据库性能影响较大,且容易造成死锁问题,目前我们在线更改表结构都使用 gh-ost 工具进行修改,这里记录一下使用 gh-ost 过程中的问题,以作记录;首先先复习一下gh-ost的基本实现,gh-ost的基本实现原理如下图所示:

根据源码,核心步骤如下:

1. initiateStreaming: 初始化 binlog events streaming
2. initiateApplier: 初始化 applier
3. addDMLEventsListener: 添加对指定表的binlog event过滤
4. ReadMigrationRangeValues: 获取对应表唯一索引的 min & max 值
5. executeWriteFuncs: 通过applier向ghost表写入数据, binlog event 相比 copy rows具有更高优先级
6. iterateChunks: 根据 min & max的值, 批量插入数据到 ghost 表
7. cutOver: rename & drop新旧表

问题一:gh-ost导致最新一次写操作丢失

原因分析:

在 initiateStreaming 的过程中通过 show master status 获取主节点当前的 binlog name & pos & Executed_Gtid_Set,然后通过 binlog name & pos 和当前的数据库节点建立复制通道,而后在 ReadMigrationRangeValues 的过程中通过 select min(unique_key) 和 select max(unique_key) 快照读的方式获取原表数据的范围。

问题就出在这里,根据事务的提交流程,如果sync_binlog != 1,那么 binlog name & pos 是在binlog flush阶段之后进行更新;如果 sync_binlog = 1,那么 binlog name & pos 是在 binlog sync 阶段之后进行更新,这时事务还没有在 Innodb 中完成 commit。因此,最新的一次事务对于 select min() & max() 这样的快照读是不可见的,最终造成了写操作的丢失。

如何修复:

这里有两种解决办法:1. 虽然 binlog name & pos 的信息是在 Innodb memory commit 之前进行更新,但是show master status 的 Executed_Gtid_Set 是在 Innodb memory commit 完成之后进行更新的,因此 gh-ost 可以使用 Executed_Gtid_Set 来与数据库节点建立复制通道来解决这个问题。

2. 在 ReadMigrationRangeValues 的过程中使用 select min() & max() lock in share mode 当前读来解决这个问题;

问题二:高并发写入时gh-ost无法结束:

如截图所示,Applied一直在增大,而 Copy保持不变。这是因为在通过 Applier 向 ghost 表中写数据时,binlog events apply 相比rows copy 具有更高的优先级;同时,由于 gh-ost 是通过监听 mysql binlog的方式获取增量写操作,对源MySQL节点的侵入较小;因此,在MySQL实例高并发写入时,gh-ost会忙于 apply binlog events而无法结束。

扩展一:gh-ost的cutover过程:

如上图所示,根据gh-ost  -cut-over参数的不同会选择不同的 cur-over 算法,默认是github的 atomic算法,也可以选择 facebook的 OSC算法。

atomic算法// atomicCutOver

这里为什么需要 magic_old表呢?

是为了防止lockSessionId被意外关闭后,可以阻塞rename操作。lockSessionId被意外关闭后,original table可以被写入,会造成数据不一致。至于为何要对 magic_old表加锁,我个人认为是防止magic_old表被意外删除。

源码实现如下:

func (this *Migrator) atomicCutOver() (err error) {
     // 设置 cutover 标记, 限流函数会使用此标记
atomic.StoreInt64(&this.migrationContext.InCutOverCriticalSectionFlag, 1)
defer atomic.StoreInt64(&this.migrationContext.InCutOverCriticalSectionFlag, 0)
     // 通知释放锁的通道
okToUnlockTable := make(chan bool, 4)
     // 最后删除 magic old table
defer func() {
okToUnlockTable <- true
this.applier.DropAtomicCutOverSentryTableIfExists()
}()
     //
atomic.StoreInt64(&this.migrationContext.AllEventsUpToLockProcessedInjectedFlag, 0)
   // 存储会话ID
lockOriginalSessionIdChan := make(chan int64, 2)
// 表锁定后通知通道
tableLocked := make(chan error, 2)
// 表锁释放后通知通道
tableUnlocked := make(chan error, 2)
go func() {
          /*
          * 在 cutover 阶段对表加锁, 共分为以下几步:
* 1. 打开一个新的会话,将会话ID存入 lockOriginalSessionIdChan 中
2. 调用 get_lock(key, timeout) 对应用进行加锁。需要一个字符串key和加锁超时等待时间,sessionA 调用 get_lock 对key加锁,
              sessionB也调用 get_lock 对相同的key加锁会等待,直到超时或 sessionA 断开。
3. 通过 set session lock_wait_timeout 设置表切换锁超时时间为CutOverLockTimeoutSeconds * 2, 默认为 6s,CutOverLockTimeoutSeconds可以人为指定。
4. 创建 cutover 过程中的 magic old表
5. 通过 lock tables write 对原始表和 magic old 表加 write锁
6. 向 tableLocked 通道中写入nil, 通知其他协程对原表和 magic old表加锁完成,可以开始后续工作
7. 当前协程阻塞,通过 <- okToUnlockTable 等待 unlocks table 信号
8. 删除 magic old 表, 删除之后,原始表仍然处于 lock 状态
9. 通过 unlock tables 对表进行解锁
10. 解锁成功后,向 tableUnlocked 通道写入 nil, 通知其他协程
          */
if err := this.applier.AtomicCutOverMagicLock(lockOriginalSessionIdChan, tableLocked, okToUnlockTable, tableUnlocked); err != nil {
log.Errore(err)
}
}()
// 这里阻塞等待原始表和 magic old 表加锁完成
if err := <-tableLocked; err != nil {
return log.Errore(err)
}
// 获取加锁的 sessionId
lockOriginalSessionId := <-lockOriginalSessionIdChan
log.Infof("Session locking original & magic tables is %+v", lockOriginalSessionId)
// At this point we know the original table is locked.
// We know any newly incoming DML on original table is blocked.
// 这里,原始表已被锁定,原始表不会产生新的 binlog event。我们需要等待 apply 原始表的 binlog events 到最新位置,等待
// 时间默认为 CutOverLockTimeoutSeconds(3s, 可以人为指定)
if err := this.waitForEventsUpToLock(); err != nil {
return log.Errore(err)
} // Step 2
// We now attempt an atomic RENAME on original & ghost tables, and expect it to block.
this.migrationContext.RenameTablesStartTime = time.Now()
// rename 操作是否被阻塞标志
var tableRenameKnownToHaveFailed int64
// 存储 rename 会话ID
renameSessionIdChan := make(chan int64, 2)
// rename 成功或失败后消息通知通道
tablesRenamed := make(chan error, 2)
// 异步的进行 rename 操作
go func() {
/**
           * rename 表操作
1. 打开新的 session,并将 sessionId 存入 renameSessionIdChan 中
2. 通过 set session lock_wait_timeout:= CutOverLockTimeoutSeconds 来设置 rename 操作超时时间
3. 通过 rename originalTable to magic_oldTable, ghost_table to originalTable 来进行rename操作,因为在之前对 originalTable
和 magic_oldTable 加有锁,正常情况下会阻塞
     4. 而后向 tablesRenamed 通道写入信息,如果rename成功写入 nil,rename失败则写入 err
          */
if err := this.applier.AtomicCutoverRename(renameSessionIdChan, tablesRenamed); err != nil {
// Abort! Release the lock
// 终止,释放锁
atomic.StoreInt64(&tableRenameKnownToHaveFailed, 1)
okToUnlockTable <- true
}
}()
// 这里获取 rename sessionId
renameSessionId := <-renameSessionIdChan
log.Infof("Session renaming tables is %+v", renameSessionId) waitForRename := func() error { if atomic.LoadInt64(&tableRenameKnownToHaveFailed) == 1 {
// We return `nil` here so as to avoid the `retry`. The RENAME has failed,
// it won't show up in PROCESSLIST, no point in waiting
// 当 rename 操作失败时, 返回 nil避免重试。
return nil
}
return this.applier.ExpectProcess(renameSessionId, "metadata lock", "rename")
}
// Wait for the RENAME to appear in PROCESSLIST
// 等待 rename 操作出现在 processlist 中
if err := this.retryOperation(waitForRename, true); err != nil {
// Abort! Release the lock
// 终止! 释放锁
okToUnlockTable <- true
return err
}
// tableRenameKnownToHaveFailed = 0 表示 rename 操作被阻塞,符合预期
if atomic.LoadInt64(&tableRenameKnownToHaveFailed) == 0 {
log.Infof("Found atomic RENAME to be blocking, as expected. Double checking the lock is still in place (though I don't strictly have to)")
}
// 探测对 original table 和 magic old table 加锁的线程还继续存在, 如果不存在则无法保证数据一致性
if err := this.applier.ExpectUsedLock(lockOriginalSessionId); err != nil {
// Abort operation. Just make sure to drop the magic table.
return log.Errore(err)
}
log.Infof("Connection holding lock on original table still exists") // Now that we've found the RENAME blocking, AND the locking connection still alive,
// we know it is safe to proceed to release the lock
// 这里 rename 操作被阻塞,并且加锁的连接仍然有效,继续释放锁是能保证数据安全的,这里通知释放锁
okToUnlockTable <- true
// BAM! magic table dropped, original table lock is released
// -> RENAME released -> queries on original are unblocked.
// 阻塞等待锁被释放
if err := <-tableUnlocked; err != nil {
return log.Errore(err)
}
// 阻塞等待 rename 操作完成 (锁被释放后rename操作会优先其他dml操作执行)
if err := <-tablesRenamed; err != nil {
return log.Errore(err)
}
this.migrationContext.RenameTablesEndTime = time.Now() // ooh nice! We're actually truly and thankfully done
// 整个切换完成
lockAndRenameDuration := this.migrationContext.RenameTablesEndTime.Sub(this.migrationContext.LockTablesStartTime)
log.Infof("Lock & rename duration: %s. During this time, queries on %s were blocked", lockAndRenameDuration, sql.EscapeName(this.migrationContext.OriginalTableName))
return nil
}

  facebook OSC算法// cutOverTwoStep

cutOverTwoStep很巧妙地利用MySQL不同 session 下 alter table x rename to x1; 和 rename table x1 to x; 不同的锁机制进行 cutover,值得深入研究。

源码如下:

/*
* cutOverTwoStep() 将阻塞原始表,等待原始表上的binlog events全部应用到 ghost 表,然后进行非原子的表rename操作,original->old, then new->original;
* 在rename过程中,原始表不存在,查询操作将失败。
*/
func (this *Migrator) cutOverTwoStep() (err error) {
atomic.StoreInt64(&this.migrationContext.InCutOverCriticalSectionFlag, 1)
defer atomic.StoreInt64(&this.migrationContext.InCutOverCriticalSectionFlag, 0)
atomic.StoreInt64(&this.migrationContext.AllEventsUpToLockProcessedInjectedFlag, 0)
     // 首先通过 lock tables write; 对原始表加锁
if err := this.retryOperation(this.applier.LockOriginalTable); err != nil {
return err
}
// 等待原始表上的 binlog events 全部应用到 ghost 表
if err := this.retryOperation(this.waitForEventsUpToLock); err != nil {
return err
}
     // 1. 这里使用和 LockOriginalTable 操作相同的session来执行 alter original_table rename magic_old_table; 来重
// 命名原表,该操作不会被 LockOriginalTable 操作加的锁阻塞,相同的 session 执行 rename table original to magic 将阻塞。
// 2. 将 ghost 表重命名为 original 表; 这里采用在另一个 session 上执行 rename table ghost to original;来进行,因此在
// 原 session 上执行将被锁阻塞。
if err := this.retryOperation(this.applier.SwapTablesQuickAndBumpy); err != nil {
return err
}
// 对原表解锁
if err := this.retryOperation(this.applier.UnlockTables); err != nil {
return err
} lockAndRenameDuration := this.migrationContext.RenameTablesEndTime.Sub(this.migrationContext.LockTablesStartTime)
renameDuration := this.migrationContext.RenameTablesEndTime.Sub(this.migrationContext.RenameTablesStartTime)
log.Debugf("Lock & rename duration: %s (rename only: %s). During this time, queries on %s were locked or failing", lockAndRenameDuration, renameDuration, sql.EscapeName(this.migrationContext.OriginalTableName))
return nil
}

  

gh-ost使用问题记录的更多相关文章

  1. Ghost-无损DDL

    目录 一.什么是DDL? 二.表级锁和元数据锁 2.1.什么是表锁? 2.2.什么是MDL? 三.什么是无损DDL? 四.DDL重建表 Mysql5.5之前重建表 Mysql5.6之后重建表 五.gh ...

  2. CSS3中的动画效果记录

    今天要记录的是CSS3中的三种属性transform.transition以及animation,这三个属性大大提升了css处理动画的能力. 一.Transform 变形 CSS中transform ...

  3. 关于table的一些记录

    HTML有10个表格相关标签 <caption> 表格的大标题,该标记可以出现在<table> 之间的任意位置.它对于搜索引擎的机器人记录信息十分重要.参数有align.val ...

  4. 爱上MVC3系列~监视Action的运行时间,并提供超时记录机制

    回到目录 文章出现的原因 很久没写关于MVC的文章了,原因是将关注点移向了MVVM和DDD这边,而这篇文章完全是因为公司项目的需要,因为公司网站总是不定时的502,而这由可能是程序超时所引起的,为了分 ...

  5. DNS安全浅议、域名A记录(ANAME),MX记录,CNAME记录

    相关学习资料 http://baike.baidu.com/link?url=77B3BYIuVsB3MpK1nOQXI-JbS-AP5MvREzSnnedU7F9_G8l_Kvbkt_O2gKqFw ...

  6. DNS安全浅议、域名A记录(ANAME),MX记录,CNAME记录(转)

    http://www.cnblogs.com/LittleHann/p/3828927.html 相关学习资料 http://baike.baidu.com/link?url=77B3BYIuVsB3 ...

  7. Python_socket常见的方法、网络编程的安全注意事项、socketsever模块、浏览器中在一段时间记录用户的登录验证机制

    1.socket常见的方法 socket_常见方法_服务器端 import socket from socket import SOL_SOCKET,SO_REUSEADDR sk = socket. ...

  8. oracle一些单记录函数

    单记录函数 1.0 NVL() 作用:从两个表达式返回一个非NULL值 用法:NVL(表达式1, 表达式2) 如果表达式1的结果不为NULL,返回表达式1的结果:如果表达式1的结果为NULL,返回表达 ...

  9. 利用jdk中工具完成Java程序监控方法记录

    转载加自己整理的部分内容,转载自:http://jiajun.iteye.com/blog/810150 记录下JConsole使用方法 一.JConsole是什么    从Java 5开始 引入了 ...

  10. DNS安全浅议、域名A记录(ANAME),MX记录,CNAME记录 专题

    首先要做的就是配置域名的MX 记录啦: 先添加一条A记录: mail.abc.com 指向 你的服务器IP! 然后添加域名的MX 记录,指向mail.abc.com A 记录( 即域名MX 记录的值为 ...

随机推荐

  1. django之model,crm操作

    一.字段 AutoField(Field) - int自增列,必须填入参数 primary_key=True BigAutoField(AutoField) - bigint自增列,必须填入参数 pr ...

  2. [SPDK/NVMe存储技术分析]012 - 用户态ibv_post_send()源码分析

    OFA定义了一组标准的Verbs,并提供了一个标准库libibvers.在用户态实现NVMe over RDMA的Host(i.e. Initiator)和Target, 少不了要跟OFA定义的Ver ...

  3. IIS短文件猜解

    1.IIS短文件漏洞 Microsoft IIS 短文件/文件夹名称信息泄漏最开始由Vulnerability Research Team(漏洞研究团队)的Soroush Dalili在2010年8月 ...

  4. ESP8266 NodeMCU引脚说明,CH340和CP2102两款

    开发ESP8266 NodeMCU GPIO功能时,虽然知道ESP8266 NodeMCU丝印引脚编号如下图所示(CP2102款),但是和实际对应的GPIO编号完全不一样.   CP2102款 引脚说 ...

  5. Spring核心思想:IOC(控制反转)、DI(依赖注入)和AOP(面向切面编程)

    Spring有三大核心思想,分别是控制反转(IOC,Inversion Of Controller),依赖注入(DI,Dependency Injection)和面向切面编程(AOP,Aspect O ...

  6. Java线程通信

    Java线程通信 螣蛇乘雾,终为土灰. 多个线程协同工作完成某个任务时就会涉及到线程间通信问题.如何使各个线程之间同时执行,顺序执行.交叉执行等. 一.线程同时执行 创建两个线程a和b,两个线程内调用 ...

  7. PHP--腾讯云服务器上部署LNMP环境

    本文参考 云服务器上部署Laravel的实例教程 腾讯云 创建 Laravel5.4 项目 阿里云服务器部署php的laravel项目,在阿里云买ECS 搭建 Linux+Nginx+Mysql+PH ...

  8. Notion-douan:搭建自己的阅读清单

    前言 交完论文盲审稿,终于从接近一年的实习.秋招和论文的忙碌中闲下来. 在复盘秋招的时候发现自己虽然看过不少书,但缺少整理和思考,所以想趁这个机会梳理一下自己的阅读习惯,希望以后再读新的东西可以更系统 ...

  9. SpringBoot+Vue+mysql 搭建(二)node 和gulp环境的设置

    安装node不再写    gulp 参考以下文档 https://blog.csdn.net/a599174211/article/details/82878095 1.使用npm命令安装,一下两个命 ...

  10. mybatis基础(全)

    参考链接:Mybatis学习系列(一)入门简介 Mybatis学习系列(二)Mapper映射文件 Mybatis学习系列(三)动态SQL Mybatis学习系列(四)Mapper接口动态代理 Myba ...