Galera Cluster能否使用Online DDL?

MySQL 从5.6版本开始支持Online DDL,即允许表在schema变更过程中继续让业务读写,最大程度减少部分DDL操作的锁表时间。Online DDL的出现给经历过MySQL老版本的人提供了极大的便利,而如果基于MySQL的PXC(Percona XtraDB Cluster)使用了5.6以上版本,能否也收益于Online DDL,加快大表变更速度呢?本文将探讨下Galera Cluster使用Online DDL可行性以及以PXC为例如何处理DDL。

Online DDL Overview

5.6版本之前,DDL执行过程是:获取metadata lock,全量拷贝数据至内部新表(rebuild table),拷贝完内部互换并删除老表;整个数据拷贝期间,业务无法写表,锁表时间取决于表大小。

5.6+版本分几种类型的DDL来优化操作时间:

  1. 操作只需要修改metadata,不需要拷贝全表数据;比如更改列comment/default value
  2. 操作不需要修改metadata,也不需要rebuild table;比如加二级索引
  3. 需要修改metedata+rebuild table的,则本地拷贝源表数据,拷贝完执行应用变更日志,这期间业务可写;如加列

从类型上说,Online DDL只支持部分DDL,不过已能够覆盖绝大部分日常变更。无外乎加列、加索引……

DDL on Galera Cluster

PXC支持多点写入,任何时刻多个节点之间表数据一致。Galera使用了Certification Based Replication的方式进行多节点数据同步:

Certification Based Replication

  1. 单个节点事务提交时,将修改的事务信息以write-set形式广播给其他节点
  2. 其他节点收到write-set后,通过主键、全局ID等信息判断是否和自身已有的事务冲突
  3. 如果有一个冲突,发起提交的节点提交失败,事务回滚;否则提交成功,所有节点写入write-set

Galera集群会在事务发起之前,为每个事务分配一个全局唯一序列号,在提交时比对该序列号和最后一次成功提交之间的差值;如果这个期间有事务进行,并且和该事务主键相同,则相当于发生冲突,事务不能提交。因为其他节点发起的事务其实并不知晓这个事务,直接提交则会发生覆盖更新。这种认证方式旨在保证同样主键数据在同一事务窗口内不能在其他节点并发,从而确保数据一致。

不过该方式只针对DML,DDL的处理方法则相对”粗暴“的多。为了保证全局表的schema一致,Galera使用了特殊的方法来处理DDL,目前有TOI(default)、RSU两种模式。

使用PMM快速构建MySQL监控系统

PMM是Percona开源的监控系统(Percona Monitoring and Management),主要用于监控MySQL、MongoDB等数据库的性能指标,目前最新版本是1.11.0。工作原因对其进行了一段时间的调研,本文将介绍如何在生产环境使用PMM快速构建MySQL数据库监控系统,包含基本原理、部署和优缺点比较等。

Architecture

关于PMM的架构,最主要是理解它是个基于Prometheus的系统,PMM的一些组件相当于对Prometheus中各个模块的封装,架构图如下:

PMM Architecture Overview

结构分成了client-server部分:

  • Client:包含了各种exporter的daemon,以及用于管理他们的pmm-admin命令行工具
  • Server - Metrics Monitor:本地存储的Prometheus、consul用于服务发现、Grafana出图;这块都是Prometheus技术栈
  • Server - Query Analytics:Percona自研的查询分析组件,用于分析在DB端收集的慢查询,存储于MySQL中,最终以新的datasource注册至Grafana出图展示
  • Server - Orchestrator:第三方工具orchestrator,图形化展示和管理MySQL实例的复制拓扑关系

OSC工具如何处理外键约束

首先不讨论MySQL 5.6+版本的Online DDL特性,因为它可以比较好的兼容外键表,这里说的是OSC工具操作外键表的场景,比如使用pt-online-schema-change/gh-ost/Facebook-OSC……

以上所有的工具都绕不开一个关键的cut-over环节,即用已经修改好表结构的“影子”表替换原表,完成新旧表结构变更。以A表的OSC为例,待切换表为B,这个阶段的操作就是:rename A to A_BAK, B to A。

那如果A是一个有外键关联的表会发生什么:

  1. A为外键的父表:rename操作之后所有A的子表的FK都指向了A_BAK表,而A_BAK表数据已经静止,因此需要把rebuild所有子表的FK(重新指向A),这个操作影响程度视子表数量递增。
  2. A为外键的子表,创建B表时,MySQL在schema层级限制外键约束名称唯一,因此B的FK名称必须建个和A不一样的。这样切换后,A的FK定义相对旧A,名称不同,引用(REFERENCE)相同。

可以看到第一种场景重建所有子表索引代价非常大,而且这个操作最好是在rename完成后尽快进行,否则如果这个时候子表的写入过来,极有可能会因为子表外键指向了A_BAK导致失败,影响业务(pt-online-schema-change --alter-foreign-keys=rebuild_constraints)。同时假设子表数量很多,或者外键约束关系很复杂,这种变更相当于”牵一发动全身“。

