Django多租户应用中Middleware随机丢失数据库连接问题排查
问题背景
原本用子域名隔离数据库的Django多租户应用,改成通过username@tenant格式提取租户信息,流程如下:
- 未登录时,
BeforeLoginMiddleware在登录请求里解析用户名,提取租户名存入Session; - 已登录时,
AppMiddleware从Session读取租户名,设置Thread Local变量,数据库路由通过get_current_db_name()获取该变量选择对应数据库。
当前功能正常,但随机出现以下问题:
- 数据库连接丢失,用户被
login_required装饰器重定向到登录页(浏览器显示302状态码); - 触发时机无规律:多页面跳转后、登录后首个页面、页面闲置约1分钟(已设置
SESSION_COOKIE_AGE=1200); - 终端仅输出
Broken pipe from ('127.0.0.1', 随机端口),无其他错误提示; - 已尝试调整中间件配置/顺序、禁用JS、清除浏览器缓存Cookie、设置
SESSION_SAVE_EVERY_REQUEST=True、增大SESSION_COOKIE_AGE至120000,均无效。
排查思路
Thread Local变量的线程安全问题
Django的WSGI/ASGI worker是多线程/多进程模型,如果Thread Local变量在请求结束后没清理,会被后续请求复用,导致租户信息混乱,数据库路由选错库,进而引发连接异常。检查你的AppMiddleware是否在请求结束时重置Thread Local变量。
数据库连接池的租户隔离问题
如果数据库连接池没按租户(数据库名)隔离,不同租户的请求会复用同一个连接,切换租户时数据库未正确切换,就会导致连接中断。确认数据库路由的get_current_db_name()每次都能正确获取当前租户值,以及连接池是否为每个数据库名维护独立连接。
Session读取的一致性问题
- 检查
BeforeLoginMiddleware是否在登录请求的正确阶段(比如process_view或process_post)写入Session,有没有出现登录请求还没写完Session就跳转的情况; - 确认中间件顺序:
SessionMiddleware必须在AuthenticationMiddleware和你的AppMiddleware之前,否则AppMiddleware读不到Session里的租户信息。
Broken pipe的本质原因
Broken pipe一般是客户端提前关闭连接或服务器连接超时,但这里伴随登录重定向,大概率是租户信息丢失后,数据库路由返回无效数据库名,导致连接失败,触发login_required重定向,此时浏览器请求未完成就出现Broken pipe。开启Django DEBUG日志,调高django.db的日志级别,看看有没有隐藏的数据库连接错误。
闲置超时与Session的关系
虽然设置了SESSION_COOKIE_AGE=1200,但要检查服务器端Session存储的过期配置:
- 如果用数据库存Session,查看Session表的
expire_date字段是否正确; - 如果用缓存存Session,确认缓存的过期时间和
SESSION_COOKIE_AGE一致。
解决方案
1. 修复Thread Local的清理逻辑
在AppMiddleware中添加请求结束后的清理步骤,避免跨请求污染:
import threading _thread_local = threading.local() class AppMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): try: if request.user.is_authenticated: tenant_name = request.session.get('tenant_name') if tenant_name: setattr(_thread_local, 'tenant', tenant_name) response = self.get_response(request) return response finally: # 请求结束后清理Thread Local变量 if hasattr(_thread_local, 'tenant'): delattr(_thread_local, 'tenant')
2. 确保数据库连接按租户隔离
数据库路由要确保每次请求都能正确获取租户对应的数据库名,同时配置连接池按数据库名隔离(如果用第三方连接池比如django-db-connection-pool,需开启对应配置):
class TenantDatabaseRouter: def db_for_read(self, model, **hints): return get_current_db_name() def db_for_write(self, model, **hints): return get_current_db_name() def allow_relation(self, obj1, obj2, **hints): return obj1._state.db == obj2._state.db def allow_migrate(self, db, app_label, model_name=None, **hints): return db == get_current_db_name()
3. 修正中间件顺序
确保中间件顺序符合Django的执行逻辑:
# settings.py MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'yourapp.middleware.BeforeLoginMiddleware', # 放在Auth中间件之前 'django.contrib.auth.middleware.AuthenticationMiddleware', 'yourapp.middleware.AppMiddleware', # 放在Auth和Session中间件之后 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]
4. 完善Session写入逻辑
在BeforeLoginMiddleware中确保登录请求的Session正确写入,比如在process_view阶段处理登录请求:
class BeforeLoginMiddleware: def __init__(self, get_response): self.get_response = get_response def process_view(self, request, view_func, view_args, view_kwargs): # 仅处理登录视图的POST请求 if view_func.__name__ == 'login' and request.method == 'POST': username = request.POST.get('username') if '@' in username: _, tenant_name = username.split('@', 1) request.session['tenant_name'] = tenant_name return None
5. 开启详细日志排查
在settings.py中添加日志配置,捕获数据库和请求相关的详细错误:
LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django.db': { 'handlers': ['console'], 'level': 'DEBUG', 'propagate': False, }, 'django.request': { 'handlers': ['console'], 'level': 'DEBUG', 'propagate': False, }, }, }
内容的提问来源于stack exchange,提问作者Zedh

