在分布式系统中,我们知道 CAP 定理和 BASE 理论,数据的安全和性能是负相关的,数据的安全性提高了,那他的性能就会下降,相关,他的性能提高了,数据的安全性就会下降。我们从几个中间件来讨论这个问题。
1 mysql
当 mysql 性能出现瓶颈时,我们会做分库分表、读写分离等,读写分离需要我们做主从复制,对读的操作,我们指向从库,对写的操作,我们指向主库,下面看看主从复制的原理。
当业务系统提交数据的时候,master 会将这变更的数据保存到 binlog 文件,写入成后,事务提交成功。
slave 中会有一个线程,他会从 master 中读取 binlog 信息,然后写入 relay 文件。然后他还会有其他线程,读取 relay 文件的信息,把这个信息重新在 slave 库执行一遍。也就是说,如果在 master 中执行了一个 update 的语句,那在 slave 中同样也执行一模一样的 update 语句,这样两边的数据就会保持一致,由于 master 先执行,然后 slave 再执行,所以会有稍微的延迟。如果执行的语句比较久,那这个延时也会比较长,当然系统的压力也会影响延迟的时间。
除了延时以外,他还有一个比较大的问题,当写入 binlog 后,代表事务提交成功,此时 master 挂了,导致 slave 没办法读取这部分的 binlog,所以就会出现数据的丢失,两边的数据就没办法保持一致性,所以我们通常会把上面的异步复制形式设置半同步复制,也就是 semi-sync。
半同步复制与异步复制不同的是,当 master 写入到 binlog 后,他会主动的把数据同步到从库,从库把信息写入到 relay 文件,会返回 ACK 给 master,此时 master 才会认为他的事务是提交的。
如果有多个 slave 的情况,则至少返回 1 个 ACK 才认为事务的提交的。半同步复制虽然解决了数据安全的问题,但是他需要从库写入 relay 文件并返回 ACK 才算提交事务,与异步复制对比,他的性能是下降的。
2 单体
在单体中,也同样存在着数据安全和性能的负相关问题。
这里简述一下 mysql 对 update 语句的一个流程。
-
当执行 update 的时候,会先从磁盘里把数据读取到缓存。
-
写入 undo 文件,这里是在我们事务回滚的时候用的。
-
修改缓存数据,比如把 id 为 1 的 name 由张三改为李四。
-
写入 redo 缓存,redo 主要是为了宕机重启时,恢复数据用的。
-
写入 redo 缓存后,写入到磁盘。
-
写入 binlog 文件。
-
写入 binlog 后,提交 commit 给 redo,跟 redo 说已经写入到 binlog 了。
-
定期把缓存的数据写入到磁盘。
我们对数据的更新,都是在缓存中进行的,这样可以保证性能的提高。同时为了数据的安全性,还引入了 undo、redo、binlog 等东西。我们可以看 redo 和 binlog 两种写入磁盘的策略。
在 redo 中,我们可以选择 0,即不把缓存数据写入磁盘,这样可以快速执行完 redo 操作,如果此时宕机了,还没写入磁盘的数据就丢失了,虽然提高了性能,但是数据安全性没有了。如果选择 1,由于要写入磁盘才可以完成 redo 操作,虽然保证了数据的安全性,但是性能却下降了。
同样的,binlog 写入 oscache 是提高了性能,但是服务器宕机会导致 oscache 的数据不能及时的写入磁盘,导致数据的安全性没有。如果直接写入磁盘,性能又下降。
3 redis
与 mysql 已有,redis 的复制也是异步复制的,当业务系统往 master 写入数据的时候,他就会通过异步复制的方式把数据同步给 slave,所以和 mysql 类似,当业务系统认为他已经把数据写入到 redis 的时候,此时 master 挂了,但是数据还没同步到 slave,他的数据就丢失了。
另外一个场景,就是发生了脑裂,也就是 sentinel 认为 master 挂了,然后重新选举了 master,此时业务系统和 master 是正常通讯的,他把数据提交到原 master 节点,但是原 master 节点的数据此时是没办法同步到其他节点,导致数据不一致。
在这情况下,我们会做以下配置:
min-replicas-to-write 1
min-replicas-max-lag 10
这个意思是至少有 1 个 slave 已经有 10 秒没有同步,则 master 暂停接收请求。所以不会说 master 一直写入数据,而 slave 没有同步。如果发生以上两个场景,最多丢失 10 秒的数据。虽然没有严格的做到数据安全性,但是也保证了数据的不一致性不会超过 10 秒,超过 10 秒后,由于不能写数据,写性能下降为 0。
4 RocketMQ
我们看看基于 Dledger 是怎么做 broker 同步的。
首先,master broker 收到消息后,会把这个消息置为 unconmmited 状态,然后把这个消息发送给 slave broker,slave broker 收到消息后,会发送 ack 进行确认,如果有多个 slave broker,当超过一半的 slave broker 发送 ack 时,master broker 才会把这个消息置为 committed 状态。在这种机制下,保证了数据的安全性,当 master broker 挂了,我们还有至少超过一半的 slave broker 的数据是完整的,由于需要多个 slave broker 进行 ack 确认,也降低了性能。
从单体上来说,异步刷盘和同步刷盘跟 mysql 的 redo 写入磁盘的一样的。
另外,kafka 的同步机制跟这个类似,而且他也有写入 oacache 的操作。
5 Zookeeper
Zookeeper 是 CP 模型的,那他的数据安全性是可以保证的,那他的性能呢?我们假设此时 master 节点挂了,此时需要重新选主,这个时候 Zookeeper 集群是不可用状态的。那 Zookeeper 是如何保证数据的一致性呢?
我们假设 master 同步给 5 个 slave。
当 master 不用确认是否已经同步给 slave 就直接返回,这个时候性能是最高的,但是安全性是最低的,因为 master 数据没同步到 slave 的时候挂了,那这个数据是丢失的。
当 master 确认已经同步给所有 slave(这里是 5 个)才返回,这个时候,性能是最低的,但是安全性是最高的,因为不管哪个 slave,他的数据都是完整的。
不管是 RocketMQ 还是 Zookeeper,都是折中选择超过一半的 slave 同步,才算成功。当我们访问 Zookeeper 的时候,他会根据已经同步好的 slave 服务器让我们来读取对应的信息,这样我们读取的数据肯定都是最新的。(来源:segmentfault,作者:大军)
<END>