分布式环境中解决主从延时的一些思路

目录标题

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秒),在此时间内允许数据不一致。

例子

java 复制代码
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)确保至少有一个从库接收到事务后才返回成功。
  • 配置并行复制以加快复制速度。

例子

sql 复制代码
-- 启用半同步复制
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等缓存系统。
  • 在数据更新时同时更新缓存,确保缓存与数据库的一致性。

例子

java 复制代码
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)来异步处理数据同步。
  • 通过事件驱动架构来处理数据变更通知。

例子

java 复制代码
// 生产者发送用户更新事件
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来监控主从状态。
  • 自动切换主库并在新的主库上继续服务。

例子

bash 复制代码
# 安装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等中间件。
  • 配置中间件以智能路由读写请求,并提供延迟监控和故障切换功能。

例子

sql 复制代码
-- 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)来执行数据预热。

例子

bash 复制代码
# 定时任务脚本
#!/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进行数据一致性检查和修复。
  • 定期运行数据校验任务,并记录日志以便追踪问题。

例子

bash 复制代码
# 数据一致性检查
/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.适当的限流、降级

任何的服务器都是有吞吐量的限制的,没有任何一个方案可以无限制的承载用户的大量流量。所以我们必须估算好我们的服务器能够承载的流量上限是多少。

达到这个上限之后,就要采取缓存,限流,降级的这三大杀招来应对我们的流量。这也是应对主从延迟的根本处理办法。

通过这些方法,可以进一步减少主从延时,提高系统的稳定性和性能。选择哪种方法取决于具体的业务需求、技术栈以及资源条件。综合运用多种方法通常能取得更好的效果。

相关推荐
Data跳动3 小时前
Spark内存都消耗在哪里了?
大数据·分布式·spark
White_Mountain4 小时前
在Ubuntu中配置mysql,并允许外部访问数据库
数据库·mysql·ubuntu
老王笔记4 小时前
GTID下复制问题和解决
mysql
Java程序之猿4 小时前
微服务分布式(一、项目初始化)
分布式·微服务·架构
来一杯龙舌兰5 小时前
【RabbitMQ】RabbitMQ保证消息不丢失的N种策略的思想总结
分布式·rabbitmq·ruby·持久化·ack·消息确认
Lojarro6 小时前
【Spring】Spring框架之-AOP
java·mysql·spring
TianyaOAO6 小时前
mysql的事务控制和数据库的备份和恢复
数据库·mysql
Ewen Seong6 小时前
mysql系列5—Innodb的缓存
数据库·mysql·缓存
节点。csn7 小时前
Hadoop yarn安装
大数据·hadoop·分布式
W21558 小时前
Liunx下MySQL:表的约束
数据库·mysql