考虑到Galera Cluster处理DDL的特殊性,在PXC(Percona XtraDB Cluster)在线变更表结构,可选的方案非常有限。官方是建议使用pt-online-schema-changed的(后面简称为pt-osc)。鉴于都出自Percona,pt-online-schema-change无疑对PXC的兼容程度最好,比如支持监控flow control状态、Multi-Master配置。

不过实际使用起来,pt-osc在DBA友好程度上还是差gh-ost一大截,gh-ost可以动态参数调整、cut-over postpone,这些对于操作业务繁重的线上系统来说,还是多了些退路。如果你的DB系统使用了PXC,同时又对ps-osc产生的触发器、写流量翻倍有巨大的担忧,不妨考虑下gh-ost on PXC的路线。本文将包含这种方式的使用场景和用法。

PXC上使用gh-ost有几点区别需要特别注意:

一、有损切换

pt-osc的cut-over逻辑非常简单:RENAME A TO A.OLD, B TO A,这条语句可以保证新旧表在互换的时候是原子操作,即便是PXC多写模式下,RENAME也可以保证执行期间多节点数据一致;这样写入在不同Master节点上短暂阻塞后继续写向新表,对业务基本无感知。

gh-ost相比则’复杂’不少,按照之前描述的逻辑:

gh-ost完成cut-over只需要两个DB链接,下面以C10, C20标识,正常的写入请求标识为C1..C9, C11..C19, C21..C29。tbl为旧表,ghost表为已经schema changed的新表。

  1. C1..C9:对 tbl 进行正常的DML操作,包含 INSERT, UPDATE, DELETE
  2. C10:CREATE TABLE tbl_old (id int primary key) COMMENT='magic-be-here'
  3. C10:LOCK TABLES tbl WRITE, tbl_old WRITE
  4. C11..C19:新请求,对 tbl 的DML操作被LOCK阻塞
  5. C20: RENAME TABLE tbl TO tbl_old, ghost TO tbl - 该语句依旧被LOCK阻塞,但是在阻塞的队列中,优先级高于C11..C19, C1..C9,以及任何尝试对tblDML的操作
  6. C21..C29:新请求,期望操作tbl,但依旧被LOCK, RENAME阻塞
  7. C10:通过show processlist检测到C20的RENAME操作已经发起(处于blocked状态)
  8. C10:DROP TABLE tbl_old - tbl依旧被locked,所有链接仍然处于阻塞状态
  9. C10:UNLOCK TABLES - RENAME第一个被执行,ghost表被提为tbl,紧接着C1..C9, C11..C19, C21..C29的请求直接发至”新“表tbl

译自:https://github.com/github/gh-ost/issues/82

注意步骤3,LOCK TABLE操作在PXC上存在限制,PXC 5.7版本pxc_strict_mode not in [PERMISSIVE, DISABLE]上,LOCK TABLE是直接报错退出;除此之外,PXC本身不会复制LOCK TABLE语句到其他master节点,gh-ost也不会这么做,这就导致其他节点如果有写入,gh-ost的切换数据一致性无法保证。因此gh-ost只能用于单节点写入的PXC集群,并且不能使用pxc_strict_mode

再注意看步骤8,这个切换过程中,C10需要进行DROP TABLE才能继续后面的unlock操作,从而保证整个切换一致,普通版本MySQL上,C10的步骤8是执行没问题的;但是在PXC上就不行,原理在上一篇文章中有描述:“PXC的TOI模式下DDL必须串行执行,即使前一条DDL也是处于阻塞状态”。所以之前C20在步骤5发起过RENAME TABLE操作了,步骤8这里DROP TABLE就会被PXC阻塞,处于Preparing for TO isolation。结论即是gh-ost默认的无损切换方式不可用于PXC集群。

