You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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需要这个配置
  }
};

额外注意事项

  1. 清理连接池:在应用关闭时主动销毁Knex连接池,避免连接泄漏:
    // 在Koa/Express的 shutdown 钩子中添加
    process.on('SIGTERM', async () => {
      await knexSingleton.destroy();
      process.exit(0);
    });
    
  2. 事务管理:如果使用了事务,务必确保每个事务都执行commit()或rollback(),否则连接会被长期占用。
  3. 缓存有效性:你的缓存逻辑是请求内的查询复用,这个设计是合理的,但如果需要跨请求缓存,可以考虑改用Redis等全局缓存工具。

内容的提问来源于stack exchange,提问作者SILENT

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 13:17:50