1. WAL 与 fsync

1.1 WAL

WAL = Write-Ahead Logging,即预写日志。

我们可以将数据库、分布式组件当做状态机,读操作负责读取组件状态,但是写操作并不直接对应于状态的修改,而是对应追加一条日志。

总之是:读状态,写日志。

WAL 机制本质上依赖于操作日志的磁盘顺序 I/O,而其他功能往往是基于此的应用衍生。

直接修改状态机的状态代价高昂。例如,MySQL 的 InnoDB 存储引擎的数据结构是 B+ 树。为了维护树状结构索引的有序性,元素增、删时会引入额外的代价,例如,B+ 树涉及到树中节点的分裂以及合并。另一方面,树上节点的修改必将涉及随机 I/O。因此,如果每次写操作都直接修改树型结构的状态,那么代价会非常大。

为了提高操作系统的磁盘吞吐量,我们必须将多次状态的更新操作进行批处理,但是又引入了数据丢失的问题:如果机器在状态更新前宕机,对应的写操作就丢失了。引入 WAL 的主要目的就在于此:WAL 的顺序磁盘 I/O 能够以更低廉的方式提供可靠的持久化机制

WAL 机制也有其他优势,例如提供事务回滚、系统重启后恢复状态等功能。

1.2 fsync

fsync = file synchronize,即文件同步。

Linux 提供的 write 系统调用并不会确保数据一定马上落盘,在使用 buffered I/O 机制的情况下,write 直接导致的是将数据写入 page cache in file system(位于内存)中,操作系统负责定期将 dirty blocks 刷新到磁盘。这意味着存在客户端收到写成功的 ack,但是数据却丢失的问题。

开启 fsync 机制意味着数据不会丢失(当然不存在绝对的安全,例如即使开启 fsync,出现了磁盘损坏等问题还是会导致数据丢失),提高了持久化的可靠性(reliability),但是带来了新问题:操作系统磁盘 I/O 吞吐量的下降。

我们可以这样理解 fsync:货运公司总是希望货车装满后(甚至存在很多超载情况)才运输,而不是刚装上一个货物就开始运输。货运公司的目标也是提高整个公司的运输总量(吞吐量)。

1.3 关系

如果要确保彻底的更新操作落盘,那么 WAL 机制会 enable fsync。如果出于系统吞吐量的角度,那么 WAL 机制可以选择 disable fsync。

总之,WAL 与 fsync 并不是紧耦合的概念,这里的 trade-off 是:

  • 持久化的可靠性;
  • 系统的吞吐量;

不过,没有绝对的可靠性,例如即使 WAL 采用 fsync 也无法确保磁盘发生坏盘时的数据丢失。可靠性还依赖于分布式系统中 replication,当然这也无法确保绝对的可靠性。

2. 内存型分布式组件

分布型分布式组件的代表是:

  • Redis;
  • ZooKeeper;

ZooKeeper[1] 通过 forceSync 配置来控制是否开启 WAL 的 fsync,默认情况下为 yes,代码如下:

1
2
3
// org.apache.zookeeper.server.persistence.FileTxnLog 类
// zookeeper.forceSync 的默认值为 yes
private final boolean forceSync = !System.getProperty("zookeeper.forceSync", "yes").equals("no");

Redis 的 AOF(append only file)模式基于 WAL 机制[2] 实现,默认情况下每间隔 1 秒执行一次 fsync 系统调用实现更新操作的持久化。

可见,ZooKeeper 比 Redis 提供了更可靠的持久性保证,另一方面 Redis 也提供了更高的系统吞吐量。

3. 磁盘式分布式组件

磁盘式分布式组件的典型代表有:

  • Kafka
  • RocketMQ

磁盘式分布式组件比内存型分布式组件的一大区别就在于吞吐量高很多,每条写操作对应的数据量也可以大很多。

Kafka 通过 log.flush.interval.messages 以及 log.flush.interval.ms 等属性进行按照消息量以及时间间隔将数据通过 fsync 系统调用持久化到磁盘[3],在默认情况下进行异步刷盘,即每次写操作并不直接导致调用一次 fsync。

RocketMQ 是阿里开源的一款消息队列组件,关于其磁盘刷新有两种配置:SYNC_FLUSH 以及 ASYNC_FLUSH[4],RokcetMQ 与 Kafka 一样,默认采用异步刷盘机制[5],通过 flushDiskType 参数进行配置。

4. 数据库组件

