4.4 容器云上的数据库定制化方案
在本节中,我们将介绍MySQL在容器云上的定制化。下面先简单了解一下分布式数据库的发展。
4.4.1 分布式数据库的发展
数据库大致经历了三个发展阶段:RDBMS(2008年以前)→NoSQL(2008—2013年)→NewSQL(2013年以后)。
2008年的数据库的数量级都是GB级别。RDBMS解决了很多复杂的问题,如join操作、主外键等。随着互联网高速发展,数据量增长速度加快,RDBMS无法线性扩展,NoSQL应运而生。
NoSQL的全称是Not Only SQL。它的优势包括海量扩展能力、读写高性能,并且可以与关系型数据库相辅相成。
NoSQL有以下几类产品。
- 键值(KV)存储型数据库:Memcached、Redis。
- 列存储型数据库:Cassadra、HBase。
- 图形数据库(Graph):Neo4J、InfoGrid、Infinite Graph。
- 文档型数据库:MongoDB、CouchDB。
MongoDB被大量应用在容器上,其典型应用场景如下。
- 基于位置的移动搜索应用(基于自身地理空间索引)。
- 日志分析平台(MongoDB自带高性能的聚合框架)。
- 可以存储简历或投递关系等相对复杂的数据结果,如简历库。
- 存储用户数据、帖子信息。
NewSQL同时满足NoSQL以及在线交易事务,是分布式数据库的发展方向。简单而言:NewSQL=RDBMS(ACID)+SQL+NoSQL(扩展性)。
NewSQL的主要目的是替换传统数据库的分库分表,解决如MySQL+MongoDB的大数据性能瓶颈、业务层逻辑复杂度的增加(多维度映射)、运维成本高(MySQL 5.5不支持Online DDL操作)、故障切换时间长(30s至1min)、MHA/MMM二次开发等问题。
NewSQL目前有以下几种。
- Google的Spanner/F1,不开源。
- TiDB。
- CockroachDB:百度在用。
- OceanBase。
- 华为的GaussDB。
- 腾讯的TDSQL。
在以上几种NewSQL数据库中,TiDB被广泛使用,尤其是在容器云上。TiDB已经针对OpenShift发布了TiDB Operator。需要注意的是,TiDB无法直接参与到分布式事务中,它只是在写数据的时候可以强一致地写到多个节点中,即它是按照处理本地事务的方式实现分布式写数据。TiDB不支持MySQL的存储过程、触发器等。TiDB高度兼容MySQL协议,支持基于Raft算法的多副本复制。
4.4.2 MySQL的复制与高可用
MySQL是一个轻量级数据库,在容器云上部署时,有的客户会选择购买第三方服务商的MySQL容器化服务和产品,有的客户会自行构建基于容器云的MySQL集群。
要实现MySQL上容器云,需要关注MySQL两方面的技术。
- 复制技术:主从复制、半同步复制、组复制。
- 高可用技术:MHA、MGR、MySQL InnoDB Cluster。
需要指出的是,MySQL的高可用技术依赖于MySQL的复制技术。下面介绍常用的MySQL的复制技术以及高可用技术。
MySQL有多种复制技术,广为使用的复制技术主要有以下三种。
- 主从复制:在这种模式中,主从实例的复制是异步复制,主实例可读写,从实例可读。从库通过I/O线程连接主库,获取主库二进制日志写到本地中继日志,并更新master-info文件(存放主库相关信息),再利用SQL线程执行中继日志。
- 半同步复制是在第一种主从复制的基础上,利用插件完成半同步复制。对于传统的主从复制,不管从库是否正确获取到二进制日志,主库都会不断更新。而对于半同步复制,只有当确认了从库把二进制日志写入中继日志才会允许提交更新,如果从库迟迟不返回ACK确认信息,则主库会自动将半同步复制状态取消,进入最基本的主从复制模式。
- 组复制(MySQL Group Replication,MGR)。MGR是MySQL官方在5.7.17版本引进的一个数据库高可用与高扩展的解决方案,并于2016年12月正式推出。MGR 在原生复制技术之上引入分布式强一致性协议——Paxos,以插件的方式提供。MySQL官方还基于MGR推出了MySQL InnoDB Cluster,为MySQL提供完整的高可用性解决方案。
常见的几种MySQL高可用技术介绍如下。
- MHA(Master High Availability),目前在MySQL高可用方面是一个相对成熟的解决方案,也是一套优秀的MySQL高可用性环境下故障切换和主从提升的高可用软件。MHA依赖于MySQL的主从复制技术。
- 基于MGR实现的高可用。但这种方案的缺点是外部获得状态变更时需要读取数据库,且外部需要使用LVS、VIP配置。
- MySQL InnoDB Cluster。这是目前MySQL最完整的高可用解决方案,它依赖于MGR复制技术。需要注意的是,当前直接在OpenShift上实现MySQL InnoDB Cluster的难度还是比较大的。
接下来,我们介绍如何在OpenShift部署纯开源、原生的MySQL,以及MySQL容器镜像的部署方式,以期对想了解在容器云上部署原生MySQL的读者有一些帮助。
4.4.3 OpenShift提供的MySQL容器镜像
红帽OpenShift提供了MySQL的容器镜像,最新的版本是MySQL 8.0(截至2021年9月)。如图4-78所示,第一个镜像是基于RHEL7实现,第二个镜像是基于RHEL8实现。
图4-78 红帽OpenShift提供的MySQL容器镜像
我们查看第一个rhel8/mysql-80镜像的配置文件的部分内容,如下所示:
FROM ubi8/s2i-core:rhel8.2 ENV MYSQL_VERSION=8.0 \ APP_DATA=/opt/app-root/src \ HOME=/var/lib/mysql RUN yum -y module enable mysql:$MYSQL_VERSION && \ INSTALL_PKGS="policycoreutils rsync tar gettext hostname bind-utils groff-base mysql-server" && \ yum install -y --setopt=tsflags=nodocs $INSTALL_PKGS && \ rpm -V $INSTALL_PKGS && \ yum -y clean all --enablerepo='*' && \ mkdir -p /var/lib/mysql/data && chown -R mysql.0 /var/lib/mysql && \ test "$(id mysql)" = "uid=27(mysql) gid=27(mysql) groups=27(mysql)" COPY 8.0/root-common / COPY 8.0/s2i-common/bin/ $STI_SCRIPTS_PATH COPY 8.0/root /
从上面内容我们可以看出,该镜像是基于红帽RHEL8.2的UBI实现,并且具备S2I的能力。
接着查看容器镜像的Package List,如图4-79所示,可以看到MySQL的版本是8.0.17。
图4-79 rhel8/mysql-80的Package List
在介绍了OpenShift提供的MySQL容器镜像后,接下来我们介绍在OpenShift上部署MySQL的4种方式:
- 以命令行方式部署MySQL;
- 以模板方式部署MySQL;
- 使用S2I方式定制化部署MySQL;
- 使用模板部署MySQL主从复制。
4.4.4 以命令行和模板方式部署MySQL
部署MySQL最简单的方式是使用podman命令行直接部署(这里我们使用基于RHEL7的mysql8镜像进行验证):
#podman run -d --nam mysql_database -e MYSQL_USER=user -e MYSQL_PASSWORD=pass -e MYSQL_DATABASE=db -p 3306:3306 rhscl/mysql-80-rhel7
查看运行的MySQL的容器镜像,结果如图4-80所示。
图4-80 使用命令行直接部署MySQL
但是,在实际生产中直接使用podman命令行运行MySQL容器镜像显然是不合适的。此时,我们可以通过模板方式在OpenShift上部署MySQL。在OpenShift中查看MySQL模板,如图4-81所示。
图4-81 使用模板方式部署MySQL
该模板的yaml文件的地址为https://github.com/openshift/origin/blob/master/examples/db-templates/mysql-persistent-template.json,感兴趣的读者可以自行查阅更多内容。
使用模板传递参数,如图4-82所示。
图4-82 通过模板传递参数
通过模板创建成功后,可以查看Pod的相关信息:
# oc get pods |grep -i mysql1 mysql1-1-4mkc4 1/1 Running 0 37m
登录MySQL Pod查看数据库:
#oc rsh mysql1-1-4mkc4 sh-4.2$ mysql -u root
结果如图4-83所示。
图4-83 查看数据库
通过模板方式部署MySQL很方便,可以传递参数,但这种方式无法实现定制化部署。接下来,我们介绍如何使用S2I方式在OpenShift中定制化部署MySQL。
4.4.5 使用S2I方式定制化部署MySQL
接下来,我们展示如何使用S2I构建镜像的方式实现定制化部署MySQL。该镜像就是本文开头展示的mysql8.0容器镜像。在部署时,我们指定源码地址为https://github.com/sclorg/mysql-container.git。
首先导入红帽MySQL容器镜像的ImageStream,以便后续使用。
#oc import-image rhscl/mysql-80-rhel7 --from=registry.access.redhat.com/rhscl/ mysql-80-rhel7 --confirm –all -n openshift
ImageStream导入成功,如图4-84所示。
图4-84 ImageStream导入成功
查看openshift项目中的MySQL ImageStream:
# oc get is -n openshift NAME MAGE REPOSITORY TAGS UPDATED mysql-80-rhel7 8.0,8.0-13,8.0-14,8.0-15,8.0-18,8.0-20 + 16 more... About a minute ago
接下来,利用Imagestream,使用S2I方式定制化部署MySQL容器镜像:
oc new-app mysql-80-rhel7:8.0-15~https://github.com/sclorg/mysql-container.git \ --name my-mysql-rhel7 \ --context-dir=examples/extend-image \ --env MYSQL_OPERATIONS_USER=opuser \ --env MYSQL_OPERATIONS_PASSWORD=oppass \ --env MYSQL_DATABASE=opdb \ --env MYSQL_USER=user \ --env MYSQL_PASSWORD=pass
查看上面的输入参数--context-dir,将examples/extend-image目录对应的内容注入。接下来,我们分析这个目录中包含的脚本。
查看GitHub上源码examples/extend-image中包含的四个子目录,如图4-85所示。
图4-85 查看examples/extend-image子目录
接下来,我们分析四个子目录的内容。
1)mysql-cfg目录下包含myconfig.cnf脚本文件,该脚本的内容如下:
[mysqld] stored_program_cache = 524288
启动MySQL容器时,myconfig.cnf将作为mysqld守护程序的配置注入。
2)mysql-pre-init目录中包含两个Shell脚本,80-add-arbitrary-users.sh和90-init-db.sh,二者是在启动mysqld守护程序之前执行的。脚本中使用了如下变量。
- $ {mysql_flags}变量,在脚本连接到本地运行的守护程序时使用。
- $ MYSQL_RUNNING_AS_MASTER变量,在使用run-mysqld-master命令运行容器时定义。
- $ MYSQL_RUNNING_AS_SLAVE变量,在使用run-mysqld-slave命令运行容器时定义。
80-add-arbitrary-users.sh的内容如下,该脚本用于创建MySQL用户:
create_arbitrary_users() { # Do not care what option is compulsory here, just create what is specified log_info "Creating user specified by MYSQL_OPERATIONS_USER (${MYSQL_OPERATIONS_USER}) ..." mysql $mysql_flags <<EOSQL CREATE USER '${MYSQL_OPERATIONS_USER}'@'%' IDENTIFIED BY '${MYSQL_OPERATIONS_PASSWORD}'; EOSQL log_info "Granting privileges to user ${MYSQL_OPERATIONS_USER} for ${MYSQL_DATABASE} ..." mysql $mysql_flags <<EOSQL GRANT ALL ON \`${MYSQL_DATABASE}\`.* TO '${MYSQL_OPERATIONS_USER}'@'%' ; FLUSH PRIVILEGES ; EOSQL } if ! [ -v MYSQL_RUNNING_AS_SLAVE ]; then create_arbitrary_users fi
90-init-db.sh的内容如下所示,该脚本调用mysql-data/init.sql脚本创建数据库。
init_arbitrary_database() { local thisdir local init_data_file thisdir=$(dirname ${BASH_SOURCE[0]}) init_data_file=$(readlink -f ${thisdir}/../mysql-data/init.sql) log_info "Initializing the arbitrary database from file ${init_data_file}..." mysql $mysql_flags ${MYSQL_DATABASE} < ${init_data_file} } if ! [ -v MYSQL_RUNNING_AS_SLAVE ] && $MYSQL_DATADIR_FIRST_INIT ; then init_arbitrary_database fi
3)mysql-data目录中包含init.sql脚本文件,该脚本用于创建数据库表的SQL语句。如下代码所示,创建名为products的数据库表,并注入数据,该脚本被90-init-db.sh脚本调用。
CREATE TABLE products (id INTEGER, name VARCHAR(256), price FLOAT, variant INTEGER); CREATE TABLE products_variant (id INTEGER, name VARCHAR(256)); INSERT INTO products_variant (id, name) VALUES ('1', 'blue'), ('2', 'green');
4)mysql-pre-init目录中包含80-check-arbitrary-users.sh脚本文件,该脚本用于判断部署MySQL时是否指定参数,如果不指定,则会提示报错。
check_arbitrary_users() { if ! [[ -v MYSQL_OPERATIONS_USER && -v MYSQL_OPERATIONS_PASSWORD && -v MYSQL_ DATABASE ]]; then echo "You need to specify all these variables: MYSQL_OPERATIONS_USER, MYSQL_ OPERATIONS_PASSWORD, and MYSQL_DATABASE" return 1 fi } if ! [ -v MYSQL_RUNNING_AS_SLAVE ]; then check_arbitrary_users
我们查看部署结果,看到MySQL Pod部署成功:
# oc get pods NAME READY STATUS RESTARTS AGE my-mysql-rhel7-1-4v9dk 1/1 Running 0 9m24s my-mysql-rhel7-1-build 0/1 Completed 0 10m my-mysql-rhel7-1-deploy 0/1 Completed 0 9m28s
查看Pod部署的部分日志,我们看到在S2I源码仓库定义的脚本被执行:
=> sourcing 20-validate-variables.sh ... => sourcing 25-validate-replication-variables.sh ... => sourcing 30-base-config.sh ... ---> 01:39:10 Processing basic MySQL configuration files ... => sourcing 60-replication-config.sh ... => sourcing 70-s2i-config.sh ... ---> 01:39:10 Processing additional arbitrary MySQL configuration provided by s2i ... => sourcing 40-paas.cnf ... => sourcing 50-my-tuning.cnf ... => sourcing myconfig.cnf ... => sourcing 80-check-arbitrary-users.sh ... ---> 01:39:10 Initializing database ... ---> 01:39:10 Running /opt/rh/rh-mysql80/root/usr/libexec/mysqld --initialize --datadir=/var/lib/mysql/data ---> 01:39:17 Starting MySQL server with disabled networking ... ---> 01:39:17 Waiting for MySQL to start ... 2020-05-15T01:39:17.779572Z 0 [Warning] [MY-011070] [Server] 'Disabling symbolic links using --skip-symbolic-links (or equivalent) is the default. Consider not using this option as it' is deprecated and will be removed in a future release. 2020-05-15T01:39:17.782325Z 0 [System] [MY-010116] [Server] /opt/rh/rh-mysql80/ root/usr/libexec/mysqld (mysqld 8.0.13) starting as process 86 => sourcing 40-datadir-action.sh ... ---> 01:39:40 Running datadir action: upgrade-warn ---> 01:39:40 MySQL server version check passed, both server and data directory are version 8.0. => sourcing 50-passwd-change.sh ... ---> 01:39:40 Setting passwords ... => sourcing 80-add-arbitrary-users.sh ... ---> 01:39:41 Creating user specified by MYSQL_OPERATIONS_USER (opuser) ... ---> 01:39:41 Granting privileges to user opuser for opdb ... => sourcing 90-init-db.sh ... ---> 01:39:41 Initializing the arbitrary database from file /opt/app-root/src/ mysql-data/init.sql... ---> 01:39:41 Shutting down MySQL ...
部署成功后,连接部署的数据库,验证S2I注入的数据库脚本是否执行成功:
mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | opdb | | performance_schema | | sys | mysql> use opdb; Database changed
我们看到数据库表products被自动创建:
mysql> show tables; +------------------+ | Tables_in_opdb | +------------------+ | products | | products_variant | +------------------+ 2 rows in set (0.02 sec) mysql> select * from products_variant; +------+-------+ | id | name | +------+-------+ | 1 | blue | | 2 | green | +------+-------+ 2 rows in set (0.00 sec)
从上述执行结果可以看出,在部署MySQL时,S2I注入的脚本被执行。
通过使用S2I方式,我们可以实现MySQL的定制化部署,甚至可以把MySQL的复制配置、数据库数据导入的脚本放进去。
4.4.6 使用模板部署MySQL主从复制
MySQL主从复制可以使用模板进行部署。GitHub上有一主一从复制的模板(https://github.com/davidsajare/FSI-IT-construction/blob/main/Chapter4/mysql_replica.json),我们可以基于这个模板进行定制化。需要注意的是,目前这个模板只能在一个OpenShift集群内部署MySQL主从复制,而不能跨OpenShift集群部署主从复制。
在OpenShift中借鉴该模板部署MySQL主从复制,首先创建MySQL模板:
#oc apply -f mysql_replica.json -n openshift template.template.openshift.io/mysql-replication-example created # oc get template NAME DESCRIPTION PARAMETERS OBJECTS mysql-replication-example MySQL Replication Example 8 (3 generated) 6
在OpenShift UI界面,选择创建好的模板,如图4-86所示。
图4-86 选择创建好的模板
传递部署MySQL的参数,如图4-87所示。
图4-87 使用MySQL模板传递参数
Pod部署成功的效果如下所示:
# oc get pods NAME READY STATUS RESTARTS AGE mysql-master-1-deploy 0/1 Completed 0 8m16s mysql-master-1-fdb9l 1/1 Running 5 8m13s mysql-slave-1-deploy 0/1 Completed 0 8m16s mysql-slave-1-zqjc5 1/1 Running 5 8m12s
分别查看Primary和Replica pod的日志,可以看出主从复制关系已经建立。
Primary pod: # oc logs -f mysql-master-1-fdb9l 2020-05-15T06:01:28.281730Z 0 [Note] /opt/rh/rh-mysql57/root/usr/libexec/mysqld: ready for connections. Version: '5.7.24-log' socket: '/var/lib/mysql/mysql.sock' port: 3306 MySQL Community Server (GPL) 2020-05-15T06:01:35.879641Z 3 [Note] Start binlog_dump to master_thread_id(3) slave_server(3672989091), pos(, 4) Replica pod: # oc logs -f mysql-slave-1-zqjc5mysql-slave-1-zqjc5 2020-05-15T06:01:35.875170Z 2 [Note] Slave I/O thread for channel '': connected to master 'master@mysql-master:3306',replication started in log 'FIRST' at position 4
查看MySQL的Binlog状态,显示正常,如图4-88所示。
图4-88 查看MySQL Binlog状态
至此,我们完成了MySQL主从复制在OpenShift上的部署。如果想实现更为复杂的部署方式,可以基于本节的模板进行修改。
4.4.7 MySQL主从复制的限制与不足
如前文所述,由于MySQL主从复制比较容易实现,因此被广泛应用于企业中。但需要注意的是,主从复制是异步复制,存在丢数据的可能,而数据滞后取决于网络速度。如果数据不一致,就需要手动修复,前提是主库的Binlog要备份或放置到持久化存储里,以避免数据丢失。
MySQL主从复制可能出现以下几种数据不一致的情况。
1)从库出现问题:例如从库所在容器云的集群出现硬件故障。
2)主库出现问题:例如主库所在容器云的集群出现硬件故障。
3)主从库之间的复制出现问题:由于网络延迟,造成主库的Binlog没有及时复制到从库中。
针对第一种情况,直接重新创建从库,与主库建立新的主从关系即可。
针对第二种情况,首先中断复制关系,将从库提升为主库接受业务请求,将Binlog复制到新的主库,等到两个库数据一致时,再恢复主从关系。
针对第三种情况,首先查看主库和从库的状态,如果从库的状态是No,说明当前从库未同步,要是主从库数据相差不大,开启命令同步数据就可以了。如果数据相差较大,就需要手工复制Binglog,手工同步数据。
针对MySQL跨容器云的方案,比较好的做法是做读写分离,这样不仅可以提升性能,还可以降低主从库之间数据同步的压力。即主库做读写、从库做只读。为了提升性能,我们可以采用一主多从的方案。那么当Master挂了以后,选择哪个Slave提升为主库?可以根据哪个从库上同步的数据多,就将其提升为主库,如图4-89所示。
图4-89 MySQL的主从切换
4.4.8 CDC方案的选择
在容器云中的MySQL主从复制通常不能跨容器云实现。所以在更为复杂的跨数据中心场景中,我们可以考虑使用CDC(Change Data Capture,变化数据捕获)方案。即获取MySQL的变化,然后将这些信息导入消息中间件中,从而使另外一个数据中心的业务消费端可以消费这些数据。
目前开源社区提供了以下几种CDC方案,各方案对比如表4-6所示。
表4-6 开源社区几种CDC的方案对比
综合对比下,我们推荐使用Debezium方案。Debezium是一个由红帽赞助的开源项目,它为CDC提供了一个低延迟的流式处理平台。我们可以安装并且配置Debezium去监控数据库,从而使应用可以消费对数据库的每一个行级别(row-level)的更改。在这种工作模式下,只有已提交的更改才是可见的,所以我们不用担心事务(transaction)或者更改被回滚(roll back)。
Debezium为所有的数据库更改事件提供了一个统一的模型,所以应用不用担心每一种数据库管理系统的错综复杂性。另外,由于Debezium用持久化的、有副本备份的日志来记录数据库数据变化历史,因此,应用可以随时停止再重启,而不会错过它停止运行时发生的事件,保证了所有事件都能被正确、完全地处理掉。
监控数据库,并在数据变化的时候获得通知一直是很复杂的事情。虽然关系型数据库的触发器可以做到,但是它只对特定的数据库有效,而且通常只能更新数据库内的状态(无法和外部的进程通信)。一些数据库虽然提供了监控数据变化的API或者框架,但是暂没有形成一个统一的标准,每种数据库的实现方式都是不同的,我们需要掌握大量特定的知识和理解特定的代码才能很好地运用。所以,确保以相同的顺序查看和处理所有更改,同时最小化影响数据库仍然非常具有挑战性。
Debezium利用Kafka和Kafka Connector实现了自己的持久性、可靠性和容错性。每一个部署在Kafka Connector的分布式、可扩展、容错性服务中的Connector负责监控一个上游数据库服务器,捕获所有的数据库更改,然后记录到一个或者多个Kafka Topic(通常一个数据库表对应一个Kafka Topic)中。Kafka可以确保所有这些数据更改事件都能够多副本并且总体上有序(Kafka只能保证一个Topic的单个分区内有序),这样,更多的客户端就可以独立消费同样的数据更改事件,并将对上游数据库系统造成的影响降到很低(如果N个应用都直接去监控数据库更改,则对数据库的压力为N,而用Debezium汇报数据库更改事件到Kafka,所有的应用都去消费Kafka中的消息,可以将对数据库的压力降到1)。另外,客户端可以随时停止消费,然后重启,从上次停止消费的地方继续消费。每个客户端可以自行决定它们是否需要exactly-once(严格一次)或者at-least-once(最少一次)消息交付语义保证,并且所有的数据库或者表的更改事件是按照上游数据库发生的顺序被交付的。
如果不需要这种容错级别、性能、可扩展性、可靠性的应用,也可以使用内嵌的Debezium Connector引擎直接在应用内部运行Connector。虽然这种应用仍需要消费数据库更改事件,但我们更希望将Connector直接传递给它,而不是持久化到Kafka里。
Debezium支持的数据库有MySQL、PostgreSQL、MongoDB、Microsoft SQL Server、Oracle、Apache Cassandra。
Debezium依赖于Kafaka。通过Debezium可以捕获容器化MySQL的变化数据,其架构图如图4-90所示。
图4-90 Debezium捕获MySQL变化数据的架构图
目前Debezium支持的插件如图4-91所示。
图4-91 Debezium支持的插件
如果想了解更多Debezium的具体实现效果,请关注“大魏分享”公众号中的文章《CDC(变化数据捕获)在Kubernetes上的实现》。