当然gh-ost还提供了个选项two-step,即facebook OSC的切换方式,步骤如下:

  1. C10: LOCK TABLES tbl WRITE
  2. C20: 从binlog继续追加增量,保证tbl与ghost表一致
  3. C10: ALTER TABLE tbl TO tbl_old;
  4. C10: ALTER TABLE ghost TO tbl;
  5. C10: UNLOCK TABLES;

two-steps将切换方式变成了单线程两次单独的ALTER TABLE操作,很显然这种情况下,在LOCK后到新表被ghost替换之前,业务是写不到tbl表的,相当于有损切换,尽管这个时间通常是非常短暂的……

综上,gh-ost只能用于单节点写入关闭pxc_strict_mode的PXC集群cut-over方式需要使用two-step

二、限速

通常用了PXC的集群,多半不会PXC master节点提供所有业务读写,master下面继续挂从库提供业务读很普遍,osc工具在执行的时候也必须考虑他们的吞吐量瓶颈。gh-ostpt-osc都可以从多个方面来限制osc的执行速度,以下是这两个工具都提供的:

  1. max-lag: 配置允许从库延迟的时间,超过时间则暂停
  2. max-load: 配置主库允许的最大运行连接等系统变量,超过同样暂停
  3. chunk-size: 一次从旧表复制到新表的行数
  4. chunk-time: 每次复制一个chunk数据到新表的时候,sleep指定的时间;

限速策略上,两个工具都很全面了,尤其像chunk-time(gh-ost上叫做nice-ratio)这样的选项能够很大程度减小粗暴的chunk-size + max-lag配置引起的从库延迟。不过有一点pt-osc做的更好,写入过多很可能引起PXC节点之间发生flow control,pt-osc可以通过--max-flow-ctl这样的配置进行识别限速,而gh-ost就不行了。用gh-ost的话一定要注意,只能通过其他方式来避免了,比如调小max-lag或者增加监控。

实践

下面就是一个可以用的gh-ost操作PXC命令,已经过线上验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
./gh-ost \
--max-load=Threads_running=100,Threads_connected=500 \
--critical-load=Threads_running=200 --critical-load-interval-millis=60000 \
--throttle-control-replicas="slave01.com,slave02.com" \
--chunk-size=100 \
--heartbeat-interval-millis=100 \
--max-lag-millis=10000 \
--dml-batch-size=100 \
--nice-ratio=0.1 \
--port=3306 \
--host=pxcread.com \ # 用PXC的只读节点作为源
--assume-master-host=pxcmaster.com \ # 最终要操作cut-over的是PXC的可写节点
--replica-server-id=99999 \
--database="test" \
--table="test" \
--alter="engine=innodb" \
--allow-master-master \
--verbose \
--switch-to-rbr \
--cut-over=two-step \ # 必须用two-step!!!
--exact-rowcount \
--concurrent-rowcount \
--default-retries=120 \
--skip-foreign-key-checks \ # 用ghost自然不能用外键
--discard-foreign-keys \
--panic-flag-file=./panic.flag \
--postpone-cut-over-flag-file=./postpone.flag \
--execute

总结下以上的注意点:

在PXC集群上使用gh-ost

  1. PXC 5.6版本可用,或PXC 5.7 with pxc_strict_mode in [PERMISSIVE, DISABLE]
  2. 不支持multi-master的PXC集群,必须只能有单个节点提供写,否则数据不一致
  3. 切换方式必须使用two-step,某着默认的方式会直接hang死PXC集群……
  4. 无法监控flow control情况,需要单独监控或者使用更小的max-lag-millis
  5. 操作的表不能有外键和外键引用
  6. 先线下测试……

虽然gh-ost用起来可控性更强,动态参数也很方便,不过在PXC上还是限制一堆,用起来各种牵强,tradeoff很多。不过反过来想,我们设计DB系统的时候架构其实应该尽可能的简单,比如triggerless, no FK,尽量避免多点写入…… 这样长期看,后续的运维代价会小很多,自然也就不会有本文这样的折腾了……