通过


你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

排查暂时性连接错误

适用于:Azure SQL 数据库Azure SQL 托管实例Azure Synapse AnalyticsFabric 中的 SQL 数据库

本文介绍如何防止、排除故障、诊断和缓解客户端应用程序在与 Azure SQL 数据库、Microsoft Fabric 中的 SQL 数据库、Azure SQL 托管实例和 Azure Synapse Analytics 交互时所遇到的连接错误和暂时性错误。 了解如何配置重试逻辑、生成连接字符串以及调整其他连接设置。

暂时性错误(暂时性故障)

暂时性错误(也称为暂时性故障)的根本原因很快就能自行解决。 偶尔的暂时性错误原因是当 Azure 系统快速地转移硬件资源,以更好地均衡多种工作负载时发生的。 大多数这些重新配置事件在 60 秒内就能完成。 在重新配置期间,你可能会遇到连接 SQL 数据库时的问题。 连接到你的数据库的应用程序应当构建为能预见这些暂时性错误。 为了处理这些错误,可应用程序代码中实现重试逻辑,而不是以应用程序错误的形式呈现给用户。

如果客户端程序使用 ADO.NET,系统会引发 SqlException,使程序知道已发生暂时性错误。

连接与命令

重试连接或再次建立连接,具体视以下情况而定:

  • 尝试连接期间发生暂时性错误

延迟几秒钟后,重试连接。

  • 在查询命令期间发生暂时性错误

不要立即重试该命令。 应在延迟后重新建立连接。 然后重试命令。

针对暂时性错误的重试逻辑

在偶尔会遇到暂时性错误的客户端程序中包含重试逻辑可以让它变得更稳健。 如果你的程序通过第三方中间件与你在 SQL 数据库中的数据库通信,请咨询供应商,了解该中间件是否包含暂时性错误的重试逻辑。

重试原则

  • 如果错误是暂时性的,请重试打开连接。
  • 不要直接重试执行 SELECT 时发生暂时性错误的语句。 而应建立新的连接,然后重试 SELECT
  • UPDATE 语句失败并出现暂时性错误时,请在重试 UPDATE之前建立一个新的连接。 重试逻辑必须确保整个数据库事务已完成,或整个事务已回滚。

其他重试注意事项

  • 工作时间结束后自动启动,并在早晨之前完成的批处理程序可以在重试时保持耐心,允许重试间隔较长的时间。
  • 用户界面程序应该解释用户会在长时间等待后放弃操作的倾向。 解决方案不得每隔几秒钟重试,因为该策略可能会使系统填满请求。

增大重试间隔

我们建议在第一次重试前等待 5 秒。 如果在少于 5 秒的延迟后重试,云服务有超载的风险。 对于后续的每次重试,延迟应以指数级增大,最大值为 60 秒。

要了解使用 ADO.NET 的客户端的暂停时段,请参阅连接池 (ADO.NET)

还可以设置程序在自行终止之前的重试次数上限。

重试逻辑代码示例

以下文档提供了有关重试逻辑的代码示例:

测试重试逻辑

为了测试重试逻辑,您必须在程序仍在运行时模拟或引发一个可以修正的错误。

通过断开网络连接进行测试

可以测试重试逻辑的一种方法是在程序运行时断开客户端计算机与网络的连接。 错误为:

  • SqlException.Number = 11001
  • 消息:“此主机不存在”

第一次重试时,可以将客户端计算机重新连接到网络,然后尝试连接。

要使此测试可行,请从网络中断开计算机的连接,再启动程序。 然后,程序将识别促使它执行以下操作的运行时参数:

  • 暂时将 11001 添加到视为暂时性故障的错误列表。
  • 像往常一样尝试首次连接。
  • 在捕获该错误后,从列表中删除 11001。
  • 显示一条消息,告知用户要将计算机接入网络。
  • 通过使用“Console.ReadLine” 方法或具有“确定”按钮的对话框暂停进一步执行。 将计算机接入网络后,用户按 Enter 键。
  • 重新尝试连接,预期会成功。

通过在连接时拼错用户名进行测试

在首次连接尝试之前,程序可以故意拼错用户名。 错误为:

  • SqlException.Number = 18456
  • 消息:“用户 'WRONG_MyUserName' 的登录失败。”

在首次重试过程中,程序可以更正拼写错误,并尝试连接。

