把 Lua 接进 psql:用异步脚本改造 VACUUM 输出与进度监控

2026-08-29 35 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

将 Lua 集成到 psql 客户端之后,数据库运维命令就不必局限于一条 SQL 或一段 shell 管道。Lua 可以调用 psql 的连接、查询和异步执行接口,把多个 SQL 操作组合成一个更适合人工使用的命令。

这个示例围绕 VACUUM 展开:普通模式逐表执行清理,verbose 模式显示当前表名,并定期读取 pg_stat_progress_vacuum,把清理阶段和块处理进度打印到同一行。它展示的重点不是重新实现 VACUUM,而是给 psql 增加一层可读、可扩展的交互式包装。

Lua 脚本如何组织一次 VACUUM

脚本的工作流可以拆成四步:

  1. 从系统目录中收集需要处理的表。
  2. 根据命令参数决定是否启用 verbose 模式。
  3. 在专用连接上异步发送 VACUUM,避免进度查询阻塞主操作。
  4. 在等待命令完成期间,查询 pg_stat_progress_vacuum 并刷新终端输出。

这里使用独立连接非常关键。执行 VACUUM 的连接处于忙碌状态,如果同时用同一连接查询进度,查询就无法及时执行。因此脚本先创建一个连接副本,再通过另一个连接查询进度。

VACUUM 的后台进度可以通过当前后端进程号过滤。脚本执行 select pg_backend_pid() 获取连接 PID,然后使用下面的条件查找对应记录:

SELECT phase,
       heap_blks_total,
       heap_blks_scanned,
       indexes_total,
       indexes_processed
FROM pg_stat_progress_vacuum
WHERE pid = 12345;

进度视图没有记录时,通常表示当前阶段尚未产生可见进度,或者 VACUUM 已经接近完成。终端刷新逻辑需要同时处理这两种情况。

异步接口带来的差异

同步调用会一直等待服务器返回结果,代码简单,但无法在等待期间执行进度查询。异步方式则把等待过程拆开:

  • sendquery 发送命令,不等待最终结果。
  • consumeinput 接收服务器已经返回的数据。
  • isbusy 判断当前连接是否仍在处理命令。
  • resultwait(1) 等待结果,参数可以表示等待的秒数。
  • getresult 取出最终结果,并检查 SQL 错误。

示例中还会发送一个 select pg_sleep(20)。它的作用是让同一个异步队列保持一段时间,方便观察进度刷新效果。实际脚本中可以根据客户端 API 的行为决定是否保留这一条;如果接口能够持续轮询忙碌连接,通常不需要额外的 sleep。

一个最小的轮询结构如下:

mycon:sendquery("vacuum verbose public.orders")
mycon:consumeinput()

while mycon:isbusy() do
    if mycon:resultwait(1) == 0 then
        local rs, err = psql.connect():exec([[
            SELECT phase, heap_blks_total, heap_blks_scanned,
                   indexes_total, indexes_processed
            FROM pg_stat_progress_vacuum
            WHERE pid = 12345
        ]])
        if err then
            error(err)
        end

        local row = rs:fetch()
        if row then
            io.write(string.format(
                "\rphase: %s, scanned: %d/%d, indexes: %d/%d\27[0K",
                row.phase,
                row.heap_blks_scanned,
                row.heap_blks_total,
                row.indexes_processed,
                row.indexes_total
            ))
        else
            io.write("\r\27[0K")
        end
        io.flush()
        rs:clear()
    end

    mycon:consumeinput()
end

local result, err = mycon:getresult()
if err then
    print(err)
end

运行前需要把 12345 替换成实际连接 PID,并确认当前用户有权限读取相关统计信息。这个片段假设 Lua 已经运行在支持 psql.connect()sendquery()resultwait()lua-psql 环境中。

让 verbose 输出更适合终端阅读

原始 VACUUM VERBOSE 输出通常包含大量诊断信息。脚本可以在每张表开始处理时打印一个反色标题,让用户快速定位当前对象:

print(string.format("\27[7mVACUUM %s.%s \27[27m", schema_name, table_name))

其中 \27[7m 开启终端反色显示,\27[27m 恢复默认显示。进度行使用回车符 \r 覆盖上一行,并用 \27[0K 清除行尾残留字符。这样不会为每次轮询产生一屏新日志,长时间维护任务也更容易观察。

表名来自系统目录时,脚本还需要避免直接拼接不可信标识符。可以在 SQL 中使用 quote_ident,并排除系统表:

local query = [[
    SELECT quote_ident(n.nspname) AS nspname,
           quote_ident(c.relname) AS relname
    FROM pg_class AS c
    JOIN pg_namespace AS n ON n.oid = c.relnamespace
    WHERE n.nspname <> 'information_schema'
      AND n.nspname !~ '^pg_toast'
      AND c.relkind IN ('r', 's', 'n')
]]

这里的 quote_ident 解决的是标识符转义问题,不能替代权限控制。脚本仍应限制可处理的 schema 和表范围,避免把维护命令暴露成任意对象执行器。

参数解析与安全边界

示例用法是:

\luafile ~/vacuum.lua
\lua vacuum {verbose=true}

Lua 函数收到字符串参数后,将它拼接成 return ... 并交给 load 执行。这种写法允许使用 Lua 表作为轻量配置,例如 {verbose=true},但也意味着参数本身可以执行任意 Lua 代码。

如果命令只面向可信管理员,这种方式足够直接。若脚本会被多个用户调用,建议改成显式解析,只接受布尔值和有限选项:

local function parse_options(args)
    local options = { verbose = false }
    if not args or args == "" then
        return options
    end

    if args == "{verbose=true}" then
        options.verbose = true
        return options
    end

    error("usage: \\lua vacuum {verbose=true}")
end

这会牺牲一部分 Lua 表语法的灵活性,但能清楚地限制命令行为。对于数据库运维工具,参数可控性通常比表达式解析的便利更重要。

采用时需要检查什么

这个方案适合需要交互式反馈、又不想为每个运维动作单独部署服务的场景。落地前建议检查以下事项:

  • 使用与目标 PostgreSQL 版本匹配的进度视图字段。
  • 确认客户端 API 的异步连接对象是否支持并发进度查询。
  • 对 schema、表名和用户权限设置明确边界。
  • 决定是否保留额外的 pg_sleep,避免无意义地延长任务。
  • 在非交互终端中关闭 ANSI 控制序列,改为结构化日志。
  • getresult、连接关闭和查询错误增加完整处理。

Lua 进入 psql 后,客户端不再只是“发送一条 SQL 并打印结果”的工具。通过连接克隆、异步轮询和进度视图,复杂维护命令可以保持数据库原生执行能力,同时获得更清晰的操作反馈。这个模式也可以推广到索引创建、批量迁移以及其他需要持续观察状态的后台任务。


相关推荐