维多利亚女王时代是聚积的年代;不仅仅是物质财富的聚积,而且是每一个国家能增强国力的所有因素与要素的增加和聚积。教育惠及社会各个阶层。科学打开大自然的无限宝库。宝库之门一扇一扇被打开。阴暗而神秘的宝库一个个被照亮,一个个被开发,任何人都可自由进入——每进入一个宝库,人们都会发现这个宝库至少又通向了另外两个宝库。每天早晨,人们一睁开眼,总能发现某种新机器开始了运转。每天晚上,当人们吃完晚饭之后,机器依旧在运转。在所有人进入梦乡之后机器也不会停止。 --《第一世界大战回忆录》温斯顿丘吉尔
背景
正如《第一次世界大战回忆录》中,丘吉尔对维多利亚时代的评述。人类的进步很大程度上依赖经验的传递与分享。这也是这片文章的目的,把笔者的一些经验积累沉淀下来,分享出去。
维护一个CDC模块是笔者工作(饭碗)的一部分,时不时会有同事问我一些相关的问题,比如:
- 如果我要执行这样的一个DDL,你的服务会突然接收到很多binlog事件吗?
- 如果我要改变一张表的编码格式,你的服务会收到很多binlog事件吗?
人非圣贤,怎么可能知道这么多问题的答案呢?当然是需要验证嘛。本地的验证过程如下:
- 本地启动一个MySQL的服务,
- 跑一个之前写好的测试程序(最近公司组织换电脑了,直接原地爆炸)
- 然后执行上面说诸如DDL之类的变动,再观察程序打印出来的日志,
- 验证完毕。
这样的验证方法也不能说有问题吧,唯一的槽点可能就是环境有时候可能不小心作一下就没了,或者遇到一些不可抗力因素,就乱了。所以我就再想,能不能搞成一个现成的实验环境,想用的时候用一下不用的时候放着就可以,沉淀下来。办法当然是有的啦(不然也不会有这篇文章了。)。
-
把这个实验环境需要的东西全部都打包成一个docker的image
-
再上传到docker hub
-
后面需要用的时候直接操作这个image就好,另外这个也算是分享出去了,别人也可以依赖我这个image去做一些东西。
-
然后弄一个操作文档(也就是这篇文章啦)操作这个image来观察binlog表现就好啦。
说干就干,立马动身去找家星巴克,点上一杯咖啡,打开电脑。。。。
封装实验环境
让我们来梳理一下,这个实验环境里面应该有一个微型的CDC服务,也就是可以捕捉到binlog的变化,然后要有一个MySQL,让我们在上面操作想要做的数据库变动。好,Let‘s go,立马开干。
下面采用go-mysql这个库来捕捉binlog的变动,对于每一个binlog事件我们都打印日志来观察。
package main
import (
"fmt"
"github.com/go-mysql-org/go-mysql/canal"
"github.com/go-mysql-org/go-mysql/mysql"
"github.com/go-mysql-org/go-mysql/replication"
"math/rand"
"os"
"time"
)
type MyEventHandler struct {
}
func (h *MyEventHandler) OnRotate(header *replication.EventHeader, rotateEvent *replication.RotateEvent) error {
fmt.Fprintf(os.Stdout, "reccive a OnRotate event, and the header is %+v,the event is %+v\n", header, rotateEvent)
return nil
}
func (h *MyEventHandler) OnTableChanged(header *replication.EventHeader, schema string, table string) error {
fmt.Fprintf(os.Stdout, "reccive a OnTableChanged event, and the header is %+v schema is %s, table is %s\n", header, schema, table)
return nil
}
func (h *MyEventHandler) OnDDL(header *replication.EventHeader, nextPos mysql.Position, queryEvent *replication.QueryEvent) error {
fmt.Fprintf(os.Stdout, "reccive a OnDDL event, and the header is %+v, the next position is %+v, queryEvent is %+v\n", nextPos, queryEvent)
return nil
}
func (h *MyEventHandler) OnRow(e *canal.RowsEvent) error {
fmt.Fprintf(os.Stdout, "reccive a OnRow event, and the content of this event is %+v\n", e)
return nil
}
func (h *MyEventHandler) OnXID(header *replication.EventHeader, nextPos mysql.Position) error {
fmt.Fprintf(os.Stdout, "reccive a OnXID event, and the header is %+v, the next position is %+v\n", header, nextPos)
return nil
}
func (h *MyEventHandler) OnGTID(header *replication.EventHeader, gtid mysql.GTIDSet) error {
fmt.Fprintf(os.Stdout, "reccive a OnGTID event, and header is %+v, the gtid is %+v\n", header, gtid)
return nil
}
func (h *MyEventHandler) OnPosSynced(header *replication.EventHeader, pos mysql.Position, set mysql.GTIDSet, force bool) error {
fmt.Fprintf(os.Stdout, "reccive a OnPosSynced event, and the header is %+v, the position is %+v, gtidSet is %+v\n, force is %+v", header, pos, set, force)
return nil
}
func (h *MyEventHandler) String() string {
return "MyEventHandler"
}
func main() {
cfg := new(canal.Config)
cfg.Addr = "127.0.0.1:3306"
cfg.User = "root"
cfg.Password = "123456"
cfg.ServerID = uint32(rand.New(rand.NewSource(time.Now().Unix())).Intn(1000)) + 1001
c, err := canal.NewCanal(cfg)
if err != nil {
fmt.Fprintf(os.Stdout, "encounter a error during init canal, and the error is %s", err.Error())
return
}
// Register a handler to handle RowsEvent
c.SetEventHandler(&MyEventHandler{})
// Start canal
c.Run()
select {}
}
通过下面这个Dockerfile分阶段构建我们的镜像,这里第一阶段把Go程序打包成一个可执行文件,然后第二阶段将这个文件拷贝进MySQL镜像中。
FROM golang as buildPrinterStage
WORKDIR /build
COPY ./main /build
RUN cd /build/main & go build -o binlog-printer
FROM mysql
ENV MYSQL_ROOT_PASSWORD=123456
COPY --from=buildPrinterStage /build/binlog-printer /
COPY schema.sql /schema.sql
另外,这里值得注意的是,第二阶段的构建除了拷贝了Go程序的编译产物,还拷贝了一个sql文件进去,这是为什么呢?因为MySQL镜像本身并没有测试数据,这个是我提供提供给用户的,如果你不想自己手动写任何DDL语句构建测试数据库,进入容器导入这个sql文件进MySQL是一个比较好的选择,贴心如我。sql内容如下:
-- 创建数据库
create database `docker_mysql` default character set utf8 collate utf8_general_ci;
use docker_mysql;
-- 建表
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL,
`created_at` bigint(40) DEFAULT NULL,
`last_modified` bigint(40) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`first_name` varchar(255) DEFAULT NULL,
`last_name` varchar(255) DEFAULT NULL,
`username` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- 插入数据
INSERT INTO `user` (`id`, `created_at`, `last_modified`, `email`, `first_name`, `last_name`, `username`)
VALUES
(0,1490257904,1490257904,'elliot@example.com','Elliot','Chen','user');
这时候问题又来了,我的电脑是apple M1芯片,arm架构的,可能后面有朋友是amd架构的电脑,看到我这篇文章突然感兴趣了要玩一把怎么半?那岂不是跑不起来了?问题不大,我们可以直接编译成多平台的镜像上传。使用下面这个命令:
docker buildx build --platform linux/amd64,linux/arm64/v8 -t elliotchenzichang/binlog-exprienment-env . --push
这时候打开我的docker hub。就可以看到,我只打包了amd64和arm64的镜像,相信大多数电脑都是64位的,应该足够了:
好,我们把我的镜像拉下来:
docker pull elliotchenzichang/binlog-exprienment-env:latest
直接给他启动起来:
哎哟,MySQL启动起来了哟,不错哈。让我们进入这个容器看看里面的情况,嗯?这不是我们的老熟人binlog-printer和schema.sql吗?
这个时候可以开始执行binlog-printer开始捕捉binlog变动了。他会打印很多他以前留存下来的binlog,其实不用管这些,我们只需要知道这个是程序在正常运行,一切都ok。
接下来我们再打开一个窗口导入schema.sql到MySQL测试一下他能不能捕获到这些变动。输入我们预设的密码123456就好
好,让我们切换回刚刚那个binlog-printer界面,这时候我们看到这里已经捕捉到了这些变动:
这个实验环境就搭建好啦。Perfect!