Django多租户结合graphene-django部署后响应随机异常问题
Docker+uWSGI环境下出现串租户/随机返回错误数据,核心原因是uWSGI的worker进程/线程复用机制:django-multitenant用线程本地存储(thread-local)保存租户信息,而生产环境的uWSGI多进程/线程会被重复利用,前一个请求的租户信息没被清理,后续请求复用该进程/线程时,就会拿到错误的租户数据,导致返回其他公司的内容或空结果。
本地开发是单进程单线程,请求结束后线程直接销毁,不会有残留;但生产环境的进程/线程复用机制会把旧的租户信息留下来干扰新请求。
1. 完善Django中间件:请求结束强制清理租户
修改SetTenantFromHeadersMiddleware,用try-finally块确保不管请求成功失败,都清空当前租户,避免残留:
class SetTenantFromHeadersMiddleware: """ 从请求头设置当前租户,请求结束后强制清理租户信息,避免进程/线程复用导致的串租户问题 """ def __init__(self, get_response): self.get_response = get_response def __call__(self, request): try: if request.path == '/graphql': tenant_header = request.META.get('HTTP_N1_TENANT', None) if tenant_header: _, company_id = from_global_id(tenant_header) try: company = Company.objects.get(id=company_id) set_current_tenant(company) except Company.DoesNotExist: pass response = self.get_response(request) finally: # 无论请求结果如何,强制清空当前租户 set_current_tenant(None) return response
2. 优化GraphQL中间件:独立初始化租户,避免依赖残留数据
修改MultitenantGraphQLMiddleware,不再依赖线程中可能残留的租户信息,而是从头初始化,同时标记请求已验证租户,避免多字段解析时重复执行逻辑:
from django_multitenant.utils import set_current_tenant, get_current_tenant class MultitenantGraphQLMiddleware: """ 验证租户归属,或为用户设置默认租户,确保每个请求的租户状态正确 """ @property def safe_mutations(self) -> list[str]: return [ CreateCompany.__name__, 'TokenAuth', 'RefreshToken', ] @property def safe_queries(self) -> list[str]: return ['Me'] def is_safe_operation(self, operation) -> bool: if not operation: return False is_mutation = operation.operation == OperationType.MUTATION is_safe_mutation = is_mutation and (operation.name.value in self.safe_mutations if operation.name else False) is_query = operation.operation == OperationType.QUERY is_safe_query = is_query and (operation.name.value in self.safe_queries if operation.name else False) return is_safe_mutation or is_safe_query def resolve(self, next, root, info, **kwargs): if self.is_safe_operation(info.operation): set_current_tenant(None) return next(root, info, **kwargs) # 标记该请求已验证租户,避免GraphQL多字段解析时重复执行 if hasattr(info.context, '_tenant_verified'): return next(root, info, **kwargs) # 从头初始化租户,不依赖线程残留数据 tenant_header = info.context.META.get('HTTP_N1_TENANT', None) current_tenant = None if tenant_header: _, company_id = from_global_id(tenant_header) try: current_tenant = Company.objects.get(id=company_id) except Company.DoesNotExist: pass # 验证租户归属或设置默认租户 if current_tenant: if current_tenant.id not in info.context.user.companies.values_list('id', flat=True): raise Exception('Provided company ID does not belong to authenticated user') set_current_tenant(current_tenant) else: company = info.context.user.default_company if not company: raise Exception('Cannot execute GraphQL operation as there is no current tenant') set_current_tenant(company) # 标记已验证 info.context._tenant_verified = True return next(root, info, **kwargs)
3. 调整中间件顺序(关键)
确保中间件执行顺序正确:先验证用户身份,再处理租户逻辑,同时让自定义租户中间件在官方多租户中间件之后执行:
# settings.py MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django_multitenant.middlewares.MultitenantMiddleware', 'api.middleware.SetTenantFromHeadersMiddleware', # 保持在官方多租户中间件之后 ] GRAPHENE = { 'SCHEMA': 'api.schema.schema', 'MIDDLEWARE': [ 'graphql_jwt.middleware.JSONWebTokenMiddleware', # 先验证JWT,确保用户已登录 'api.middleware.LoginRequiredMiddleware', # 再检查登录状态 'api.middleware.MultitenantGraphQLMiddleware', # 最后处理租户逻辑 ] }
4. 可选:uWSGI配置优化
如果代码修复后仍有问题,可以在uWSGI配置中添加以下参数,减少进程/线程复用的影响(不推荐依赖这个,代码层面修复更可靠):
# uwsgi.ini enable-threads = true max-requests = 1000 # 每个worker处理1000个请求后重启,避免残留 reload-on-rss = 2048 # 内存占用超过2G时重启worker
django-multitenant用线程本地存储保存当前租户,而uWSGI的worker线程会被复用给多个请求。如果请求结束后不清理租户信息,下一个请求复用该线程时,会继承上一个请求的租户,导致串数据。通过在中间件的finally块强制清空租户,确保每个请求的租户状态都是干净初始化的。
内容的提问来源于stack exchange,提问作者Cristian Rojas

