1、为什么需要高可用?

高可用HA(High Availability)是为了解决单机故障。

世界上没有任何服务或者说机器是绝对安全且可靠的,但是我们可以通过一定的方案来实现系统的高可用,从一定程度上降低单一系统宕机对整体系统带来毁灭的这种无法接受的风险。这便是高可用所要解决的痛点,分布式系统也是为了解决这个问题。

当然更高级别的高可用应该是需要考虑到并发访问,所以也应该是需要做到负载均衡的。

数据库也不例外,当然需要做到高可用。我们假设系统一直能够提供服务,那么我们就说系统的可用性是100%。如果系统每运行100个时间单位,会有1个时间单位无法提供服务,我们说系统的可用性是99%。很多公司的高可用目标是4个9,也就是99.99%,这就意味着,系统的年停机时间为8.76个小时。

2、数据库中间件

2.1、简介

数据库中间件是一类连接应用程序和数据库的计算机软件,以便于软件各部件之间额沟通,类似于tomcat等web中间件,只是连接的双方不一样而已。

原始的java应用程序直接连接数据库的方式有以下缺点:

1、java程序与数据库紧密耦合

2、高访问量并发量下对数据库产生很大压力

3、读写未分离

所以中间件看起来就像是一个逻辑数据库,真实的物理数据对外是隐藏的。这个思想和Nginx是一样的。

2.2、分类对比,及mycat的重点介绍

Cobar:属于阿里b2b事业群,始于08年,在阿里服役多年。接管3000+mysql数据库的schema集群日志处理,在线sql请求50亿以上,由于Cobar发起人的离职,停止维护。

Mycat:开源社区在阿里Cobar基础上进行的二次开发。并且加入新功能。

oneproxy:给予mysql官方的proxy思想,性能很好但是收费的。

kingshared:基于go语言,需要继续发展,开源的。

当然除此之外,还有很多中间件,例如vitess,atlas,maxscale等等。

可以直观地看到mycat应该是目前最合适的中间件,社区活跃度高切开源。

mycat的功能包括:读写分离、负载均衡、分库分表(数据分片)、多数据源整合。

Mycat原理:Mycat的原理中最重要的一个概念就是“拦截”,它拦截了用户发过来的sql语句,首先对sql语句做了一些特定的分析:如分片分析、路由分析、读写分离分析、缓存分析等等,然后将此sql发送到后端的真实数据库,并将返回的结果做适当的处理,最终再返回给用户。

3、mysql高可用主流架构

3.1、双机高可用

架构:

一台机器A作为读写库,另一台B作为备份库;A库故障后B库作为读写库;A库恢复后A作为备库。

配置:

此种情况下,数据源配置中的数据库IP地址,可采用虚拟的IP地址。虚拟IP地址由两台数据库机器上的keepalive配置,并互相检测心跳。当其中一台故障后,虚拟IP地址会自动漂移到另外一台正常的库上。数据库的主备配置、故障排除和数据补全,需要DBA和运维人员来维护。而程序代码或配置并不需要修改。

适用场景及优缺点:

读和写都不高的场景(单表数据低于500万),双机高可用。优点是一个机器故障了可以自动切换;缺点是只有一个库在工作,读写并未分离,并发有限制。

3.2、主从读写分离

架构:

一台机器A作为写库,另一台B作为读库;A库故障后B库充当读写,A修复后,B库为写库,A库为读库。

配置:

这种方案的实现,要借助数据库中间件Mycat来实现,项目开发中,要配置Mycat数据源,并实现对Mycat数据源的数据操作。数据库A和数据库B应该互为主从。数据库的主主配置、故障排除和数据补全,依然需要DBA和运维人员来维护。

使用场景及优缺点

读和写都不是非常高的场景(单表数据低于1000万),高可用。比方案一并发要高很多。优点是一个机器故障了可以自动切换;读写分离,并发有了很大的提升。缺点是引入了一个Mycat节点,若要高可用需要引入至少两个Mycat。常规的解决方案是引入haproxy和keepalive对mycat做集群。

3.3、一主多从+读写分离

架构:

一个主写库A多个从库,当主库A故障时,提升从库B为主写库,同时修改C、D库为B的从库。A故障修复后,作为B的从库。

配置:

项目开发中需要使用Mycat作为中间件,主库A故障后,Mycat会自动把从B提升为写库。而C、D从库,则可以通过MHA等工具,自动修改其主库为B。进而实现自动切换的目地。

MHA Manager可以单独部署在一台独立的机器上管理多个master-slave集群,也可以部署在一台slave节点上。MHA Node运行在每台MySQL服务器上,MHA Manager会定时探测集群中的master节点,当master出现故障时,它可以自动将最新数据的slave提升为新的master,然后将所有其他的slave重新指向新的master。整个故障转移过程对应用程序完全透明。

使用场景及优缺点

