Files
AgentLab/docs/zh-CN/postgresql-and-scaling.md
ThearchyHelios 2714890d0d 文档重新梳理:README 精简成介绍和索引,细节按主题拆进 docs/zh-CN 与 docs/en(17 篇一一对应),英文文档不含中文
README.zh-CN.md / README.md 只留:这是什么、为什么用它、主要功能(按领域一行一条,链到文档)、快速开始、部署、文档索引、
开发与测试、许可(原文不动)。原来的每一节按读者和任务分到 17 篇主题文档:ask-data、workflows、evidence、
data-sources(新增:接入数据库、只读保证、上传表格、跨库合并、遮罩列)、data-catalog、tools(新增:工具、审批档位、
能力登记、端点、网络规则——它们管所有工作流,不只应用)、apps、evaluation-and-ledger、schedules(含通知)、deployment、
configuration、postgresql-and-scaling、identity-and-security、architecture、development、upgrade-notes(按时间倒序,
每波先写要做的事)、faq。删掉的只有模块名(api/copilot_gen.py、chat/drive.py、data/schema_view.py)、重复的话和
「以前…现在…」的叙述;操作步骤、命令、配置名、限制、权限边界都有归宿。中文是原文,英文按新的中文写;英文里界面文字
只写英文名,开头注明界面目前只有简体中文,中文占位符换成英文占位符,中文数字的例子改成不依赖中文的说法。
数据剖析可以同时剖析 2–3 张表(baf23998)收进 data-catalog。
新增 scripts/check-docs.py:英文文档(README.md、docs/en/**)不许有中日韩字符和全角符号(代码块也算),
README、docs/**、deploy/README.md 的相对链接和锚点都要通,docs/zh-CN 与 docs/en 文件一一对应、标题层级相同。
中文提示里指向 README 章节的地方改指新文档:cluster/guard.py、cluster/version.py、db/base.py 两处报错,
data/schema_view.py 文档串,.env.example 三处,scripts/prod.sh、scripts/test-postgres.sh,terms.ts 两处、
DataSourcesTab.tsx、check-manage.mjs 注释,deploy/README.md 三处链接。deploy/agentlab.env 未动。
验收:check-docs 0 问题(英文文档 18 个 0 处中文,相对链接 369 条 0 断链,18 对标题层级一致);相关后端测试
117 通过、7 跳过;tsc -b、check-copy 通过。
2026-10-21 10:23:26 +08:00

22 KiB
Raw Permalink Blame History

English | 简体中文

PostgreSQL 与多进程

这篇讲把应用库换成 PostgreSQL、从 SQLite 导入数据、备份,以及起多个工作进程、多台机器时的规矩。给运维看。

什么时候换成 PostgreSQL

缺省的应用库是数据目录里的 SQLite:单机、单进程,什么都不用装。要起多个工作进程,就得换成 PostgreSQL。

换过去的是应用库(工作流、运行、事件、审批、待办、数据目录、评测……)和执行断点。工件、上传表格的版本库、原件、附件、记录表、沙箱工作区和 .secret_key 仍是数据目录里的文件,所以 AGENTLAB_DATA_DIR 照样要设。

用 PostgreSQL

设一个环境变量:

AGENTLAB_DATABASE_URL=postgresql+asyncpg://agentlab:<密码>@127.0.0.1:5432/agentlab   # postgresql:// 也认
  • 要装 [postgres] 额外依赖(environment.yml 和 Docker 镜像都已带上)。没装就设了这一项,启动时直接报要装什么:pip install -e 'backend[postgres]' -c backend/requirements.lock。
  • 库要先建好(CREATE DATABASE agentlab)。表由服务启动时建;老版本建的库,启动时补列补索引,和 SQLite 一样。
  • 启动日志第一句写明用的是哪种库、哪台主机、哪个库(不写密码)。

SSL 与断点库

  • SSL:地址后加 ?sslmode=require(或 verify-ca、verify-full;asyncpg 的写法 ?ssl=require 也认),启动时换成 asyncpg 认的参数。
  • 证书文件不写在地址里(asyncpg 不收 sslrootcert 这类参数,写了启动就报错),改设环境变量 PGSSLROOTCERT、PGSSLCERT、PGSSLKEY。应用库的 asyncpg 和断点库的 libpq 都认它们。
  • 地址里别的 libpq 参数(application_name、connect_timeout 之类)asyncpg 也不收,同样在启动时报出来。
  • 断点是 LangGraph 的四张表:checkpoints、checkpoint_blobs、checkpoint_writes、checkpoint_migrations,不和应用的表重名。缺省放在同一个库;要放别处,设 AGENTLAB_CHECKPOINT_URL=postgresql://…(这一项是 libpq 的写法,sslrootcert 等可以直接写)。

连接池与连接数

  • 每个进程常驻 AGENTLAB_DB_POOL_SIZE(缺省 10)条连接,高峰再多开 AGENTLAB_DB_MAX_OVERFLOW(缺省 20)条。取连接前探活,每条连接用 30 分钟换新。断点库另有一个小池子。
  • 一个进程最多 AGENTLAB_DB_POOL_SIZE + AGENTLAB_DB_MAX_OVERFLOW 条应用库连接(缺省 10 + 20 = 30),断点库的池子最多 min(AGENTLAB_MAX_CONCURRENT_RUNS, 20) 条(至少 4,缺省 20),合起来一个进程最多 50 条。导入命令另占几条。
  • PostgreSQL 缺省 max_connections=100,还要留几条给 psql、备份和超级用户(superuser_reserved_connections 缺省 3)。一个进程够用;起几个进程就是几倍。办法三选一:
    • 把 max_connections 调大,约「进程数 × 50 + 20」;
    • 调小两边的池子,比如 POOL_SIZE=5、MAX_OVERFLOW=10、MAX_CONCURRENT_RUNS=10,一个进程最多 25 条;
    • 前面放 PgBouncer(见直连与 PgBouncer)。
  • 多进程时,每个进程另有一条专门收发进程间通知(LISTEN / NOTIFY)的连接,不占连接池;跨进程的锁(会话级咨询锁)持锁期间占一条。

和 SQLite 一致的行为

换成 PostgreSQL 后,行为和 SQLite 上一致:

  • 字符串列不限长(SQLite 本来就不查长度)。
  • 文本按字节比大小和排序:列上带 COLLATE "C",和 SQLite 一样,不看建库时选的区域设置。
  • 整数列是 BIGINT。
  • JSON 列存原文(PostgreSQL 的 json,不是 JSONB:JSONB 会重排对象里的键)。
  • 时间存不带时区的 UTC。
  • 排序时,可空列的 NULL 放在 SQLite 的位置:升序在前、降序在后。只管直接按列排的;按聚合、coalesce 之类表达式排的不管。列表都另带了 id 之类的决胜键,同名、同时间并列时两种库排出来一样。
  • 按 JSON 里的键筛选时,按 JSON 的类型比(true 不等于 1)。
  • 两种 SQLite 照存、PostgreSQL 不收的值会被换掉:JSON 里的 NaN / Infinity 写成 null;文本里(JSON 里的、普通文本列里的)的 NUL 字符去掉。

不分大小写的搜索(运行列表按名字、对象索引按注释):文本列是 COLLATE "C",列上的 ILIKE 只认 ASCII 的大小写,所以按建库时的区域设置折大小写。建库用 en_US.UTF-8、C.UTF-8 这类区域时,É/é、全角A/a 都认;用纯 C 区域建的库只认 ASCII。

启动时等库

和库的容器一起起、库还在初始化时,启动会等库:

  • 按退避重试(1、2、4、5、5… 秒),每次打一条「应用库还没就绪」,最多等 AGENTLAB_DB_STARTUP_TIMEOUT(缺省 90)秒,再报错退出。
  • 密码不对、库不存在这类等也没用的错误(SQLSTATE 28、3D、42 类)立刻报错。
  • 建表、补列补索引在库里的咨询锁里做,默认数据在跨进程的锁里补。几个工作进程同时起,这些都只做一次。

和库一起起

  • 部署:离线包的 ./deploy.sh install --postgres 带一个内置 PostgreSQL;migrate-to-postgres 把 SQLite 的部署切过去(见 deploy/README.md 的「用 PostgreSQL」)。
  • 开发:./scripts/dev.sh --postgres 起(已经有就复用)一个本地的 postgres:16 容器 agentlab-dev-postgres:
    • 只开在 127.0.0.1:55432,数据在 docker 数据卷 agentlab-dev-pgdata;
    • 密码第一次自动生成在 data/.postgres-dev/,后端按 PGPASSFILE 读;后端的 AGENTLAB_DATABASE_URL 指向这个容器;
    • Ctrl-C 只停前后端,库的容器留着下次复用;
    • 容器名、端口、数据卷、镜像用 AGENTLAB_DEV_PG_CONTAINER、AGENTLAB_DEV_PG_PORT、AGENTLAB_DEV_PG_VOLUME、AGENTLAB_DEV_PG_IMAGE 改(环境变量,或写在 .env 里,环境变量优先)。
  • 不加 --postgres 就和原来一样:SQLite,不用 docker。

从 SQLite 搬过来

python -m app.db copy-to-postgres 把数据目录里的 agentlab.db 和 checkpoints.db 导进 PostgreSQL。停在审批上的、中断了的运行,导过去之后照样能接着跑。

# 1. 停服务(导入会检查:来源旁边还留着 agentlab.db-wal / checkpoints.db-wal,或者目标库上已经有服务连着,就拒绝)
# 2. 备份数据目录(停着服务拷一份)
# 3. 只核对:数来源有多少、目标上已有多少,什么都不写
cd backend && python -m app.db copy-to-postgres ../data postgresql+asyncpg://agentlab:<密码>@127.0.0.1:5432/agentlab --dry-run
# 4. 导入:建表、按外键先后逐表分批插入,结束时逐表按主键核对、断点按条数核对,有没导进去的退出码非零
cd backend && python -m app.db copy-to-postgres ../data postgresql+asyncpg://agentlab:<密码>@127.0.0.1:5432/agentlab
# 5. 设 AGENTLAB_DATABASE_URL(AGENTLAB_DATA_DIR 还指着原来的数据目录),起服务
  • 目标地址不给就用 AGENTLAB_DATABASE_URL;--checkpoint-url 把断点导到另一个库;--work-dir 放快照(要留出和两个库一样大的空间,缺省是系统临时目录)。
  • 读的是 SQLite 备份接口拷出的一份快照,在快照上先补到当前的表结构;来源的内容不改。
  • 重跑是幂等的:目标上已有同一主键(或撞了别的唯一约束)的行一律不动,只补上还没有的,不重复、也不覆盖。所以导入之后又在 SQLite 上改过的行,重跑不会带过去;要整份重来,就清空目标库再导。断点同理,已有的不重写。
  • 能查到的只有第 1 步那两样。服务连着别的数据目录、另一台机器上的服务连着目标库,查不出来,要自己保证。服务正常停下时会关掉所有连接,-wal 随之消失;还在的是上次强杀留下的,确认服务已停后加 --assume-stopped。
  • 老库里 SQLite 存得下、PostgreSQL 不收的数据,会先在快照上处理,并在报告里照实列出:
    • 老版本补列时没带缺省值、留着 NULL 的 NOT NULL 列,填成模型的缺省值(启动时的迁移在 SQLite 上也这么填,两边存的一样);
    • 孤儿行(外键指向已经不在的行,是 SQLite 当时没强制外键留下的):外键是 SET NULL 的,把外键置空;别的(CASCADE、不可空)不导入,按表列出条数,不算核对失败(父行被删时它们本来就该跟着删)。
  • 自增主键的序列接在 SQLite 用过的最大号后面,删过的号不复用。
  • 导完后 agentlab.db 留在数据目录里也没关系,可以当回退用;它照样算 AgentLab 自己的文件,不能登记成数据源。

备份

  • PostgreSQL 用 pg_dump(应用表和断点在同一个库里,就一次带上);数据目录照旧整份拷。
  • 两样最好在停服务时一起做。不停服务的话,先 dump 库,再拷数据目录:工件、原件按内容寻址、只增不改,库里引用到的文件拷数据目录时都在。
  • 例外是填报的记录表 records/*.db:它们是原地追加写的 SQLite 文件,开着服务直接拷,可能拷到写了一半的。要么停服务拷,要么一个个用 SQLite 的备份接口拷:
sqlite3 data/records/x.v1.db ".backup /备份目录/x.v1.db"

多进程

应用库是 PostgreSQL 时,可以起几个工作进程,指着同一个库、同一个数据目录:

AGENTLAB_DATABASE_URL=postgresql+asyncpg://… ./scripts/prod.sh --workers 4      # 一台机器、uvicorn 起 4 个进程共用一个端口
AGENTLAB_WORKERS=4 …                                                             # Docker:agentlab.env 里加这一行(deploy/README.md)
  • 几个容器、几台机器各起一份也行:前面挂一个负载均衡,WebSocket、SSE 不需要粘性会话。
  • SQLite 只支持单进程:数据目录上有一把文件锁,第二个进程拒绝启动,照实报错;AGENTLAB_WORKERS 大于 1 而应用库是 SQLite 时,同样拒绝。
  • 连接数按进程数乘,见连接池与连接数。
配置 缺省 说的是什么
AGENTLAB_WORKERS 1 起几个工作进程(prod.sh、Docker 的入口按它给 uvicorn --workers)
AGENTLAB_WORKER_HEARTBEAT_S 5 每个进程隔多久续一次租约、取一次发给自己的请求(停止之类)
AGENTLAB_WORKER_LEASE_S 30 租约多长(至少是心跳的 3 倍):进程没了(强杀、断电)多久之后别的进程接手
AGENTLAB_SCHEDULER_TICK_S 15 调度循环一拍的间隔

直连与 PgBouncer

有几样连接依赖会话状态:收发进程间通知的那一条(LISTEN)、跨进程的锁(会话级咨询锁,取锁和放锁要在同一条会话上)、断点库(预备语句,建表时取会话锁)。

  • 普通查询走 AGENTLAB_DATABASE_URL。
  • 设了 AGENTLAB_DATABASE_DIRECT_URL(写法同上)时,上面这几样走直连地址;断点库缺省也走它,AGENTLAB_CHECKPOINT_URL 另给的除外。
  • 不设,就全用同一个地址。

三种部署:

部署 怎么设
直连 PostgreSQL 只设 AGENTLAB_DATABASE_URL。最简单,多进程建议这样
PgBouncer 会话模式(pool_mode=session) AGENTLAB_DATABASE_URL 指 PgBouncer 就行:会话模式下一个客户端连接独占一条服务端连接,会话状态都在
PgBouncer 事务模式(pool_mode=transaction) AGENTLAB_DATABASE_URL 指 PgBouncer,另设 AGENTLAB_DB_PGBOUNCER=1 和 AGENTLAB_DATABASE_DIRECT_URL(直连 PostgreSQL,或指 PgBouncer 的会话模式那个库)。没给直连地址时拒绝启动

AGENTLAB_DB_PGBOUNCER=1 做的事(只对普通地址):

  • 不用客户端的连接池,交给 PgBouncer。
  • 关掉 SQLAlchemy 和 asyncpg 的预备语句缓存:事务模式下,下一条语句可能落在另一条服务端连接上,缓存着的预备语句在那边不存在。
  • asyncpg 每条语句仍先预备再执行(在同一个事务里、同一条服务端连接上),名字用随机的,不和别的客户端留下的撞名。PgBouncer 那边要配 server_reset_query = DISCARD ALL 加 server_reset_query_always = 1,收掉留下的预备语句;或者用 1.21 以上的版本,开 max_prepared_statements。
  • 事务级的咨询锁(发名额、同源队、建表时用)在事务模式下照常成立:取锁、放锁在同一个事务里。

这几条照 SQLAlchemy、asyncpg、psycopg 的文档和代码核过,但没有在真的 PgBouncer 上跑过。

认领、租约与接手

  • 每个进程在库里登记一行(主机、pid、启动时的随机串),每一拍心跳续一次租约。
  • 运行、问数据的一轮、后台任务(探查、起草、剖析、评测、影子起草)、处理中的文档、执行中的发布单,都记着认领它的进程。租约按进程算:进程没了,它认领的行一起到期。
  • 平稳关停的进程会交班、删掉自己的登记,别的进程马上接手;强杀的,要等租约过期。
  • 启动时的整理只碰属主不在了的行,绝不碰别的活进程的。

属主不在了之后:

什么 怎么处理
在跑的运行 标中断(可以从断点继续),补一条「服务上次未正常关闭」。不自动重跑有副作用的步骤,由人点「继续运行」
排队的运行 标失败(从没开始执行,没有断点,「恢复」只会伪造一份结果),补一条失败原因
停在审批上的运行 不属于任何进程:谁收到决定谁接着跑,同一个决定只会被一个进程执行(审批和运行都是带条件的更新)
问数据的一轮 还在建图的标中断(可以「重试本轮」);已经发起运行的,接手的进程接着等这次运行,照常复核收尾
起草、剖析、探查、评测、影子起草 标中断(没做完的表、题记成停止),在界面上点「重试」接着做
处理中的文档、执行中的发布单 文档标失败(请重新上传);发布单改成停下,停在没做完的那一步,可以接着做
待审批、结果未知的写入 缺了待办的补建;结果未知、还没安排核实的补上核实(所属运行还在别的活进程里执行的不碰)

停止

运行、问数据的一轮、后台任务在别的进程上跑时,「停止」写一条给属主的请求(PostgreSQL 上用 NOTIFY 叫醒它,最迟一拍心跳)。属主照本进程的规矩停下、收尾,run.cancelled 带上是谁停的。界面上和单进程时一样。属主不在了的,按上表接手。

运行并发

每段执行(发起、审批之后接着跑、继续运行)开始前要拿一个名额。名额按来源分四类,几个进程合起来算:

类别 包括 缺省优先级
交互 问数据、画布试运行、手动发起的正式运行、蓝图试运行、个人应用刷新 40
应用 应用入口、按需视图、钩子、接口 30
定时 定时任务、事件触发 20
系统 评测、口径召回、对账、补算、按口径正式出具 10
  • 全站上限是 AGENTLAB_MAX_CONCURRENT_RUNS(缺省 20)。
  • 其中一部分留给交互:缺省是全站上限的 1/4,至少 1,至多全站上限减 1。应用、定时、系统三类合起来永远用不到留底,所以有人问数据、试运行时总有名额。
  • 留底以外的名额,其他三类可以借用:三类各自的上限缺省就是「全站上限 − 留底」,没人用问数据时评测也能用满这些名额;可以在设置里调小。交互自己的上限缺省等于全站上限。
  • 等名额时按优先级排,同级先来先得;已经在执行的不打断。
  • 在「设置 → 偏好设置 → 运行并发」里改(管理员)。改了之后,最迟几秒各进程按新的值发名额。
  • 单进程时也照这套规则发名额。满载时评测、召回会慢一些,换来问数据总有名额。

调度当班

只有一个进程当班(库里的一份租约)。下面这些事只在当班的进程里做:定时认领、事件派发、Webhook 投递、流程的时限和核实、每天的汇总、各类周期清理、口径比对的补比和自动召回的续发,以及接手不在了的进程留下的行。

  • 其余进程待命;当班的没了,待命的接班。平稳关停的进程马上交班。
  • 切换的那一会儿不会重复:定时认领是带条件的更新,触发和投递记录有唯一约束。

实时事件

  • 运行事件、问数据一轮的事件都先落库。页面的 WebSocket / SSE 连到的进程不在执行它时,从库里按序号追。
  • 执行的进程每提交一批事件,发一个 NOTIFY(只带运行或轮次的 id 和序号),别的进程收到就回库里读。NOTIFY 丢了也有兜底:运行的流每 2 秒、问数据的流每一拍心跳回库里看一次。
  • 不落库的两样——正文 token 和思考增量、问数据助手的保活心跳——只转给有人在看的进程(攒一小段放进 NOTIFY)。
  • 回放、重连、心跳的行为不变。序号由执行它的进程发;别的进程要往执行中的运行里记事件,交给执行的进程记;停着的运行追加事件时,按库里的最后一条接号(带条件的更新,防撞号)。

进程间一致的状态

  • 全站一致、记在库里的:每日花费账(裁判、核查、评测),先占住那一行再读写;访问密钥每分钟的限流;登录失败的退避;同源队(同一个数据源上的起草、剖析、探查一个接一个)。
  • 用跨进程锁(PostgreSQL 的咨询锁)的:按需探查、加入探查范围、上传表格的版本存储、记录表的写入、按需视图的单飞和每日上限、个人应用刷新、出具、召回、补算、定时发起。
  • 在各进程内存里、改了会通知的:登录设置、对外访问地址、向量模型、能力登记的治理快照、数据源的连接池、应用页面的缓存。改的那个进程马上刷新,别的进程收到通知(或最迟一拍心跳)跟着重读。
  • 纯性能的缓存留在各进程里,过期无害:问数据的向量、取值猜测、表结构快照、取数快照的遮罩集合、OIDC 的元数据、DoH 解析、xlsx 的列扫描、口径比对的 SQL 形状。
  • 访问密钥的请求记录先攒在各进程自己的内存里,每一拍心跳写库。
  • 各进程自己的并发闸门(口径比对、自动对比、起草的模型调用、上传解析)按进程算,进程多了,总量跟着多。

多台机器

几台机器要用同一份数据目录:

  • 数据目录放在共享存储(NFS 之类)上,每台挂在同一个绝对路径(库里记着上传表格各版本库文件的绝对路径)。
  • 各台机器的时钟要对时(NTP)。租约按库的时钟算,机器之间差多少都不影响谁算活着;但「停了两个租约时长」这类判断,看的是各机器按本机时钟写下的时间。
  • 库服务器自己的时钟往前跳超过一个租约时(少见),所有进程续租时都会发现自己的租约已经过期,一起隔离:执行到一半的运行标成中断,在运行详情里点「继续运行」从断点接着执行;后台任务按「属主不在了」的规矩处理;各进程自动换身份重新登记,之后发起的照常。往回跳时不隔离,只是真没了的进程要多等跳回去的那么久才被接手。
  • 本机墙钟跳了(NTP 一步校时、手动改时间)不会隔离:判续租晚没晚,看的是计入休眠的单调钟;对不上时以库里的租约为准。
  • 文件都是先写临时文件再改名,读的一方看不到写了一半的;写上传版本库、记录表时,在库里的锁下一个接一个。

要说实话:数据目录里还有 SQLite 文件——上传表格的各版本库(发布后只读)、填报的记录表(records/*.db,原地追加写)、沙箱里代码自己建的库。SQLite 文件放在网络文件系统上,读写一致性靠文件锁,而 NFS 上的文件锁不可靠:一台机器在追加时另一台读,可能读到不一致的页。所以多台机器时,记录表写得多的部署,建议用一台机器多进程,或者把数据目录放在支持可靠文件锁的存储上。

沙箱会话(microVM)在各进程自己手上,不跨进程共享。

升级

不支持新旧版本的进程同时连着一个库,代码里拦着:

  • 每个进程登记时带上代码版本(包版本、构建号或 git 提交、源码指纹)和库结构版本。源码指纹包括依赖锁文件,所以只升了依赖也算版本不同。
  • 库里还有活着的、版本不同的进程时,新版本的进程在补列之前就拒绝启动;报错里列出全部这样的进程、各是什么版本、心跳还在不在走。同版本的照常加入。

升级步骤:

  1. 停掉全部旧版本的进程。平稳关停的马上删掉自己的登记;强杀的,要等它的租约过期,新进程启动时会等。
  2. 备份:pg_dump 加数据目录(见备份)。
  3. 起新版本:第一个进程补列补索引,之后的照常加入。

只停了一部分就起新版本,新版本起不来,旧版本照常跑,不会两边各按各的规矩写同一批行。Docker 部署的 ./deploy.sh upgrade 就是按这个顺序做的。

测试

  • scripts/test-postgres.sh 用本机 docker 已有的 postgres:16 镜像起一个临时容器(只绑 127.0.0.1 的随机端口,跑完删掉),在上面跑后端全量;参数原样交给 pytest。只对 SQLite 成立的测试标了 sqlite_only,在 PostgreSQL 上跳过。
  • scripts/test-multiprocess.sh 用本机 docker 的 postgres:16 起临时库,在上面起两个工作进程,用演示模型跑端到端(backend/tests/test_multiprocess_e2e.py,缺省跳过,AGENTLAB_MULTIPROCESS_E2E=1 时跑)。它验证:运行由任一进程认领;另一个进程上的流收得到实时事件;同一个决定只执行一次;跨进程停止;定时只触发一次;评测和问数据并发时优先级生效;每日花费上限不越过;强杀一个进程之后被接手或照实标中断。
  • 前端检查脚本的 --stacks(隔离的副本服务)只支持 SQLite。