)
Hasura GraphQL Engine 源码级解析Source Catalog 迁移期间的数据库锁与统计日志方案RFC【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址: https://gitcode.com/gh_mirrors/gr/graphql-engine导读本文基于 Hasura GraphQL Engine 开源仓库中的 RFC 文档rfcs/catalog-migration-db-stats-logging.md深入剖析在 source catalog 迁移期间记录数据库统计信息这一提案的来龙去脉。文章涵盖该 RFC 要解决的真实痛点DDL 迁移锁库导致 GraphQL Engine 无法启动且无任何日志、提案中的核心 SQL 监控查询及其输出样例、logPGSourceCatalogStats函数的实现设计类型签名、forkManagedT线程模型、以及配套的40_to_41/41_to_40迁移改造方案。阅读本文后你将掌握如何用一条基于pg_stat_activity的 SQL 在 5 秒周期内持续观测hdb_catalog上的阻塞/等待锁该功能在引擎中应落在哪个代码层、为何需要独立线程以及把元数据目录迁移降级为 source catalog 迁移时需要处理的边界情况如locked列类型转换与幂等降级。背景与问题迁移期间的静默锁库Hasura GraphQL Engine 在启动时会对hdb_catalog元数据目录catalog执行一系列结构变更例如新增一列把既有列的类型从一种变换为另一种在事件触发相关的表上重建默认值或索引。这些变更通常以 DDL 语句执行。正如 RFC 指出很多用户在迁移过程中遇到的典型故障是迁移期间执行的 DDL 查询把数据库锁住导致 graphql-engine 无法正常启动。更糟糕的是在这种场景下引擎没有输出任何日志用户完全无从得知引擎卡在了哪里问题难以定位体验非常沮丧。因此该 RFC 的核心目标很明确在 source catalog 迁移期间周期性输出数据库侧的统计快照让用户能够看到哪些查询处于锁等待状态、被谁阻塞从而把黑盒式的启动失败变成可观测的故障诊断信息。为什么是 source catalog 而非元数据目录Hasura v2 引入了多数据源multi-source架构后目录被拆分为两类从 server/src-lib/Hasura/Backends/Postgres/DDL/Source.hs 的源码可以看出Metadata catalog元数据目录存放引擎全局元数据迁移脚本位于server/src-rsr/migrations/例如41_to_42.sql、42_to_41.sql等成对出现的升级/降级脚本Source catalog数据源目录每个数据库源Postgres 等内部的hdb_catalog结构迁移脚本位于 server/src-rsr/pg_source_migrations/当前为0_to_1.sql、1_to_2.sql、2_to_3.sql、3_to_4.sql。RFC 之所以特别关注 source catalog 迁移是因为它直接运行在用户业务数据库上而不是引擎自带的元数据库一旦 DDL 长时间持有锁用户业务也会被连带阻塞。而迁移逻辑本身在 migrateSourceCatalogFrom 中实现它会从当前版本开始按顺序执行sourceMigrations列表中所有尚未应用的迁移脚本若迁移导致 source catalog 结构变化还会返回RETRecreate以重建既有事件触发器。核心提案周期性输出数据库统计信息监控 SQL 查询RFC 给出的核心方案是在 source catalog 迁移进行期间每隔五秒执行一次下面的 SQL 查询直到迁移完成SELECT json_agg(json_build_object(query, psa.query, lock_granted, pl.granted, lock_mode, pl.mode, transaction_start_time, psa.xact_start, query_start_time, psa.query_start, wait_event_type, psa.wait_event_type, blocking_query, SUBSTRING(blocking.query, 1, 20) ) order BY psa.query_start) FROM pg_stat_activity psa JOIN pg_stat_activity blocking ON blocking.pid ANY(pg_blocking_pids(psa.pid)) LEFT JOIN pg_locks pl ON psa.pid pl.pid WHERE psa.query LIKE %hdb_catalog% AND psa.wait_event_type IS NOT NULL AND psa.query ilike any (array [%create%, %drop%, %alter%]);逐段拆解这条查询的设计意图片段作用pg_stat_activity psaPostgreSQL 的活动会话视图含当前执行中的query、事务开始时间xact_start、查询开始时间query_start、等待事件类型wait_event_type等关键诊断字段JOIN pg_stat_activity blocking ON blocking.pid ANY(pg_blocking_pids(psa.pid))自连接通过内置函数pg_blocking_pids(pid)找出阻塞当前会话的其它会话得到blocking.queryLEFT JOIN pg_locks pl ON psa.pid pl.pid关联锁信息得到pl.granted锁是否已授予与pl.mode锁模式如ExclusiveLockWHERE psa.query LIKE %hdb_catalog%只关心hdb_catalogschema 上的操作与 source catalog 迁移主题严格对齐避免输出与迁移无关的业务噪音psa.wait_event_type IS NOT NULL只保留真正处于等待状态的会话psa.query ilike any (array [%create%, %drop%, %alter%])只匹配 DDL 类语句CREATE / DROP / ALTER因为这类语句正是锁库的元凶SUBSTRING(blocking.query, 1, 20)只输出阻塞查询的前 20 个字符防止把可能包含敏感数据的完整阻塞 SQL 写入日志json_agg(... ORDER BY psa.query_start)按查询开始时间排序后聚合成 JSON 数组便于日志系统单条输出查询输出样例RFC 给出的执行结果形如[ { query: INSERT INTO authors (name) values (boo);, lock_granted: true, lock_mode: ExclusiveLock, transaction_start_time: 2021-10-19T12:08:14.90334200:00, query_start_time: 2021-10-19T12:08:14.90334200:00, wait_event_type: Lock, blocking_query: LOCK authors IN ACCESS EXCLUSIVE MODE; }, { query: INSERT INTO authors (name) values (boo);, lock_granted: false, lock_mode: RowExclusiveLock, transaction_start_time: 2021-10-19T12:08:14.90334200:00, query_start_time: 2021-10-19T12:08:14.90334200:00, wait_event_type: Lock, blocking_query: LOCK authors IN ACCESS EXCLUSIVE MODE; } ]可以看出输出中的每个元素都包含五个高价值诊断字段query当前会话执行的查询全文lock_granted/lock_mode会话在pg_locks中持有的锁是否被授予及其模式如ExclusiveLock、RowExclusiveLocktransaction_start_time/query_start_time事务与查询的开始时间用于判断卡了多久wait_event_type等待事件类型这里是Lockblocking_query被截断为 20 字符的阻塞查询用于快速定位是谁在持锁。借助这份输出用户就可以确认数据库上确实有查询处于被锁locked状态从而把引擎启动失败的归因从引擎自身 bug转向数据库侧存在持锁会话进而去排查持锁事务。实现细节logPGSourceCatalogStats 函数设计RFC 同时给出了服务端 Haskell 侧的落地设计核心是新增一个独立函数函数签名与语义logPGSourceCatalogStats :: forall pgKind m . (MonadIO m, MonadTx m) Logger Hasura - SourceConfig (Postgres pgKind) - m Void要点解读MonadIO m允许在任意 IO 能力的事务栈上运行因为周期性查询本身是 IO 密集型操作MonadTx m允许在引擎统一的事务抽象MonadTx内执行数据库查询与引擎其它数据库操作共用同一套错误处理与事务语义Logger Hasura日志器句柄用于把每次查询得到的 JSON 统计输出到引擎日志SourceConfig (Postgres pgKind)目标 Postgres 数据源的连接配置类型层面区分具体 PG 变体如 Citus / Cockroach 等返回m Void该函数无限循环运行在迁移期间持续以 5 秒间隔采样直到迁移完成被外部终止。独立线程forkManagedTRFC 明确要求logPGSourceCatalogStats必须运行在独立线程中以免阻塞迁移本身并建议通过forkManagedT函数完成线程派生。从仓库源码看forkManagedT位于 server/src-lib/Control/Concurrent/Extended.hs并在引擎多处如 Hasura/Server/Init/Config.hs、Hasura/Server/Migrate.hs、Hasura/App.hs被用于派生受管理的异步任务。这一设计保证采样查询与迁移 DDL 互不阻塞线程的生命周期由引擎统一管理迁移结束或失败时可被正确回收即便数据库侧持续处于锁等待引擎也能稳定地输出观测日志而不是一起卡死。与现有 source catalog 迁移流程的衔接当前仓库中 source catalog 迁移的入口位于 migrateSourceCatalogFrom若prevVersion latestSourceCatalogVersion返回RETDoNothing与SCMSNothingToDo即无需迁移否则按sourceMigrations列表由pg_source_migrations/目录下的N_to_M.sql文件在编译期通过 Template Haskell 动态生成见 sourceMigrations依次执行升级脚本迁移完成后返回RETRecreate要求重建事件触发器以适配新的 catalog 结构。RFC 中的logPGSourceCatalogStats正是要挂接在这一迁移过程中在启动迁移前派生采样线程迁移完成后回收线程。同时由于 source catalog 迁移直接执行在用户数据库上持锁场景最常见因此该日志功能被设计为仅服务于Postgres source catalog 迁移而不覆盖元数据目录metadata catalog迁移。迁移脚本改造40_to_41 的 no-op 化与 0_to_1 的接管RFC 还给出了一个关键的配套改造目的是避免为元数据目录迁移重复实现这套日志逻辑转而把相关结构变更下沉到 source catalog 迁移中。改造前40_to_41.sql承载locked列改造原方案中hdb_catalog.event_log.locked列由布尔类型改为时间戳TIMESTAMPTZ的逻辑位于元数据目录迁移40_to_41.sql中。RFC 提出将其变为no-op空操作并把实际逻辑迁移到pg_source_migrations/0_to_1.sql。改造后pg_source_migrations/0_to_1.sql的真实实现当前仓库中的 server/src-rsr/pg_source_migrations/0_to_1.sql 正是该改造的落地成果内容如下ALTER TABLE hdb_catalog.event_log ALTER COLUMN id SET DEFAULT hdb_catalog.gen_hasura_uuid(); ALTER TABLE hdb_catalog.event_invocation_logs ALTER COLUMN id SET DEFAULT hdb_catalog.gen_hasura_uuid(); ALTER TABLE hdb_catalog.event_log RENAME COLUMN locked TO locked_boolean; ALTER TABLE hdb_catalog.event_log ADD COLUMN IF NOT EXISTS locked TIMESTAMPTZ; UPDATE hdb_catalog.event_log SET locked NOW() WHERE locked_boolean t; ALTER TABLE hdb_catalog.event_log DROP COLUMN locked_boolean;该脚本体现了完整的布尔 → 时间戳数据迁移思路先把event_log、event_invocation_logs的id列默认值统一为gen_hasura_uuid()保证新库/旧库行为一致将旧布尔列重命名为locked_boolean保留旧数据新增locked TIMESTAMPTZ列IF NOT EXISTS保证幂等用UPDATE ... SET locked NOW()把旧布尔值为真的行迁移为当前时间即把已锁定语义从布尔标志转成锁定发生时间戳最后删除旧列locked_boolean。降级脚本41_to_40.sql的条件保护RFC 要求对应的41_to_40.sql降级脚本先检查locked列是否为timestamp with time zone仅在满足条件时才把它改回布尔类型否则什么都不做。仓库中的 server/src-rsr/migrations/41_to_40.sql 精确实现了这一点DO $$ BEGIN IF (SELECT EXISTS (SELECT 1 from information_schema.columns where table_name event_log and table_schema hdb_catalog and column_name locked and data_type timestamp with time zone)) THEN ALTER TABLE hdb_catalog.event_log ALTER COLUMN locked TYPE BOOLEAN USING locked IS NOT NULL; ALTER TABLE hdb_catalog.event_log ALTER COLUMN locked SET NOT NULL; ALTER TABLE hdb_catalog.event_log ALTER COLUMN locked SET DEFAULT false; END IF; END $$;这段脚本使用information_schema.columns检查locked列当前的数据类型仅当其确为timestamp with time zone时才执行ALTER COLUMN locked TYPE BOOLEAN USING locked IS NOT NULL把非空时间戳转换为布尔有锁定时间 →trueSET NOT NULL与SET DEFAULT false恢复原布尔列约束与默认值。脚本头部的注释解释了为什么需要这个保护存在一个边界场景——用户从 v1无事件触发器升级到 v2 时由于元数据中没有事件触发器source catalog 迁移根本不会运行此时若用户再从 v2 降级到 v1event_log仍保持 v1.3.3 的旧结构locked仍是布尔如果41_to_40.sql盲目执行降级就会去降级一个从未升级过的东西而报错。条件检查正是为了规避这一非幂等陷阱。为什么下沉到 source catalog 迁移从代码路径看元数据目录迁移由 Hasura/Server/Migrate/Version.hs 管理而 source catalog 迁移由 migrateSourceCatalog 管理两者生命周期与触发条件不同。event_log.locked列属于hdb_catalog它本质上是每个 Postgres 数据源各自持有的结构事件触发器存储于源库因此在多源架构下把它移入 source catalog 迁移更合理每个数据源在接入时都会按自身版本执行pg_source_migrations脚本从而保证所有源的event_log结构一致同时避免在引擎的全局元数据目录迁移中引入只与特定源相关的 DDL。开放问题与后续思考RFC 在文末保留了一个开放问题用户从上述信息中能做什么来解除锁这份日志是否仅仅只是信息性的informational这个问题实际上点出了该方案的功能边界当前提案只解决可观测性不解决自动恢复。周期性统计日志的价值在于让用户在引擎启动失败时第一时间看到数据库侧的锁等待全景明确指向持锁会话blocking_query的前 20 字符与锁模式为人工介入终止持锁事务、等待长事务结束、在业务低峰执行升级提供决策依据。至于未来是否要在此基础上加入自动终止阻塞会话、迁移前锁检测并提示等主动干预能力则超出了本 RFC 的范围属于后续可演进的方向。总结本文围绕 rfcs/catalog-migration-db-stats-logging.md 完整还原了 Hasura GraphQL Engine 在 source catalog 迁移期间引入数据库统计日志的设计问题迁移 DDL 锁库导致引擎无法启动且无任何日志用户陷入黑盒方案以 5 秒为周期执行基于pg_stat_activity/pg_locks/pg_blocking_pids的聚合查询输出处于锁等待状态的hdb_catalogDDL 会话及其阻塞者阻塞查询仅保留 20 字符以规避敏感数据泄露实现新增logPGSourceCatalogStats :: (MonadIO m, MonadTx m) Logger Hasura - SourceConfig (Postgres pgKind) - m Void通过forkManagedT放入独立线程不阻塞迁移主流程配套改造将40_to_41.sqlno-op 化逻辑下沉到 pg_source_migrations/0_to_1.sql同时让 41_to_40.sql 带条件地执行降级以覆盖升级后未实际迁移又降级的边界场景边界该功能仅覆盖 Postgres source catalog 迁移且定位为信息性观测不包含自动解除锁的干预能力。对于正在排查 Hasura 升级/启动卡死问题的使用者这套设计思路本身也提供了一条可直接借鉴的运维手段即便不在引擎层实现也完全可以在升级前手动执行文中的监控 SQL观察hdb_catalog上的 DDL 是否处于锁等待状态从而在迁移开始前就发现潜在的长事务持锁风险。【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址: https://gitcode.com/gh_mirrors/gr/graphql-engine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考