由于与代码无关的原因,与SQL Server和Azure SQL的连接可能会暂时失败:
- AlwaysOn 可用性组故障转移。
- 在连接设置过程中,网络会丢弃数据包。
- Resource Governor限制数据库。
- Azure SQL 副本会在缩放或升级期间被回收。
其中大多数故障在几秒钟内就清除。 本文介绍如何在使用 mssql-django 后端的 Django 应用程序中重试瞬时错误,以及如何配置 Django 和 ODBC 驱动程序,以便在空闲连接断开后自动恢复。
暂时性错误
暂时性错误是自行解决的临时故障。 在短暂延迟后重试操作通常成功。
在建立连接期间发生或向服务器发送请求时,以下错误是暂时性的。 在短的、受限的退避时重试。 在几次重试之后保留的错误通常表示配置问题(服务器错误、权限缺失、配额耗尽),重试无法解决。
| Error | Message | Troubleshooting |
|---|---|---|
64 |
A connection was successfully established with the server, but then an error occurred during the login process. (provider: TCP Provider, error: 0 - The specified network name is no longer available.) |
TCP 连接在握手过程中中断。 并非凭据错误。 如果问题仍然存在,请检查是否存在客户端网络不稳定的情况,或是否有会丢弃半建立连接的中间设备。 |
233 |
The client was unable to establish a connection because of an error during connection initialization process before login. |
预登录传输或 TLS 失败。 服务器通常会在无法接受该连接时返回该响应(例如资源耗尽、已达到最大连接数,或客户端不受支持)。 并非凭据错误。 验证服务器运行状况,然后检查客户端登录超时、TLS 设置和客户端/服务器 TLS 版本兼容性。 |
4060 |
Cannot open database "%.*ls" requested by the login. The login failed. |
登录已通过身份验证,但无法打开所请求的数据库。 暂时原因包括数据库处于转换过程中(故障转移、还原、扩缩容)或已自动暂停。 永久性原因(数据库不存在,登录缺少访问权限)不会通过重试来修复;检查数据库名称、登录映射和数据库状态。 |
4221 |
Login to read-secondary failed due to long wait on 'HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING'. |
该副本无法用于登录访问,因为在回收该副本时,当时正在进行中的事务缺少行版本。 回滚或提交主节点上的活动事务以解决该问题。 通过避免主数据库上的长时间写入事务来缓解此问题。 |
10053 |
A transport-level error has occurred when sending the request to the server. (provider: TCP Provider, error: 0 - An established connection was aborted by the software in your host machine.) |
本地端中止连接。 检查客户端网络运行状况以及任何本地防火墙或 VPN 客户端。 |
10054 |
A transport-level error has occurred when sending the request to the server. (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) |
远程端发送 TCP 重置。 常见原因包括:对端进程崩溃、防火墙注入了重置包,或 Azure SQL 网关关闭了空闲连接。 对于空闲重置这类情况,请在客户端启用 TCP 保活,或缩短连接池的空闲超时时间。 |
10928 |
Resource ID: %d. The %s limit for the database is %d and has been reached. See 'http://go.microsoft.com/fwlink/?LinkId=267637' for assistance. |
数据库超过Azure SQL资源治理限制。 资源 ID 1 表示工作线程限制;资源 ID 2 表示会话限制。 从消息中识别限制类型,然后降低并发、扩容数据库,或缩短长时间占用该资源的操作的执行时间。 |
10929 |
Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d, and the current usage for the database is %d. However, the server is currently too busy to support requests greater than %d for this database. |
数据库已超过其最低保障值,底层服务器正在对其进行限流。 当邻居负载下降时,重试通常成功。 持续出现表示需要更高的服务层级或不太干扰的环境。 |
40020、40143、40166、40540 |
在故障转移期间,于错误 40197 的 Error code %d 槽中报告。 |
嵌入在 40197 故障转移消息中的子代码,在某些路径中会显示为顶层错误代码。 将它们视为 40197。 |
40197 |
The service has encountered an error processing your request. Please try again. Error code %d. |
Azure SQL 中的一次软件升级、硬件故障或其他故障转移事件。 重新连接会将你路由到一个健康的副本。 嵌入的错误代码标识故障转移类型。 如果错误仍然存在,请捕获会话跟踪 ID 并联系支持人员。 |
40501 |
The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. Code: %d. |
Azure SQL 引擎限流。 建议的最低退避时间为 10 秒。 持续受限流表明工作负载已超过数据库的资源分配;请提升服务层级或减少并发。 |
40613 |
Database '%.*ls' on server '%.*ls' is not currently available. Please retry the connection later. If the problem persists, contact customer support, and provide them with the session tracing ID of '%.*ls'. |
数据库不可用,通常发生在故障转移过程中,或在缩放操作期间短暂出现。 按退避策略重试;如果问题持续几分钟以上,请记录会话跟踪 ID 并创建支持工单。 |
42108 |
Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again. |
专用 SQL 池(Synapse)处于暂停状态。 只有在池恢复后,重试才会成功。 显式地恢复池,或将工作负载安排在池恢复后运行。 |
42109 |
The SQL pool is warming up. Please try again. |
专用 SQL 池正在恢复。 按退避策略重试,直到池上线;预热通常需要几分钟。 |
49918 |
Cannot process request. Not enough resources to process request. The service is currently busy. Please retry the request later. |
服务器当前无法分配足够的资源来满足请求。 在退避时重试。 如果错误仍然存在,请纵向扩展数据库或弹性池。 |
49919 |
Cannot process create or update request. Too many create or update operations in progress for subscription "%ld". |
管理操作的订阅级并发限制。 减少并行创建/更新调用或将它们错开。 |
49920 |
Cannot process request. Too many operations in progress for subscription "%ld". |
针对正在进行的操作的订阅级并发限制。 降低并行度,或等待当前正在进行的操作完成。 |
语句级错误不在此列表中,因为它们发生在连接建立之后,而且即使失败,会话仍可继续使用。 最常见的可重试语句错误是 1205(死锁受害者)和 1222(锁请求超时)。 重试整个事务,而不是单个失败语句。
错误消息文本来自Azure SQL暂时性连接错误。 各个驱动程序维护各自的内置重试列表;此目录说明了在 SQL Server、Azure SQL 数据库、Azure SQL 托管实例、Microsoft Fabric 中的 SQL 数据库以及 Azure Synapse Analytics 中的专用 SQL 池中,哪些错误符合重试条件。
ODBC 驱动程序空闲连接复原能力
Microsoft ODBC Driver for SQL Server 通过 ConnectRetryCount 和 ConnectRetryInterval 连接字符串关键字提供内置的空闲连接恢复能力。 在涉及应用程序代码之前,这些设置在驱动程序级别处理已删除的空闲连接。
在 extra_params 中启用空闲连接复原:
DATABASES = {
"default": {
"ENGINE": "mssql",
"NAME": "<your-database>",
"HOST": "<your-server>.database.windows.net",
"PORT": "1433",
"OPTIONS": {
"driver": "ODBC Driver 18 for SQL Server",
"extra_params": "ConnectRetryCount=3;ConnectRetryInterval=10",
},
},
}
| 关键 字 | 默认 | Description |
|---|---|---|
ConnectRetryCount |
1 | 空闲连接的自动重新连接尝试次数。 |
ConnectRetryInterval |
10 | 每次重连尝试之间的间隔秒数。 |
Note
空闲连接恢复功能可重新建立在空闲期间中断的连接。 它不会重试失败的查询,也不会从活动事务期间发生的错误中恢复。 对于这些方案,请使用应用程序级重试逻辑。
用于重试的 Django 数据库中间件
创建一个 Django 中间件,用于捕获暂时性错误并重试数据库操作。 此方法适用于视图级请求处理:
# myproject/middleware.py
import random
import re
import time
import logging
from django.db import OperationalError, connection
logger = logging.getLogger(__name__)
TRANSIENT_ERROR_CODES = {
"64", "233", "4221",
"10053", "10054", "10928", "10929",
"40197", "40501", "40613",
"49918", "49919", "49920",
# Include "4060" only if targeting Azure SQL with geo-replication failover.
# It is usually a permanent error (wrong database name or missing permissions).
}
# Microsoft ODBC driver formats native error codes as "(<number>)" in the
# message. Extracting parenthesized codes avoids false positives that a plain
# substring match would produce for short codes like "64".
_CODE_RE = re.compile(r"\((\d+)\)")
def is_transient(error):
codes_in_message = set(_CODE_RE.findall(str(error)))
return bool(codes_in_message & TRANSIENT_ERROR_CODES)
class DatabaseRetryMiddleware:
"""Retry database operations on transient errors."""
def __init__(self, get_response):
self.get_response = get_response
self.max_retries = 3
self.base_delay = 1 # seconds; doubled each attempt
self.max_delay = 30 # cap on a single sleep, regardless of attempt
def __call__(self, request):
for attempt in range(self.max_retries + 1):
try:
return self.get_response(request)
except OperationalError as e:
if attempt < self.max_retries and is_transient(e):
# Exponential backoff with full jitter, capped at max_delay.
# Jitter spreads simultaneous retries so many clients
# don't hammer the server in lock-step during an outage.
capped = min(self.max_delay, self.base_delay * (2 ** attempt))
delay = random.uniform(0, capped)
logger.warning(
"Transient DB error (attempt %d/%d), retrying in %.2fs: %s",
attempt + 1, self.max_retries, delay, e
)
connection.close()
time.sleep(delay)
continue
raise
在settings.py中注册中间件:
MIDDLEWARE = [
"myproject.middleware.DatabaseRetryMiddleware",
"django.middleware.security.SecurityMiddleware",
# ... other middleware
]
Important
将 DatabaseRetryMiddleware 放在其他会访问数据库的中间件之前,以便它能够捕获并重试整个请求管道中的瞬时错误。
用于特定操作的重试修饰器
若要进行更细粒度的控制,请在各个单独的函数上使用修饰器:
import random
import re
import time
import functools
import logging
from django.db import OperationalError, connection
logger = logging.getLogger(__name__)
TRANSIENT_ERROR_CODES = {
"64", "233", "4221",
"10053", "10054", "10928", "10929",
"40197", "40501", "40613",
"49918", "49919", "49920",
# Include "4060" only if targeting Azure SQL with geo-replication failover.
}
_CODE_RE = re.compile(r"\((\d+)\)")
def is_transient(error):
codes_in_message = set(_CODE_RE.findall(str(error)))
return bool(codes_in_message & TRANSIENT_ERROR_CODES)
def retry_on_transient(max_retries=3, base_delay=1, max_delay=30):
"""Retry on transient database errors with exponential backoff and full jitter."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except OperationalError as e:
if attempt < max_retries and is_transient(e):
# Exponential cap doubled per attempt, then jittered
# within [0, cap] and limited by max_delay.
capped = min(max_delay, base_delay * (2 ** attempt))
delay = random.uniform(0, capped)
logger.warning(
"Transient error in %s (attempt %d/%d), retrying in %.2fs: %s",
func.__name__, attempt + 1, max_retries, delay, e
)
connection.close()
time.sleep(delay)
continue
raise
return wrapper
return decorator
将修饰器应用于数据库密集型函数:
from myproject.retry import retry_on_transient
@retry_on_transient(max_retries=3, base_delay=2)
def process_order(order_id):
"""Process an order with automatic retry on transient failures."""
order = Order.objects.select_for_update().get(id=order_id)
order.status = "processing"
order.save()
return order
使用事务重试
当事务中发生瞬态错误时,服务器会回滚整个事务。 重试完整的事务,而不仅仅是失败的语句:
from django.db import transaction
@retry_on_transient(max_retries=3)
def transfer_funds(from_account_id, to_account_id, amount):
"""Transfer funds between accounts with retry."""
with transaction.atomic():
from_account = Account.objects.select_for_update().get(id=from_account_id)
to_account = Account.objects.select_for_update().get(id=to_account_id)
from_account.balance -= amount
to_account.balance += amount
from_account.save()
to_account.save()
注意
不要在 transaction.atomic() 内部重试。 重试装饰器必须包裹整个 atomic() 代码块,以便每次重试都会开启一个全新的事务。
语句级错误
上一部分中的错误列表涵盖连接级故障。 另外两种错误通常也会在语句级别进行重试:
- 1205:该会话被选作死锁牺牲品。 重新运行事务。
-
1222:已超过锁请求超时时间。 重新运行事务;如果默认值过于咄咄逼人,则重新运行事务或增加
LOCK_TIMEOUT会话。
ConnectRetryCount 重试断开的连接,因此它不适用于这些语句级错误。 对于可安全重新运行的事务,通过将 "1205" 和 "1222" 添加到 TRANSIENT_ERROR_CODES,使用相同的装饰器模式来处理它们。
CONN_MAX_AGE 和陈旧连接
设置时 CONN_MAX_AGE ,Django 会跨请求重复使用数据库连接。 如果服务器关闭该连接(例如,在Azure SQL缩放操作或防火墙超时期间),长期连接可能会过时。
将 CONN_MAX_AGE 设置为在复用和陈旧性之间取得平衡:
DATABASES = {
"default": {
"ENGINE": "mssql",
"NAME": "<your-database>",
"HOST": "<your-server>.database.windows.net",
"OPTIONS": {
"driver": "ODBC Driver 18 for SQL Server",
},
"CONN_MAX_AGE": 600, # Close and reopen connections after 10 minutes
},
}
-
CONN_MAX_AGE=0(默认值):关闭每个请求末尾的连接。 最安全但最慢。 -
CONN_MAX_AGE=600:重用连接 10 分钟。 大多数 Web 应用程序的平衡良好。 -
CONN_MAX_AGE=None:无限期保持连接打开状态。 仅可与针对失效连接的重试机制一起使用。
CONN_HEALTH_CHECKS (Django 4.1 及更高版本)
Django 4.1 引入了 CONN_HEALTH_CHECKS,用于在每次请求之前验证复用的连接。 将它与 CONN_MAX_AGE 一起启用,以自动检测失效连接:
DATABASES = {
"default": {
"ENGINE": "mssql",
"NAME": "<your-database>",
"HOST": "<your-server>.database.windows.net",
"OPTIONS": {
"driver": "ODBC Driver 18 for SQL Server",
},
"CONN_MAX_AGE": 600,
"CONN_HEALTH_CHECKS": True,
},
}
启用运行状况检查后,Django 会在重用连接之前发出轻型验证查询。 如果连接断开,Django 会透明地打开一个新连接,而不是引发错误。
最佳做法
- 将指数退避与完全抖动一起使用。 每次尝试都将上限值翻倍,然后在
[0, cap]范围内随机等待一段时间。 抖动可防止大量客户端在区域故障期间同步重试,否则原本短暂的故障可能会演变为持续过载。 限制每次尝试睡眠(例如 30 秒),因此总恢复时间保持有限。 - 设置重试上限。 使用指数退避的三次重试是合理的默认值。 重试超过 5 次通常表明存在非暂时性问题。
- 在重试之前关闭连接。 调用
connection.close()以便 Django 在下一次尝试时打开新的连接。 - 记录每次重试。 静默成功的重试可能会掩盖性能问题。 以
WARNING级别记录,以便跟踪频率。 - 不要重试非暂时性错误。 身份验证失败、权限错误和语法错误不受益于重试。
- 重试整个事务。 将
transaction.atomic()包裹在重试逻辑中,而不是反过来。 -
启用
CONN_HEALTH_CHECKS(Django 4.1 及更高版本),用于使用CONN_MAX_AGE的 Web 应用程序。