本文是“Phorge 现代化改造实战”系列第十六篇。上一篇把工作队列和消费者迁入 Gorge;这一篇继续处理数据库诊断,把节点健康、复制状态、结构差异和升级状态查询交给独立的 Go 服务,并用跨仓契约避免 PHP 与 Go 在字段变化后各说各话。
系列导航
- 从没有官方镜像到 Docker Compose 跑起来:建立最小可运行基线;
- 改进容器化的七个细节:补齐权限、持久化、依赖和探活;
- 接入 Stargate:把 Forward Auth 的信任边界做完整;
- 拆分模块到 Gorge:无侵入改造不等于不碰文件;
- 替换 diff 子进程:兼容不等于逐字一致;
- 替换实时通知服务:为什么 HTTP 501 反而表示正常;
- 迁移邮件服务:先分清哪些失败不该重试;
- 迁移搜索服务:写进索引不等于搜得到;
- 迁移文件存储:写得进去也要读得回来;
- 迁移 Webhook 投递服务:先解决重复投递;
- 升级 Gorge 的 HTTP 框架:接口没变,行为也不能变;
- 兼容 Elasticsearch 5、6、7:版本配置决定整个索引结构;
- 联调六项外部服务:容器在运行,不代表业务已经切换;
- 为 Go 服务建立统一的 Phorge API 入口:网关可以换框架,Conduit 协议不能变;
- 用 Go 接管 Phorge 工作队列:如何避免新旧消费者互相抢任务;
- 把数据库自省搬进 Gorge:跨仓契约不能靠手工同步;
- 恢复简体中文支持:用工具持续维护 2.7 万行翻译文件。
写在前面
Phorge 的数据库管理页面会连接 MySQL,读取节点状态、复制延迟、表结构和升级记录。单节点环境里,这些查询通常很轻;到了多主库、多副本或应用分区环境,一次检查可能要依次访问多台机器。连接超时、权限不足和复制状态查询都会占用 PHP Web 进程。
gorge-db-api 将主要的数据库诊断收进独立服务。Phorge 仍负责呈现管理页面、判断缺少哪些升级补丁,以及执行少量依赖本地状态的 MySQL 检查;Go 服务负责采集节点、复制、结构和迁移状态。未启用服务时,Phorge 继续执行原来的 SQL 查询,现有安装无需立刻切换。
这类拆分有一个很隐蔽的风险:接口可以返回 200,PHP 却因为字段名或字段含义不一致生成错误对象。页面可能把复制延迟显示为空,把致命安装问题当成普通提示,或者错误判断数据库已经完成升级。HTTP 状态正常,反而让问题更晚暴露。
本文以 2026 年 9 月 9 日配套发布的 soulteary/gorge@2026.09.09-r3 和 soulteary/phorge@2026.09.09-r3 为准。Gorge 标签指向 59f051e1,Phorge 标签指向 27ec67c9。r3 补齐了字段映射、契约握手、拓扑校验和鉴权限制,部署时应成对升级。
一、db-api 接管哪些数据库检查
迁移后的调用链很短:
flowchart LR
U["Phorge 管理页面"] --> P["PHP 适配器"]
P -->|"/api/db/*"| G["gorge-db-api :8080"]
G --> M[("MySQL master")]
G --> R[("MySQL replica")]
r3 提供八个只读接口:
| 路径 | 用途 |
|---|---|
/api/db/servers |
返回全部配置节点的连接与复制状态 |
/api/db/servers/:ref/health |
检查一个 host:port 节点 |
/api/db/schema-diff |
返回服务器、数据库、表、字段和索引组成的实际结构树 |
/api/db/schema-issues |
返回扁平的结构问题列表 |
/api/db/setup-issues |
检查 MySQL 版本、存储引擎、参数与初始化状态 |
/api/db/charset-info |
返回字符集与排序规则能力 |
/api/db/migrations/status |
返回各主库已经应用的升级补丁和集群状态摘要 |
/api/db/meta |
返回契约版本、数据库前缀、拓扑来源和能力列表 |
前七个接口实时查询数据库;/api/db/meta 只读取启动时已经确定的配置。服务不建库、不改表、不执行升级,也不缓存健康和复制结果。每个请求使用短连接完成只读查询,用完立即关闭。
r3 还把迁入初版中未被这些接口调用的路由、事务和写重试代码移到 internal/dbproxy。dbapi 的生产路径只留下当前在用的读取能力,权限边界更容易审查。
所有 /api/db/** 路由共用 X-Service-Token 鉴权。token 留空时关闭鉴权;生产环境应明确配置。/healthz、/readyz 和根路径探针不需要 token,容器编排可以直接访问。
二、接口返回 200,字段仍可能已经错了
r2 的 Go 输出和 PHP 客户端使用了不同的字段名:
| 数据 | Go 输出 | r2 PHP 读取 |
|---|---|---|
| 复制状态 | replicationStatus |
replicaStatus |
| 复制延迟 | secondsBehindMaster |
replicaDelaySec |
| 数据库名 | databaseName |
database |
| 表名 | tableName |
table |
| 字段名 | columnName |
column |
| 安装问题标识 | issueKey |
key |
JSON 可以正常解析,PHP 的 idx() 也会为缺少的键返回默认值,所以这些错误不会自然变成 5xx。服务器列表、安装检查和结构对比仍能渲染,内容却已经失真。
r3 将线协议集中到 Gorge 的 internal/contracts/dbapi.go,PHP 按同一组 camelCase 字段恢复原有对象。数据库结构转换也补齐了之前遗漏的属性:
- 数据库的字符集、排序规则与
accessDenied; - 表的排序规则、存储引擎与索引;
- 字段的完整类型、字符集、可空性与自增属性;
- 索引的字段顺序、前缀长度、唯一性与类型。
升级状态的职责也重新划清。db-api 只返回数据库中已经观察到的状态:initialized、appliedPatches、clusterStatePresent 和 clusterStateDigest。预期补丁列表继续由 Phorge 的 PhabricatorSQLPatchList 维护,PHP 用它计算缺失项。
clusterStatePresent 用来区分“记录不存在”和“记录存在但内容为空或异常”。clusterStateDigest 是 cluster.databases 原始状态的 SHA-256 摘要,可用于比较多主库是否一致,同时避免把完整数据库拓扑放进接口响应。PHP 依赖这些新增语义,因此契约版本在 r3 提升到 1.1。
三、PHP 先检查 /meta,再决定是否切换
只靠两个仓库约定“都使用 camelCase”仍会留下升级顺序问题。新版 PHP 可能连接旧版 Gorge,旧版 PHP 也可能读到自己不认识的新结构。r3 为此增加 /api/db/meta:
{
"data": {
"contractVersion": "1.1",
"namespace": "phorge",
"topologySource": "file",
"capabilities": [
"servers",
"server-health",
"schema-diff",
"schema-issues",
"setup-issues",
"charset-info",
"migrations-status"
]
}
}
PhabricatorGorgeDBClient::shouldUseService() 会检查四项内容:
- 契约主版本必须是
1,次版本至少为1; namespace必须等于 Phorge 的storage.default-namespace;capabilities必须包含 PHP 当前依赖的servers、schema-diff、setup-issues、charset-info和migrations-status;topologySource会显示服务正在使用拓扑文件还是单节点变量,便于部署检查。
切换结果分成三类:
| 情况 | Phorge 的行为 |
|---|---|
没有配置 gorge.db.uri |
直接使用原生 SQL |
/meta 可读,但版本、数据库前缀或能力不兼容 |
当前请求使用原生 SQL,并记录安装问题 |
| 超时、401、5xx、非法 JSON 或错误信封 | 抛出异常,保留实际故障 |
网络和鉴权故障不会触发静默回落。否则页面虽然还能打开,运维人员却无法判断请求访问了 Gorge 还是 MySQL。兼容性结果按服务 URI 和数据库前缀存进 PHP 的请求级缓存,同一个请求只握手一次。
四、同一份标准 JSON 同时约束 Go 和 PHP
运行时握手可以阻止已知的不兼容版本组合,开发阶段仍要及时发现字段变化。r3 增加了三层检查。
第一层放在 Gorge。tests/contract/dbapi/canonical/ 保存 servers、schema-diff、setup-issues、charset-info 和 migrations-status 的标准 JSON;Go 测试使用真实契约类型生成数据,再逐字段比较。字段名、类型或省略规则发生变化时,生产端测试会失败。
第二层放在 Phorge。PHP 仓库保存同样的 JSON,PhabricatorGorgeDBContractTestCase 将它们转换成 PhabricatorDatabaseRef、数据库结构对象和安装问题,再检查最终属性。这样可以发现 PHP 读错键、漏设索引或混淆集群状态等问题。
第三层由 Gorge 的 db-api-cross-repo.yml 连接两个仓库:
- 检出明确指定的 Gorge 与 Phorge 版本;
- 检查两边保存的标准 JSON 是否逐字一致;
- 启动真实 MySQL,并执行
bin/storage upgrade; - 先让 Phorge 直接查询数据库,再让它通过 db-api 查询同一实例;
- 精确比较稳定字段,对连接耗时等易变值检查类型和范围。
这条比较检查的是 PHP 最终得到的对象,能够同时覆盖 Go 输出、PHP 映射和原生 SQL 路径。以后修改 db-api 契约时,两个仓库的配对版本与真实 MySQL 对比结果都应进入发布检查,避免各自 CI 通过、组合运行却出错。
五、多节点配置必须选对主库和副本
db-api 支持两种拓扑来源:
| 模式 | 配置方式 | 适用场景 |
|---|---|---|
| 单节点 | GORGE_DB_MYSQL_HOST/PORT/USER/PASS 与 GORGE_DB_NAMESPACE |
默认 Compose 和普通单机安装 |
| 多节点 | GORGE_DB_CONFIG_FILE 指向 Phorge 风格 JSON,从 cluster.databases 读取 |
主从、分区和多主库环境 |
设置了 GORGE_DB_CONFIG_FILE 后,文件配置拥有更高优先级。解析器需要处理默认分区、应用分区、主库与副本角色、节点级账号,以及标量或列表形式的 partition。节点密码覆盖全局密码,显式空密码也要保留。
r2 及更早版本在文件缺失或 JSON 非法时会退回单节点变量。这可能让所有应用都落到默认主机,并让节点报告看起来仍然正常。r3 改为启动失败:文件不可读、字段类型错误或没有任何节点被解析为主库角色时,都会直接返回配置错误。只有完全没有设置 GORGE_DB_CONFIG_FILE 时,服务才建立单节点拓扑。若文件里只有被禁用的主库,进程可以启动,但 /readyz 会保持失败。
分区路由遵循 Phorge 的选择顺序:先找显式服务该应用的节点,再找默认分区,最后兼容旧配置中的未分区节点。副本使用自己的分区属性参与选择,避免把某个应用的查询发给另一个分区的副本。
启动后可以查看 /api/db/meta 的 topologySource。file 表示拓扑文件已经生效,single-node 表示服务正在使用标量环境变量。这个字段能快速发现卷未挂载或环境变量未生效的问题。
六、复制状态和 schema 查询要兼容真实数据库
数据库连通只完成了第一步。管理页面还要知道副本线程是否运行、复制延迟是否异常,以及实际表结构能否完整读取。
兼容新旧复制术语
MySQL 逐步替换了复制相关术语:
| 较旧名称 | 新名称 |
|---|---|
SHOW SLAVE STATUS |
SHOW REPLICA STATUS |
Slave_IO_Running / Slave_SQL_Running |
Replica_IO_Running / Replica_SQL_Running |
Seconds_Behind_Master |
Seconds_Behind_Source |
服务优先执行 SHOW REPLICA STATUS;服务器返回 1064 语法错误时,再尝试旧语句。权限错误会原样进入权限分类,不会被当成语法兼容问题。即使复制延迟是 0,也要同时确认接收线程和执行线程都在运行。
每次探测都会创建新的结果对象。节点本次连接失败时,旧的复制状态会被清空,响应中不会同时出现“连接失败”和“复制正常”。
schema 查询只返回完整结果
结构查询按 Server → Database → Table → Column 遍历,并额外读取表索引:
| 层级 | 保留的主要信息 |
|---|---|
| Database | 名称、字符集、排序规则、访问被拒标记 |
| Table | 名称、排序规则、存储引擎、索引 |
| Column | 名称、完整类型、字符集、排序规则、可空性、自增属性 |
| Index | 名称、字段顺序、前缀长度、唯一性、索引类型 |
实现还处理了几处常见边界:数据库前缀进入 LIKE 条件前先转义 % 和 _;view 的 TABLE_COLLATION 与 ENGINE 允许为 NULL;索引按 SEQ_IN_INDEX 排序;遍历结束后检查 rows.Err()。
连接中断、扫描失败或 INFORMATION_SCHEMA 查询报错时,整次接口直接失败。返回半棵结构树会让 PHP 把“没有读到”误判成“数据库里不存在”,因此这里不保留部分成功结果。调用方通过 databases= 显式请求一个存在但无权查看的库时,服务会保留该节点并标记 accessDenied: true,让 Phorge 区分“库不存在”和“账号看不到”。
七、健康、就绪和错误分别说明什么
db-api 的几个检查入口含义不同:
| 检查 | 能确认的事情 |
|---|---|
/healthz 200 |
HTTP 进程已经启动 |
/readyz 200 |
至少一台启用的主库可以连接并完成 ping |
/api/db/meta 正常 |
token、契约版本、数据库前缀和拓扑来源可读 |
/api/db/servers 正常 |
节点探测和复制状态查询可以执行 |
/api/db/schema-diff、/api/db/setup-issues 正常 |
数据库、权限和自省查询链路可用 |
| 管理页面与原生路径结果一致 | PHP 字段映射和业务语义已经完成接管 |
/readyz 只检查启用的主库,副本单独连通不会让服务进入就绪状态。探针 DSN 不带数据库名,也不检查 {namespace}_meta_data 和 patch_status。因此 MySQL 已启动但 Phorge 还没执行 bin/storage upgrade 时,db-api 可以先进入就绪;更深一层的问题交给诊断接口报告。
Compose 中 Phorge 对 db-api 使用 service_started,避免可选的诊断服务阻塞 Phorge 首次启动和数据库升级。容器自己的 healthcheck 访问 /healthz,数据库暂时不可达时进程仍可接受请求并通过 /readyz 明确报告未就绪。
数据库错误会映射成可供调用方区分的状态:
| 错误码 | HTTP 状态 | 含义 |
|---|---|---|
ERR_DB_UNREACHABLE |
503 | 数据库不可达或连接中断 |
ERR_DB_ACCESS_DENIED |
403 | 数据库账号缺少权限 |
ERR_READONLY |
409 | 只读连接拒绝写操作 |
错误信封只写通用说明,不会直接返回原始 SQL、数据库名或主机名;无法分类的内部错误交给平台日志记录。/api/db/servers 属于管理报告,会在 200 响应中返回节点地址、账号、状态和 connectionMessage。它把“某个节点连不上”当成被观察到的状态,因此不能只看 HTTP 状态判断集群健康。通过 token 的调用方仍需位于可信网络中。
ERR_READONLY 是只读连接层保留的保护。当前公开路由都是 GET,不会主动触发写入。
r3 关闭了查询参数传 token 的兼容方式,只接受 X-Service-Token 请求头。URL 中的 token 很容易进入代理日志、浏览器历史和监控标签,db-api 没有保留这种传递方式的需要。
八、部署 r3 时需要确认的几件事
两个仓库成对升级
契约 1.1、/api/db/meta、完整字段转换和新的切流判断都在 r3 中。新版 PHP 连接契约 1.0 的 Gorge 时会继续使用原生 SQL;旧版 PHP 无法读取 r3 新增的字段语义。部署时应同时升级 Phorge 与 Gorge,并将镜像标签固定为同一配对版本:
GORGE_IMAGE_TAG=2026.09.09-r3
Gorge 镜像采用统一格式:
ghcr.io/soulteary/gorge:<service>-<version>
db-api 对应 ghcr.io/soulteary/gorge:db-api-2026.09.09-r3。
Compose 命令始终带上叠加文件
docker-compose.gorge.yml 是叠加编排。后续执行 up、exec 和 logs 时都要带同一组 -f,可以先定义一个函数:
dc() {
docker compose \
-f docker-compose.yml \
-f docker-compose.gorge.yml \
"$@"
}
dc up -d --build
dc exec gorge-db-api wget -qO- http://127.0.0.1:8080/healthz
dc exec gorge-db-api wget -qO- http://127.0.0.1:8080/readyz
dc exec phorge /opt/phorge/phorge/bin/config get gorge.db.uri
默认编排不会把 db-api 的 8080 端口发布到宿主机,只有 Compose 网络内的 Phorge 可以访问。.env 中显式设置 GORGE_DB_URL= 可以阻止 entrypoint 再写入服务地址;如果 gorge.db.uri 已经存在于 local.json,还要手工清除旧值。
多节点只挂载 db-api 需要的配置
完整的 Phorge local.json 可能包含邮件、OAuth 和其它应用密钥。多节点部署更适合准备一份只包含数据库前缀、节点、角色、分区和账号的拓扑文件,并以只读卷挂载:
services:
gorge-db-api:
environment:
GORGE_DB_CONFIG_FILE: /etc/gorge/gorge-db-cluster.json
GORGE_DB_MYSQL_PASS: ${GORGE_DB_RO_PASSWORD:-}
volumes:
- ./deploy/gorge-db-cluster.json:/etc/gorge/gorge-db-cluster.json:ro
修改 environment 或 volume 后,要重新创建容器,单纯 restart 不会应用 Compose 配置变化。重建后可在容器内检查元数据:
dc up -d --force-recreate gorge-db-api phorge
dc exec gorge-db-api wget -qO- \
--header="X-Service-Token: $GORGE_DB_TOKEN" \
http://127.0.0.1:8080/api/db/meta
返回的 namespace 应与 storage.default-namespace 一致,topologySource 应符合当前部署方式,contractVersion 应为 1.1 或 PHP 能接受的更高 1.x 版本。
db-api 只需要读取 INFORMATION_SCHEMA、复制状态、patch_status 和 hoststate。生产环境应使用单独的只读账号,并在目标 MySQL 或 MariaDB 版本上验证 SHOW VIEW、REPLICATION CLIENT 与结构自省所需权限。
最后
配置 db-api 后,Phorge 的数据库管理页面通过独立服务采集节点、复制、schema 和升级状态;没有配置服务时,原生 SQL 路径继续可用。数据库连接与兼容逻辑集中到一个可单独部署、单独审计的边界内。
这里需要重点防范的故障没有 5xx:Go 返回合法 JSON,PHP 却按另一套字段生成了错误对象。r3 用三项约束减少这类问题:/meta 在运行时检查版本和数据库前缀;两仓共享标准 JSON;跨仓测试比较 PHP 最终得到的对象。后续修改契约时,还应继续用真实 MySQL 对比 Gorge 与原生 SQL 两条路径,并把配对版本写进发布检查。
–EOF