相比之下,第二种情况没有额外操作容易了不少,只是A的外键定义变了,影响逻辑checksum的结果。

看下OSC工具实际怎么支持这两种:

pt-online-schema-change:两种都支持,第一种会在rename后自动rebuild所有子表的FK,但是这期间也可能出现上述的写入失败问题,虽然概率相对小,不过仍有风险。第二种场景则是直接在rename后表的FK名称前面增加一个下划线前缀,以示区分,绕过MySQL的限制。

gh-ost则是两种场景都不支持,见到外键直接报错,当然你可以用它来给表去掉外键……

记一个GTID复制bug

MySQL基于GTID的复制强调主从库GTID的连续和一致,比如主库如果执行了1-4,那从库IO线程拿到1-4的变更后,不管有没有被SQL线程过滤,从库的GTID也得保留1-4。

这就会出现一个现象:假定从库使用MySQL的过滤规则不执行主库发送的一部分变更(配置replication-wild-do-table=A.%,GTID=uuid:2被过滤),那MySQL这个时候会在从库用空事务的方式填充binlog,从而保证uuid:2在从库不丢失,主从“GTID”一致。也就是说主从数据可以不一致,但是GTID一致。印证了“从GTID连续一致指标判断主从一致”的依据是不正确的。

那MySQL不做这个填充,会发生什么?一个是从库出现很多”GTID空隙“,Executed_Gtid_Set变得特别长。再者,如果某个期间你期望通过MASTER_AUTO_POSITION=1切换新主库,新主库一看从库缺了这么多”空隙“,那从自己的binlog里面找到发给从库让它补上,结果就是从库依旧过滤这些变更不执行,依旧补不上”空隙“。更多的时候切换的新主库会因为binlog rotate原因,包含“空隙GTID”的binlog已不存在,CHANGE MASTER持续报1236错误,切换不成功,非常恼火。

既然DBA显式指定的跳过一部分表的变更,GTID也得保持主库连续。从以往的使用经验来看,MySQL确实也是这么做的,而这次碰到的bug则是这个地方MySQL实现的不完善引起的。

BUG简述:在使用过滤选项进行GTID主从复制时,主库的CREATE DATABASE, ALTER DATABASE & DROP DATABASE变更如果在从库被过滤,该GTID将不会在从库Executed_Gtid_Set中保留。

判断GTID切换是否会出现Error:1236问题

先从线上的一个CASE说起,在使用GTID的AUTO_POSITION=1模式切换主库时,遭遇Error: 1236错误:

2018-05-08 14:45:52 26147 [ERROR] Slave I/O: Got fatal error 1236 from master when reading data from binary log: ‘The slave is connecting using CHANGE MASTER TO MASTER_AUTO_POSITION = 1, but the master has purged binary logs containing GTIDs that the slave requires.’, Error_code: 1236

错误日志的说明是:从库需要一部分GTID集合来补齐数据,而缺失的部分恰好被主库purged了,所以没法建立复制关系。但是缺失的GTID集合是哪些,怎么计算出来的没说。

从MySQL代码里面找下线索,先从官方文档里得知Error:1236对应错误ER_MASTER_FATAL_ERROR_READING_BINLOG,然后定位到sql/rpl_master.cc文件1007行,MySQL版本:5.6.39:

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
29
30
31
32
33
/*
Setting GTID_PURGED (when GTID_EXECUTED set is empty i.e., when
previous_gtids are also empty) will make binlog rotate. That
leaves first binary log with empty previous_gtids and second
binary log's previous_gtids with the value of gtid_purged.
In find_first_log_not_in_gtid_set() while we search for a binary
log whose previous_gtid_set is subset of slave_gtid_executed,
in this particular case, server will always find the first binary
log with empty previous_gtids which is subset of any given
slave_gtid_executed. Thus Master thinks that it found the first
binary log which is actually not correct and unable to catch
this error situation. Hence adding below extra if condition
to check the situation. Slave should know about Master's purged GTIDs.
If Slave's GTID executed + retrieved set does not contain Master's
complete purged GTID list, that means Slave is requesting(expecting)
GTIDs which were purged by Master. We should let Slave know about the
situation. i.e., throw error if slave's GTID executed set is not
a superset of Master's purged GTID set.
The other case, where user deleted binary logs manually
(without using 'PURGE BINARY LOGS' command) but gtid_purged
is not set by the user, the following if condition cannot catch it.
But that is not a problem because in find_first_log_not_in_gtid_set()
while checking for subset previous_gtids binary log, the logic
will not find one and an error ER_MASTER_HAS_PURGED_REQUIRED_GTIDS
is thrown from there.
*/
if (!gtid_state->get_lost_gtids()->is_subset(slave_gtid_executed))
{
errmsg= ER(ER_MASTER_HAS_PURGED_REQUIRED_GTIDS);
my_errno= ER_MASTER_FATAL_ERROR_READING_BINLOG;
global_sid_lock->unlock();
GOTO_ERR;
}

