3.5. 可管理性

3.5.1. 内存
3.5.2. 存储
3.5.3. 日志记录和调试
3.5.4. VACUUM
3.5.5. 升级

3.5.1. 内存

大的结果集导致客户端内存不足

  • 当运行 SELECT 语句时,ltsql 会检索整个结果集并将所有行存储在客户端内存中。

  • 通过设置 FETCH_COUNT 变量,例如使用 "ltsql -v FETCH_COUNT=100 ...", ltsql 将使用游标并发出 DECLARE, FETCH 和 CLOSE 命令来分批检索结果集。

  • 客户端驱动程序也有类似的功能,例如 psqlODBC 的 UseDeclareFetch 和 ltJDBC 的 defaultRowFetchSize 连接参数。

导致服务器端内存不足(OOM)问题的常见原因

  • 较多的连接数

    • 即使是空闲的连接也可能持续占用大量内存。Lightdb 在会话期间会在内存中保留数据库对象的元数据。这是为了性能考虑。您可以通过膨胀的 CacheMemoryContext 注意到这一点。

    • 如果使用了连接池,许多连接可能会随着时间推移消耗大量的内存。这是因为连接从池中随机挑选出来用于访问某些关系后,再释放回池中,这会导致许多会话累积多个关系的元数据。

  • 较高的 work_mem 值

    • 不建议在实例级别(lightdb.conf)或数据库级别(ALTER DATABASE)设置较高的 work_mem 值。许多会话可能会同时分配该数量的内存。更糟的是,每个 SQL 语句可以并行执行排序和/或哈希操作,每个操作都可以分配相当于 work_mem 的内存。

    • 对于基于哈希的操作,最多将分配 work_mem * hash_mem_multiplier 字节的工作内存。

  • 较低的 max_locks_per_transaction

    • 每个锁定的对象(例如,表、索引、序列、XID,但不是行)在被锁定时都会在锁表中分配一个条目。该条目表示锁定的对象、授予者、等待者以及已授予/请求的锁模式。

    • 锁表是在共享内存中分配的。其大小在服务器启动时固定。

    • max_locks_per_transaction 的默认值为 64。这意味着每个事务预期锁定 64 个或更少的对象。

    • 锁表中的条目数为 (max_connections + max_prepared_transactions + alpha) * max_locks_per_transaction。

    • 如果每个事务需要使用超过 max_locks_per_transaction 的条目,只要可用就可以使用更多条目。

    • 如果许多并发事务可能访问更多的对象,比如触碰数百或数千个分区,请增加 max_locks_per_transaction 的值。

无法检索大容量的 bytea 值

  • 例如,在成功插入 550MB 的 bytea 列值之后,尝试获取它会因为类似 "invalid memory aloc request size 1277232195" 的错误消息而失败。

  • 为什么?

    • 当 Lightdb 服务器向客户端发送查询结果时,它要么将数据转换为文本格式,要么以二进制格式返回。

    • ltsql 和客户端驱动程序指示服务器使用文本格式。

    • Lightdb 在转换 bytea 数据为文本格式时使用十六进制或转义格式。默认格式是十六进制。十六进制和转义格式分别使用 2 和 4 字节来表示每个原始字节在文本格式下的内容。

      • 例如 SELECT 'abc'::bytea; 返回 \x616263

    • Lightdb 服务器分配一块连续的内存区域来将每列值转换为文本格式。此分配大小限制为 1GB 减 1。这种限制与 TOAST 处理变长数据类型有关。

    • 因为此限制,Lightdb 无法以文本格式返回超过 500MB 的 bytea 数据。


3.5.2. 存储

存储空间满的常见原因

  • 表膨胀:因为 VACUUM 不能移除死行。死行存在的原因另见说明。

  • WAL 文件积累:WAL 文件量持续增长的原因另见说明。

  • 服务器日志文件

    • 由于使用了 ltAudit、auto_explain 以及其他如 log_statement 和 log_min_duration_statement 等日志参数而导致的日志过多。

    • 日志轮换和清除配置不当:log_rotation_age、log_rotation_size、log_truncate_on_rotation。

  • 创建临时文件

    • 工作内存(work_mem)较小和/或查询计划较差。

    • 保持打开的可保持游标。

      • 例如 DECLARE CURSOR cur WITH HOLD FOR SELECT * FROM mytable; COMMIT;

      • 在提交过程中,可保持游标的查询结果会被存储在一个大小由 work_mem 决定的工作内存区域中,超出 work_mem 的内容会被溢出到临时文件。

    • 使用以下方式检查临时文件使用情况:

      • pg_stat_database 中的 temp_files 和 temp_bytes

      • log_temp_files = on,当文件被删除时记录文件路径和大小

      • 通过 EXPLAIN ANALYZE 或 auto_explain 获取的查询计划

存储配额

  • Lightdb 除了对临时文件外无法约束存储使用。

  • temp_file_limit 可以限制每个会话即时使用的临时文件总大小。超出此限制的事务将被终止。

  • 如果想限制数据库、表或 WAL($LTDATA/lt_wal/) 的大小,可以将其放在一个有大小限制的文件系统上的表空间中。

    • 数据库/表的表空间可以通过 CREATE/ALTER DATABASE/TABLE ... TABLESPACE 明确指定,或者通过默认的表空间参数 default_tablespace 隐式指定。

    • temp_tablespaces 可用于指定临时表/索引以及排序/哈希操作创建的临时文件的位置。

    • WAL 目录可以通过 lt_initdb 的 --waldir 选项指定。此外,在数据库集群创建后,也可以将其移出数据目录并通过符号链接关联。

3.5.3. 日志记录和调试

FATAL: 数据库正在启动

  • 在旧版本中,此信息可以在服务器启动期间每隔一秒输出一次。

  • 这看起来令人惊讶,但实际上并不是一个问题。

  • “为什么?lt_ctl start”命令在后台启动 postmaster,并试图每隔一秒连接到数据库。如果连接成功,lt_ctl 返回成功。否则,如果服务器仍在进行恢复且无法接受连接,则会报告上述信息。

  • 在新版本中,你不会再看到这个信息。lt_ctl 不再尝试连接。相反地,postmaster 在能够接受连接时会在 postmaster.pid 文件中写入“ready”,然后 lt_ctl 检查这个状态。

通过限制目标来避免过多的日志记录

  • 不仅仅日志参数,许多其他参数也可以针对每个用户、数据库或它们的组合进行配置。例如:

    • ALTER USER oltp_user SET log_min_duration_statement = '3s';

    • ALTER DATABASE analytics_db SET log_min_duration_statement = '60s';

    • ALTER USER batch_user IN DATABASE oltp_db SET log_min_duration_statement = '30s';

可以在不干扰服务器日志的情况下启用调试日志

  • 全局设置 log_min_messages 为 DEBUG1 - DEBUG5 可能不可取,因为那会产生大量的日志。

  • 你可以仅在客户端为特定操作获取调试消息,如下所示:

    • export LTOPTIONS="-c client_min_messages=DEBUG5"

ltsql -d postgres -c "select 1"

查看 ltsql 的反斜杠命令做了什么

  • 使用 ltsql 的 -E/--echo-hidden 选项。它会显示后台发出的查询。

删除重复行

  • 下面的查询删除重复行,只留下具有最小 ctid 的那一行,并显示被删除的行内容。

  • ctid 是一个系统列,代表行版本在其表中的物理位置:(块号,项目ID)。由于 UPDATE 和 VACUUM FULL,ctid 可能会改变,所以在执行此操作时,可能需要锁定表至 Share 或更强模式。

WITH x AS (SELECT some_table dup, min(ctid)
    FROM        some_table
    GROUP BY 1
    HAVING count(*) > 1
)
DELETE FROM    some_table
USING     x
WHERE     (some_table) = (dup)
    AND some_table.ctid <> x.min
RETURNING some_table.*;

3.5.4. VACUUM

VACUUM 的目的

  • 回收或重用被更新或删除的行所占用的磁盘空间。

  • 更新 Lightdb 查询规划器使用的数据统计信息。

  • 更新可见性映射,这可以加速索引扫描。

  • 防止由于事务ID或多事务ID循环而导致非常旧的数据丢失。

VACUUM 类型

  • 并发(懒惰或普通)VACUUM

    • 对目标关系获取 Share Update Exclusive 锁。不影响 SELECT 和 DML 命令。

    • 保留原始数据文件并修改它们。TIDs 不变。

    • 当文件末尾有超过一定数量的连续空闲块时,数据文件才会缩小。文件中间未使用的空间留待重用。

    • 在 pg_stat_progress_vacuum 视图中报告其进度。

  • FULL VACUUM

    • 对目标关系获取 Access Exclusive 锁。阻止 SELECT 和 DML 命令。

    • 将存活的元组从旧的数据文件复制到新的数据文件,并移除旧的数据文件。重建索引。TIDs 改变。

    • 数据文件将被完全且最小化地打包。

    • 可能会使用两倍的磁盘空间:一个是现有的关系,另一个是新的关系。

    • 总是积极地冻结元组。

    • 实际处理与 CLUSTER 相同。

    • 在 pg_stat_progress_cluster 视图中报告其进度。

  • 自动 VACUUM 从不运行 FULL VACUUM。

VACUUM 的主要步骤

  1. 开始一个事务。

    • 当存在多个目标关系时,VACUUM 对每个关系开始并提交一个事务以便尽快释放锁。

  2. 获取堆的 Share Update Exclusive 锁并打开它。非防止循环的 VACUUM 如果关系无法获取锁,则放弃对该关系的 VACUUM,并发出以下信息。

    • LOG: 跳过 VACUUM "rel_name" --- 锁不可用

  3. 获取索引的 Row Exclusive 锁并打开它们。

  4. 分配工作内存以累积已删除元组的 TIDs。

  5. 重复以下步骤直到整个堆被处理:

    • 扫描堆:在工作内存中累积已删除元组的 TIDs,直到内存填满或到达堆的末尾。保留已删除元组的项ID。同时,根据需要修剪和碎片整理每一页,并可能冻结存活元组。

    • 清理索引:删除包含已删除元组 TIDs 的索引条目。

    • 清理堆:回收已删除元组的项ID。这在这里完成而不是在扫描堆时完成,因为在指向它的索引条目被删除之前,项ID不能被释放。

    • 在上述处理过程中更新 FSM 和 VM。

  6. 清理索引。

    • 更新每个索引在 pg_class 的 relpages 和 reltuples 中的统计信息。

    • 关闭索引但保留其锁直到事务结束。

  7. 截断堆以返回关系末尾的空页面给操作系统。

    • 如果堆在其末尾至少有 1,000 块或 (关系大小 / 16) 的连续空闲块,则数据文件被截断。

    • 获取堆的 Access Exclusive 锁。如果另一个事务持有冲突的锁,则最多等待 5 秒钟。如果无法获取锁,则放弃截断。

    • 向后扫描堆以验证末尾页面仍然为空。定期检查是否有其他事务在等待冲突的锁。如果有其他人等待,则释放 Access Exclusive 锁并放弃截断。

  8. 更新关系统计信息。

    • 更新 pg_class 的 relpages、reltuples、relallvisible、relhasindex、relhasrules、relhastriggers、relfrozenxid 和 relminmxid。

  9. 关闭关系。

  10. 提交一个事务。

  11. 清理关系的 TOAST 表。

  12. 对每个关系重复上述处理过程。

  13. 更新数据库统计信息。

    • 更新 pg_database.datfrozenxid 为 pg_class.relfrozenxid 值中的最小值,并截断 lt_xact/ 中的提交日志。

    • 更新 pg_database.datminmxid 为 pg_class.relminmxid 值中的最小值,并截断 lt_multixact/ 中的 MultiXact 数据。

自动 VACUUM 被设计为非侵入性的

  • 自动 VACUUM 每次完成一定量的工作后都会休息(休眠)。因此,它不会持续消耗资源。

    • “一定量的工作”和休眠时间可以通过 autovacuum_vacuum_cost_limit 和 autovacuum_vacuum_cost_delay 分别进行配置。autovacuum_vacuum_cost_delay 默认是 2 毫秒。

  • 如果自动 VACUUM 由于某些冲突的锁而无法获得关系上的锁,则自动 VACUUM 会跳过该关系。防止循环的自动 VACUUM 不这样做。

  • 如果并发事务在等待一个冲突的关系锁 deadlock_timeout 秒后发现锁是由自动 VACUUM 占用的,则取消非激进的自动 VACUUM。可以看到以下消息:

    • ERROR: 取消自动 VACUUM 任务

    • DETAIL: 自动 VACUUM 表 "mytable"

  • 如果 VM 显示数据页仅包含对所有事务都可见的元组(VM 中设置了全可见位),则 VACUUM 会跳过读取这些数据页。激进的 VACUUM 即使是这样的页面也会读取以冻结元组。

  • 如果 VM 显示数据页仅包含冻结的元组(VM 中设置了全冻结位),则 VACUUM 会跳过读取这些数据页。

  • 如果自动 VACUUM 无法获得数据页的排他 LWLock,则在数据页上执行减少的工作。防止循环的自动 VACUUM 不这样做。

  • 如果另一事务持有或正在等待目标关系上的锁,则 VACUUM 放弃截断该关系。

自动 VACUUM 不会对某个关系运行

  • 查看以下内容来确认是否是这种情况。

    • pg_stat_all_tables 中的 last_autovacuum 和 autovacuum_count 列

    • 设置 log_autovacuum_min_duration 为 0 后的日志记录

  • 常见原因

    • 该关系必须符合自动 VACUUM 的条件。

      • UPDATE/DELETE 为主的表:更新/删除的元组 >= autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * pg_class.reltuples

      • INSERT 为主的表:插入的元组 >= autovacuum_vacuum_insert_threshold + autovacuum_vacuum_insert_scale_factor * pg_class.reltuples

    • 自动 VACUUM 工作者正忙于处理其他许多和/或大型关系。

    • 某些事务长时间请求或持有冲突的关系锁。非防止循环的 VACUUM 会放弃这种关系。

    • 自动 VACUUM 无法清理临时表。需要手动运行 VACUUM。这可能导致 XID 循环和数据库关闭。

    • 由于崩溃或归档恢复(包括故障转移)导致存储在 lt_stat/ 中的统计信息丢失。这些统计信息总是在恢复期间重置。自动 VACUUM 依赖于可通过 pg_stat_all_tables 查看的这些统计信息来确定是否需要 VACUUM。

为什么 VACUUM 不会移除死元组

  • 缓慢的自动 VACUUM

  • 长时间运行的事务

  • 物理备用机上启用了 hot_standby_feedback

  • 未使用的复制槽

  • 孤立的准备事务

减少 XID 循环的风险

  • 减少 XID 的消耗。

    • 每个子事务分配自己的 XID。子事务由 SAVEPOINT 和 PL/lightdb 的异常块(BEGIN ... EXCEPTION)启动。

    • 某些客户端驱动程序提供语句级别的回滚。它用 SAVEPOINT 和 RELEASE SAVEPOINT 包装每个 SQL 语句。

  • 让自动 VACUUM 平稳运行(见上文)。

  • 降低 autovacuum_vacuum_insert_scale_factor 或 autovacuum_freeze_max_age 使得自动 VACUUM 更频繁地处理表。

  • 定期安排 VACUUM FREEZE 运行。

加速自动 VACUUM 的配置

  • 对于大表,降低 autovacuum_vacuum_threshold, autovacuum_vacuum_scale_factor, autovacuum_vacuum_insert_threshold, autovacuum_vacuum_insert_scale_factor。

  • 减少 autovacuum_naptime

    • 如果写负载很重并且主机有许多 CPU 核心,即使是 1 秒也是实用的。

  • 增加 autovacuum_max_workers

    • 当存在大量关系时有效。每个关系只由一个自动 VACUUM 工作者处理。

    • 同时增加 autovacuum_vacuum_cost_limit。否则,由于成本限制是在所有活动的自动 VACUUM 工作者之间共享的,每个自动 VACUUM 工作者将更频繁地休眠。

  • 增加 maintenance_work_mem/autovacuum_work_mem

    • 工作内存存储一个死元组 TID 的数组。TID 是 (块号, 项号),占用 6 字节。

    • 设置较大的值可以减少索引扫描的次数。

    • 无论参数值多大,最大分配大小都是 1GB - 1。

    • 不总是分配指定的大小。实际大小足够容纳所有可能的 TID,所以对于小表来说会很小。

    • 对于没有索引的表,只会分配不到 2KB。VACUUM 只在一个表块中累积 TID,因为它不需要扫描索引。

  • 增加 vacuum_buffer_usage_limit

    • VACUUM 默认使用 256KB 的环形缓冲区来缓存数据页,这样就不会驱逐应用程序可能会用到的页。

    • VACUUM 也从缓存页面中受益:堆页面会被读取两次,并且索引页面可能会被读取多次。

    • 将此设置为 0 允许 VACUUM 无限制地使用共享缓冲区。

  • 减少 autovacuum_vacuum_cost_delay,增加 autovacuum_vacuum_cost_limit

    • 将 autovacuum_vacuum_cost_delay 设置为 0,使自动 VACUUM 如手动 VACUUM 一样运行。

  • 将大表分区以便多个自动 VACUUM 工作者能够并行处理其分区。

  • 删除不必要的索引。

    • 自动 VACUUM 一次处理一个索引。(手动 VACUUM 可以使用其 PARALLEL 选项并行处理它们。)


3.5.5. 升级

版本特性

  • 主版本

    • 包含新功能和不兼容性。

    • 每年发布一次。

    • 敏感的错误修复仅合并到最新的主版本中。“敏感”包括可能导致不兼容性的修复、不利影响如不稳定性和安全性,或者需要大量代码更改但收益不大的修复。

    • 升级可以跳过中间的主版本。

    • 总是需要仔细规划和测试以应对不兼容的变化。

  • 次版本

    • 仅包含常见的错误、安全问题和数据损坏问题的修复,以减少升级的风险。

    • 始终推荐运行最新的次版本。社区认为不升级的风险大于升级的风险。

    • 至少每三个月发布一次,在二月、五月、八月、十一月的第二个星期四。如果需要解决紧急问题,可能会额外发布次版本。

    • 升级可以跳过中间的次版本。

    • 通常不需要备份和恢复;您可以停止数据库服务器,安装更新的二进制文件,然后重新启动服务器。

    • 对于某些次版本,可能需要额外的手动步骤来修正已修复的错误带来的不良影响,例如重建受影响的索引。请参阅发行说明中的“迁移到版本<主要>.<次要>”部分。

主要升级方法

  1. lt_dumpall/lt_dump 和 ltsql/lt_restore:简单,长时间停机

  2. lt_upgrade:相对简单,较短时间停机

  3. 逻辑复制:复杂的设置和操作,最短时间停机

lt_upgrade 概览

  • 在不进行用户数据的备份/恢复的情况下升级数据库集群到更高版本。

  • 不是原地升级:从旧数据库集群迁移数据到用 lt_initdb 新创建的数据库集群。

  • 基本思想是由于关系数据存储格式很少改变,只有系统目录布局改变,因此 lt_upgrade 仅仅是转储并恢复数据库模式,并直接使用关系数据文件。

  • 不支持降级。

  • 不迁移 pg_statistic 中的数据库统计信息。用户需要在 lt_upgrade 完成后在每个数据库中运行 ANALYZE。

lt_upgrade 的主要步骤

  1. 创建用于日志和临时文件的输出目录。

  2. 检查目标集群的主要版本较新,并且通过比较 lt_control 中的信息确认旧集群和新集群二进制兼容。

  3. 获取旧集群中的数据库和关系(表、索引、TOAST 表)列表。

  4. 获取包含 C 语言函数的库名称列表。

  5. 执行各种检查以发现升级障碍,如无法连接数据库或存在预提交事务。

  6. 通过运行 lt_dumpall --globals-only 创建全局对象的转储。

  7. 通过运行 lt_dump --schema-only 创建每个数据库的转储。当指定了 --jobs 时,这将通过启动一个进程或线程为每个数据库并行执行。

  8. 通过运行 LOAD 检查之前提取的包含 C 语言函数的可加载库是否存在于新集群中。

  9. 从旧集群复制 lt_xact/ 中的提交日志文件和 lt_multixact/ 中的 MultiXact 文件到新集群。

  10. 设置新集群的下一个 XID 和 MultiXact ID 以接管旧集群。

  11. 通过运行 ltsql 在新集群中恢复全局对象。

  12. 通过运行 lt_restore 在新集群中恢复数据库模式。当指定了 --jobs 时,这将通过启动一个进程或线程为每个数据库并行执行。

  13. 获取新集群中的数据库和关系(表、索引、TOAST 表)列表。

  14. 通过启动一个进程或线程为每个表空间链接或复制用户关系文件从旧集群到新集群。当指定了 --jobs 时,这将是并行化的。

  15. 设置新集群的下一个 OID。

  16. 创建删除旧集群的脚本(delete_old_cluster.sh)。该脚本移除数据目录和表空间版本目录。

  17. 报告应更新的扩展,并创建 update_extensions.sql。该脚本包含 ALTER EXTENSION ... UPDATE 命令列表。

用于排错 lt_upgrade 的日志文件

  • 存储在 $NEWPGDATA/pg_upgrade_output.d/<时间戳>/ 中

  • 当 lt_upgrade 成功完成时会被移除。

  • 日志文件:

    • pg_upgrade_server.log:lightdb 服务器日志。指定为 lt_ctl 的 -l 参数。

    • pg_upgrade_dump_<DB-OID>.log:lt_dump 和 lt_restore 的日志。

    • pg_upgrade_utility.log:lt_upgrade 运行的各种命令的日志,例如 ltsql、lt_resetwal。这包括用于转储和恢复全局对象的 lt_dumpall/ltsql。

    • pg_upgrade_internal.log:其他 lt_upgrade 日志。

    • loadable_libraries.txt:在旧集群中存在但在新集群中未找到的 C 语言函数库列表。