要使此测试可行,程序需识别促使它执行以下操作的运行时参数:

  • 暂时将 18456 添加到视为暂时性故障的错误列表。
  • 故意将“WRONG_”添加到用户名。
  • 在捕获该错误后,从列表中删除 18456。
  • 从用户名中删除“WRONG_”。
  • 重新尝试连接,预期会成功。

.NET SqlConnection 连接重试参数

如果客户端程序使用 .NET 类 Microsoft.Data.SqlClient.SqlConnectionSystem.Data.SqlClient.SqlConnection 连接到 Azure SQL 数据库中的数据库,则可以使用连接重试功能。 对于 System.Data.SqlClient,请使用 .NET Framework 4.6.1 或更高版本(或 .NET Core)。 有关此功能的详细信息,请参阅 SqlConnection.ConnectionString 属性

备注

Microsoft.Data.SqlClient 建议使用 ADO.NET 数据提供程序进行新的应用程序开发。 除了连接重试之外,它还支持对连接重试和命令重试 的可配置重试逻辑 。 有关详细信息,请参阅 Microsoft.Data.SqlClient 命名空间简介

SqlConnection 对象生成连接字符串时,请在以下参数之间协调值:

  • ConnectRetryCount:默认值为 1。 范围为0255
  • ConnectRetryInterval:默认值为 10 秒。 范围为 1 到 60。
  • Connection Timeout:默认值为 15 秒。 范围为 0 到 2147483647。

连接重试设置(ConnectRetryCountConnectRetryInterval)适用于连接弹性。 连接复原包括以下不同类型:

  • 打开连接弹性是指初始 SqlConnection.Open 或 OpenAsync() 方法。 第一次连接尝试算作尝试零。 ConnectRetryCount 适用于后续重试。 因此,如果连接零失败(这可能不会立即发生),则会首先应用ConnectRetryInterval,然后在每次尝试中应用ConnectRetryCount(以及ConnectRetryInterval)。 若要利用所有重试尝试,连接超时属性必须为所有尝试提供时间。

  • 空闲连接复原是指自动检测并重新连接已断开的现有空闲连接。 重新连接断开的空闲连接的第一次尝试算作第一次重试尝试。 若要利用所有重试尝试,命令超时必须为所有尝试提供时间。

示例:假设 ConnectRetryCount 和 ConnectRetryInterval 参数的值如下:

ConnectRetryCount:3 ConnectRetryInterval:10 秒

了解以下场景中如何使用这些值:

场景:新建连接

4:10:00 - Connection.Open() - 零尝试

4:10:01 - 检测到连接失败

4:10:11 - 重试 1 -- > 第一次重试发生在 ConnectRetryInterval 之后

4:10:21 - 重试 2

4:10:31 - 重试 3

对于此场景,所选值应满足以下条件:
Connection Timeout > = ConnectRetryCount * ConnectionRetryInterval

例如,如果计数为 3 且间隔为 10 秒,超时值仅为 29 秒未给系统足够的时间进行其第三次也是最后一次连接重试:

29 < 3 × 10

场景:空闲连接

ConnectRetryCount:3 ConnectRetryInterval:10 秒

4:10:00 - 执行命令时检测到连接断开

4:10:00 - 重试 1 -->首次重试立即发生

4:10:10 - 重试 2

4:10:20 - 重试 3

这不是初始连接。 因此,连接超时不适用。 但是,由于连接恢复在命令执行期间发生,因此执行该语句的 SqlCommandCommand Timeout 属性确实适用。 命令超时时间的默认值为 30 秒。 虽然在典型情况下连接恢复很快,但间歇性中断可能会导致恢复占用一些命令执行时间。

对于这个场景,如果你想充分利用空闲连接恢复重试,所选值应满足以下条件:
Command Timeout > (ConnectRetryCount - 1) * ConnectionRetryInterval