该架构适合写并发不大、但是读并发大的很的场景。由于配置了多个读节点,读并发的能力有了质的提高。理论上来说,读节点可以多个,可以负载很高级别的读并发。当然,Mycat依然需要设计高可用方案。

3.4、MariaDB Galera Cluster

架构:

多个数据库,在负载均衡作用下,可同时进行写入和读取操作;各个库之间以Galera Replication的方法进行数据同步,即每个库理论上来说,数据是完全一致的。

配置:

数据库读写时,只需要修改数据库读写IP为keepalive的虚拟节点即可;数据库配置方面相对比较复杂,需要引入haproxy、keepalive、Galaera等各种插件和配置。

使用场景及优缺点:

该方案适合读写并发较大、数据量不是非常大的场景。

优点:

1)可以在任意节点上进行读2)自动剔除故障节点3)自动加入新节点4)真正并行的复制,基于行级5)客户端连接跟操作单数据库的体验一致\6) 同步复制,因此具有较高的性能和可靠性。

缺点:

1)DELETE操作不支持没有主键的表,没有主键的表在不同的节点顺序将不同2)处理事务时,会运行一个协调认证程序来保证事务的全局一致性,若该事务长时间运行,就会锁死节点中所有的相关表,导致插入卡住(这种情况和单表插入是一样的)3)整个集群的写入吞吐量是由最弱的节点限制,如果有一个节点变得缓慢,那么整个集群将是缓慢的。为了稳定的高性能要求,所有的节点应使用统一的硬件4)如果DDL语句有问题将破坏集群,建议禁用\5) Mysql数据库5.7.6及之后的版本才支持此种方案

3.5、数据库中间件

架构:

采用Mycat进行分片存储,可以解决写负载均衡和数据量过大问题;每个分片配置多个读从库,可以减少单个库的读压力。

配置:

此种情况,需要配置Haproxy、keepalive和mycat集群,每个分片上又需要配置一主多从的集群。每个分片上的完整配置,具体请参考方案三,可以简单地把方案三理解为一个分片结构。因此,配置和维护量都比较大。

场景及优缺点:

读写并发都很大并且数据量非常大的场景。

优点:终极的解决高并发高数据量的方法。

缺点:配置和维护都比较麻烦,需要的软硬件设备资源大。

大多数的高可用架构都需要进行主从复制操作,因为这是确保多节点数据一致性的最佳办法。

4、主从复制

综合上述架构,我们可以看到如下场景为我们其实都需要做主从复制的备份:

1、在业务复杂的系统中,有这么一个情景,有一句sql语句需要锁表,导致暂时不能使用读的服务,那么就很影响运行中的业务,使用主从复制,让主库负责写,从库负责读,这样,即使主库出现了锁表的情景,通过读从库也可以保证业务的正常运行。2、做数据的热备,主库宕机后能够及时替换主库,保证业务可用性。3、架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低磁盘I/O访问的频率,提高单个机器的I/O性能。

4.1、原理

MySQL主从复制是一个异步的复制过程,主库发送更新事件到从库,从库读取更新记录,并执行更新记录,使得从库的内容与主库保持一致。流程大致如下:

1、主库db的更新事件(update、insert、delete)被写到binlog2、主库创建一个binlog dump thread,把binlog的内容发送到从库3、从库启动并发起连接,连接到主库4、从库启动之后,创建一个I/O线程,读取主库传过来的binlog内容并写入到relay log(中继日志)5、从库启动之后,创建一个SQL线程,从relay log里面读取内容,从Exec_Master_Log_Pos位置开始执行读取到的更新事件,将更新内容写入到slave

主从复制流程

4.2、延迟问题

4.2.1、延迟的原因

1、MySQL数据库主从同步延迟原理mysql主从同步原理

主库针对写操作,顺序写binlog,从库单线程去主库顺序读”写操作的binlog”,从库取到binlog在本地原样执行(随机写),来保证主从数据逻辑上一致。mysql的主从复制都是单线程的操作,主库对所有DDL和DML产生binlog,binlog是顺序写,所以效率很高,slave的Slave_IO_Running线程到主库取日志,效率比较高,下一步,问题来了,slave的Slave_SQL_Running线程将主库的DDL和DML操作在slave实施。DML和DDL的IO操作是随即的,不是顺序的,成本高很多,还可能可slave上的其他查询产生lock争用,由于Slave_SQL_Running也是单线程的,所以一个DDL卡主了,需要执行10分钟,那么所有之后的DDL会等待这个DDL执行完才会继续执行,这就导致了延时。有朋友会问:“主库上那个相同的DDL也需要执行10分,为什么slave会延时?”,答案是master可以并发,Slave_SQL_Running线程却不可以。

2、MySQL数据库主从同步延迟是怎么产生的?

当主库的TPS并发较高时,产生的DDL数量超过slave一个sql线程所能承受的范围,那么延时就产生了,当然还有就是可能与slave的大型query语句产生了锁等待。首要原因:数据库在业务上读写压力太大,CPU计算负荷大,网卡负荷大,硬盘随机IO太高次要原因:读写binlog带来的性能影响,网络传输延迟。

