跳转到主要内容

11 日志模式

PostgreSQL 18 · 基于俄文 PG18 原版的简体中文译本

11.1. 性能

服务器正常运行时,会持续顺序写入日志文件。由于几乎不存在随机访问,HDD 也完全可以应付这项工作。但这种负载特征与数据文件的访问方式有很大差异。因此,把日志放在单独的物理存储上可能更有利:可以将 PGDATA/pg_wal 目录替换为指向已挂载文件系统目录的符号链接。(v. 17)

说明

日志文件有两种情况不仅需要写入,还需要读取。第一种是显而易见的故障恢复。第二种出现在使用流复制时。walsender 进程1会尽量从日志缓存2读取 WAL 记录;但如果副本来不及接收日志记录,而所需页面仍在主服务器内存中,数据就会从磁盘读取。不过即使在这种情况下,访问仍然是顺序的,而不是随机的。

日志写入有两种模式:

  • 同步模式——提交事务时,只有等该事务的所有日志记录都写入磁盘后,才能继续工作;

  • 异步模式——事务立即完成,日志在后台写入。

该模式由 synchronous_commit 参数设置。(默认值: on)

同步模式。 要可靠地保存提交这一事实,仅仅把日志记录交给操作系统还不够;还必须确保它们已经与磁盘完成同步。由于同步涉及实际的(也就是缓慢的)输入输出,因此应尽量少执行同步。

为此,负责完成事务并写入日志的服务进程可以按照 commit_delay 参数指定的时间短暂暂停。不过,只有系统中至少有 commit_siblings 个活跃事务时才会这样做3:等待期间,其中一些事务可能完成,这样服务器就能一次性同步所有日志记录。这很像扶住电梯门,让别人也能赶上。(默认值: 0s) (默认值: 5)

使用默认值时不会发生暂停。只有在系统执行大量短 OLTP 事务时,修改 commit_delay 参数才有意义。

可能的暂停结束后,负责完成事务的进程会把积累的所有日志记录刷到磁盘,并执行同步(重要的是,磁盘上必须有提交记录以及此前与该事务有关的所有记录;其余记录被写入,只是因为这样不会增加成本)。

从这一刻起,事务就被视为可靠完成4,并保证满足 ACID 要求中的持久性(Durability)。因此,同步模式默认启用。

同步写入的代价是响应时间变长(COMMIT 命令要等同步结束才会返回控制权),系统吞吐量降低,尤其是在 OLTP 负载下。

异步模式。 要启用异步写入5,需要把 synchronous_commit 参数设置为 off。

在异步模式下,日志记录由 walwriter 进程写入磁盘6;该进程在工作周期和等待之间交替运行。暂停时长由 wal_writer_delay 参数设置。(默认值: 200ms)

每次暂停后醒来时,进程都会检查缓存中是否出现了新的完全填满的 WAL 页面。如果出现了,就把这些页面写入磁盘,跳过当前页面。如果从上次运行以来没有任何日志页面完全填满,进程就写入当前尚未填满的页面,毕竟它已经醒来了7。

这个算法的目标是尽量避免多次刷写同一个页面;在修改量很大的负载下,这一点很重要。

虽然日志缓存采用环形缓冲区,但写入到达缓存最后一个页面时就会停止;暂停后,下一轮写入从第一个页面开始。因此在最坏情况下,walwriter 要到第三轮才能处理某条日志记录:先刷写位于缓存末尾的所有已填满页面,再回到开头,最后处理包含目标记录的未填满页面。不过,大多数情况下一到两轮就够了。

每写入 wal_writer_flush_after MB 数据后都会同步一次,并在写入周期结束时再同步一次。(默认值: 1MB)

异步写入比同步写入更高效,因为提交不必等待物理写入磁盘。但可靠性会降低:如果故障发生在提交后不到 3 × wal_writer_delay 个时间单位之内,已经提交的数据可能会丢失;按默认设置,这段时间为 0.6 秒。

实际情况是,这两种模式相互补充。即使采用同步提交,长事务的日志记录也可以异步写入,以释放 WAL 缓冲区。反过来,如果从缓冲区缓存逐出页面时发现相应日志记录还没有写入磁盘,那么即使处于异步模式,也会立即刷写该记录,否则系统无法继续工作。

不过,系统设计者通常仍然要在效率和持久性之间做出艰难选择。

synchronous_commit 参数也可以针对单个事务设置。如果应用层能够把事务分为绝对关键的事务(例如处理财务数据的事务)和不太重要的事务,就可以只牺牲部分事务的可靠性来提高性能。

为了大致了解异步提交带来的收益,我们使用 pgbench 基准测试8比较两种模式下的响应时间和吞吐量。

初始化所需的表,并在同步模式下运行 30 秒测试:

postgres$ /usr/local/pgsql/bin/pgbench -i internals
postgres$ /usr/local/pgsql/bin/pgbench -T 30 internals
pgbench (18.3)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 30 s
number of transactions actually processed: 42850
number of failed transactions: 0 (0.000%)
latency average = 0.700 ms
initial connection time = 0.515 ms
tps = 1428.341142 (without initial connection time)

然后在异步模式下运行相同的测试:

=> ALTER SYSTEM SET synchronous_commit = off;
=> SELECT pg_reload_conf();
postgres$ /usr/local/pgsql/bin/pgbench -T 30 internals
pgbench (18.3)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 30 s
number of transactions actually processed: 68607
number of failed transactions: 0 (0.000%)
latency average = 0.437 ms
initial connection time = 0.710 ms
tps = 2286.926300 (without initial connection time)

在这个简单测试中,异步提交显著降低了响应时间(latency),并提高了吞吐量(tps)。当然,具体系统在具体负载下会得到不同的比例,但可以看出,对短 OLTP 事务来说,效果可能非常明显。

恢复参数的默认值:

=> ALTER SYSTEM RESET synchronous_commit;
=> SELECT pg_reload_conf();

11.2. 可靠性

显然,日志机制必须保证在各种情况下都能从故障中恢复(当然不包括存储介质本身被毁坏的情况)。有许多因素会影响数据一致性,其中最重要的是缓存、数据损坏和写入非原子性9。

缓存

数据到达非易失性存储(例如硬盘盘片)之前,会经过多层缓存。

向磁盘写入的系统调用只会让操作系统把数据移入自己的缓存(这个缓存也位于运行内存中,同样容易因故障丢失)。真正的写入会根据操作系统输入输出调度器的设置异步进行。

调度器决定写入数据后,数据会进入存储设备(硬盘)的缓存。存储设备的电子部件也可能推迟写入,例如把适合同时写入的数据收集成一组。RAID 控制器又在操作系统和磁盘之间增加了一层缓存。

如果不采取特殊措施,就无法知道数据何时可靠地保存下来。通常这并不重要,因为我们有日志;但日志记录本身必须立即可靠地保存10。异步模式也一样,否则无法保证日志记录先于修改后的数据写入磁盘。

检查点也必须可靠地保存数据,确保脏页真正刷到磁盘,而不是留在操作系统缓存中。此外,检查点还要同步此前由其他进程执行的所有文件操作(例如写入页面或删除文件):检查点完成时,这些操作的结果也必须已经可靠地写入磁盘11。

还有一些情况也要求可靠写入,例如在日志级别 minimal 下执行无日志操作。

操作系统提供了多种手段,用来保证数据立即写入非易失性存储。

这些手段可以归结为两类:写入之后执行单独的同步命令(fsync、fdatasync),或者在打开文件或向文件写入时指定同步要求(甚至指定绕过操作系统缓存直接写入)。

pg_test_fsync 工具可以帮助确定特定操作系统和文件系统最合适的日志同步方式;选定的方式由 wal_sync_method 参数指定。其他情况会自动选择同步方式12。

选择方法时还要考虑硬件特性,这是一个容易被忽略的细节。例如,如果使用带备用电池的控制器,就没有理由不用它的缓存,因为电池可以在断电时保护数据。

异步提交和不进行同步是两个完全不同的模式。关闭同步(设置 fsync 参数)会进一步提高系统速度,但任何故障都会导致集簇中的全部数据永久丢失。异步模式可以保证故障后恢复到一致的数据状态,但恢复后的状态中可能缺少最近的一部分事务。(默认值: on)

数据损坏

硬件并不完美,数据可能在内存或存储介质中损坏,也可能在接口电缆传输过程中发生变化。这些错误有一部分由硬件层处理,但并非全部如此。

为了及时发现问题,日志记录总是附带校验和。

数据页面也可以使用校验和保护13。既可以在初始化集簇时启用,也可以在服务器停止后使用 pg_checksums 工具14启用15。(v. 12)

在生产环境中必须启用校验和,尽管计算和检查校验和会带来少量开销。这会提高及时发现故障的机会,但不能提供完整保证:

  • 只有访问页面时才会检查校验和,因此数据损坏可能长时间不被发现,甚至进入所有备份,最终不再留下任何正确数据的来源;

  • 填零的页面会被视为正确页面,因此如果文件系统错误地把文件清零,检查也不会发现问题;

  • 校验和只保护关系的主分支,其他分支和所有其他文件(例如 clog 中的事务状态)都不受保护。

通过检查只读参数 data_checksums 的值,确认校验和已经启用:

=> SHOW data_checksums;
 data_checksums
----------------
 on
(1 row)

停止服务器,并把表的主分支的第 0 页中的几个字节清零:

=> SELECT pg_relation_filepath('wal');
 pg_relation_filepath
----------------------
 base/16391/16586
(1 row)
postgres$ pg_ctl stop
postgres$ dd if=/dev/zero of=/usr/local/pgsql/data/base/16391/16586 \
oflag=dsync conv=notrunc bs=1 count=8
8+0 records in
8+0 records out
8 bytes copied, 0.0036238 s, 2.2 kB/s

再次启动服务器:

postgres$ pg_ctl start -l /home/postgres/logfile

实际上也可以不停止服务器。只要页面被写入磁盘并从缓存中逐出即可(否则服务器会继续使用缓存中的页面)。不过,这种情形更难复现。

现在尝试读取该表:

=> SELECT * FROM wal LIMIT 1;
ERROR:  invalid page in block 0 of relation "base/16391/16586"

如果无法从备份恢复数据,至少可以尝试读取损坏的页面(当然有得到失真信息的风险)。为此,需要设置 ignore_checksum_failure 参数:(默认值: off)

=> SET ignore_checksum_failure = on;
=> SELECT * FROM wal LIMIT 1;
WARNING:  ignoring checksum failure in block 0 of relation
"base/16391/16586"
 id
----
  2
(1 row)

本例之所以成功,是因为损坏的是页头中不重要的部分(最后一条日志记录的 LSN),而不是数据本身。

写入非原子性

数据库页面通常占用 8 KB,但底层写入按块进行,而块在多数情况下更小(通常是 512 字节或 4 KB)。因此发生故障时,数据页面可能只被部分写入。恢复时,对这样的损坏页面应用普通日志记录没有意义。

为了防止部分写入,PostgreSQL 会在检查点开始后第一次修改页面时把整页镜像(full page image,FPI)保存到日志中。(第 223 页)

这一行为由 full_page_writes 参数控制。关闭整页镜像可能导致无法修复的数据损坏,但即使关闭该参数,提示位变化也可能让日志中出现整页镜像。提示位的变化本身被认为不重要,因此不会单独记录;访问页面的第一个查询会重新设置所需的位。然而,任何位(即使是不重要的位)发生变化都会改变校验和。因此,启用校验和(或设置 wal_log_hints 参数)时,检查点之后第一次修改提示位会记录整页镜像16,不受 full_page_writes 的值影响。(默认值: on)(*第 86 页)(默认值: off)

恢复时如果在日志中遇到整页镜像,就会无条件地(不检查 LSN)将它写入磁盘,因为它和所有日志记录一样受到校验和保护,不可能在不被发现的情况下损坏。然后,普通日志记录会应用到这个已经确定正确的镜像上。

整页镜像会显著增加生成的日志量,尽管日志机制会从镜像中排除页面内部的空闲空间17。可以通过 wal_compression 参数启用整页镜像压缩,从而大幅改善这一情况。(默认值: off)

使用 pgbench 工具做一个简单实验。执行检查点,然后立即运行一个事务数量固定的测试:

=> CHECKPOINT;
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/87C95BC8
(1 row)
postgres$ /usr/local/pgsql/bin/pgbench -t 20000 internals
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/8928F088
(1 row)

日志记录大小:

=> SELECT pg_size_pretty('2/8928F088'::pg_lsn - '2/87C95BC8'::pg_lsn);
 pg_size_pretty
----------------
 22 MB
(1 row)

本例中整页镜像占生成总量的很大一部分,可以通过 pg_waldump 工具的 --stats 选项或 pg_walinspect 扩展的 pg_get_wal_stats 函数查看统计信息。(v. 15)

表中列出了每种资源类型(Type)的日志记录数量(N)、普通记录大小(Record size)和整页镜像大小(FPI size):

postgres$ /usr/local/pgsql/bin/pg_waldump --stats \
-p /usr/local/pgsql/data/pg_wal -s 2/87C95BC8 -e 2/8928F088
WAL statistics between 2/87C95BC8 and 2/8928F088:
Type            N      (%)   Record size      (%)   FPI size      (%)
----            -      ---   -----------      ---   --------      ---
XLOG         1810 (  1.48)         88690 (  1.11)   14572848 ( 98.95)
Transaction 20001 ( 16.38)        680114 (  8.50)          0 (  0.00)
Storage         1 (  0.00)            42 (  0.00)          0 (  0.00)
Standby         3 (  0.00)           222 (  0.00)          0 (  0.00)
Heap2       20233 ( 16.57)       1317150 ( 16.47)      16384 (  0.11)
Heap        80015 ( 65.54)       5911923 ( 73.91)      77844 (  0.53)
Btree          15 (  0.01)           888 (  0.01)      59920 (  0.41)
           ------               --------            --------
Total      122078                7999029 [35.20%]   14726996 [64.80%]

如果页面能在两次检查点之间多次发生变化,这个比例就会降低。这也是不要过于频繁执行检查点的另一个理由。

现在仍以这个例子看看压缩能带来什么帮助:

=> ALTER SYSTEM SET wal_compression = on; -- или pglz
=> SELECT pg_reload_conf();
=> CHECKPOINT;
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/8928F158
(1 row)
postgres$ /usr/local/pgsql/bin/pgbench -t 20000 internals
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/89D256F8
(1 row)

压缩后的日志记录大小:

=> SELECT pg_size_pretty('2/89D256F8'::pg_lsn - '2/8928F158'::pg_lsn);
 pg_size_pretty
----------------
 11 MB
(1 row)
postgres$ /usr/local/pgsql/bin/pg_waldump --stats \
-p /usr/local/pgsql/data/pg_wal -s 2/8928F158 -e 2/89D256F8
WAL statistics between 2/8928F158 and 2/89D256F8:
Type            N      (%)   Record size      (%)   FPI size      (%)
----            -      ---   -----------      ---   --------      ---
XLOG         1793 (  1.47)         91399 (  1.14)    2792815 ( 98.79)
Transaction 20001 ( 16.39)        680114 (  8.50)          0 (  0.00)
Storage         1 (  0.00)            42 (  0.00)          0 (  0.00)
CLOG            1 (  0.00)            34 (  0.00)          0 (  0.00)
Standby         4 (  0.00)           276 (  0.00)          0 (  0.00)
Heap2       20227 ( 16.57)       1320696 ( 16.50)        255 (  0.01)
Heap        80005 ( 65.56)       5909689 ( 73.85)      17798 (  0.63)
Btree           5 (  0.00)           275 (  0.00)      16283 (  0.58)
           ------               --------            --------
Total      122037                8002525 [73.89%]    2827151 [26.11%]

除了传统的 PGLZ 压缩算法外,还有两种算法可用:LZ4 和 Zstandard。LZ4 通常占用较少的 CPU 资源,但压缩率大致相同。Zstandard 把字典压缩与熵编码结合起来;它对 CPU 的负担更重,但压缩效果更好。(v. 15)