例如,如果计数为 3 且间隔为 10 秒,命令超时值仅为 20 秒不能给出足够的时间进行第三次也是最后一次连接重试:(3 - 1) * 10 = 20`

另外,请考虑在连接恢复后命令本身还需要时间来执行。

备注

这些场景中提供的持续时间值仅用于演示。 这两种场景中的实际检测时间取决于底层基础设施。

连接与命令

ConnectRetryCountConnectRetryInterval 参数使 SqlConnection 对象在重试连接操作时不用通知或麻烦你的程序(例如,将控制权返还给你的程序)。 在以下情况下可能会进行重试:

  • SqlConnection.Open 方法调用
  • SqlCommand.Execute* 方法调用

这有些微妙。 如果正在执行查询时发生暂时性错误,SqlConnection 对象不会重试连接操作。 肯定不会重试查询。 但是, SqlConnection 在发送要执行的查询前会非常快速地检查连接。 如果快速检查检测到连接问题, SqlConnection 会重试连接操作。 如果重试成功,则会发送查询以执行。

应该将 ConnectRetryCount 与应用程序的重试逻辑结合起来吗?

假设应用程序具有功能强大的自定义重试逻辑。 它可能会重试连接操作四次。 如果将 ConnectRetryIntervalConnectRetryCount =3 添加到连接字符串,则会将重试计数增加到 4 * 3 = 12 次重试。 可能未打算重试这么多次。

在 SQL 数据库中连接到你的数据库

连接:连接字符串

连接到你的数据库所需的连接字符串与连接到 SQL Server 所需的字符串稍有不同。 可以通过 Azure 门户复制数据库的连接字符串。

从 Azure 门户获取连接字符串

使用 Azure 门户获取客户端程序与 Azure SQL 数据库进行交互所需的连接字符串。

  1. 选择所有服务>SQL 数据库

  2. “SQL 数据库”窗格左上角附近的筛选器文本框中输入数据库的名称。

  3. 选择数据库所对应的行。

  4. 显示数据库的窗格后,为了便于查看,请选择“最小化”按钮,以折叠浏览和数据库筛选用的面板。

  5. 在数据库窗格上,选择“显示数据库连接字符串”

  6. 复制相应的连接字符串。 例如,如果要使用 ADO.NET 连接库,请从 ADO.NET 选项卡中复制相应的字符串。

    显示如何复制数据库的 ADO 连接字符串的屏幕截图。

  7. 根据需要编辑连接字符串。 在此示例中,请将密码插入连接字符串,或者如果用户名或服务器名称过长,请从用户名中删除 <server-name>

  8. 通过这种或那种格式,将连接字符串信息粘贴到客户端程序代码中。

有关详细信息,请参阅连接字符串和配置文件

连接:IP 地址

必须配置 SQL 数据库,使其接受来自托管你的客户端程序的计算机 IP 地址的通信。 若要设置此配置,可以通过 Azure 门户编辑防火墙设置。

如果忘记了配置 IP 地址,程序会失败,并显示简单的错误消息,指出所需的 IP 地址。

  1. 登录到 Azure 门户

  2. 在左侧的列表中,选择“所有服务”

  3. 滚动并选择“SQL Server”。

    门户中“查找 Azure SQL 数据库服务器”的屏幕截图。

  4. 在筛选器文本框中,开始键入服务器的名称。 您的行已显示。

  5. 选择您的服务器所在的行。 此时会显示服务器的窗格。

  6. 在服务器窗格上选择“设置”

  7. 选择“防火墙”

    “选择设置”> 防火墙的屏幕截图。

  8. 选择“添加客户端 IP”。 在第一个文本框中键入新规则的名称。

  9. 键入要启用的范围的下限和上限 IP 地址值。

    • 将低值以 .0 结尾,高值以 .255结尾可能会很有用。
  10. 选择“保存”。

有关详细信息,请参阅 Azure SQL 数据库和 Azure Synapse IP 防火墙规则

连接:端口

通常,您需要确保在托管您客户端程序的计算机上,只开放端口 1433 以进行出站通信。

例如,当客户端程序托管在 Windows 计算机上时,则可以使用主机上的 Windows 防火墙打开端口 1433。

  1. 打开控制面板。
  2. 选择所有控制面板项>Windows 防火墙高级设置>出站规则操作>新建规则。

如果您的客户端程序托管在 Azure 虚拟机 (VM) 上,请查看 针对 ADO.NET 4.5 的 1433 端口及以上端口

有关数据库中端口和 IP 地址配置的背景信息,请参阅 Azure SQL 数据库和 Azure Synapse IP 防火墙规则

连接:ADO.NET 4.6.2 或更高版本

如果程序使用 System.Data.SqlClient.SqlConnection 等 ADO.NET 类来连接到 SQL 数据库,我们建议使用 .NET Framework 4.6.2 或更高版本。 对于新应用程序,请考虑使用 Microsoft.Data.SqlClient,它提供与增强功能相同的连接。

从 ADO.NET 4.6.2 开始

  • Azure SQL 将立即重试连接打开的尝试,从而提升云应用的性能。

从 ADO.NET 4.6.1 开始

  • 对于 SQL 数据库,使用 SqlConnection.Open 方法打开连接可以获得更高的可靠性。 此 Open 方法现在结合了应对暂时性故障的最佳效果重试机制,用于处理连接超时期间发生的特定错误。
  • 支持连接池,其中包括有效验证提供给程序的连接对象是否正常运行的功能。

若要从连接池使用连接对象,我们建议,如果程序未立即使用连接,应暂时关闭连接。 重新打开连接并不昂贵,而是要创建新连接。

如果使用 ADO.NET 4.0 或更低版本,我们建议升级到最新的 ADO.NET。 从 2018 年 8 月开始,可以下载 ADO.NET 4.6.2

诊断

诊断:测试实用程序是否可以连接

如果你程序无法连接到你在 SQL 数据库中的数据库,可通过一个诊断选项尝试使用某个实用程序进行连接。 理想的情况下,该实用工具将使用程序所用的同一个库进行连接。

可以在任何 Windows 计算机上尝试以下实用程序:

  • SQL Server Management Studio (ssms.exe),它使用 ADO.NET 进行连接
  • sqlcmd.exe,它使用 ODBC 进行连接

连接程序后,测试一个简短的 SQL SELECT 查询是否可以正常工作。

诊断:检查打开的端口

如果你怀疑连接尝试由于端口问题而失败,可以在计算机上运行报告端口配置的实用工具。

在 Linux 上,以下实用工具可能很有用:

  • netstat -nap
  • nmap -sS -O 127.0.0.1:请将示例值更改为你的 IP 地址。

在 Windows 上,PortQry.exe 实用工具可能很有用。 下面是一个示例执行,它在 SQL 数据库中某个数据库上查询端口情况,并在笔记本电脑上运行:

[C:\Users\johndoe\]
>> portqry.exe -n johndoesvr9.database.windows.net -p tcp -e 1433

Querying target system called: johndoesvr9.database.windows.net

Attempting to resolve name to IP address...
Name resolved to 23.100.117.95

querying...
TCP port 1433 (ms-sql-s service): LISTENING

[C:\Users\johndoe\]
>>

诊断:记录错误

有时,诊断间歇性问题的最好方式是每隔数天或数周检测常规模式。

客户端可以通过记录其所遇到的所有错误来帮助你进行诊断。 可将日志条目与 SQL 数据库本身内部记录的错误数据相关联。

Enterprise Library 6 (EntLib60) 提供了 .NET 托管类来帮助进行日志记录。 有关详细信息,请参阅:5 - 与写入日志一样简单:使用日志记录应用程序块

诊断:在系统日志中检查错误

下面是查询错误日志和其他信息的一些 Transact-SQL SELECT 语句。

日志查询 说明
SELECT e.*
FROM sys.event_log AS e
WHERE e.database_name = 'myDbName'
AND e.event_category = 'connectivity'
AND 2 >= DateDiff
  (hour, e.end_time, GetUtcDate())
ORDER BY e.event_category,
  e.event_type, e.end_time;
sys.event_log 视图提供有关各个事件的信息,包括一些可能导致暂时性错误或连接故障的事件。

理想情况下,可以将 start_timeend_time 值与有关客户端程序遇到问题时的信息相关联。

必须连接到 master 数据库才能运行此查询。
SELECT c.*
FROM sys.database_connection_stats AS c
WHERE c.database_name = 'myDbName'
AND 24 >= DateDiff
  (hour, c.end_time, GetUtcDate())
ORDER BY c.end_time;
sys.database_connection_stats 视图提供用于其他诊断的事件类型聚合计数。

必须连接到 master 数据库才能运行此查询。

诊断:在 SQL 数据库日志中搜索问题事件

可以在 SQL 数据库日志中搜索有关问题事件的条目。 在 master 数据库中尝试运行以下 Transact-SQL SELECT 语句:

SELECT
   object_name
  ,CAST(f.event_data as XML).value
      ('(/event/@timestamp)[1]', 'datetime2')                      AS [timestamp]
  ,CAST(f.event_data as XML).value
      ('(/event/data[@name="error"]/value)[1]', 'int')             AS [error]
  ,CAST(f.event_data as XML).value
      ('(/event/data[@name="state"]/value)[1]', 'int')             AS [state]
  ,CAST(f.event_data as XML).value
      ('(/event/data[@name="is_success"]/value)[1]', 'bit')        AS [is_success]
  ,CAST(f.event_data as XML).value
      ('(/event/data[@name="database_name"]/value)[1]', 'sysname') AS [database_name]
FROM
  sys.fn_xe_telemetry_blob_target_read_file('el', null, null, null) AS f
WHERE
  object_name != 'login_event'  -- Login events are numerous.
  and
  '2015-06-21' < CAST(f.event_data as XML).value
        ('(/event/@timestamp)[1]', 'datetime2')
ORDER BY
  [timestamp] DESC
;

sys.fn_xe_telemetry_blob_target_read_file 中已返回的若干行

以下示例显示返回的行的类似内容。 在其他行中,显示的 null 值通常不为 null。

object_name                   timestamp                    error  state  is_success  database_name

database_xml_deadlock_report  2015-10-16 20:28:01.0090000  NULL   NULL   NULL        AdventureWorks

企业库 6

Enterprise Library 6 (EntLib60) 是 .NET 类的框架,可帮助你实施云服务(包括 SQL 数据库)的可靠客户端。 要查找 EntLib60 可以提供帮助的各个领域的相关主题,请参阅 Enterprise Library 6

用于处理暂时性错误的重试逻辑是 EntLib60 可以提供帮助的一个领域。 有关详细信息,请参阅 4 - 锲而不舍是一切成功的秘密:使用暂时性故障处理应用程序块

备注

EntLib60 的源代码可从下载中心公开下载。 Microsoft 不打算对 EntLib 做进一步的功能更新或维护更新。

用于暂时性错误和重试的 EntLib60 类

以下 EntLib60 类对重试逻辑特别有用。 可以在 Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling 命名空间或其子级中找到所有这些类。

在命名空间 Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling 中:

  • RetryPolicy
    • ExecuteAction 方法
  • ExponentialBackoff
  • SqlDatabaseTransientErrorDetectionStrategy
  • ReliableSqlConnection类
    • ExecuteCommand 方法

在命名空间 Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.TestSupport中:

  • AlwaysTransientErrorDetectionStrategy
  • NeverTransientErrorDetectionStrategy

以下是 EntLib60 相关信息的某些链接:

EntLib60:日志记录块

  • 日志记录块是极其灵活且可配置的解决方案,可用于:
    • 创建日志消息并将其存储在各种不同的位置。
    • 分类与筛选消息。
    • 收集有助于调试和跟踪的上下文信息,以及用于满足审核和一般日志记录要求的上下文信息。
  • 日志记录块可以从日志目标抽象化日志记录功能,使应用程序代码保持一致,无论目标日志记录存储的位置和类型为何。

有关详细信息,请参阅:5 - 与写入日志一样简单:使用日志记录应用程序块

EntLib60 IsTransient 方法的源代码

接下来,SqlDatabaseTransientErrorDetectionStrategy 类包含 IsTransient 方法的 C# 源代码。 该源代码阐明了哪些错误被视为暂时性错误并值得重试。

public bool IsTransient(Exception ex)
{
  if (ex != null)
  {
    SqlException sqlException;
    if ((sqlException = ex as SqlException) != null)
    {
      // Enumerate through all errors found in the exception.
      foreach (SqlError err in sqlException.Errors)
      {
        switch (err.Number)
        {
            // SQL Error Code: 40501
            // The service is currently busy. Retry the request after 10 seconds.
            // Code: (reason code to be decoded).
          case ThrottlingCondition.ThrottlingErrorNumber:
            // Decode the reason code from the error message to
            // determine the grounds for throttling.
            var condition = ThrottlingCondition.FromError(err);

            // Attach the decoded values as additional attributes to
            // the original SQL exception.
            sqlException.Data[condition.ThrottlingMode.GetType().Name] =
              condition.ThrottlingMode.ToString();
            sqlException.Data[condition.GetType().Name] = condition;

            return true;

          case 10928:
          case 10929:
          case 10053:
          case 10054:
          case 10060:
          case 40197:
          case 40540:
          case 40613:
          case 40143:
          case 233:
          case 64:
            // DBNETLIB Error Code: 20
            // The instance of SQL Server you attempted to connect to
            // does not support encryption.
          case (int)ProcessNetLibErrorCode.EncryptionNotSupported:
            return true;
        }
      }
    }
    else if (ex is TimeoutException)
    {
      return true;
    }
    else
    {
      EntityException entityException;
      if ((entityException = ex as EntityException) != null)
      {
        return this.IsTransient(entityException.InnerException);
      }
    }
  }

  return false;
}