Supavisor是Supabase生态中承担Postgres连接池任务的核心组件,它位于客户端与真实Postgres实例之间,负责管理多租户连接、执行认证转发并输出连接状态指标。在本地开发或自动化测试环节,直接依赖真实Supavisor往往意味着要预备完整的Elixir运行时、配置租户映射并连通后端数据库,整体成本偏高。SupavisorMock2Image这个思路就是用Node.js实现一个行为接近Supavisor的轻量替身,再将它做成Docker镜像随取随用,让测试链路不必被外部依赖卡住。

一、为什么需要Mock Supavisor
Supavisor的价值在于把大量短连接汇聚成少量长连接,降低Postgres的连接开销,同时提供租户隔离和查询路由。但这些能力在单元测试或本地联调阶段并不是必需的,真正需要验证的往往是业务代码能否正确执行SQL、事务是否按预期提交或回滚。如果每次跑测试都要拉起完整Supavisor和真实数据库,CI时间会被显著拉长,网络抖动还会带来偶发失败。
Node.js实现的Mock服务能在毫秒级完成启动,监听与Supavisor相同的默认端口,对标准Postgres客户端返回合法的认证成功和就绪消息。这样无论是pg驱动、psql命令行还是ORM框架,都能在不修改连接串的前提下把请求打到Mock节点上。对于只关心连接建立和协议握手成功率的场景,这种替代方式完全够用,而且避免了原生Elixir环境带来的维护负担。
除了测试提速,Mock服务还能制造一些真实Supavisor不易模拟的边界状态。例如通过配置环境变量让服务在收到特定启动包时主动断开连接,或者延迟发送ReadyForQuery消息,用来验证客户端超时重连逻辑。把行为参数化之后,同一个镜像可以覆盖更多失败注入场景。
二、用Node.js实现Postgres协议轻量Mock
Postgres前端协议在建立连接时先由客户端发送启动包,其中包含协议版本号、用户名字符串和数据库名字符串。服务端需要回应认证消息,对于无密码或信任模式,直接返回AuthenticationOk即可,随后发送ReadyForQuery表示可以处理SQL命令。Node.js的net模块足够完成这些原始字节的读写,不需要额外的协议库。
下面这段代码演示了最简实现:创建TCP服务,解析启动包中的用户和数据库,然后连续写入两条协议消息。虽然它没有实现完整的查询响应,但已经能让绝大多数客户端完成连接握手并进入空闲事务状态。对于需要执行简单查询的测试,可以在ReadyForQuery之后继续监听Simple Query消息并回写固定结果集,这里先聚焦连接层。
const net = require('net');
function readCString(buf, start) {
let end = start;
while (end < buf.length && buf[end] !== 0) {
end += 1;
}
return {
value: buf.toString('utf8', start, end),
next: end + 1,
};
}
const server = net.createServer((socket) => {
socket.once('data', (chunk) => {
// 忽略SSLRequest,假设客户端直接发启动包
if (chunk.length >= 8) {
const length = chunk.readUInt32BE(0);
const protocol = chunk.readUInt32BE(4);
if (protocol === 80877103 || protocol === 196608) {
let offset = 8;
const userInfo = readCString(chunk, offset);
const dbInfo = readCString(chunk, userInfo.next);
console.log(`连接请求 user=${userInfo.value} db=${dbInfo.value}`);
// 发送AuthenticationOk,消息类型R,长度8,认证类型0
const authOk = Buffer.from([0x52, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00]);
socket.write(authOk);
// 发送ReadyForQuery,消息类型Z,长度5,状态I表示空闲
const ready = Buffer.from([0x5a, 0x00, 0x00, 0x00, 0x05, 0x49]);
socket.write(ready);
return;
}
}
socket.destroy();
});
socket.on('error', () => {});
});
const port = Number(process.env.MOCK_PORT || 5432);
server.listen(port, () => {
console.log(`supavisor mock listening on ${port}`);
});
上面代码中readCString函数负责从Buffer中截取以零字节结尾的字符串,Postgres启动包里用户和数据库字段都采用这种编码。协议版本号在二进制层面通常是196608对应3.0协议,80877103是SSL请求的特殊标识。这个实现默认不做SSL,因此收到SSL请求时会直接关闭连接,实际使用中可以通过环境变量控制是否返回N拒绝SSL并继续等待明文启动包。
如果把Mock服务放在容器内,建议把端口默认值设为5432,这样与应用连接串里的Supavisor端口保持一致。监听地址可以绑定0.0.0.0,但为了安全,生产调试时也可以通过环境变量限制为127.0.0.1。本示例未绑定host,Node.js默认监听所有接口,这在本地测试是可接受的。
三、打包为可复用的Docker镜像
Node.js服务编写完成后,打包成镜像只需要一个精简的Dockerfile。选择node:20-alpine作为基础镜像可以减少体积,同时把源码直接复制到工作目录。为了便于CI中复用,可以用ARG接收版本信息并写入镜像标签,实际构建时通过--build-arg传入。
FROM node:20-alpine WORKDIR /app COPY package.json ./ RUN npm install --omit=dev COPY server.js ./ ENV MOCK_PORT=5432 EXPOSE 5432 CMD ["node", "server.js"]
这个例子假设项目根目录存在一个只含基础元数据的package.json,因为源码只依赖Node.js内置模块,所以npm install并不会真正拉取额外包。如果后续需要加入pg-protocol之类的解析库,这条安装命令仍然能正常复用。使用--omit=dev是为了确保镜像内只包含运行依赖,避免把测试框架带进生产环境。
构建命令很简单:
docker build -t supavisor-mock:latest . docker run --rm -p 5432:5432 --name supavisor-mock supavisor-mock:latest
映射端口后,本地psql -h 127.0.0.1 -p 5432 -U test -d testdb就能连接到Mock服务并看到连接日志。如果应用容器和Mock服务都运行在同一个Docker网络中,可以通过服务名直接访问,不必对外暴露端口。健康检查方面,Node.js本身没有HTTP接口,可以在Dockerfile中加入HEALTHCHECK,利用pg_isready命令探测TCP握手是否成功,但需要额外安装postgresql-client,会略微增大镜像体积。
四、与真实Supavisor的差异和适用边界
Mock服务与真实Supavisor最大的区别在于它不维护任何后端连接池,也不执行真实的SQL转发。连接一旦完成握手,客户端发送的查询消息会被Mock服务忽略或仅返回伪数据。因此它适合用来验证连接管理、重试策略、认证流程和客户端启动逻辑,但不适合执行涉及真实数据写入、事务隔离或多租户路由的集成测试。
另一个需要注意的差异是Supavisor会提供管理API和Prometheus指标,Mock服务默认不会暴露这些端点。如果应用代码中有健康检查依赖Supavisor的/metrics接口,可以在Node.js服务中增加一个极简HTTP服务补上对应路径,返回固定文本即可。这样在容器编排平台里,Mock实例就能通过同样的就绪探针规则。
在CI流水线中,推荐把SupavisorMock2Image与真实数据库容器配合使用:Mock负责接住应用启动时的连接握手,Postgres负责执行后续SQL。这样既避免了真实Supavisor的部署复杂度,又不牺牲SQL层面的验证能力。对于单纯验证数据库驱动是否正常集成的冒烟用例,单独使用Mock镜像已经足够,还能把测试时间从分钟级压缩到秒级。
总体来看,Node.js实现SupavisorMock2Image的价值在于降低测试环境对复杂连接池的依赖,同时保持协议层面的兼容性。它不会替代真实Supavisor在生产环境中的角色,但作为本地开发和自动化测试的加速器,这种轻量替代方案能显著改善反馈速度,减少因基础设施不稳定带来的无谓排查。