从代码和注释内容可以了解到,从库executed+retrieved的gtid集合如果不完全包含主库purged的gtid集合,则说明从库无法从主库binlog中拿到想要的gtid集合。转换下即是主库与从库executed gtid的差集,如果已经在gtid_purged集合中,即无法建立同步,发生1236错误。

因此,可以在主库通过下面方法提前判断切换到新主库时是否会出现1236错误:

  1. 去从库停止同步后的gtid_executed集合,记为slave.gtid_executed
  2. 新主库计算gtid_subtract(@@global.gtid_executed, slave.gtid_executed)结果
  3. 对于2步结果,每个uuid的gtid集合,计算gtid_subset(set, @@global.gtid_purged),为1即出错

伪代码如下:

1
2
3
4
5
6
7
for set in `gtid_subtract(@@master.gtid_executed, @@slave.gtid_executed)`; do
if gtid_subset(set, @@master.gtid_purged); {
return ER_MASTER_HAS_PURGED_REQUIRED_GTIDS
}
done
return 0

gh-ost无损cut-over方案

gh-ost作为Github开源的在线表结构修改工具,工作原理上和社区已有的OSC工具基本相似,概括来说:

  1. 主库创建和旧表相同schema的空表,alter table table_new…
  2. 导入源表数据,追加增量(gh-ost使用的binlog回放)
  3. 等待“恰当”的时机,rename table完成新旧表的互换(cut-over)

gh-ost overview

除了提供了一系列DBA友好的配置/操作方法外,gh-ost另一个亮点在于和Facebook OSC相比,它提供了默认无损的cut-over方案,后者则可能在这期间出现一定的请求失败(table not found)

Quick Manual of mysqlbinlog

In case of Database Recovery after unexpected data corruption, list some commonly used scenarios of mysqlbinlog for processing binary log files.

Display Row Events

Convert binary events to SQL statements, DDL output will be displayed as a raw statement, DML output will be presented as lines beginning with ###, those are “pseudo-SQL” statements which are not executable.

For DML pseudo-SQL statements, they do not correspond exactly to the original SQL statements that generated the events, The original column names are lost and replaced by @*N*, where N is a column number.

Also if you are using multi-character set data, those character set information is not available in the display.

为DirectAdmin站点开启HTTPS

自从把博客切成静态站点后访问速度秒WordPress几条街,所以顺势也就把网站上了HTTPS,目前看来效果不错,看着浏览器的绿🔓感觉莫名的安心。

本文介绍如何为部署在DirectAdmin上的站点开启HTTPS访问。

申请SSL证书

既然是免费SSL证书,第一个想到的就是Let's Encrypt,DirectAdmin在1.5版本开始就集成了Let's Encrypt服务,所以在面板上点点就能拿到一个永久的SSL证书,对于在VPS上部署DirectAdmin的人来说真是lucky。

然而我的站点还架在老版本DirectAdmin的虚拟空间上,只能自己找SSL证书了……

目前了解到的BAT三家、七牛等国内云服务商都可以申请到1年免费的SSL证书,来自Symantec。对于个人站点来说,使用这类免费证书都是推荐的。要注意有一些免费的证书提供商今年因为安全等原因相继被各个浏览器弃用,所以申请的时候要优先考虑公信力高的品牌和机构。

Discourse国内建站攻略

最近帮朋友建了个关于数据科学的交流论坛。说实话,近几年运营社区相当不易,网站推广、内容筛选、站务管理一堆麻烦事,不免勾起了学生时代论坛撕逼的黑历史,而当年技术圈的“知名”论坛,现在很多沦为了广告集中地,内容质量已大不如前,操作习惯上也还停留在十年前,体验和SEO都很糟糕。

一开始我是拒绝的,直到某土豪一言不合的买ECS,后来心想不如借此熟悉下ECS和Discourse,一番折腾后论坛上线了:DataGeekers

当然本文只记录建站方法,不讲论坛😶

database/sql包使用须知

Golang的database/sql包为SQL类数据库提供了通用的API操作,它简化了对不同数据库driver的依赖,总体来说是使用简单、操作可靠同时维持了代码的扩展性,不过在使用过程中仍然有一些陷阱和误区,本文就记录下使用database/sql过程中的一些注意点,穿插一部分实现。文中部分例子来自database/sql的使用说明,示例Go版本1.6。

database/sql包的结构其实就分为驱动接口(driver)和DB操作(sql)两部分。源码并没有实现任何一种数据库的驱动,提倡以实现database/sql/driver包的形式来注册第三方的DB驱动,对开发者尽可能的屏蔽特定数据库的feature,算是go的一贯风格吧;如果你使用的SQLite、MySQL、PG这样的数据库,直接使用database/sql就可以完成绝大部分DB操作。

关于驱动

如上,数据库的驱动是第三方实现的,在Go里面,通过匿名导入的方式注册一个附加的driver这种方式很常见,下面就是MySQL的注册方式: