读写分离
读写分离的主要目标就是分摊主库的压力。
有两种读写分离的架构:
- 客户端直连(客户端控制连接逻辑),查询性能稍微好一点,架构简单,排查问题方便。对后端要求比较高, 主备切换,库迁移等操作,客户端会感知到并且需要调整数据库连接信息。
- proxy架构,对客户端友好。客户端不需要额外的工作。高可用。proxy 整体方案比较复杂。
”过期读“
由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态。这种“在从库上会读到系统的一个过期状态”的现象,在这里,我们暂且称之为“过期读”。
处理过期读(主备延迟)问题的方法
- 强制走主库方案;
- sleep方案;
- 判断主备无延迟方案;
- 配合semi-sync方案;
- 等主库位点方案;
- 等GTID方案。
强制走主库方案;
强制走主库方案其实就是,将查询请求做分类。通常情况下,我们可以将查询请求分为这么两类:
- 对于必须要拿到最新结果的请求,强制将其发到主库上。
- 对于可以读到旧数据的请求,才将其发到从库上。
Sleep 方案
主库更新后,读从库之前先sleep一下。具体的方案就是,类似于执行一条select sleep(1)命令。 具体的方案就是,类似于执行一条select sleep(1)命令。
判断主备无延迟方案
要确保备库无延迟,通常有三种做法。
- 第一种确保主备无延迟的方法是,每次从库执行查询请求前,先判断seconds_behind_master是否已经等于0。如果还不等于0 ,那就必须等到这个参数变为0才能执行查询请求。
- 第二种方法,对比位点确保主备无延迟:Master_Log_File和Read_Master_Log_Pos,表示的是读到的主库的最新位点,Relay_Master_Log_File和Exec_Master_Log_Pos,表示的是备库执行的最新位点。如果两组值完全相同表示已经同步完成。
- 第三种方法,对比GTID集合确保主备无延迟(两个集合相同,也表示备库接收到的日志都已经同步完成):
- Auto_Position=1 ,表示这对主备关系使用了GTID协议。
- Retrieved_Gtid_Set,是备库收到的所有日志的GTID集合;
- Executed_Gtid_Set,是备库所有已经执行完成的GTID集合。
配合semi-sync方案
半同步复制(semi-sync replication) : 启用了semi-sync,就表示所有给客户端发送过确认的事务,都确保了备库已经收到了这个日志。
semi-sync+位点判断的方案,只对一主一备的场景是成立的。在一主多从场景中,主库只要等到一个从库的ack,就开始给客户端返回确认
判断同步位点的方案还有另外一个潜在的问题,即:如果在业务更新的高峰期,主库的位点或者GTID集合更新很快,那么上面的两个位点等值判断就会一直不成立,很可能出现从库上迟迟无法响应查询请求的情况。
semi-sync配合判断主备无延迟的方案,存在两个问题:
-
一主多从的时候,在某些从库执行查询请求会存在过期读的现象;
-
在持续延迟的情况下,可能出现过度等待的问题。
等主库位点方案;
要理解等主库位点方案,需要先介绍一条命令:
select master_pos_wait(file, pos[, timeout]);
条命令的逻辑如下:它是在从库执行的;参数file和pos指的是主库上的文件名和位置;timeout可选,设置为正整数N表示这个函数最多等待N秒。 返回的结果是一个正整数M,表示从命令开始执行,到应用完file和pos表示的binlog位置,执行了多少事务。
除了正常返回一个正整数M外,这条命令还会返回一些其他结果,包括:
-
如果执行期间,备库同步线程发生异常,则返回NULL;
-
如果等待超过N秒,就返回-1;
-
如果刚开始执行的时候,就发现已经执行过这个位置了,则返回0。
方案的具体执行流程
- 事务更新完成后,马上执行show master status得到当前主库执行到的File和Position;
- 选定一个从库执行查询语句;
- 在从库上执行select master_pos_wait(File, Position, 1);
- 如果返回值是>=0的正整数,则在这个从库执行查询语句;否则,到主库执行查询语句。
等GTID方案
如果你的数据库开启了GTID模式,对应的也有等待GTID的方案。 MySQL中同样提供了一个类似的命令:
select wait_for_executed_gtid_set(gtid_set, 1);
这条命令的逻辑是:等待,直到这个库执行的事务中包含传入的gtid_set,返回0;超时返回1。
在前面等位点的方案中,我们执行完事务后,还要主动去主库执行show master status。而MySQL 5.7.6版本开始,允许在执行完更新类事务后,把这个事务的GTID返回给客户端,这样等GTID的方案就可以减少一次查询。
等GTID的执行流程
- 事务更新完成后,从返回包直接获取这个事务的GTID,记为gtid1;
- 选定一个从库执行查询语句;
- 在从库上执行 select wait_for_executed_gtid_set(gtid1, 1);
- 如果返回值是0,则在这个从库执行查询语句;否则,到主库执行查询语句。
跟等主库位点的方案一样,等待超时后是否直接到主库查询,需要业务开发同学来做限流考虑。
事务更新完成后,从返回包直接获取这个事务的GTID。问题是,怎么能够让MySQL在执行事务后,返回包中带上GTID呢?
需要将参数session_track_gtids设置为OWN_GTID,然后通过API接口mysql_session_track_get_first从返回包解析出GTID的值即可。
(29讲)
检测主库健康的方法
select 1判断 :
实际上,select 1成功返回,只能说明这个库的进程还在,并不能说明主库没问题。
并发连接
show processlist的结果里,看到的几千个连接,指的就是并发连接。
并发查询
innodb_thread_concurrency参数的目的是,控制InnoDB的并发线程上限。也就是说,一旦并发线程数达到这个值,InnoDB在接收到新请求的时候,就会进入等待状态,直到有线程退出。
在InnoDB中,innodb_thread_concurrency这个参数的默认值是0,表示不限制并发线程数量。但是,不限制并发线程数肯定是不行的。因为,一个机器的CPU核数有限,线程全冲进来,上下文切换的成本就会太高。
实际上,在线程进入锁等待以后,并发线程的计数会减一,也就是说等行锁(也包括间隙锁)的线程是不算在128里面的。进入锁等待的线程已经不吃CPU了;更重要的是,因为一旦一个事物堵住其他多数其他线程中的数据,那么系统吞吐量一下就下来了。
(执行的语句超过了设置的innodb_thread_concurrency的值,这时候系统其实已经不行了,但是通过select 1来检测系统,会认为系统还是正常的。)
查表判断
为了能够检测InnoDB并发线程数过多导致的系统不可用情况,我们需要找一个访问InnoDB的场景。一般的做法是,在系统库(mysql库)里创建一个表,比如命名为health_check,里面只放一行数据,然后定期执行:
select * from mysql.health_check;
binlog所在磁盘的空间占用率达到100%,那么所有的更新语句和事务提交的commit语句就都会被堵住。但是,系统这时候还是可以正常读数据的。
更新判断
既然要更新,就要放个有意义的字段,常见做法是放一个timestamp字段,用来表示最后一次执行检测的时间。这条更新语句类似于:
update mysql.health_check set t_modified=now();
检测逻辑都需要一个超时时间N。执行一条update语句,超过N秒后还不返回,就认为系统不可用。
外部检测都需要定时轮询,所以系统可能已经出问题了,但是却需要等到下一个检测发起执行语句的时候,我们才有可能发现问题。而且,如果你的运气不够好的话,可能第一次轮询还不能发现,这就会导致切换慢的问题。
内部统计
针对磁盘利用率这个问题,如果MySQL可以告诉我们,内部每一次IO请求的时间,那我们判断数据库是否出问题的方法就可靠得多了。
MySQL 5.6版本以后提供的performance_schema库,就在file_summary_by_event_name表里统计了每次IO请求的时间。
file_summary_by_event_name表里有很多行数据,我们先来看看event_name='wait/io/file/innodb/innodb_log_file’这一行。
因为我们每一次操作数据库,performance_schema都需要额外地统计这些信息,所以我们打开这个统计功能是有性能损耗的。
我的测试结果是,如果打开所有的performance_schema项,性能大概会下降10%左右。
建议你只打开自己需要的项进行统计。你可以通过下面的方法打开或者关闭某个具体项的统计。 如果要打开redo log的时间监控,你可以执行这个语句:
update setup_instruments set ENABLED='YES', Timed='YES' where name like '%wait/io/file/innodb/innodb_log_file%';
# 可以通过MAX_TIMER的值来判断数据库是否出问题了。 单次IO请求时间超过200毫秒属于异常
select event_name,MAX_TIMER_WAIT
FROM performance_schema.file_summary_by_event_name
where event_name in ('wait/io/file/innodb/innodb_log_file','wait/io/file/sql/binlog')
and MAX_TIMER_WAIT>200*1000000000;