3、如何查看是否延迟?

首先在服务器上执行show slave satus;可以看到很多同步的参数:

Master_Log_File: SLAVE中的I/O线程当前正在读取的主服务器二进制日志文件的名称Read_Master_Log_Pos: 在当前的主服务器二进制日志中,SLAVE中的I/O线程已经读取的位置Relay_Log_File: SQL线程当前正在读取和执行的中继日志文件的名称Relay_Log_Pos: 在当前的中继日志中,SQL线程已读取和执行的位置Relay_Master_Log_File: 由SQL线程执行的包含多数近期事件的主服务器二进制日志文件的名称Slave_IO_Running: I/O线程是否被启动并成功地连接到主服务器上Slave_SQL_Running: SQL线程是否被启动Seconds_Behind_Master: 从属服务器SQL线程和从属服务器I/O线程之间的时间差距,单位以秒计

4、从库同步延迟情况出现的

● show slave status显示参数Seconds_Behind_Master不为0,这个数值可能会很大● show slave status显示参数Relay_Master_Log_File和Master_Log_File显示bin-log的编号相差很大,说明bin-log在从库上没有及时同步,所以近期执行的bin-log和当前IO线程所读的bin-log相差很大● mysql的从库数据目录下存在大量mysql-relay-log日志,该日志同步完成之后就会被系统自动删除,存在大量日志,说明主从同步延迟很厉害

4.2.2、解决方案

1. 半同步复制

从MySQL5.5开始,MySQL已经支持半同步复制了,半同步复制介于异步复制和同步复制之间,主库在执行完事务后不立刻返回结果给客户端,需要等待至少一个从库接收到并写到relay log中才返回结果给客户端。相对于异步复制,半同步复制提高了数据的安全性,同时它也造成了一个TCP/IP往返耗时的延迟。

2. 主库配置sync_binlog=1,innodb_flush_log_at_trx_commit=1

sync_binlog的默认值是0,MySQL不会将binlog同步到磁盘,其值表示每写多少binlog同步一次磁盘。innodb_flush_log_at_trx_commit为1表示每一次事务提交或事务外的指令都需要把日志flush到磁盘。注意:将以上两个值同时设置为1时,写入性能会受到一定限制,只有对数据安全性要求很高的场景才建议使用,比如涉及到钱的订单支付业务,而且系统I/O能力必须可以支撑!

其他方案:

\1. 优化网络\2. 升级Slave硬件配置\3. Slave调整参数,关闭binlog,修改innodb_flush_log_at_trx_commit参数值\4. 升级MySQL版本到5.7,使用并行复制

优酷的解决方案:数据库分片技术,而抛弃了由于数据量的越来越多导致复制延迟的问题。按照user_id进行分片,这样必须有一个全局的表来管理用户与shard的关系,根据user_id可以得到share_id,然后根据share_id去指定的分片查询指定的数据

淘宝的解决方案:修改源码,对应的机制是Transfer机制,此处通过对Binlog日志重做采用多线程实现,从而提高slave的QPPS

附加、如何确保mysql与redis的数据一致性?

单一线程下,删除缓存再去更新数据库,可以做到redis与mysql的一致性,但是高并发下,当存在如下情况:

不一致场景一:A线程需要更新id=1 column1=3,此时数据库是2,当A删除缓存,还没来得及更新数据库,线程B进行query访问,查询id=3的column1的值,发现数据库是2,然后成功返回,并更新缓存(因为缓存被A已经删除,所以需要更新),此时A完成了对数据的更新,数据库的值变成了3。而缓存内仍然是2然后线程C准备查询,就会查到这个缓存中的脏数据2,而不是真实值3。这就是redis 与数据库不一致产生的场景。

不一致场景二:双写不一致

双写不一致

可能还会有更复杂的场景,但是它们产生的原因是一样的,缓存系统在承担了分散数据库压力的同时,难免出现这种滞后性,因为传统单一的数据库操作是没有这种风险的。根本在于原先的只需要维护一套系统的简单操作变成了两个系统的分离操作。

解决这个问题的思路有如下几个:

1、利用特殊值进行线程阻塞,当A准备更新数据库时,先将缓存内的值修改为-9999之类,然后B查询时,发现如果是约定的特殊值,则进入休眠,直到A完成操作更新cache,这种思路的本质是串行化,

2、或者所有的update操作在java代码中就进行串行化。

3、经典延时双删:如果数据update操作频繁,仍然会存在脏数据问题。

读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。所以,如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案。串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。

举报/反馈

高级互联网专家

8.7万获赞 2.9万粉丝
互联网软件“卓越技术顾问”,顶级软件架构开发者(承接各类软件项目开发,欢迎合作)<网站、管理后台、app、小程序、服务系统、技术支持等>!
科技领域创作者
关注
0
0
收藏
分享