Heroku部署Django应用PostgreSQL连接不释放问题求助
PostgreSQL连接泄漏问题排查与解决
我当前使用的技术栈:Django、Heroku、WebSocket、AJAX、Celery、PostgreSQL、Consumer、ASGI(uvicorn,也试过Daphne)。
核心问题:仅刷新页面就会创建新的PostgreSQL连接,且长时间闲置也不会释放,很快触发Heroku PostgreSQL的连接数上限(20/20)。本地环境无此问题,仅在Heroku生产环境出现。
已尝试的操作
- 查看
pg_stat_activity,发现泄漏的连接来自特定视图代码的查询,多数处于空闲状态:for post in posts: temp_dict = defaultdict(list) recommendations_ordered = post.recommendations.all().order_by('created_at') ...and so on, including request.user. - 数据库配置如下:
if ENVIRONMENT == 'development': DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'redacted', 'USER': 'redacted', 'PASSWORD': 'redacted', 'HOST': 'localhost', 'PORT': '5432', } } else: DATABASES = { 'default': dj_database_url.config(default=os.environ.get('DATABASE_URL')) } DATABASES['default']['CONN_MAX_AGE'] = 0 DATABASES['default']['ATOMIC_REQUESTS'] = True - 在涉及的视图和Celery任务中手动添加了
close.connection操作 - Procfile配置:
web: gunicorn -w 1 -k uvicorn.workers.UvicornWorker decide.asgi:application worker: celery -A decide worker -l info --pool=threads
可能相关的其他配置
CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": os.environ.get('REDIS_URL', 'redis://localhost:6379/0'), "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } } } if ENVIRONMENT == 'production': CACHES['default']['OPTIONS']['CONNECTION_POOL_KWARGS'] = {"ssl": True} # Cache settings CACHE_TTL = 300 # Use Django's cache API CACHE_MIDDLEWARE_ALIAS = "default" CACHE_MIDDLEWARE_SECONDS = CACHE_TTL CACHE_MIDDLEWARE_KEY_PREFIX = "" # Celery Configuration if ENVIRONMENT == 'development': CELERY_BROKER_URL = 'redis://localhost:6379/0' CELERY_RESULT_BACKEND = 'redis://localhost:6379/0' else: # production CELERY_BROKER_URL = os.getenv('REDIS_URL', 'redis://localhost:6379/0') CELERY_RESULT_BACKEND = os.getenv('REDIS_URL', 'redis://localhost:6379/0') CELERY_ACCEPT_CONTENT = ['json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_TIMEZONE = 'UTC' CELERY_TASK_DEFAULT_QUEUE = 'default' CELERY_QUEUES = ( Queue('default', Exchange('default'), routing_key='default'), ) django_heroku.settings(locals())
问题原因分析与解决方案
1. CONN_MAX_AGE=0的反向作用
CONN_MAX_AGE=0意味着每次请求都会创建新连接且不复用,但ASGI是异步/长连接模型,请求上下文销毁时机与WSGI不同,导致连接无法被正确回收。
解决:将生产环境的CONN_MAX_AGE设为合理值(如60秒),让连接池复用连接:
DATABASES['default']['CONN_MAX_AGE'] = 60 # 替换原来的0
2. ATOMIC_REQUESTS=True的隐式连接持有
开启全局事务后,若请求处理中出现异常(比如WebSocket连接异常断开),事务可能无法正确回滚,导致连接被长期占用。
解决:关闭ATOMIC_REQUESTS,在需要事务的视图/函数中手动用transaction.atomic()装饰器或上下文管理器。
3. ASGI Worker的连接回收问题
gunicorn + UvicornWorker的组合可能导致异步环境下连接无法自动回收。
解决:
- 改用纯Uvicorn启动:
web: uvicorn decide.asgi:application --host 0.0.0.0 --port ${PORT:-8000} - 若保留Gunicorn,增加
--max-requests参数让Worker处理一定请求后重启:web: gunicorn -w 1 -k uvicorn.workers.UvicornWorker --max-requests 100 decide.asgi:application
4. 循环查询的N+1问题
代码中的循环查询会触发多次数据库交互,积累闲置连接。
解决:用prefetch_related优化查询,批量获取关联数据:
from django.db.models import Prefetch posts = Post.objects.prefetch_related( Prefetch('recommendations', queryset=Recommendation.objects.order_by('created_at')) ).all()
5. Celery Worker的连接管理
线程池模式下,每个线程可能持有独立连接,任务结束后未释放会导致泄漏。
解决:
- 在Celery任务中主动关闭旧连接:
from django.db import close_old_connections from celery import shared_task @shared_task def your_task(): try: # 任务逻辑 finally: close_old_connections() - 增加
--max-tasks-per-child参数让Worker进程处理一定任务后重启:worker: celery -A decide worker -l info --pool=threads --max-tasks-per-child 50
6. django_heroku.settings()的覆盖问题
该调用可能会覆盖你手动设置的数据库参数,导致配置不生效。
解决:将其放在配置最后,并重新设置数据库参数:
django_heroku.settings(locals()) # 重新设置参数避免被覆盖 if ENVIRONMENT == 'production': DATABASES['default']['CONN_MAX_AGE'] = 60 DATABASES['default']['ATOMIC_REQUESTS'] = False
内容的提问来源于stack exchange,提问作者superbot
相关产品推荐
相关产品推荐

