分布式环境中解决主从延时的一些思路
目录标题
- MySQL主从复制复习
- 为什么要做主从复制?
- 主从复制的原理
- 主从延迟的原因?
- 解决思路
- 1. 读写分离与延迟容忍
- 2. 异步复制优化
- 3. 缓存机制(常用)
- 4. 最终一致性方案(常用)
- 5. 主从切换与自动故障恢复(DBA常用)
- 6. 使用中间件或代理
- 7. 数据预热(营销常用)
- 8. 优化网络和硬件
- 9. 多活架构
- 10. 数据校验与修复
- 11.少量读业务直连主库
- 12.适当的限流、降级
MySQL主从复制复习
主从复制,是指建立一个和主数据库完全一样的数据库环境(称为从数据库),并将主库的操作行为进行复制的过程:将主数据库的DDL和DML的操作日志同步到从数据库上,然后在从数据库上对这些日志进行重新执行,来保证从数据库和主数据库的数据的一致性。
为什么要做主从复制?
1、在复杂的业务操作中,经常会有操作导致锁行甚至锁表的情况,如果读写不解耦,会很影响运行中的业务,使用主从复制,让主库负责写,从库负责读。即使主库出现了锁表的情景,通过读从库也可以保证业务的正常运行。
2、保证数据的热备份,主库宕机后能够及时替换主库,保障业务可用性。
3、架构的演进:业务量扩大,I/O访问频率增高,单机无法满足,主从复制可以做多库方案,降低磁盘I/O访问的频率,提高单机的I/O性能。
4、本质上也是分治理念,主从复制、读写分离即是压力分拆的过程。
5、读写比也影响整个拆分方式,读写比越高,主从库比例应越高,才能保证读写的均衡,才能保证较好的运行性能。读写比下的主从分配方法下:
读写比 | 主库 | 从库 |
---|---|---|
50:50 | 1 | 1 |
66:33 | 1 | 2 |
80:20 | 1 | 4 |
主从复制的原理
当在从库上启动复制时,首先创建I/O线程连接主库,主库随后创建Binlog Dump线程读取数据库事件并发送给I/O线程,I/O线程获取到事件数据后更新到从库的中继日志Relay Log中去,之后从库上的SQL线程读取中继日志Relay Log中更新的数据库事件并应用,
如下图所示:
细化一下有如下几个步骤:
1、MySQL主库在事务提交时把数据变更(insert、delet、update)作为事件日志记录在二进制日志表(binlog)里面。
2、主库上有一个工作线程 binlog dump thread,把binlog的内容发送到从库的中继日志relay log中。
3、从库根据中继日志relay log重做数据变更操作,通过逻辑复制来达到主库和从库的数据一致性。
4、MySQL通过三个线程来完成主从库间的数据复制,其中binlog dump线程跑在主库上,I/O线程和SQL线程跑在从库上。拥有多个从库的主库会为每一个连接到主库的从库创建一个binlog dump线程。
主从延迟的原因?
MySQL主从复制,读写分离是我们常用的数据库架构,但是在并发量较大、数据变化大的场景下,主从延时会比较严重。
延迟的本质原因是:系统TPS并发较高时,主库产生的DML(也包含一部分DDL)数量超过Slave一个Sql线程所能承受的范围,效率就降低了。
我们看到这个sql thread 是单个线程,所以他在重做RelayLog的时候,能力也是有限的。
解决思路
在分布式环境下,主从数据库的延时是一个常见的问题,尤其是在高并发场景下。以下是一些解决主从延时的思路和实践经验,并附上具体的例子:
1. 读写分离与延迟容忍
思路:将读操作分配到从库,写操作分配到主库。通过设置合理的延迟容忍时间来处理短暂的主从延时。
实践经验:
- 在应用层实现读写分离逻辑。
- 设置一个可接受的延迟容忍时间(例如5秒),在此时间内允许数据不一致。
例子:
public class UserService {
private final JdbcTemplate masterJdbcTemplate;
private final JdbcTemplate slaveJdbcTemplate;
private static final int TOLERANCE_DELAY = 5000; // 5秒
public User getUserById(int id) {
// 从从库读取数据
User user = slaveJdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", new Object[]{id}, new BeanPropertyRowMapper<>(User.class));
// 获取当前时间和上次更新时间
long currentTimeMillis = System.currentTimeMillis();
long lastUpdatedTimeMillis = user.getLastUpdated().getTime();
// 计算时间差
long timeDifference = currentTimeMillis - lastUpdatedTimeMillis;
// 如果时间差小于延迟容忍时间,则认为数据是一致的
if (timeDifference < TOLERANCE_DELAY) {
return user;
} else {
// 否则从主库读取数据
return masterJdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", new Object[]{id}, new BeanPropertyRowMapper<>(User.class));
}
}
}
2. 异步复制优化
思路:优化主从复制的配置,减少复制延迟。可以使用半同步复制或并行复制来提高效率。
实践经验:
- 使用MySQL的半同步复制(semi-sync replication)确保至少有一个从库接收到事务后才返回成功。
- 配置并行复制以加快复制速度。
例子:
-- 启用半同步复制
CHANGE MASTER TO MASTER_HOST='master_host', MASTER_PORT=3306, MASTER_USER='repl_user', MASTER_PASSWORD='repl_password', MASTER_AUTO_POSITION=1, MASTER_CONNECT_RETRY=10, MASTER_HEARTBEAT_PERIOD=10, MASTER_DELAY=0 FOR CHANNEL 'group_replication_recovery';
-- 配置并行复制
[mysqld]
slave_parallel_workers=4
slave_parallel_type=LOGICAL_CLOCK
3. 缓存机制(常用)
思路:使用缓存来减轻数据库的压力,并减少主从延时的影响。对于频繁读取的数据,可以先从缓存中读取。
实践经验:
- 使用Redis或Memcached等缓存系统。
- 在数据更新时同时更新缓存,确保缓存与数据库的一致性。
例子:
public class UserService {
private final JdbcTemplate jdbcTemplate;
private final RedisTemplate<String, User> redisTemplate;
public User getUserById(int id) {
// 先从缓存中读取
User user = redisTemplate.opsForValue().get("user:" + id);
if (user == null) {
// 缓存中没有,从数据库读取
user = jdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", new Object[]{id}, new BeanPropertyRowMapper<>(User.class));
// 更新缓存
redisTemplate.opsForValue().set("user:" + id, user, 1, TimeUnit.HOURS);
}
return user;
}
public void updateUser(User user) {
// 更新数据库
jdbcTemplate.update("UPDATE users SET name = ?, email = ? WHERE id = ?", user.getName(), user.getEmail(), user.getId());
// 更新缓存
redisTemplate.opsForValue().set("user:" + user.getId(), user, 1, TimeUnit.HOURS);
}
}
4. 最终一致性方案(常用)
思路:采用最终一致性方案,允许短期内的数据不一致,但保证最终数据会一致。
实践经验:
- 使用消息队列(如Kafka、RabbitMQ)来异步处理数据同步。
- 通过事件驱动架构来处理数据变更通知。
例子:
// 生产者发送用户更新事件
kafkaTemplate.send("user-update-topic", user.getId(), user);
// 消费者处理用户更新事件
@KafkaListener(topics = "user-update-topic")
public void onUserUpdate(ConsumerRecord<Integer, User> record) {
User user = record.value();
// 更新从库
jdbcTemplate(slaveDataSource).update("UPDATE users SET name = ?, email = ? WHERE id = ?", user.getName(), user.getEmail(), user.getId());
}
5. 主从切换与自动故障恢复(DBA常用)
思路:通过自动化工具(如MHA、Orchestrator)实现主从切换和自动故障恢复,减少因主库故障导致的延时。
实践经验:
- 部署MHA或Orchestrator来监控主从状态。
- 自动切换主库并在新的主库上继续服务。
例子:
# 安装MHA
apt-get install mha4mysql-manager
# 配置MHA
cat > /etc/masterha/app1.cnf <<EOF
[server default]
manager_workdir=/var/log/mha/app1
manager_log=/var/log/mha/app1/manager.log
ssh_user=root
repl_user=repl
repl_password=repl_password
[server1]
hostname=192.168.1.1
port=3306
[server2]
hostname=192.168.1.2
port=3306
EOF
# 启动MHA
masterha_manager --conf=/etc/masterha/app1.cnf
当然,除了之前提到的方法,还有几种常见的解决主从延时的思路和实践经验。以下是更多的一些方法及其具体例子:
6. 使用中间件或代理
思路:使用中间件或代理来管理主从数据库之间的读写分离和负载均衡,减少应用层的复杂性。
实践经验:
- 使用如MaxScale、ProxySQL等中间件。
- 配置中间件以智能路由读写请求,并提供延迟监控和故障切换功能。
例子:
-- ProxySQL配置示例
INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (10, 'master_host', 3306);
INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (20, 'slave1_host', 3306);
INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (20, 'slave2_host', 3306);
-- 设置读写分离规则
INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (1, 1, '^SELECT.*', 20, 1); -- 读操作路由到从库
INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (2, 1, '^INSERT.*|^UPDATE.*|^DELETE.*', 10, 1); -- 写操作路由到主库
-- 加载规则
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL QUERY RULES TO DISK;
7. 数据预热(营销常用)
思路:在业务低峰期预先加载数据到从库,减少高峰时段的数据同步压力。
实践经验:
- 在夜间或其他低峰时段进行批量数据同步。
- 使用定时任务或调度系统(如Cron)来执行数据预热。
例子:
# 定时任务脚本
#!/bin/bash
# 每天凌晨2点执行数据同步
0 2 * * * /usr/local/bin/pt-table-sync --sync-to-master h=master_host,u=repl_user,p=repl_password
8. 优化网络和硬件
思路:优化网络连接和硬件配置,提高数据传输速度和处理能力。
实践经验:
- 使用高速网络连接(如10Gbps)。
- 升级服务器硬件,特别是CPU和内存。
- 使用SSD硬盘以提高I/O性能。
例子:
- 确保主从服务器之间的网络带宽足够高,减少网络延迟。
- 使用高性能的SSD硬盘替换传统的HDD硬盘。
9. 多活架构
思路:采用多活架构,多个数据中心同时提供服务,每个数据中心都有自己的主从集群。
实践经验:
- 在多个地理区域部署独立的主从集群。
- 使用全局负载均衡器(如DNS轮询、GSLB)将请求分发到最近的数据中心。
例子:
- 在北京、上海、广州分别部署一套主从集群。
- 使用阿里云的全球负载均衡服务(GSLB)将用户请求分发到最近的数据中心。
10. 数据校验与修复
思路:定期进行数据校验,发现并修复主从数据不一致的问题。
实践经验:
- 使用工具如pt-table-checksum和pt-table-sync进行数据一致性检查和修复。
- 定期运行数据校验任务,并记录日志以便追踪问题。
例子:
# 数据一致性检查
/usr/local/bin/pt-table-checksum --host=master_host --user=repl_user --password=repl_password
# 数据修复
/usr/local/bin/pt-table-sync --sync-to-master h=master_host,u=repl_user,p=repl_password
11.少量读业务直连主库
业务量不多的情况下,不做主从分离.既然主从延迟是由于从库同步写库不及时引起的,那我们也可以在有主从延迟的地方改变读库方式,由原来的读从库改为读主库。当然这也会增加代码的一些逻辑复杂性。
这边需要注意的是,直接读主库的业务量不宜多,而且是读实时一致性有刚性需求的业务才这么做。否则背离读写分离的目的。
12.适当的限流、降级
任何的服务器都是有吞吐量的限制的,没有任何一个方案可以无限制的承载用户的大量流量。所以我们必须估算好我们的服务器能够承载的流量上限是多少。
达到这个上限之后,就要采取缓存,限流,降级的这三大杀招来应对我们的流量。这也是应对主从延迟的根本处理办法。
通过这些方法,可以进一步减少主从延时,提高系统的稳定性和性能。选择哪种方法取决于具体的业务需求、技术栈以及资源条件。综合运用多种方法通常能取得更好的效果。