Heroku免费版Postgres连接耗尽,Knex连接池未重用问题排查修复
问题分析与解决方案
你的核心问题确实出在Knex的使用方式上——每个请求都创建了全新的Knex实例,而每个Knex实例都会初始化独立的数据库连接池。当请求量上来时,多个连接池同时占用数据库连接,很快就会耗尽Heroku免费Postgres的连接配额(通常是10个左右)。
问题根源拆解
你在KnexCache的类定义里,每次实例化都会执行knexDefault(ISDEV ? development : production)创建新的Knex实例。而Apollo Server的context函数会在**每个请求(或每个客户端连接)**中生成新的KnexCache对象,这意味着:
- 每个请求都自带一个独立的Knex连接池
- 连接池中的连接无法在请求间复用
- 大量闲置连接堆积,最终触发Heroku的连接数限制
修复方案:全局单例Knex实例
我们需要让所有请求共享同一个Knex连接池,同时保留你原本的请求内查询缓存逻辑。
步骤1:创建全局Knex单例
单独抽离一个文件初始化Knex,确保整个应用只有一个连接池:
// src/utils/knexSingleton.ts import knexDefault from "knex"; import { development, production } from "../ConfigKnex"; import { ENVTRANS } from "./Consts"; const { ISDEV } = ENVTRANS; // 全局唯一的Knex实例,所有请求共享这个连接池 export const knexSingleton = knexDefault(ISDEV ? development : production);
步骤2:修改KnexCache类,注入单例Knex
调整KnexCache的构造逻辑,不再内部创建Knex实例,而是依赖注入全局单例:
import { knexSingleton } from "./knexSingleton"; import type { Knex } from "knex"; export class KnexCache { private knex: Knex; transaction = this.knex.transaction.bind(this.knex); // 缓存Map保留请求内独立的特性,Knex实例复用全局单例 constructor( private cache: Map<string, any> = new Map(), knexInstance: Knex = knexSingleton ) { this.knex = knexInstance; } private CacheIt(action: string, table: string, props: any, fn: any) { let tm = this.cache.get(table); if (!tm) { tm = new Map<string, any>(); this.cache.set(table, tm); } const key = `${action}|${JSON.stringify(props)}`; let res = tm.get(key); if (res) return res; res = fn(); tm.set(key, res); return res; } async SelectAsync<T>( table: string, where: Partial<T>, db = this.knex, ): Promise<T[]> { return this.CacheIt("SelectAsync", table, { where }, () => db(table).where(where).select()); } // ... 其他方法保持不变 }
步骤3:适配Apollo Context配置
Apollo的context逻辑可以保持不变(每个请求创建新的KnexCache),但此时所有KnexCache实例共享同一个全局Knex连接池:
const server = new ApolloServer({ typeDefs, resolvers, debug: ISDEV, tracing: ISDEV, playground: { settings: { "request.credentials": "include" }, }, context: async (context) => { // 每个请求的缓存Map独立,Knex连接池全局共享 context.k = new KnexCache(); return context as Context; }, });
步骤4:优化连接池配置(关键)
针对Heroku免费版Postgres的连接限制,在Knex配置中设置合理的连接池参数:
// ConfigKnex.ts - production配置 export const production = { client: 'postgresql', connection: process.env.DATABASE_URL, pool: { min: 1, // 闲置时保留的最小连接数 max: 5, // Heroku免费版建议不超过5,留余量给系统内部连接 }, ssl: { rejectUnauthorized: false // Heroku Postgres需要这个配置 } };
额外注意事项
- 清理连接池:在应用关闭时主动销毁Knex连接池,避免连接泄漏:
// 在Koa/Express的 shutdown 钩子中添加 process.on('SIGTERM', async () => { await knexSingleton.destroy(); process.exit(0); }); - 事务管理:如果使用了事务,务必确保每个事务都执行
commit()或rollback(),否则连接会被长期占用。 - 缓存有效性:你的缓存逻辑是请求内的查询复用,这个设计是合理的,但如果需要跨请求缓存,可以考虑改用Redis等全局缓存工具。
内容的提问来源于stack exchange,提问作者SILENT
相关产品推荐
相关产品推荐