数据库组件的典型代表有:

  • MySQL 的 InnoDB 与 MyISAM;
  • PostgreSQL

MySQL 的 MyISAM 存储引擎并不支持事务,实际上也不对持久性(durability)做出任何保障,因此 MyISAM 对写操作并不会通过 fsync 系统调用将数据同步到磁盘。

MySQL 的 InnoDB 存储引擎支持事务,因此也确保持久性,在默认配置下,InnoDB 在事务提交之后,能够确保通过 fsync 系统调用将写操作对应的数据写入持久化存储介质中[6]。

PostgreSQL 的默认机制是打开 fsync 配置(至少是 8.1 版本及其后续版本)[7],在事务提交之后,也能够确保通过 fsync 系统调用将写操作对应的数据写入持久化存储介质中。

5. 设计上的区别

1.内存型分布式组件

ZooKeeper 与 Redis 同样作为内存型 key-value 组件,但是应用场景有很大的不同。根据各自的官网,我们可以分别得知:

  • ZooKeeper:ZooKeeper is a centralized service for maintaining configuration information, naming, providing distributed synchronization, and providing group services.
  • Redis:Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker.

ZooKeeper 的主要功能是提供配置信息、名称服务、分布式同步等服务。而 Redis 可以作为数据库、内存,甚至消息中间件。显然,从组件的应用场景看,Redis 要求更好的吞吐量,而 ZooKeeper 则选择牺牲了吞吐量,追求可靠的持久化机制。

ZooKeeper 与 Redis 的更多区别可以参考:Redis vs Zookeeper

2.磁盘型分布式组件

磁盘型分布式组件的代表是消息中间件,包括 Kafka 以及 RocketMQ。

同样可以作为消息中间件,Redis 与 Kakfa 有什么区别?

内存型消息中间件非常限制消息中间件的应用场景。内存通常比磁盘小非常多,内存型消息中间件的消息暂存能力差,这导致了如果 Consumer 与 Provider 的速率一旦不匹配,内存型消息中间件在高 OPS 的场景下很快会出现不可用(内存中装满数据了)。当然这里的 trade-off 是 Redis is faster than Kafka,以至于 Redis 可以对外宣传 How fast is Redis? – Redis

Kafka 与 RocketMQ 为了追求高吞吐量,默认情况下会选择将 fsync 配置关闭,这也意味着消息可能会丢失。不过,由于 Kafka 会根据 partiton 进行分布式 replication 冗余,因此彻底丢失一个消息也比较难以发生。

事实上,正如前文所述,没有绝对的可靠性。即使你在应用层面做了确保 fsync 机制,还是避免不了因为磁盘故障导致的数据丢失。即使我们引入了分布式 replication 机制,如果所有主机的磁盘都发生故障,数据还是会丢失。谨记一个原则:持久化可靠性提高的交换条件是应用吞吐量的降低。

3.数据库

数据库似乎与基于磁盘的消息中间件很像,但还是有显著的区别:Kafka 关注于 “High-throughput”, “Distributed” 以及 “Scalable”,而 MySQL 的特点则是**“Sql”**, **“Free”** 以及 **“Easy”**,可见最大的区别在于 MySQL 支持 SQL 式的增删改查,而 Kafka 则更简单一些,只是一个消息中间件,不提供复杂的查询 API。

从组件之间的关系上看,Kafka 作为中间层存在,而 MySQL 以及 PostgreSQL 作为存放数据的终端存在。Is Apache Kafka a Database? 一文将 Kafka 与数据库进行比较。

MySQL 的 InnoDB 提供事务 ACID 的保障,默认情况下开启 fsync 来提供更可靠的持久性。另一方面,MySQL 与 Kafka 等消息中间件有着不同的应用场景,MySQL 更适合读多写少的应用,而 Kafka 读写请求量类似的。

6. 总结

本文首先介绍了分布式系统中常见的 WAL 机制,然后接着分析了操作系统提供的 fsync 系统调用,以及 WAL 与 fsync 之间的关系。其次,依次介绍了 ZooKeeper、Redis 两个内存型分布式组件的 WAL 的 fsync 配置,Kakfa、RocketMQ 两个磁盘型分布式组件的 WAL 的 fsync 配置,最后介绍了 MySQL InnoDB 与 MyISAM 以及 PostgreSQL 数据库的 WAL 的 fsync 配置。

最后需要说明的一点是,理解分布式系统设计思路的关键是理解其 trade-off,不存在合适所有应用场景的分布式组件(没有银弹)。

REFERENCE

参考地址: