分类:
2008-05-20 17:49:10
|
1. 综述; 为了保持所安装的 PostgreSQL 服务器平稳运行, 我们必须做一些日常性的维护工作。我们在这里讨论的这些工作都是经常重复的事情,可以很容易地使用标准的 Unix 工具,比如cron 脚本来实现。 不过,设置合适的脚本以及检查它们是否成功执行则是数据库管理员的责任,一件很明显的维护工作就是经常性地创建数据的备份拷贝。 如果没有最近的备份,那么您就没有从灾难中恢复的机会(比如磁盘坏了,失火,误删了表等等)。 其它主要的维护范畴的工作包括周期性的 "vacuuming" (清理)数据库。 其它需要周期性注意的东西是日志文件的管理。 PostgreSQL 和其它数据库产品比较起来是低维护量的。 但是,适当在这些任务上放一些注意将更加能够确保我们的愉快工作和获取对这个系统富有成效的经验。 操作环境:PostgreSQL8.2+Ubuntu 7.04 2. 日常清理; 2.1 VACUUM; 2.1.1 语法结构; VACUUM [ FULL | FREEZE ] [ VERBOSE ] [ table ] VACUUM [ FULL | FREEZE ] [ VERBOSE ] ANALYZE [ table [ (column [, ...] ) ] ] 2.1.2 描述; VACUUM 回收已删除元组占据的存储空间。 在一般的 PostgreSQL 操作里, 那些已经 DELETE 的元组或者被 UPDATE 过后过时的元组是没有从它们所属的表中物理删除的; 在完成 VACUUM 之前它们仍然存在。因此我们有必须周期地运行 VACUUM, 特别是在常更新的表上,如果没有参数,VACUUM 处理当前数据库里每个表, 如果有参数,VACUUM 只处理那个表,简单的 VACUUM (没有FULL) 只是简单地回收空间并且令其可以再次使用; 2.1.3 参数; FULL ------选择"完全"清理,这样可以恢复更多的空间, 但是花的时间更多并且在表上施加了排它锁。 FREEZE ---------选择激进的元组"冻结"。 VERBOSE --------- 为每个表打印一份详细的清理工作报告。 ANALYZE --------- 更新用于优化器的统计信息,以决定执行查询的最有效方法。 table ------- 要清理的表的名称(可以有模式修饰)。缺省时是当前数据库中的所有表。 column ---------要分析的具体的列/字段名称。缺省是所有列/字段。 2.1.4 为什么要用VACUUM; VACUUM命令的含义为:垃圾收集以及可选地分析一个数据库。VACUUM回收已删除元组占据的存储空间。在一般的 PostgreSQL 操作里,那些已经 DELETE 的元组或者被 UPDATE 过后过时的元组是没有从它们所属的表中物理删除的; 在完成 VACUUM 之前它们仍然存在。 由于以下几个原因,我们必须周期性运行 PostgreSQL 的 VACUUM 命令∶ 1.恢复那些由已更新的或已删除的行占据的磁盘空间。 2.更新 PostgreSQL 查询规划器使用的数据统计信息。 3.避免因为事务 ID 重叠造成的老旧数据的丢失。 对上面每个条件进行 VACUUM 操作的频率和范围因不同的节点而不同。 因此,数据库管理员必须理解这些问题并且开发出合适的维护策略。 建议在经常VACUUM(清理)(至少每晚一次)生产数据库, 以保证不断地删除失效的行。尤其是在增删了大量记录之后, 对受影响的表执行 VACUUM ANALYZE 命令是一个很好的习惯。例如: sir=# VACUUM VERBOSE ANALYZE access ; 信息: 正在清理 (vacuum) "public.access" 信息: index "access_pkey" now contains 0 row versions in 1 pages DETAIL: 0 index row versions were removed. 0 index pages have been deleted, 0 are currently reusable. CPU 0.00s/0.00u sec elapsed 0.00 sec. 信息: "access": found 0 removable, 0 nonremovable row versions in 0 pages DETAIL: 0 dead row versions cannot be removed yet. There were 0 unused item pointers. 0 pages contain useful free space. 0 pages are entirely empty. CPU 0.00s/0.00u sec elapsed 0.00 sec. 信息: 正在清理 (vacuum) "pg_toast.pg_toast_16464" 信息: index "pg_toast_16464_index" now contains 0 row versions in 1 pages DETAIL: 0 index row versions were removed. 0 index pages have been deleted, 0 are currently reusable. CPU 0.00s/0.00u sec elapsed 0.00 sec. 信息: "pg_toast_16464": found 0 removable, 0 nonremovable row versions in 0 pages DETAIL: 0 dead row versions cannot be removed yet. There were 0 unused item pointers. 0 pages contain useful free space. 0 pages are entirely empty. CPU 0.00s/0.00u sec elapsed 0.00 sec. 信息: 正在分析 "public.access" 信息: "access": scanned 0 of 0 pages, containing 0 live rows and 0 dead rows; 0 rows in sample, 0 estimated total rows VACUUM 这样做将更新系统目录为最近的更改,并且允许 PostgreSQL 查询优化器在规划用户查询时有更好的选择。 不建议日常使用 FULL 选项,但是可以在特殊情况下使用。 一个例子就是在您删除了一个表的大部分行之后,希望从物理上缩小该表以减少磁盘空间占用。VACUUM FULL 通常要比单纯的 VACUUM 收缩更多表的尺寸; 2.2 恢复磁盘空间; 2.2.1 概述; 在正常的 PostgreSQL 操作里, 对一行的UPDATE或DELETE并未立即删除旧版本的数据行。这个方法对于获取多版本并行控制的好处是必要的: 如果一个行的版本仍有可能被其它事务看到,那么您就不能删除它。但到了最后,不会有任何事务对过期的或者已经删除的元组感兴趣。 而它占据的空间必须为那些新的元组使用而回收,以避免对磁盘空间增长的无休止的需求。这件事是通过运行 VACUUM 实现的。 很明显,那些经常更新或者删除元组的表需要比那些较少更新的表清理的更频繁一些。 所以,设置一个周期性的 cron 任务 VACUUM 那些选定的表,而忽略那些已经知道变化比较少的表. 这个方法只是在您拥有大量更新频繁的表和大量很少更新的表的时候有意义 — 清理一个小表的额外开销根本不值得担心; VACUUM 命令有两个变种: 第一种形式,叫做"懒汉 vacuum"或者只是 VACUUM,在表和索引中标记过期的数据为将来使用;它并不试图立即恢复这些过期数据使用的空间。因此,表文件不会缩小,并且任何文件中没有使用的空间都不会返回给操作系统。 这个变种的 VACUUM 可以和通常的数据库操作并发执行; 第二种形式是 VACUUM FULL 命令。 这个形式使用一种更加激进的算法来恢复过期的的行版本占据的空间。 任何 VACUUM FULL 释放的空间都立即返回给操作系统。 但是,这个形式的 VACUUM 命令在进行 2.2.2 VACUUM FULL; VACUUM FULL 一个表的时候在其上要求一个排他锁。 因此,经常使用 VACUUM FULL 会对并发数据库查询有着非常糟糕的影响; 标准形式的 VACUUM 最适合用于维护相当程度的磁盘用量的稳定状态。 如果您需要把磁盘空间归还给操作系统,那么您可以使用 VACUUM FULL — 不过如果释放的磁盘空间又会很快再次被分配又怎样? 如果维护更新频繁的表,那么中等频率的多次标准 VACUUM 运行方法比很低频率的 VACUUM FULL 更好; 对于大多数节点而言,我们推荐的习惯是在一天中的低使用的时段安排一次整个数据库的 VACUUM, 必要时外加对更新频繁的表的更经常的清理。(有些环境下,对那些更新非常频繁的表可能会每几分钟就 VACUUM 一次。) 如果您的集群中有多个数据库,别忘记对每个库进行清理; vacuumdb脚本可能会帮您的忙; 如果您知道自己刚删除掉一个表中大部分的行,那么我们建议使用VACUUM FULL, 这样该表的稳定态尺寸可以因为VACUUM FULL更富侵略性的方法而显著减小。 日常的磁盘空间清理,请使用 VACUUM,而不是 VACUUM FULL; 如果您有一个表,它的内容经常被完全删除,那么可以考虑用 TRUNCATE,而不是后面跟着 VACUUM 的 DELETE。 TRUNCATE 立即删除整个表的内容, 而不要求随后的 VACUUM 或者VACUUM FULL 来恢复现在没有用的磁盘空间; 2.3 更新规划器统计; PostgreSQL 的查询规划器依赖一些有关表内容的统计信息用以为查询生成好的规划。 这些统计是通过ANALYZE 命令获得的,您可以直接调用这条命令, 也可以把它当做 VACUUM 里的一个可选步骤来调用。拥有合理准确的统计是非常重要的,否则,选择了恶劣的规划很可能会降低数据库的性能; 和为了回收空间做清理一样,经常更新统计信息也是对更新频繁的表更有用。不过,即使是更新非常频繁的表,如果它的数据的统计分布并不经常改变,那么也不需要更新统计信息。一条简单的拇指定律就是想想表中字段的最大很最小值改变的幅度。 比如,一个包含行更新时间的 timestamp 字段将是随着行的追加和更新稳定增长最大值的; 这样的字段可能需要比那些包含访问网站的 URL 的字段更频繁一些更新统计信息。 那些 URL 字段可能改变得一样频繁,但是其数值的统计分布的改变相对要缓慢得多; 我们可以在特定的表,甚至是表中特定的字段上运行 ANALYZE, 所以如果您的应用有需求的话,我们是可以对某些信息更新得比其它信息更频繁的。 不过,在实际中,这种做法的实用性是值得怀疑的。 ANALYZE 是一项相当快的操作,即时在大表上也很快, 因为它使用了统计学上的随机采样的方法进行行采样, 而不是把每一行都读取进来。因此,每隔一段时间对整个数据库运行一便这条命令可能更简单; 注: 尽管用 ANALYZE 按字段进行挖掘的方式可能不是很实用, 但您可能还是会发现值得按字段对 ANALYZE 收集的统计信息的详细级别进行调整。 那些经常在WHERE子句里使用的字段如果有非常不规则的数据分布,那么就可能需要比其它字段更细致的数据图表.参阅 ALTER TABLE SET STATISTICS. 我们对大多数节点都建议在每天的低使用时段安排一次数据库范围的 ANALYZE: 这个任务可以有效地和每天的 VACUUM 组合在一起。 不过,这对那些表统计信息改变相对缓慢的节点可能会过于夸张, 而且少一些的 ANALYZE 也足够了; 2.4 避免事务 ID 重叠造成的问题; PostgreSQL 的 MVCC 事务语意依赖于比较事务 ID(XID)的数值: 一条带有大于当前事务的 XID 的插入 XID 的行版本是"属于未来的", 并且不应为当前事务可见。但是因为事务 ID 的大小有限(在我们写这些的时候是 32 位),如果一次集群如果运行的时间很长(大于 4 十亿次事务), 那么它就要受到事务 ID 重叠的折磨:XID 计数器回到零位,然后突然间所有以前的事务就变成看上去是在将来的 — 这意味着它们的输出将变得可见。简而言之,可怕的数据丢失,(实际上数据仍然在那里,但是如果您无法获取数据,这么说也只是幸灾乐祸。) 在 PostgreSQL 7.2 之前, 防御 XID 重叠的唯一办法就是至少每40亿事务就重新做一次initdb。这种做法对高流量的节点而言当然不是令人满意的做法,所以我们设计了更好的方法。 新的方法允许某个服务器仍然保持运行状态,不需要 initdb 或者任何类型的重启。 代价就是下面这样的维护要求: 数据库中的每个表都必须在每十亿次事务中至少清理一次 . 从实际角度出发,这个要求不算一个很繁重的要求,但是因为如果我们没能满足这个要求的后果是全部数据的丢失(而不仅仅是磁盘空间的浪费或者性能的下降),我们制作了一些特殊的东西来帮助数据库管理员避免灾难的发生。 对于集群中的每个数据库,PostgreSQL 都跟踪自上次全数据库范围 VACUUM 以来的时间。 如果任何数据库接近了十亿次事务的危险级别,系统就开始发出警告信息。如果什么都不干,那么系统最终会停止正常的操作,直到进行了合适的手工操作。 本节剩下的部分给出这方面的细节。 XID 比较的新方法剥离出两个特殊的 XID,数字 1 和 2 (BootstrapXID 和 FrozenXID)。 这两个 XID 总是被认为表任何普通的 XID 旧。普通的 XID(那些大于 2 的)使用模-231运算进行比较。 这就意味着对于每个普通的 XID,总是有二十亿个 XID 是"更旧"以及二十亿个 XID"更新"; 表达这个意思的另外一个方法是普通的 XID 空间是没有终点的环。因此,一旦一条元组带着特定的普通 XID 创建出来,那么该元组 将在以后的二十亿次事务中表现得是"在过去",而不管我们说的是哪个普通 XID。如果该元组在超过二十亿次事务之后仍然存在, 那么它就会突然变成在将来的元组。为了避免数据丢失,老的元组必须在到达二十亿次事务的年龄之前的某个时候赋予 XID FrozenXID。 一旦它被赋予了这个特殊的 XID,那么它们在所有普通事务面前表现为 "在过去",而不管事务 ID 是否重叠,因此这样的元组直到删除之前都会完好,不管要保存多长时间.这个 XID 的重新赋值是VACUUM 控制的. VACUUM 的正常策略是给任何其普通 XID 有超过十亿次已过去事务行版本重新赋值为 FrozenXID。 这个策略保留了原来的插入 XID 直到该数值不再令人感兴趣为止。 (实际上,大多数行版本将可能在还没有"冻结"之前就完成生存和消亡了)。 在这个策略下,任何表在两次 VACUUM 运行之间的最大的安全间隔是十亿次事务:如果您等的时间更长,那么最后就可能就会有一条不够老的行版本在重新赋值时变成比二十亿次事务更老, 并因此重叠到了未来 — 也就是说,您失去它了。(当然,它在另外二十亿次事务之后会重新出现,不过那样也无济于事。) 因为上面的原因,我们需要周期性地运行 VACUUM, 所以很难有哪个表会到十亿次事务还没有清理过。但是,为了帮助管理员确保满足了这个要求, VACUUM 在系统表pg_database 里存储了事务 ID 统计。 尤其是一个数据库的 pg_database 行中的 datfrozenxid 字段在任何数据库范围的 VACUUM 操作(也就是没有声明任何表的VACUUM)之后将会被更新。这个字段里存储的数值是该 VACUUM 命令使用的冻结终止的 XID。 系统保证在该数据库中所有比这个终止 XID 老的普通 XID 都被 FrozenXID 代替。 检查这个信息的一个便利的方法是执行下面的查询 SELECT datname, age(datfrozenxid) FROM pg_database; age 字段用于测量从中止 XID 到当前事务的 XID 的数目。 使用了这种标准的冻结策略,对一个刚清理过的数据库而言, age 字段将从十亿处开始。当age到达二十亿次的时候,数据库必须再次清理以避免事务标识重叠造成的问题。 我们建议的策略是至少每半个十亿次(5亿次)事务 VACUUM 一次数据库,这样就可以保证足够的安全边界范围.为了帮助满足这条规则, 如果有任何 pg_database 记录显示出超过15亿次事务的 age,那么每次数据库范围的VACUUM 都会自动发出一条警告,比如: play=# VACUUM; WARNING: database "mydb" must be vacuumed within 177009986 transactions HINT: To avoid a database shutdown, execute a full-database VACUUM in "mydb". VACUUM 如果忽略了上面这样的 VACUUM 信息,如果距离事务 ID 重叠小于 1 千万次, 那么 PostgreSQL 就会在每次事务开始前发出类似上面的警告。 如果这些警告还是被忽略了,那么系统将在距离重叠小于 1 百万次的时候关闭,并且拒绝执行任何新的事务: play=# select 2+2; ERROR: database is shut down to avoid wraparound data loss in database "mydb" HINT: Stop the postmaster and use a standalone backend to VACUUM in "mydb". 这个 1 百万的事务安全边界留下来用于让管理员在不丢失数据的情况下进行恢复, 方法是手工执行所需要的 VACUUM 命令。不过,因为一旦进入了安全关闭模式,系统就不能再执行命令, 做这件事情的唯一的方法是停止 postmaster,使用一个单独运行的后端来执行 VACUUM。 关闭模式不会强制于独立运行的后端。 带着 FREEZE 选项的 VACUUM 使用了更大胆的冻结策略: 如果行版本已经老得被所有打开的事务看做是良好的,那么就都冻结.特别是如果在一个空闲的数据库上运行 VACUUM FREEZE,那么就保证该数据库中所有的行版本都被冻结。因此,只要该数据库没有其它的变化,那么它就不需要后续的清理以避免事务 ID 重叠问题。 这个技巧被 initdb 用于准备 template0数据库。 我们也应该用这个方法对所有在 pg_database表里标记着 datallowconn = false的数据库进行初始化, 因为我们还没有任何便利的方法 VACUUM 一个您无法联接的数据库。 2.5 auto-vacuum 守护进程; 从 PostgreSQL 8.1 开始,系统带有一个额外的可选服务进程, 叫做 autovacuum 守护进程,它的目的是自动执行 VACUUM 和 ANALYZE 命令。在打开这个选项之后,autovacuum 守护进程将周期性运行并且检查那些有大量插入,更新或者删除元组操作的表。 这些检查使用行级别的统计收集设施;因此,除非把 stats row level和 stats row level设置为 true,否则无法使用 autovacuum 守护。 还有,在为superuser reserved connections选择数值的时候,不要忘记给 autovacuum 进程保留一个槽位。 如果打开了 autovacuum 守护,那么它会每隔autovacumm natime秒钟运行一次,并且检查应该处理哪个数据库。任何临近事务 ID 重叠的数据库都会被立即处理。这个时候,autovacuum 发出一个数据库范围的 VACUUM 调用,如果是模板数据库,则发出 VACUUM FREEZE, 然后终止。如果没有数据库复合这个标准,则选择被上次 autovacuum 处理时间最远的那个数据库。 这种情况下,该数据库里的表被检查,然后根据需要发出独立的 VACUUM 或者 ANALYZE 命令。 对于每个表,用两个条件来判断应该使用哪个操作。 如果上次 VACUUM 之后的过期元组的数量超过了"清理阈值(vacuum threshold)", 那么就清理改表。清理阈值是定义为: 清理阈值 = 清理基本阈值 + 清理缩放系数 * 元组数 (vacuum threshold = vacuum base threshold + vacuum scale factor * number of tuples) 这里的清理基本阈值是autovacuum_vacuum_threshold, 清理的缩放系数是 autovacuum_vacuum_scale_factor, 元组的数目是失效的元组数目是从统计收集器里面获取的;这事一个半精确的计数,由每次 UPDATE 和 DELETE 操作更新。(它只是半精确的是因为在重载下,有些信息可能会丢失。) 为了分析,使用了一个类似的条件:分析阈值,定义为 分析阈值 = 分析基本阈值 + 分析缩放系数 * 元组数目 (analyze threshold = analyze base threshold + analyze scale factor * number of tuples) 它会和上次 ANALYZE 插入,更新,或者删除的元组总数进行比较。 缺省的阈值和伸缩系数是从 postgresql.conf 里面取得的,不过,我们可以以每个表独立设置的方式覆盖它,方法就是在系统表pg_autovacuum里输入记录。 如果 pg_autovacuum 里面存在对某个特定表的行,那么就使用它声明的设置; 否则使用全局设置。 除了基本阈值和缩放系数之外,在 pg_autovacuum 里还有三个参数可以为每个表进行设置。首先,pg_autovacuum.enabled 可以设置为 false, 让 autovacuum 守护进程完全忽略某个表。这种情况下,autovacuum 只有在为了避免事务 ID 重叠清理整个数据库的时候才会动那个表。另外两个参数,清理开销延迟 (pg_autovacuum.vac_cost_delay)和清理开销限制(pg_autovacuum.vac_cost_limit), 用于为基于开销的清理延迟特性设置表相关的数值。 如果在 pg_autovacuum 里任何数值设置为负数, 或者在 pg_autovacuum 里就根本没有出现特定表的数据行, 那么使用 postgresql.conf 里面对应的数值。 目前没有任何制作 pg_autovacuum 记录的支持, 只能手工向该系统表中 INSERT。这个特性将在以后的版本中改进, 并且这个系统表的定义也很有可能会改变。 3. 经常重建索引; 有时候我们值得用REINDEX命令周期的重建索引; 在 PostgreSQL 版本 7.4 之前,我们经常有必要避免"索引膨胀", 因为缺乏在 B-tree 索引内部的空间恢复机制。一个情况是就是索引健字的范围随着时间而变化 — 比如,一个在某个表的时间戳上的索引,随着时间的推移,旧的记录会最终被删除 — 就会导致膨胀,因为那些用于不再使用的键字范围的索引页面不回得到重复使用。 随着时间的推移,索引的尺寸可能会变得比里面的有用的数据大得多。 从 PostgreSQL 7.4 开始,那些已经完全清空的索引页会得到重复使用。不过这样仍然会有不充分使用空间的可能:如果一个页面中大多数索引键字被删除,只留下很少的部分呢,那么该页仍然将被分配。所以,如果使用模式是这样的:每个范围里除了少数键字之外,其他大部分键字最终都被删除;那么这样也会导致空间的低效使用。膨胀的可能性不是无穷的 — 最差的情况是每个页面至少还有一个键字 — 但是对这样的使用模式,我们可能仍然值得安排周期性的重新索引; 对于非 B-tree 索引的膨胀可能还没有很好地定量分析。 在使用非 B-tree 索引的时候保持对索引的物理尺寸的监控是个很好的主意; 还有,对于 B-tree 索引,一个新建立的索引从某种意义上比更新了多次的访问起来要快,因为在新建立的索引上,逻辑上连接的页面通常物理上也连接在一起。 (这样的考虑目前并不适用于非 B-tree 索引。)仅仅从提高访问速度角度出发, 可能我们也值得周期性的重建索引。 4. 日志文件维护; 把数据库服务器的日志输出保存在一个地方是个好主意。 在碰到危险的问题的时候,日志输出是非常宝贵的。 不过,日志输出可能很庞大(特别是在比较高的调试级别上), 而且您不会无休止地保存它们.您需要"旋转"日志文件, 这样生成新的日志文件并且经常抛弃老的; 如果您简单地把postmaster的stderr定向到一个文件中, 您会有日志输出, 但是截断日志文件的唯一的方法是停止并重起postmaster。 这样做对于开发环境中使用 PostgreSQL 可能是可以的,但是您肯定不想在生产环境上这么干; 一个更好的办法是把 postmaster 的 stderr (错误流 ‘standard error’)输出发送到某种日志旋转程序里。我们有一个内置的日志旋转程序,您可以通过在 postgresql.conf 里设置配置参数 redirect_stderr 为 true 的办法打开它,终端下输入: xiaop@xiaop-laptop:~$ sudo vim /etc/postgresql/8.2/main/postgresql.conf 找到:#redirect_stderr = off 一项,将#号注掉,然后将off改为on; 另外,您可能会觉得把 postmaster 的stderr 输出给某些日志旋转脚本会更好些,特别是您已经在其它服务器上用了这个程序的时候。 比如,包含在 Apache 发布里的 rotatelogs 工具就可以用于 PostgreSQL。 要这么做,只需要把 postmaster 的 stderr 重定向到指定程序。 如果您用 pg_ctl 启动服务器,那么 stderr 已经重定向到 stdout, 因此您只需要一个管道命令,比如: pg_ctl start | rotatelogs /var/log/pgsql_log 86400 另外一种生产级的管理日志输出的方法就是把它们发送给 syslog,让 syslog 处理文件旋转。 要利用这个工具,我们需要设置 postgresql.conf 里的 log_destination 配置参数设置为 syslog (记录 syslog 日志) 终端下输入: xiaop@xiaop-laptop:~$ sudo vim /etc/postgresql/8.2/main/postgresql.conf 然后修改 #log_destination = 'stderr' 将stderr改为syslog; 然后在您想强迫 syslog 守护进程开始写入一个新日志文件的时候, 您就可以发送一个 SIGHUP 信号给它。 如果您想自动旋转日志文件,那么我们可以配置 logrotate 程序处理 syslog 的日志文件。 不过,在很多系统上,syslog 不是非常可靠,特别是在大型日志信息的情况下; 它可能在您最需要那些信息的时候截断或者丢弃它们。 还有,在 linux 上,syslog 会把每个消息刷新到磁盘上, 导致很恶劣的性能。 (您可以在 syslog 配置文件里面的文件名开头使用一个 - 来关闭这个行为。) 请注意上面描述的所有解决方案关注的是在可配置的间隔上开始一个新的日志文件, 它们并没有处理删除旧的,不再需要的日志文件的事情。您可能还需要设置一个批处理,周期地删除旧日志文件。 另外一个可能的解法是配置日志旋转程序,让它周期地覆盖旧的日志文件。 |