=> ALTER SYSTEM SET wal_compression = zstd;
=> SELECT pg_reload_conf();
=> CHECKPOINT;
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/89D257C8
(1 row)
postgres$ /usr/local/pgsql/bin/pgbench -t 20000 internals
=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/8A6C6378
(1 row)

使用 Zstandard 压缩后的日志记录大小:

=> SELECT pg_size_pretty('2/8A6C6378'::pg_lsn - '2/89D257C8'::pg_lsn);
 pg_size_pretty
----------------
 9859 kB
(1 row)

结论是:当页面整页镜像很多时(由于启用了校验和或 full_page_writes,这几乎总是如此),使用压缩很有意义。多种算法可以让我们在额外 CPU 负载和日志量减少之间找到折中。

11.3. 日志级别

预写式日志的主要任务是保证故障恢复。但扩大日志记录的范围后,日志还可以用于其他任务。日志级别有 minimal、replica 和 logical 三种。每个更高的级别都包含前一级别写入日志的全部内容,并增加一些新信息。

使用的级别由 wal_level 参数设置;修改该参数需要重启服务器。(默认值: replica)

Minimal

最低级别 minimal 只保证故障恢复。为了节省空间,如果当前事务中创建或清空的关系涉及大量数据插入,就不会记录相关操作(特别是 CREATE TABLE AS SELECT 和 CREATE INDEX 这类命令)18。系统不会记录这些操作,而是立即把所需数据刷到磁盘;系统目录的变化会在事务提交时变得可见。

如果操作执行过程中发生故障,已经写入磁盘的数据仍然不可见,不会破坏一致性。如果操作完成后才发生故障,那么应用后续日志记录所需的全部数据已经在磁盘上。

要让这种优化对刚创建的关系生效,需要写入的数据量由 wal_skip_threshold 参数决定。(v. 13)(默认值: 2MB)

看看 minimal 级别会写入哪些内容。

默认使用更高的 replica 级别来支持复制。设置 minimal 级别时,还必须把 max_wal_senders 参数清零;该参数决定允许多少个 walsender 进程:(默认值: 10)

=> ALTER SYSTEM SET wal_level = minimal;
=> ALTER SYSTEM SET max_wal_senders = 0;

修改参数需要重启服务器:

postgres$ pg_ctl restart -l /home/postgres/logfile

记下当前日志位置:

=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/8A6C82B0
(1 row)

清空表,并在同一个事务中插入新行,使其超过 wal_skip_threshold 的限制:

=> BEGIN;
=> TRUNCATE TABLE wal;
=> INSERT INTO wal
     SELECT id FROM generate_series(1,100_000) id;
=> COMMIT;
说明

我使用 TRUNCATE 命令,而不是创建单独的表,因为这样生成的日志记录更少。

使用熟悉的 pg_walinspect 扩展查看日志内容。(v. 15)

=> SELECT start_lsn,
  resource_manager AS rmgr,
  record_type,
  (regexp_match(block_ref, '[0-9]+\/[0-9]+\/[0-9]+'))[1] AS rel
FROM -- pg_get_wal_records_info_till_end_of_wal до v.16
        pg_get_wal_records_info('2/8A6C82B0','FFFFFFFF/FFFFFFFF');
 start_lsn  |    rmgr     | record_type |       rel
------------+-------------+-------------+-----------------
 2/8A6C82B0 | Storage     | CREATE      |
 2/8A6C82E0 | Heap        | UPDATE      | 1663/16391/1259
 2/8A6C8360 | Btree       | INSERT_LEAF | 1663/16391/2662
 2/8A6C83A0 | Btree       | INSERT_LEAF | 1663/16391/2663
 2/8A6C83E0 | Btree       | INSERT_LEAF | 1663/16391/3455
 2/8A6C8420 | Transaction | COMMIT      |
(6 rows)

第一条记录(CREATE)表示为关系创建了一个新文件(因为清空表实际上会重写该表)。(第 177 页)

接下来是几条与系统目录有关的记录。它们反映了 pg_class 表的变化(UPDATE),以及向该表上三个索引中添加记录(INSERT_LEAF)。把 rel 列显示的文件路径与关系名称对应起来,就可以验证这一点。(第 30 页)

最后是事务提交记录。向表中插入数据不会被记录到日志。

Replica

系统发生故障后恢复时,会通过日志记录把磁盘上的数据更新到一致状态。从备份恢复时也是如此。但在这种情况下,还可以使用归档日志,把数据状态推进到目标恢复点,而不只是恢复一致性。这些记录的数量可能很大(例如覆盖数天),恢复期间会经过多个检查点,而不是一个。因此 minimal 日志级别不够:没有记录的操作无法重放。要支持从备份恢复,日志中必须包含所有操作。

复制也是如此:没有记录到日志中的内容不会传送到副本,也不会在副本上重放。

如果副本还要用于执行查询,事情会更加复杂。首先,需要知道主服务器上出现的排他锁,因为它们可能与副本上的查询冲突。其次,需要能够构建数据快照,这需要知道正在运行的事务。对副本来说,不仅要考虑本地事务,还要考虑主服务器上的事务。(*第 261 页)(*第 97 页)

把这些信息传给副本的唯一方式,是定期将其写入日志19。这项工作由后台写入进程 bgwriter 负责20:系统有活动时,它每 15 秒(间隔不可配置)创建一条 RUNNING_XACTS 类型的记录。

从备份恢复数据和使用物理复制的能力由 replica 日志级别保证。

默认使用的正是这个级别,因此只需重置上面设置的参数并重新加载服务器:

=> ALTER SYSTEM RESET wal_level;
=> ALTER SYSTEM RESET max_wal_senders;
postgres$ pg_ctl restart -l /home/postgres/logfile

重复上一次的操作顺序(但只插入一行,以免输出过长):

=> SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn
---------------------------
 2/8ACE9650
(1 row)
=> BEGIN;
=> TRUNCATE TABLE wal;
=> INSERT INTO wal VALUES (42);
=> COMMIT;

现在检查日志记录。

=> SELECT start_lsn,
  resource_manager AS rmgr,
  record_type,
  (regexp_match(block_ref, '[0-9]+\/[0-9]+\/[0-9]+'))[1] AS rel
FROM pg_get_wal_records_info('2/8ACE9650','FFFFFFFF/FFFFFFFF');
 start_lsn  |    rmgr     |   record_type   |       rel
------------+-------------+-----------------+------------------
 2/8ACE9650 | Heap2       | PRUNE_ON_ACCESS | 1663/16391/1259
 2/8ACE9688 | Standby     | LOCK            |
 2/8ACE96B8 | Storage     | CREATE          |
 2/8ACE96E8 | Heap        | UPDATE          | 1663/16391/1259
 2/8ACE9768 | Btree       | INSERT_LEAF     | 1663/16391/2662
 2/8ACE97A8 | Btree       | INSERT_LEAF     | 1663/16391/2663
 2/8ACE97E8 | Btree       | INSERT_LEAF     | 1663/16391/3455
 2/8ACE9828 | Heap        | INSERT+INIT     | 1663/16391/24814
 2/8ACE9868 | Standby     | LOCK            |
 2/8ACE9898 | Standby     | RUNNING_XACTS   |
 2/8ACE98D0 | Transaction | COMMIT          |
(11 rows)

minimal 级别会出现的记录之外,还增加了以下记录:

  • 与复制有关的 Standby 资源管理器记录:LOCK(锁)和 RUNNING_XACTS(活跃事务);

  • 初始化新页面并同时向其中插入一行的记录 INSERT+INIT。

Logical

最后,最高级别 logical 提供逻辑解码和逻辑复制能力。发布服务器必须启用这个级别。

从日志记录的角度看,这个级别与 replica 几乎没有区别:现有记录中会加入行标识符21;还会出现与复制来源和缓存失效有关的记录,以及应用可以添加的任意逻辑记录。不过,逻辑解码主要依赖正在运行的事务信息(RUNNING_XACTS):这些信息用于构建数据快照,以跟踪系统目录的变化。